@rethunk/mcp-multi-root-git 3.1.0 → 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 (66) hide show
  1. package/AGENTS.md +21 -15
  2. package/CHANGELOG.md +114 -39
  3. package/HUMANS.md +20 -39
  4. package/README.md +30 -13
  5. package/dist/repo-paths.js +57 -13
  6. package/dist/server/batch-commit-tool.js +187 -46
  7. package/dist/server/error-codes.js +25 -7
  8. package/dist/server/git-blame-tool.js +6 -3
  9. package/dist/server/git-branch-tool.js +151 -0
  10. package/dist/server/git-cherry-pick-tool.js +220 -11
  11. package/dist/server/git-conflicts-tool.js +230 -0
  12. package/dist/server/git-diff-summary-tool.js +36 -25
  13. package/dist/server/git-diff-tool.js +55 -27
  14. package/dist/server/git-grep-tool.js +188 -0
  15. package/dist/server/git-inventory-tool.js +26 -6
  16. package/dist/server/git-log-tool.js +35 -4
  17. package/dist/server/git-merge-tool.js +70 -12
  18. package/dist/server/git-parity-tool.js +13 -4
  19. package/dist/server/git-push-tool.js +3 -1
  20. package/dist/server/git-refs.js +48 -17
  21. package/dist/server/git-revert-tool.js +160 -0
  22. package/dist/server/git-show-tool.js +7 -3
  23. package/dist/server/git-stash-tool.js +110 -67
  24. package/dist/server/git-tag-tool.js +7 -6
  25. package/dist/server/git-worktree-tool.js +65 -53
  26. package/dist/server/git.js +116 -24
  27. package/dist/server/inventory.js +90 -32
  28. package/dist/server/presets-resource.js +37 -20
  29. package/dist/server/presets.js +87 -15
  30. package/dist/server/roots.js +13 -1
  31. package/dist/server/schemas.js +9 -2
  32. package/dist/server/test-harness.js +11 -4
  33. package/dist/server/tool-parameter-schemas.js +44 -55
  34. package/dist/server/tools.js +36 -17
  35. package/dist/server.js +1 -1
  36. package/docs/install.md +52 -5
  37. package/docs/mcp-tools.md +385 -178
  38. package/package.json +6 -6
  39. package/schemas/batch_commit.json +2 -2
  40. package/schemas/git_blame.json +1 -1
  41. package/schemas/git_branch.json +54 -0
  42. package/schemas/git_cherry_pick.json +12 -2
  43. package/schemas/git_cherry_pick_continue.json +34 -0
  44. package/schemas/{git_reflog.json → git_conflicts.json} +12 -12
  45. package/schemas/git_diff.json +11 -3
  46. package/schemas/git_grep.json +85 -0
  47. package/schemas/git_inventory.json +19 -5
  48. package/schemas/git_log.json +7 -6
  49. package/schemas/git_merge.json +2 -2
  50. package/schemas/git_parity.json +0 -5
  51. package/schemas/git_revert.json +47 -0
  52. package/schemas/git_show.json +1 -1
  53. package/schemas/git_stash_push.json +47 -0
  54. package/schemas/git_status.json +0 -5
  55. package/schemas/git_worktree_add.json +1 -1
  56. package/schemas/git_worktree_remove.json +1 -1
  57. package/schemas/index.json +28 -23
  58. package/schemas/list_presets.json +0 -5
  59. package/tool-parameters.schema.json +326 -167
  60. package/dist/server/git-branch-list-tool.js +0 -132
  61. package/dist/server/git-fetch-tool.js +0 -257
  62. package/dist/server/git-reflog-tool.js +0 -120
  63. package/schemas/git_branch_list.json +0 -30
  64. package/schemas/git_fetch.json +0 -46
  65. package/schemas/git_stash_list.json +0 -24
  66. package/schemas/git_worktree_list.json +0 -24
package/docs/mcp-tools.md CHANGED
@@ -12,28 +12,29 @@ MCP clients expose tools as `{serverName}_{toolName}`. With the server registere
12
12
  | Short id | Client id (server `rethunk-git`) | Purpose |
13
13
  |----------|-----------------------------------|---------|
14
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. `nestedRoots`, `preset`, `presetMerge`, `maxRoots`, `format`, plus `root` (array form cannot combine with `preset`/`nestedRoots`). **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
16
  | `git_parity` | `rethunk-git_git_parity` | Compare `git rev-parse HEAD` for path pairs. `pairs`, `preset`, `presetMerge`, `format`, plus `root`. **Read-only.** |
17
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. Args: `since`, `paths`, `grep`, `author`, `maxCommits`, `branch`, plus `root` + `format` (`markdown`/`json`/`oneline`). **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.** |
19
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.** |
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`, `format`, `base?`, `head?`, `path?`, `paths?`, `unified?`, `staged?`. **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 `workspaceRoot` + `format`. **Read-only.** |
22
- | `git_worktree_list` | `rethunk-git_git_worktree_list` | List all worktrees (`git worktree list --porcelain`). `workspaceRoot` + `format`. **Read-only.** |
23
- | `git_stash_list` | `rethunk-git_git_stash_list` | List `git stash` entries for one repo. Args: `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
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
- | `git_branch_list` | `rethunk-git_git_branch_list` | List local branches (sha, current marker, upstream); optional `includeRemotes` adds remote-tracking branches. Args: `includeRemotes?`, plus `workspaceRoot` + `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 `workspaceRoot` + `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 `workspaceRoot` + `format`. **Mutating — refs 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.** |
28
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.** |
29
- | `git_tag` | `rethunk-git_git_tag` | Create/delete annotated or lightweight tags for one repo. Args: `tag`, `message?`, `ref?`, `delete?`, plus `workspaceRoot` + `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 `workspaceRoot` + `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 `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.** |
32
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.** |
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 `workspaceRoot` + `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 + `workspaceRoot` + `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?` + `workspaceRoot` + `format`. **Mutating.** |
36
- | `git_stash_apply` | `rethunk-git_git_stash_apply` | Apply or pop a stash entry for one repo. Args: `index?`, `pop?`, plus `workspaceRoot` + `format`. **Mutating.** |
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
 
@@ -83,13 +84,14 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
83
84
 
84
85
  | Parameter | Type | Default | Notes |
85
86
  |-----------|------|---------|-------|
86
- | `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` or `preset`. |
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`). |
87
88
  | `preset` | string | — | Preset name from `.rethunk/git-mcp-presets.json`. Loads `nestedRoots` from the preset's entry. Cannot combine with an array `root`. |
88
89
  | `presetMerge` | boolean | `false` | When `true`, merge inline `nestedRoots` with preset roots instead of replacing. |
89
90
  | `remote` | string | — | Fixed remote for ahead/behind tracking. Must be paired with `branch`. |
90
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. |
91
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. |
92
- | `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`.
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`. |
93
95
  | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
94
96
 
95
97
  ### `git_inventory` — JSON shape (`format: "json"`)
@@ -97,7 +99,7 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
97
99
  ```json
98
100
  {
99
101
  "inventories": [{
100
- "workspace_root": "/abs/path",
102
+ "workspaceRoot": "/abs/path",
101
103
  "entries": [{
102
104
  "label": ".",
103
105
  "path": "/abs/path",
@@ -106,7 +108,13 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
106
108
  "headAbbrev": "a1b2c3d",
107
109
  "upstreamRef": "origin/main",
108
110
  "ahead": "2",
109
- "behind": "0"
111
+ "behind": "0",
112
+ "compareRefs": {
113
+ "left": "main",
114
+ "right": "feature",
115
+ "ahead": "1",
116
+ "behind": "0"
117
+ }
110
118
  }],
111
119
  "nestedRootsTruncated": true,
112
120
  "nestedRootsOmittedCount": 3,
@@ -115,7 +123,7 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
115
123
  }
116
124
  ```
117
125
 
118
- `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 *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.
119
127
 
120
128
  ### `git_inventory` — error codes
121
129
 
@@ -124,14 +132,13 @@ Error payloads appear as top-level JSON or inline in individual repo rows (`ok:
124
132
  | `git_not_found` | `git` binary not on `PATH`. |
125
133
  | `remote_branch_mismatch` | Only one of `remote` / `branch` was provided; supply both or neither. |
126
134
  | `invalid_remote_or_branch` | `remote` or `branch` contains characters outside the safe token set. |
135
+ | `unsafe_ref_token` | A `compareRefs.left` / `compareRefs.right` token failed `isSafeGitAncestorRef` validation. |
127
136
  | `root_list_nested_or_preset_conflict` | `root` array combined with `nestedRoots` or `preset`. |
128
- | `root_list_preset_conflict` | `root` array combined with a `preset` argument. |
129
137
  | `invalid_root_path` | A `root` array entry is empty or not a git-recognized directory. |
130
138
  | `root_list_too_many` | More than 256 entries in the `root` array. |
131
139
  | `root_list_empty` | The `root` array resolved to zero git toplevels. |
132
140
  | `preset_not_found` | Named preset does not exist in the preset file. |
133
- | `invalid_json` | Preset file contains invalid JSON. |
134
- | `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`. |
135
142
 
136
143
  Skip entries (individual repos that could not be inventoried) appear inline in `entries[*]` with `skipReason` rather than as top-level errors.
137
144
 
@@ -152,7 +159,7 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
152
159
  ```json
153
160
  {
154
161
  "parity": [{
155
- "workspace_root": "/abs/path",
162
+ "workspaceRoot": "/abs/path",
156
163
  "status": "MISMATCH",
157
164
  "pairs": [
158
165
  {
@@ -175,7 +182,7 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
175
182
  }
176
183
  ```
177
184
 
178
- `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.
179
186
 
180
187
  ### `git_parity` — error codes
181
188
 
@@ -188,8 +195,7 @@ Skip entries (individual repos that could not be inventoried) appear inline in `
188
195
  | `root_list_too_many` | More than 256 entries in the `root` array. |
189
196
  | `root_list_empty` | The `root` array resolved to zero git toplevels. |
190
197
  | `preset_not_found` | Named preset does not exist in the preset file. |
191
- | `invalid_json` | Preset file contains invalid JSON. |
192
- | `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`). |
193
199
 
194
200
  Path-escape and `rev-parse` failures are reported inline in `pairs[*].error`, not as top-level error codes.
195
201
 
@@ -232,8 +238,7 @@ Path-escape and `rev-parse` failures are reported inline in `pairs[*].error`, no
232
238
  |------|---------|
233
239
  | `git_not_found` | `git` binary not on `PATH`. |
234
240
  | `not_a_git_repository` | Root is not inside a git repository (reported inline in the `roots[*].error` field, not top-level). |
235
- | `invalid_json` | Preset file is not valid JSON (inline in `roots[*].error`). |
236
- | `invalid_schema` | Preset file fails schema validation (inline in `roots[*].error`). |
241
+ | `preset_file_invalid` | Preset file failed to load (inline in `roots[*].error`). Discriminator `kind`: `"invalid_json"` or `"schema"`. |
237
242
  | `invalid_root_path` | A `root` array entry is empty or not a git-recognized directory. |
238
243
  | `root_list_too_many` | More than 256 entries in the `root` array. |
239
244
  | `root_list_empty` | The `root` array resolved to zero git toplevels. |
@@ -242,7 +247,11 @@ Path-escape and `rev-parse` failures are reported inline in `pairs[*].error`, no
242
247
 
243
248
  ## JSON responses
244
249
 
245
- Tool JSON bodies are minified and contain only the payload — no `rethunkGitMcp` envelope. Current `MCP_JSON_FORMAT_VERSION` is **`"4"`** (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.
246
255
 
247
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`.
248
257
 
@@ -250,22 +259,22 @@ The package also ships **`tool-parameters.schema.json`**, generated from the reg
250
259
 
251
260
  ### Field omission (consumer contract, v2+)
252
261
 
253
- 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).
254
263
 
255
264
  **`git_inventory` → `inventories[*]`**
256
265
 
257
- - Always present: `workspace_root`, `entries`.
266
+ - Always present: `workspaceRoot`, `entries`.
258
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).
259
268
 
260
269
  **`git_inventory` → `entries[*]` (`InventoryEntryJson`)**
261
270
 
262
271
  - Always present: `label`, `path`, `upstreamMode` (`"auto"` or `"fixed"`).
263
- - 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).
264
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).
265
274
 
266
275
  **Errors** (any tool)
267
276
 
268
- - 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.
269
278
 
270
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).
271
280
 
@@ -274,13 +283,14 @@ To keep responses compact, **optional fields are omitted when they would be empt
274
283
  | Parameter | Type | Default | Notes |
275
284
  |-----------|------|---------|-------|
276
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`). |
277
- | `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"`. |
278
288
  | `grep` | string | — | Filter by commit message regex (git `--grep`, always case-insensitive). |
279
289
  | `author` | string | — | Filter by author name or email (`--author=`). |
280
290
  | `maxCommits` | int | `50` | Max commits per root. Hard cap: `500`. |
281
291
  | `branch` | string | `HEAD` | Ref/branch to log from. |
282
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. |
283
- | `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. |
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. |
284
294
 
285
295
  ### `git_log` — JSON shape (`format: "json"`)
286
296
 
@@ -317,14 +327,60 @@ v2 field-omission rules still apply: `filesChanged`, `insertions`, `deletions` o
317
327
  | `git_not_found` | `git` binary not on `PATH`. |
318
328
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
319
329
  | `invalid_since` | The `since` string contains shell metacharacters and was rejected. |
320
- | `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. |
321
332
  | `unsafe_ref_token` | `branch` contains characters outside the argv-safe subset. |
322
333
  | `git_log_failed` | `git log` exited non-zero (e.g. unknown branch ref). |
323
- | `root_list_preset_conflict` | `root` array was combined with a `preset` argument (root resolution). |
324
334
  | `invalid_root_path` | A `root` array entry is empty, not inside a git worktree, or not a directory git recognizes. |
325
335
  | `root_list_too_many` | More than 256 entries in the `root` array. |
326
336
  | `root_list_empty` | The `root` array produced zero git toplevels after resolution. |
327
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
+ ---
383
+
328
384
  ### `git_parity` — `root` array example
329
385
 
330
386
  Pass a `root` array when the same parity pair should be checked across sibling clones that are not all MCP workspace roots:
@@ -342,11 +398,13 @@ Pass a `root` array when the same parity pair should be checked across sibling c
342
398
 
343
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.
344
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`.
402
+
345
403
  ### `git_diff_summary` — parameters
346
404
 
347
405
  | Parameter | Type | Default | Notes |
348
406
  |-----------|------|---------|-------|
349
- | `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. |
350
408
  | `fileFilter` | string | — | Glob pattern to restrict output to matching files (e.g. `"*.ts"`, `"src/**"`). |
351
409
  | `maxLinesPerFile` | int | `50` | Max diff lines to include per file (1–2000). |
352
410
  | `maxFiles` | int | `30` | Max files to include in output (1–500). |
@@ -374,7 +432,7 @@ The response contains one **`parity[]`** entry per resolved git toplevel. An arr
374
432
  }
375
433
  ```
376
434
 
377
- `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). `truncatedFiles` and `excludedFiles` are omitted when zero/empty (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).
378
436
 
379
437
  ### `git_diff_summary` — error codes
380
438
 
@@ -391,26 +449,29 @@ The response contains one **`parity[]`** entry per resolved git toplevel. An arr
391
449
 
392
450
  | Parameter | Type | Default | Notes |
393
451
  |-----------|------|---------|-------|
394
- | `base` | string | — | Base ref for a revision diff. When omitted with no `staged`, the tool shows unstaged changes. |
395
- | `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. |
396
454
  | `path` | string | — | Optional single file path to scope the diff. Confined to the repo (`path_escapes_repo` on escape). |
397
455
  | `paths` | string[] | — | Multiple file paths to scope the diff; unioned with `path` (deduped). Each confined to the repo. |
398
456
  | `unified` | integer | — | Context lines around each change (passed as `-U<n>`, 0–100). Omit for git's default (3). |
399
457
  | `staged` | boolean | `false` | When `true`, runs `git diff --staged`. Ignored when `base` is provided. |
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). |
400
459
  | `workspaceRoot` | string | — | Repo path. Default: first MCP root / cwd. |
401
460
  | `format` | `"markdown"` \| `"json"` | `"markdown"` | Output format. |
402
461
 
403
- `git_diff` is a single-repo tool; use `workspaceRoot` 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.
404
463
 
405
464
  ### `git_diff` — JSON shape (`format: "json"`)
406
465
 
407
466
  ```json
408
467
  {
409
468
  "range": "HEAD~1..HEAD (src/server.ts)",
410
- "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
411
471
  }
412
472
  ```
413
473
 
474
+ `truncated: true` is present only when the `maxBytes` cap fired.
414
475
  ### `git_diff` — error codes
415
476
 
416
477
  | Code | Meaning |
@@ -426,8 +487,8 @@ The response contains one **`parity[]`** entry per resolved git toplevel. An arr
426
487
 
427
488
  | Parameter | Type | Notes |
428
489
  |-----------|------|-------|
429
- | `ref` | string | Commit, branch, tag, or other git rev-spec to inspect. |
430
- | `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. |
431
492
  | `stat` | boolean | When `true`, runs `git show --stat` — commit message plus per-file diffstat, no full patch (`statOutput` in JSON). |
432
493
  | `paths` | string[] | Filter the shown patch/stat to these repo-relative paths; unioned with `path`. Each confined to the repo. |
433
494
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
@@ -451,69 +512,50 @@ The response contains one **`parity[]`** entry per resolved git toplevel. An arr
451
512
  |------|---------|
452
513
  | `unsafe_ref_token` | `ref` contains characters outside the argv-safe subset. |
453
514
  | `path_escapes_repo` | `path` resolves outside the git toplevel. |
454
- | `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. |
455
516
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
456
517
 
457
518
  ---
458
519
 
459
- ### `git_stash_list` — parameters
460
-
461
- | Parameter | Type | Notes |
462
- |-----------|------|-------|
463
- | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
464
-
465
- ### `git_stash_list` — JSON shape (`format: "json"`)
466
-
467
- ```json
468
- {
469
- "stashes": [
470
- { "index": 0, "message": "WIP on main: abc1234 feat: add tool", "sha": "abc1234" }
471
- ]
472
- }
473
- ```
474
-
475
- ### `git_stash_list` — error codes
476
-
477
- | Code | Meaning |
478
- |------|---------|
479
- | `stash_list_failed` | `git stash list` failed unexpectedly. |
480
- | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
481
-
482
- ---
483
-
484
- ### `git_fetch` — parameters
520
+ ### `git_conflicts` — parameters
485
521
 
486
522
  | Parameter | Type | Default | Notes |
487
523
  |-----------|------|---------|-------|
488
- | `remote` | string | `"origin"` | Remote to fetch from. |
489
- | `branch` | string | | Optional branch/ref to fetch from that remote. |
490
- | `prune` | boolean | `false` | Pass `--prune` to remove deleted remote-tracking refs. |
491
- | `tags` | boolean | `false` | Pass `--tags` to also fetch all tags. |
492
- | `workspaceRoot`, `format` | — | Standard single-repo 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. |
493
527
 
494
- ### `git_fetch` — JSON shape (`format: "json"`)
528
+ ### `git_conflicts` — JSON shape (`format: "json"`)
495
529
 
496
530
  ```json
497
531
  {
498
- "ok": true,
499
- "remote": "origin",
500
- "updatedRefs": ["abc1234..def5678 main -> origin/main"],
501
- "newRefs": ["[new tag] v2.0.0 -> v2.0.0"],
502
- "updated": [{ "ref": "refs/remotes/origin/main", "oldSha": "abc1234…", "newSha": "def5678…", "flag": " " }],
503
- "created": [{ "ref": "refs/tags/v2.0.0", "newSha": "0a1b2c3…", "flag": "*" }],
504
- "pruned": [{ "ref": "refs/remotes/origin/old" }],
505
- "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
+ ]
506
547
  }
507
548
  ```
508
549
 
509
- `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
510
553
 
511
- ### `git_fetch`error codes
554
+ Only the shared single-repo prelude errors apply the tool itself never fails on conflict state:
512
555
 
513
556
  | Code | Meaning |
514
557
  |------|---------|
515
- | `unsafe_remote_token` | `remote` contains characters outside the argv-safe subset. |
516
- | `unsafe_ref_token` | `branch` contains characters outside the argv-safe subset. |
558
+ | `git_not_found` | `git` binary not on `PATH`. |
517
559
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
518
560
 
519
561
  ---
@@ -523,9 +565,9 @@ The response contains one **`parity[]`** entry per resolved git toplevel. An arr
523
565
  | Parameter | Type | Default | Notes |
524
566
  |-----------|------|---------|-------|
525
567
  | `path` | string | — | **Required.** Repo-relative file to annotate. Confined to the repo (`path_escapes_repo` on escape). |
526
- | `ref` | string | working tree | Commit-ish to blame at. Validated as a safe ref token. |
527
- | `startLine` | int | — | Start of a line range (`-L`). Requires `endLine`. |
528
- | `endLine` | int | — | End of the line range, inclusive. Requires `startLine`. |
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`. |
529
571
  | `maxLines` | int | `2000` | Max blamed lines to return (1–10000). Excess lines are dropped and signalled via `truncated`/`omittedLines`. |
530
572
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
531
573
 
@@ -568,74 +610,17 @@ One `groups` entry per **contiguous run of lines last touched by the same commit
568
610
 
569
611
  ---
570
612
 
571
- ### `git_branch_list` — parameters
572
-
573
- | Parameter | Type | Default | Notes |
574
- |-----------|------|---------|-------|
575
- | `includeRemotes` | boolean | `false` | Also list remote-tracking branches (`refs/remotes`); symbolic `origin/HEAD` is skipped. |
576
- | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
577
-
578
- ### `git_branch_list` — JSON shape (`format: "json"`)
579
-
580
- ```json
581
- {
582
- "branches": [
583
- { "name": "main", "sha": "a1b2c3d4…", "current": true, "upstream": "origin/main" },
584
- { "name": "feature/x", "sha": "b2c3d4e5…", "current": false }
585
- ],
586
- "remotes": [{ "name": "origin/main", "sha": "a1b2c3d4…" }]
587
- }
588
- ```
589
-
590
- `upstream` is omitted when a branch has no upstream. `remotes` is present only when `includeRemotes: true`.
591
-
592
- ### `git_branch_list` — error codes
593
-
594
- | Code | Meaning |
595
- |------|---------|
596
- | `branch_list_failed` | `git for-each-ref` exited non-zero. `detail` carries stderr. |
597
- | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
598
-
599
- ---
600
-
601
- ### `git_reflog` — parameters
602
-
603
- | Parameter | Type | Default | Notes |
604
- |-----------|------|---------|-------|
605
- | `ref` | string | `HEAD` | Ref whose reflog to show. Validated as a safe ref token. |
606
- | `maxEntries` | int | `30` | Max entries to return (1–200). |
607
- | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
608
-
609
- ### `git_reflog` — JSON shape (`format: "json"`)
610
-
611
- ```json
612
- {
613
- "ref": "HEAD",
614
- "entries": [
615
- { "sha": "a1b2c3d4…", "selector": "HEAD@{0}", "message": "commit: feat: add tool" }
616
- ]
617
- }
618
- ```
619
-
620
- ### `git_reflog` — error codes
621
-
622
- | Code | Meaning |
623
- |------|---------|
624
- | `unsafe_ref_token` | `ref` contains characters outside the argv-safe subset. |
625
- | `reflog_failed` | `git reflog show` exited non-zero. `detail` carries stderr. |
626
- | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
627
-
628
- ---
629
-
630
613
  ### `batch_commit` — atomic staging semantics
631
614
 
632
615
  **Critical for AI agents:** Each call to `batch_commit` is **self-contained and atomic per-commit entry**.
633
616
 
634
- - **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.
635
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.
636
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.
637
- - **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.
638
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).
639
624
 
640
625
  Example: to commit two related changes atomically, pass both entries in one call:
641
626
  ```json
@@ -653,7 +638,7 @@ Do NOT do this: make two separate calls hoping to stage files incrementally. Tha
653
638
 
654
639
  | Parameter | Type | Notes |
655
640
  |-----------|------|-------|
656
- | `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`). |
657
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. |
658
643
  | `dryRun` | boolean | Default `false`. When `true`, stages each entry, reports what would be committed (`staged`, `diffStat`), then unstages everything without writing commits. |
659
644
  | `workspaceRoot` | string | Repo path. Default: first MCP root / cwd. |
@@ -696,6 +681,8 @@ The `push` object is present only when `push: "after"` was requested **and** eve
696
681
  | Code | Meaning |
697
682
  |------|---------|
698
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`. |
699
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. |
700
687
  | `commit_failed` | `git commit` failed (e.g. nothing staged, hooks rejected). |
701
688
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
@@ -716,9 +703,9 @@ The `push` object is present only when `push: "after"` was requested **and** eve
716
703
  |-----------|------|-------|
717
704
  | `sources` | `string[]` | Source branches to merge, in order. 1–20 entries. Each must be a valid git ref token. |
718
705
  | `into` | string | Destination branch. Defaults to the currently checked-out branch. Rejected when HEAD is detached. |
719
- | `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. |
720
707
  | `message` | string | Merge commit message, used only when a merge commit is created. Defaults to `Merge branch '<source>' into <into>`. |
721
- | `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. |
722
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. |
723
710
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
724
711
 
@@ -753,7 +740,7 @@ The `push` object is present only when `push: "after"` was requested **and** eve
753
740
 
754
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.
755
742
 
756
- 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.
757
744
 
758
745
  ### `git_merge` — error codes
759
746
 
@@ -767,8 +754,10 @@ On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` wi
767
754
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
768
755
  | `source_not_found` (per source) | A source branch name does not resolve. |
769
756
  | `cannot_fast_forward` (per source) | `strategy: "ff-only"` refused because branches have diverged. |
770
- | `rebase_conflicts` (per source) | Rebase encountered conflicts. Repo state is cleaned before returning. |
771
- | `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. |
772
761
  | `merge_failed` (per source) | `git merge --ff-only` failed unexpectedly. `detail` carries stderr. |
773
762
  | `merge_base_failed` (per source) | `git merge-base` failed (usually unrelated histories). |
774
763
 
@@ -778,11 +767,12 @@ On conflict: top-level `ok` is `false`, the conflicting entry has `ok: false` wi
778
767
 
779
768
  | Parameter | Type | Notes |
780
769
  |-----------|------|-------|
781
- | `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`. |
782
771
  | `onto` | string | Destination branch. Defaults to the currently checked-out branch. Rejected when HEAD is detached. |
783
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. |
784
773
  | `deleteMergedWorktrees` | boolean | Default `false`. After success, remove any local worktree attached to a branch-kind source (`git worktree remove`). Protected tails always skipped. |
785
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. |
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). |
786
776
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
787
777
 
788
778
  ### `git_cherry_pick` — JSON shape (`format: "json"`)
@@ -823,7 +813,26 @@ On conflict, the response has `ok: false` and a top-level `conflict` object:
823
813
  }
824
814
  ```
825
815
 
826
- 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
+ ```
827
836
 
828
837
  ### `git_cherry_pick` — error codes
829
838
 
@@ -832,10 +841,67 @@ Repo state is cleaned (`git cherry-pick --abort`) before returning — no partia
832
841
  | `unsafe_ref_token` | A source or `onto` contains characters outside the argv-safe subset. |
833
842
  | `onto_detached_head` | HEAD is detached and no `onto` was given. |
834
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. |
835
845
  | `checkout_failed` | Could not switch to `onto`. |
836
846
  | `destination_not_found` | `onto` does not resolve to a commit. |
837
847
  | `source_not_found` | A source spec resolves to neither a branch, a range, nor a commit. |
838
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. |
839
905
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
840
906
 
841
907
  ---
@@ -903,6 +969,40 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
903
969
 
904
970
  ---
905
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
+
906
1006
  ### `git_reset_soft` — parameters
907
1007
 
908
1008
  | Parameter | Type | Notes |
@@ -922,22 +1022,66 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
922
1022
  |------|---------|
923
1023
  | `unsafe_ref_token` | `ref` contains characters outside the ancestor-safe token set. |
924
1024
  | `working_tree_dirty` | Working tree has uncommitted/unstaged changes; clean up before resetting. |
925
- | `status_failed` | `git status` failed unexpectedly. |
926
1025
  | `reset_failed` | `git reset --soft` failed (e.g. ref does not exist). |
927
1026
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
928
1027
 
929
1028
  ---
930
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
+
931
1073
  ### `git_stash_apply` — parameters
932
1074
 
933
1075
  | Parameter | Type | Default | Notes |
934
1076
  |-----------|------|---------|-------|
935
- | `index` | int | `0` | Stash index to apply/pop (`stash@{index}`). |
1077
+ | `index` | int | `0` | Stash index to apply/pop (`stash@{index}`). Max `10000`. |
936
1078
  | `pop` | boolean | `false` | When `true`, runs `git stash pop` instead of `git stash apply`. |
937
1079
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
938
1080
 
939
1081
  ### `git_stash_apply` — JSON shape (`format: "json"`)
940
1082
 
1083
+ Success (`applied: true`, no `error`):
1084
+
941
1085
  ```json
942
1086
  {
943
1087
  "applied": true,
@@ -947,31 +1091,76 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
947
1091
  }
948
1092
  ```
949
1093
 
950
- `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.
951
1108
 
952
1109
  ### `git_stash_apply` — error codes
953
1110
 
954
1111
  | Code | Meaning |
955
1112
  |------|---------|
956
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`). |
957
1115
 
958
1116
  ---
959
1117
 
960
- ### `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:
961
1131
 
962
1132
  ```json
963
- { "worktrees": [{ "path": "/abs/path", "branch": "feature/x", "head": "a1b2c3d…" }] }
1133
+ {
1134
+ "stashed": true,
1135
+ "ref": "stash@{0}",
1136
+ "sha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
1137
+ "message": "On main: wip: pushed"
1138
+ }
964
1139
  ```
965
1140
 
966
- `branch` is `null` for detached-HEAD worktrees.
1141
+ Nothing to stash (git exits 0 printing "No local changes to save"):
1142
+
1143
+ ```json
1144
+ { "stashed": false, "reason": "no_local_changes" }
1145
+ ```
1146
+
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
+ ---
967
1156
 
968
1157
  ### `git_worktree_add` — parameters
969
1158
 
970
1159
  | Parameter | Type | Notes |
971
1160
  |-----------|------|-------|
972
- | `path` | string | Filesystem path for the new worktree. Relative paths are resolved from the git toplevel. |
973
- | `branch` | string | Branch to check out. Created from `baseRef` if it does not already exist. |
974
- | `baseRef` | string | Commit-ish for branch creation. Default: `HEAD`. Ignored when `branch` already exists. |
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. |
975
1164
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
976
1165
 
977
1166
  ### `git_worktree_add` — JSON shape (`format: "json"`)
@@ -986,6 +1175,7 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
986
1175
  |------|---------|
987
1176
  | `unsafe_ref_token` | `branch` or `baseRef` contains disallowed characters. |
988
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. |
989
1179
  | `worktree_add_failed` | `git worktree add` exited non-zero. `detail` carries stderr. |
990
1180
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
991
1181
 
@@ -993,16 +1183,23 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
993
1183
 
994
1184
  | Parameter | Type | Notes |
995
1185
  |-----------|------|-------|
996
- | `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. |
997
1187
  | `force` | boolean | Default `false`. Pass `--force` to allow removal with uncommitted changes. |
998
1188
  | `workspaceRoot`, `format` | — | Standard single-repo pick + output format. |
999
1189
 
1190
+ ### `git_worktree_remove` — JSON shape (`format: "json"`)
1191
+
1192
+ ```json
1193
+ { "ok": true, "path": "/abs/worktree" }
1194
+ ```
1195
+
1000
1196
  ### `git_worktree_remove` — error codes
1001
1197
 
1002
1198
  | Code | Meaning |
1003
1199
  |------|---------|
1004
1200
  | `cannot_remove_main_worktree` | `path` resolves to the main (non-linked) worktree. |
1005
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. |
1006
1203
  | `worktree_remove_failed` | `git worktree remove` failed. Pass `force: true` if there are uncommitted changes. |
1007
1204
  | `not_a_git_repository` | The resolved workspace root is not inside a git repository. |
1008
1205
 
@@ -1012,9 +1209,9 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1012
1209
 
1013
1210
  | Variable | Default | Notes |
1014
1211
  |----------|---------|-------|
1015
- | `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`). |
1016
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). |
1017
- | `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`. |
1018
1215
 
1019
1216
  ## Resource
1020
1217
 
@@ -1022,20 +1219,30 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1022
1219
  |-----|---------|
1023
1220
  | `rethunk-git://presets` | JSON snapshot of `.rethunk/git-mcp-presets.json` at the resolved git toplevel (or structured errors). |
1024
1221
 
1222
+ Error payloads from this resource may include:
1223
+
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
+
1025
1232
  ## Root resolution
1026
1233
 
1027
1234
  Every tool carries exactly **one** routing parameter:
1028
1235
 
1029
1236
  | Tools | Parameter | Accepts |
1030
1237
  |-------|-----------|---------|
1031
- | Fan-out: `git_status`, `git_inventory`, `git_parity`, `list_presets`, `git_log` | `root` | string (one repo path) \| string[] (explicit repo list) \| `"*"` (every MCP root) |
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) |
1032
1239
  | All other 18 tools | `workspaceRoot` | string (one repo path) |
1033
1240
 
1034
1241
  ### `root` forms (fan-out tools)
1035
1242
 
1036
1243
  - **string** — resolve that one path (same semantics as `workspaceRoot`).
1037
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`).
1038
- - **`"*"`** — every `file://` root reported by the MCP client; markdown output emits one `# {tool}` header with per-root subsections (`git_inventory` uses `### {gitTop}`; `git_status` uses `### MCP root: ...`), or combined JSON.
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.
1039
1246
 
1040
1247
  Example — two sibling repos in one `git_status` call:
1041
1248