@rethunk/mcp-multi-root-git 2.8.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -54,7 +54,7 @@ IDEs injecting this as context: do not re-link from rules.
54
54
 
55
55
  ## Validate + CI
56
56
 
57
- Local: `bun run build` | `bun run check` | `bun run schema:tools:check` | `bun run test`. CI ([`ci.yml`](.github/workflows/ci.yml)) runs same on PRs + `main` after `bun install --frozen-lockfile`, then `bun run test:coverage` + `bun run coverage:check`, and uploads prerelease `npm pack` artifact. Tag `v*.*.*` matching `package.json` version → [`release.yml`](.github/workflows/release.yml) publishes to GitHub Packages as `@rethunk-ai/mcp-multi-root-git` + cuts GitHub Release. npmjs publish is manual (see [HUMANS.md](HUMANS.md)).
57
+ Local: `bun run build` | `bun run lint` | `bun run schema:tools:check` | `bun run test`. CI ([`ci.yml`](.github/workflows/ci.yml)) runs same on PRs + `main` after `bun install --frozen-lockfile`, then `bun run test:coverage` + `bun run coverage:check`, and uploads prerelease `npm pack` artifact. Tag `v*.*.*` matching `package.json` version → [`release.yml`](.github/workflows/release.yml) publishes to GitHub Packages as `@rethunk-ai/mcp-multi-root-git` + cuts GitHub Release. npmjs publish is manual (see [HUMANS.md](HUMANS.md)).
58
58
 
59
59
  Optional [`.githooks/`](.githooks): `bun run setup-hooks` once per clone. pre-commit=`check`; pre-push=frozen install + build + check + test.
60
60
 
package/HUMANS.md CHANGED
@@ -205,7 +205,7 @@ It verifies the `package.json` version has a matching `CHANGELOG.md` section, bo
205
205
 
206
206
  npmjs no longer fits an unattended CI publish flow for this org; **do not** rely on automation to [npmjs](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git). To publish the **same** package name consumers already use (**`@rethunk/mcp-multi-root-git`**):
207
207
 
208
- 1. On a clean checkout at the release commit (usually **`main`** after bumping version), run **`bun run prepublishOnly`** (or `bun run build && bun run check && bun run test`).
208
+ 1. On a clean checkout at the release commit (usually **`main`** after bumping version), run **`bun run prepublishOnly`** (or `bun run build && bun run lint && bun run test`).
209
209
  2. Log in to the public registry once per machine: **`npm login`** (or `npm adduser`) so **`npm whoami`** shows the account that owns **`@rethunk`** on npmjs.
210
210
  3. Ensure **`package.json`** still has **`"name": "@rethunk/mcp-multi-root-git"`** and **`publishConfig.access`** is **`"public"`** (no **`publishConfig.registry`** pointing at GitHub — leave default registry for npmjs).
211
211
  4. Publish: **`npm publish --access public`** (runs **`prepublishOnly`** again unless you pass **`--ignore-scripts`** after you already verified locally).
@@ -24,23 +24,20 @@ const CommitEntrySchema = z.object({
24
24
  files: z
25
25
  .array(FileEntrySchema)
26
26
  .min(1)
27
- .describe("Paths to stage, relative to the git root. Each can be a string path or { path, lines } for hunk-level staging. " +
28
- "Deleted files (missing on disk but tracked in HEAD) are staged as removals via `git rm --cached`. " +
29
- "Combining { path, lines } with a deleted file is an error."),
27
+ .describe("Paths to stage, relative to git root. String or `{ path, lines }` for hunk-level staging. " +
28
+ "Deleted tracked files are staged via `git rm --cached`. Cannot combine `lines` with a deleted file."),
30
29
  });
31
30
  const PushModeSchema = z
32
31
  .enum(["never", "after"])
33
32
  .optional()
34
33
  .default("never")
35
- .describe("`never` (default): no push. `after`: push the current branch to its upstream once all commits succeed; " +
36
- "fails with `push_no_upstream` if the branch has no upstream (commits are NOT rolled back). " +
37
- "Enum reserved for future modes such as `force-with-lease`.");
34
+ .describe("`never` (default): no push. `after`: push current branch to upstream after all commits succeed; " +
35
+ "fails with `push_no_upstream` if no upstream (commits are NOT rolled back).");
38
36
  const DryRunSchema = z
39
37
  .boolean()
40
38
  .optional()
41
39
  .default(false)
42
- .describe("When true, stage files, collect preview (files staged, commit messages), return preview response without writing commits. " +
43
- "Unstages any files that were staged for the preview. Response indicates DRY RUN mode.");
40
+ .describe("Stage files and return a preview without writing commits; unstages afterwards. Response is marked DRY RUN.");
44
41
  /**
45
42
  * Parses a unified diff to extract hunks that overlap with a given line range.
46
43
  * Returns a partial patch containing only the overlapping hunks, including header lines.
@@ -220,11 +217,9 @@ export async function runPushAfter(gitTop) {
220
217
  export function registerBatchCommitTool(server) {
221
218
  server.addTool({
222
219
  name: "batch_commit",
223
- description: "Create multiple sequential git commits in a single call. " +
224
- "Each entry stages the listed files then commits with the given message. " +
225
- 'Stops on first failure. Optional `push: "after"` pushes the current branch ' +
226
- "to its upstream once all commits succeed. " +
227
- "Optional `dryRun: true` previews what would be staged/committed without writing commits.",
220
+ description: "Create multiple sequential git commits in one call. " +
221
+ "Each entry stages its files then commits. Stops on first failure. " +
222
+ 'Optional `push: "after"` pushes after all commits succeed. `dryRun: true` previews without writing.',
228
223
  annotations: {
229
224
  readOnlyHint: false,
230
225
  destructiveHint: false,
@@ -235,7 +230,7 @@ export function registerBatchCommitTool(server) {
235
230
  .array(CommitEntrySchema)
236
231
  .min(1)
237
232
  .max(50)
238
- .describe("Commits to create, applied in order."),
233
+ .describe("Ordered list of commits to create."),
239
234
  push: PushModeSchema,
240
235
  dryRun: DryRunSchema,
241
236
  }),
@@ -100,9 +100,7 @@ function renderBranchListMarkdown(result) {
100
100
  export function registerGitBranchListTool(server) {
101
101
  server.addTool({
102
102
  name: "git_branch_list",
103
- description: "List local branches (and optionally remote-tracking branches) for a single git repository. " +
104
- "Returns `{ branches: [{ name, sha, current, upstream? }], remotes?: [{ name, sha }] }`. " +
105
- "The current branch is marked with `current: true`. Remote branches are included when `includeRemotes: true`.",
103
+ description: "List local (and optionally remote-tracking) branches. Current branch marked `current: true`. Set `includeRemotes: true` for remotes.",
106
104
  annotations: {
107
105
  readOnlyHint: true,
108
106
  },
@@ -117,7 +115,7 @@ export function registerGitBranchListTool(server) {
117
115
  .boolean()
118
116
  .optional()
119
117
  .default(false)
120
- .describe("When true, also return remote-tracking branches from refs/remotes (excluding symbolic origin/HEAD)."),
118
+ .describe("Include remote-tracking branches from refs/remotes (symbolic origin/HEAD excluded)."),
121
119
  }),
122
120
  execute: async (args) => {
123
121
  const pre = requireSingleRepo(server, args);
@@ -94,14 +94,11 @@ async function filterAndDedupe(gitTop, onto, resolved) {
94
94
  export function registerGitCherryPickTool(server) {
95
95
  server.addTool({
96
96
  name: "git_cherry_pick",
97
- description: "Play commits from one or more sources onto a destination. Sources may be SHAs, " +
98
- "`A..B` ranges, or branch names (expanded to `onto..<branch>`, oldest-first). " +
99
- "Commits already reachable from the destination are skipped. Refuses on dirty tree; " +
100
- "stops on the first conflict and reports paths. Optional flags auto-delete source " +
101
- "branches and worktrees after success; deletion uses patch-id equivalence by default " +
102
- "(content-identical commits with different SHAs are treated as merged, which is the " +
103
- "normal cherry-pick outcome). Pass `strictMergedRefEquality: true` for strict `git branch -d` " +
104
- "ancestry semantics. Protected branch names always skipped.",
97
+ description: "Cherry-pick commits from one or more sources onto a destination. Sources: SHAs, `A..B` ranges, " +
98
+ "or branch names (expanded to `onto..<branch>`, oldest-first). Already-reachable commits skipped. " +
99
+ "Refuses on dirty tree; stops on first conflict. Optional flags delete source branches/worktrees " +
100
+ "after success using patch-id equivalence (set `strictMergedRefEquality: true` for strict ancestry). " +
101
+ "Protected names always skipped.",
105
102
  annotations: {
106
103
  readOnlyHint: false,
107
104
  destructiveHint: false,
@@ -112,8 +109,7 @@ export function registerGitCherryPickTool(server) {
112
109
  .array(z.string().min(1))
113
110
  .min(1)
114
111
  .max(50)
115
- .describe("Sources to cherry-pick: SHA, `A..B` range, or branch name. Branch sources " +
116
- "resolve to `onto..<branch>` (only commits missing from destination)."),
112
+ .describe("Sources: SHA, `A..B` range, or branch name (resolves to `onto..<branch>`)."),
117
113
  onto: z
118
114
  .string()
119
115
  .optional()
@@ -122,24 +118,18 @@ export function registerGitCherryPickTool(server) {
122
118
  .boolean()
123
119
  .optional()
124
120
  .default(false)
125
- .describe("After all commits apply, delete each branch-kind source locally " +
126
- "(`git branch -d`) when it is fully merged into the destination. " +
127
- "Protected names always skipped; never touches remote refs."),
121
+ .describe("Delete branch-kind sources locally after success. Protected names and remote refs unaffected."),
128
122
  deleteMergedWorktrees: z
129
123
  .boolean()
130
124
  .optional()
131
125
  .default(false)
132
- .describe("After success, remove any local worktree attached to a branch-kind source " +
133
- "(`git worktree remove`). Protected tails always skipped."),
126
+ .describe("Remove local worktrees on branch-kind sources after success. Protected tails skipped."),
134
127
  strictMergedRefEquality: z
135
128
  .boolean()
136
129
  .optional()
137
130
  .default(false)
138
- .describe("When false (default), branch deletion uses patch-id equivalence: a source branch " +
139
- "is deleted when every commit it contains has a content-equivalent commit on the " +
140
- "destination (same diff, different SHA — the normal cherry-pick outcome). " +
141
- "Set to true to require strict ref ancestry (`git branch -d` semantics), which " +
142
- "will refuse deletion after a cherry-pick because the SHA differs."),
131
+ .describe("false (default): delete branch when every commit is content-equivalent on destination (patch-id, normal cherry-pick outcome). " +
132
+ "true: require strict ref ancestry (`git branch -d` semantics — will refuse after cherry-pick due to SHA mismatch)."),
143
133
  }),
144
134
  execute: async (args) => {
145
135
  const pre = requireSingleRepo(server, args);
@@ -218,7 +208,7 @@ export function registerGitCherryPickTool(server) {
218
208
  if (allOk) {
219
209
  for (let i = 0; i < perSourceReport.length; i++) {
220
210
  const src = perSourceReport[i];
221
- if (!src || src.kind !== "branch")
211
+ if (src?.kind !== "branch")
222
212
  continue;
223
213
  if (isProtectedBranch(src.raw))
224
214
  continue;
@@ -179,9 +179,8 @@ function rangeLabel(range, diffArgs) {
179
179
  export function registerGitDiffSummaryTool(server) {
180
180
  server.addTool({
181
181
  name: "git_diff_summary",
182
- description: "Structured, token-efficient diff viewer. Returns per-file diffs with additions/deletions, " +
183
- "truncated to configurable line limits, with noise files (lock files, dist, etc.) excluded by default. " +
184
- "Use `range` to target staged, HEAD, or any revision range.",
182
+ description: "Structured diff viewer: per-file diffs with counts, truncated to configurable limits. " +
183
+ "Noise files (lock files, dist, etc.) excluded by default. Use `range` to target staged, HEAD, or a revision range.",
185
184
  annotations: {
186
185
  readOnlyHint: true,
187
186
  },
@@ -214,7 +213,7 @@ export function registerGitDiffSummaryTool(server) {
214
213
  excludePatterns: z
215
214
  .array(z.string())
216
215
  .optional()
217
- .describe("Glob patterns to exclude. Defaults to common noise: lock files, dist, vendor, etc."),
216
+ .describe("Glob patterns to exclude. Default: lock files, dist, vendor, etc."),
218
217
  }),
219
218
  execute: async (args) => {
220
219
  const pre = requireSingleRepo(server, args);
@@ -61,10 +61,8 @@ function rangeLabel(opts) {
61
61
  export function registerGitDiffTool(server) {
62
62
  server.addTool({
63
63
  name: "git_diff",
64
- description: "Get diff text for scoped file(s) or range. Returns the raw diff output. " +
65
- "Use `staged: true` for staged changes, `base`/`head` for revision ranges, " +
66
- "`path` to scope to a single file, `paths` to scope to multiple files, " +
67
- "and `unified` to control the number of context lines.",
64
+ description: "Raw diff text for scoped file(s) or range. `staged: true` for staged changes, " +
65
+ "`base`/`head` for revision ranges, `path`/`paths` to scope, `unified` for context lines.",
68
66
  annotations: {
69
67
  readOnlyHint: true,
70
68
  },
@@ -75,37 +73,31 @@ export function registerGitDiffTool(server) {
75
73
  base: z
76
74
  .string()
77
75
  .optional()
78
- .describe('Base ref (e.g. "main", "HEAD~3"). Required for range diffs. ' +
79
- "If omitted and `staged: false`, shows unstaged changes."),
76
+ .describe('Base ref (e.g. "main", "HEAD~3"). Omit for unstaged changes.'),
80
77
  head: z
81
78
  .string()
82
79
  .optional()
83
- .describe('Head ref (e.g. "feature-branch"). If omitted, defaults to HEAD. ' +
84
- "Only used if `base` is provided."),
80
+ .describe('Head ref (e.g. "feature-branch"). Defaults to HEAD. Used only when `base` is set.'),
85
81
  path: z
86
82
  .string()
87
83
  .optional()
88
- .describe('Scope diff to a single file path (e.g. "src/main.ts"). ' +
89
- "For multiple files, prefer `paths`. If both `path` and `paths` are given, they are unioned."),
84
+ .describe("Scope to a single file. Unioned with `paths` if both given."),
90
85
  paths: z
91
86
  .array(z.string())
92
87
  .optional()
93
- .describe('Scope diff to multiple file paths (e.g. ["src/a.ts", "src/b.ts"]). ' +
94
- "Each path is validated and must lie within the repository root. " +
95
- "If both `path` and `paths` are given, they are unioned."),
88
+ .describe("Scope to multiple files (must be within repo root). Unioned with `path` if both given."),
96
89
  staged: z
97
90
  .boolean()
98
91
  .optional()
99
92
  .default(false)
100
- .describe("If true, show staged changes (git diff --staged). " + "Ignored if `base` is provided."),
93
+ .describe("Show staged changes (`git diff --staged`). Ignored if `base` is set."),
101
94
  unified: z
102
95
  .number()
103
96
  .int()
104
97
  .min(0)
105
98
  .max(100)
106
99
  .optional()
107
- .describe("Number of context lines to show around each change (passed as -U<n> to git diff). " +
108
- "Defaults to git's built-in default (3). Use 0 for no context."),
100
+ .describe("Context lines around each change (`-U<n>`). Default: 3. Use 0 for no context."),
109
101
  }),
110
102
  execute: async (args) => {
111
103
  const pre = requireSingleRepo(server, args);
@@ -176,8 +176,7 @@ function renderLogMarkdown(group, filterSummary) {
176
176
  export function registerGitLogTool(server) {
177
177
  server.addTool({
178
178
  name: "git_log",
179
- description: "Path-filtered, time-windowed read-only `git log` across one or more workspace roots. " +
180
- "Returns structured commit history with author, date, subject, and optional diff stats.",
179
+ description: "Read-only `git log` across one or more roots. Returns author, date, subject, optional diff stats.",
181
180
  annotations: {
182
181
  readOnlyHint: true,
183
182
  },
@@ -186,26 +185,21 @@ export function registerGitLogTool(server) {
186
185
  .enum(["markdown", "json", "oneline"])
187
186
  .optional()
188
187
  .default("markdown")
189
- .describe("`markdown` (default): headed sections per root. " +
190
- "`json`: structured groups array. " +
191
- "`oneline`: `<sha7> <subject>` per line, no headers (single-root) or `### repo (branch)` separator per group (multi-root). Lowest-token option for post-commit verification."),
188
+ .describe("`markdown` (default): headed sections per root. `json`: groups array. " +
189
+ "`oneline`: `<sha7> <subject>` per line; lowest-token option for post-commit verification."),
192
190
  since: z
193
191
  .string()
194
192
  .optional()
195
- .describe("Passed to `git log --since=`. Accepts ISO timestamps or git relative forms like " +
196
- "`48.hours` or `2.weeks.ago`. Default: `7.days`."),
193
+ .describe("Passed to `--since=`. ISO timestamp or git relative form (`48.hours`, `2.weeks.ago`). Default: `7.days`."),
197
194
  paths: z
198
195
  .array(z.string())
199
196
  .optional()
200
- .describe("Limit to commits touching these paths (passed as `-- <paths>`)."),
197
+ .describe("Limit to commits touching these paths (`-- <paths>`)."),
201
198
  grep: z
202
199
  .string()
203
200
  .optional()
204
- .describe("Filter commits whose message matches this regex (git `--grep`, case-insensitive)."),
205
- author: z
206
- .string()
207
- .optional()
208
- .describe("Filter by author name or email (passed as `--author=`)."),
201
+ .describe("Filter by commit message regex (git `--grep`, case-insensitive)."),
202
+ author: z.string().optional().describe("Filter by author name or email (`--author=`)."),
209
203
  maxCommits: z
210
204
  .number()
211
205
  .int()
@@ -213,7 +207,7 @@ export function registerGitLogTool(server) {
213
207
  .max(MAX_COMMITS_HARD_CAP)
214
208
  .optional()
215
209
  .default(DEFAULT_MAX_COMMITS)
216
- .describe(`Maximum commits to return per root (hard cap ${MAX_COMMITS_HARD_CAP}). Default ${DEFAULT_MAX_COMMITS}.`),
210
+ .describe(`Max commits per root (hard cap ${MAX_COMMITS_HARD_CAP}, default ${DEFAULT_MAX_COMMITS}).`),
217
211
  branch: z.string().optional().describe("Ref/branch to log from. Default: HEAD."),
218
212
  }),
219
213
  execute: async (args) => {
@@ -207,12 +207,10 @@ async function maybeDeleteBranch(gitTop, source, enabled, into) {
207
207
  export function registerGitMergeTool(server) {
208
208
  server.addTool({
209
209
  name: "git_merge",
210
- description: "Merge one or more source branches into a destination. Default strategy `auto` " +
211
- "cascades fast-forward → rebase → merge-commit per source, preferring linear history. " +
212
- "Refuses if the working tree is dirty. Stops on the first conflict and reports " +
213
- "the affected paths. Optional flags auto-delete merged branches and worktrees, " +
214
- "skipping protected names (main, master, dev, develop, stable, trunk, prod, " +
215
- "production, release/*, hotfix/*).",
210
+ description: "Merge one or more source branches into a destination. `auto` cascades " +
211
+ "fast-forward → rebase → merge-commit, preferring linear history. " +
212
+ "Refuses on dirty tree; stops on first conflict. Optional flags delete merged branches/worktrees " +
213
+ "(protected names skipped: main, master, dev, develop, stable, trunk, prod, production, release/*, hotfix/*).",
216
214
  annotations: {
217
215
  readOnlyHint: false,
218
216
  destructiveHint: false,
@@ -237,14 +235,12 @@ export function registerGitMergeTool(server) {
237
235
  .boolean()
238
236
  .optional()
239
237
  .default(false)
240
- .describe("After all sources merge cleanly, delete each source branch locally (`git branch -d`). " +
241
- "Protected names always skipped. Never affects remote branches."),
238
+ .describe("Delete each source branch locally after clean merge (`git branch -d`). Protected names and remote refs unaffected."),
242
239
  deleteMergedWorktrees: z
243
240
  .boolean()
244
241
  .optional()
245
242
  .default(false)
246
- .describe("After all sources merge cleanly, remove any local worktree currently checked out " +
247
- "on a source branch (`git worktree remove`). Protected tails always skipped."),
243
+ .describe("Remove local worktrees on source branches after clean merge (`git worktree remove`). Protected tails skipped."),
248
244
  }),
249
245
  execute: async (args) => {
250
246
  const pre = requireSingleRepo(server, args);
@@ -8,10 +8,8 @@ import { WorkspacePickSchema } from "./schemas.js";
8
8
  export function registerGitResetSoftTool(server) {
9
9
  server.addTool({
10
10
  name: "git_reset_soft",
11
- description: "Soft-reset the current branch to a reference (`git reset --soft <ref>`). " +
12
- "Moves the branch pointer back while keeping all changes from the rewound commits " +
13
- "in the staging index — use this to re-split an already-committed chunk. " +
14
- "Refuses when the working tree has any uncommitted or unstaged changes (run on a clean tree).",
11
+ description: "`git reset --soft <ref>`: moves HEAD back while keeping rewound changes staged. " +
12
+ "Use to re-split committed work. Refuses on a dirty tree.",
15
13
  annotations: {
16
14
  readOnlyHint: false,
17
15
  destructiveHint: false,
@@ -21,8 +19,7 @@ export function registerGitResetSoftTool(server) {
21
19
  ref: z
22
20
  .string()
23
21
  .min(1)
24
- .describe("Commit to reset to. Accepts ancestor notation (`HEAD~1`, `HEAD~3`), " +
25
- "branch names, or full SHAs."),
22
+ .describe("Commit to reset to: ancestor notation (`HEAD~1`, `HEAD~3`), branch name, or SHA."),
26
23
  }),
27
24
  execute: async (args) => {
28
25
  const pre = requireSingleRepo(server, args);
@@ -10,7 +10,7 @@ import { WorkspacePickSchema } from "./schemas.js";
10
10
  export function registerGitStashListTool(server) {
11
11
  server.addTool({
12
12
  name: "git_stash_list",
13
- description: "List all git stashes. Returns array of `{ index: number, message: string, sha: string }`.",
13
+ description: "List all git stashes.",
14
14
  annotations: {
15
15
  readOnlyHint: true,
16
16
  },
@@ -77,9 +77,7 @@ export function registerGitStashListTool(server) {
77
77
  export function registerGitStashApplyTool(server) {
78
78
  server.addTool({
79
79
  name: "git_stash_apply",
80
- description: "Apply or pop a git stash. `index` defaults to 0 (stash@{0}). " +
81
- "Set `pop: true` to run `git stash pop` instead of `git stash apply` (removes stash after applying). " +
82
- "Returns `{ applied: boolean, stashIndex: number, popped: boolean, output: string }`.",
80
+ description: "Apply or pop a git stash. `index` defaults to 0. `pop: true` removes stash after applying.",
83
81
  annotations: {
84
82
  readOnlyHint: false,
85
83
  destructiveHint: false,
@@ -103,7 +101,7 @@ export function registerGitStashApplyTool(server) {
103
101
  .boolean()
104
102
  .optional()
105
103
  .default(false)
106
- .describe("If true, runs `git stash pop` instead of `git stash apply` (removes stash after applying)."),
104
+ .describe("Run `git stash pop` instead of `git stash apply` (removes stash after applying)."),
107
105
  }),
108
106
  execute: async (args) => {
109
107
  const pre = requireSingleRepo(server, args);
@@ -49,8 +49,7 @@ export function registerGitWorktreeListTool(server) {
49
49
  export function registerGitWorktreeAddTool(server) {
50
50
  server.addTool({
51
51
  name: "git_worktree_add",
52
- description: "Add a new git worktree. If `branch` does not exist it is created from `baseRef` " +
53
- "(defaults to HEAD). Refuses to create on a protected branch name.",
52
+ description: "Add a new git worktree. Creates `branch` from `baseRef` (default: HEAD) if it doesn't exist. Refuses protected branch names.",
54
53
  annotations: {
55
54
  readOnlyHint: false,
56
55
  destructiveHint: false,
@@ -60,15 +59,15 @@ export function registerGitWorktreeAddTool(server) {
60
59
  path: z
61
60
  .string()
62
61
  .min(1)
63
- .describe("Filesystem path for the new worktree. Relative paths are resolved from the git toplevel."),
62
+ .describe("Filesystem path for the new worktree (relative paths resolved from git toplevel)."),
64
63
  branch: z
65
64
  .string()
66
65
  .min(1)
67
- .describe("Branch to check out in the new worktree. Created from `baseRef` if absent."),
66
+ .describe("Branch to check out; created from `baseRef` if it doesn't exist."),
68
67
  baseRef: z
69
68
  .string()
70
69
  .optional()
71
- .describe("Commit-ish to base the new branch on when it does not yet exist. Default: HEAD."),
70
+ .describe("Commit-ish to base the new branch on. Default: HEAD."),
72
71
  }),
73
72
  execute: async (args) => {
74
73
  const pre = requireSingleRepo(server, args);
@@ -132,9 +131,7 @@ export function registerGitWorktreeAddTool(server) {
132
131
  export function registerGitWorktreeRemoveTool(server) {
133
132
  server.addTool({
134
133
  name: "git_worktree_remove",
135
- description: "Remove a git worktree (`git worktree remove`). " +
136
- "Pass `force: true` to remove a worktree with uncommitted changes. " +
137
- "Refuses to remove the main worktree.",
134
+ description: "Remove a git worktree. Pass `force: true` to remove with uncommitted changes. Refuses to remove the main worktree.",
138
135
  annotations: {
139
136
  readOnlyHint: false,
140
137
  destructiveHint: true,
@@ -149,7 +146,7 @@ export function registerGitWorktreeRemoveTool(server) {
149
146
  .boolean()
150
147
  .optional()
151
148
  .default(false)
152
- .describe("Pass `--force` to allow removal of worktrees with uncommitted changes."),
149
+ .describe("Allow removal of worktrees with uncommitted changes (`--force`)."),
153
150
  }),
154
151
  execute: async (args) => {
155
152
  const pre = requireSingleRepo(server, args);
@@ -4,24 +4,24 @@ const FormatSchema = z.enum(["markdown", "json"]).optional().default("markdown")
4
4
  /** Max paths in `absoluteGitRoots` (matches `git_inventory` `maxRoots` hard cap). */
5
5
  export const MAX_ABSOLUTE_GIT_ROOTS = 256;
6
6
  export const WorkspacePickSchema = z.object({
7
- workspaceRoot: z.string().optional().describe("Highest-priority override."),
7
+ workspaceRoot: z.string().optional().describe("Highest-priority root override."),
8
8
  rootIndex: z
9
9
  .number()
10
10
  .int()
11
11
  .min(0)
12
12
  .optional()
13
- .describe("0-based index into the MCP file roots list; ignored when workspaceRoot is set."),
13
+ .describe("0-based index into MCP roots; ignored if workspaceRoot set."),
14
14
  allWorkspaceRoots: z
15
15
  .boolean()
16
16
  .optional()
17
17
  .default(false)
18
- .describe("Fan out across all MCP file roots."),
18
+ .describe("Fan out across all MCP roots."),
19
19
  /** Independent git worktrees (sibling clones). Mutually exclusive with workspaceRoot, rootIndex, allWorkspaceRoots, and (git_inventory) preset/nestedRoots. */
20
20
  absoluteGitRoots: z
21
21
  .array(z.string())
22
22
  .max(MAX_ABSOLUTE_GIT_ROOTS)
23
23
  .optional()
24
- .describe("Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory."),
24
+ .describe("Absolute paths to git repo roots; use for sibling clones under a non-git parent."),
25
25
  format: FormatSchema,
26
26
  });
27
27
  export { MAX_INVENTORY_ROOTS_DEFAULT };
package/docs/mcp-tools.md CHANGED
@@ -666,7 +666,7 @@ Do NOT do this: make two separate calls hoping to stage files incrementally. Tha
666
666
  | Parameter | Type | Notes |
667
667
  |-----------|------|-------|
668
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. |
669
- | `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. Enum reserved for future modes such as `"force-with-lease"`. |
669
+ | `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
670
  | `dryRun` | boolean | Default `false`. When `true`, stages each entry, reports what would be committed (`staged`, `diffStat`), then unstages everything without writing commits. |
671
671
  | `workspaceRoot` | string | Explicit root; highest priority. |
672
672
  | `rootIndex` | int | Pick one of several MCP roots (0-based). |
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@rethunk/mcp-multi-root-git",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "MCP stdio server: multi-root git status, inventory, and HEAD parity checks. Generic tools usable by any workspace.",
5
5
  "type": "module",
6
6
  "private": false,
7
- "packageManager": "bun@1.3.13",
7
+ "packageManager": "bun@1.3.14",
8
8
  "engines": {
9
9
  "node": ">=22.0.0"
10
10
  },
@@ -31,8 +31,6 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "rimraf dist && tsc",
34
- "check": "biome check .",
35
- "check:fix": "biome check --write .",
36
34
  "coverage:check": "bun scripts/check-coverage.ts",
37
35
  "schema:tools": "bun scripts/generate-tool-parameters-schema.ts",
38
36
  "schema:tools:check": "bun scripts/generate-tool-parameters-schema.ts --check",
@@ -41,8 +39,12 @@
41
39
  "publish:preflight": "bun scripts/publish-preflight.ts",
42
40
  "test": "bun test src/",
43
41
  "test:coverage": "bun test src/ --coverage",
44
- "prepublishOnly": "bun run schema:tools:check && bun run schema:individual && bun run build && bun run check && bun run test",
45
- "setup-hooks": "git config core.hooksPath .githooks"
42
+ "prepublishOnly": "bun run ci",
43
+ "setup-hooks": "git config core.hooksPath .githooks",
44
+ "lint": "biome check .",
45
+ "format": "biome check --write .",
46
+ "typecheck": "tsc --noEmit",
47
+ "ci": "bun run schema:tools:check && bun run lint && bun run typecheck && bun run test && bun run build"
46
48
  },
47
49
  "keywords": [
48
50
  "mcp",
@@ -77,11 +79,6 @@
77
79
  "overrides": {
78
80
  "@hono/node-server": "^1.19.14",
79
81
  "hono": "^4.12.18",
80
- "fast-uri": "^3.1.2",
81
- "postcss": "^8.5.10",
82
- "vite": "^7.3.2",
83
- "ip-address": "^10.1.1",
84
- "axios": "^1.15.2",
85
- "follow-redirects": "^1.15.12"
82
+ "fast-uri": "^3.1.2"
86
83
  }
87
84
  }
@@ -9,22 +9,22 @@
9
9
  "type": "object",
10
10
  "properties": {
11
11
  "workspaceRoot": {
12
- "description": "Highest-priority override.",
12
+ "description": "Highest-priority root override.",
13
13
  "type": "string"
14
14
  },
15
15
  "rootIndex": {
16
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
16
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
17
17
  "type": "integer",
18
18
  "minimum": 0,
19
19
  "maximum": 9007199254740991
20
20
  },
21
21
  "allWorkspaceRoots": {
22
22
  "default": false,
23
- "description": "Fan out across all MCP file roots.",
23
+ "description": "Fan out across all MCP roots.",
24
24
  "type": "boolean"
25
25
  },
26
26
  "absoluteGitRoots": {
27
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
27
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
28
28
  "maxItems": 256,
29
29
  "type": "array",
30
30
  "items": {
@@ -56,22 +56,22 @@
56
56
  "type": "object",
57
57
  "properties": {
58
58
  "workspaceRoot": {
59
- "description": "Highest-priority override.",
59
+ "description": "Highest-priority root override.",
60
60
  "type": "string"
61
61
  },
62
62
  "rootIndex": {
63
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
63
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
64
64
  "type": "integer",
65
65
  "minimum": 0,
66
66
  "maximum": 9007199254740991
67
67
  },
68
68
  "allWorkspaceRoots": {
69
69
  "default": false,
70
- "description": "Fan out across all MCP file roots.",
70
+ "description": "Fan out across all MCP roots.",
71
71
  "type": "boolean"
72
72
  },
73
73
  "absoluteGitRoots": {
74
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
74
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
75
75
  "maxItems": 256,
76
76
  "type": "array",
77
77
  "items": {
@@ -128,22 +128,22 @@
128
128
  "type": "object",
129
129
  "properties": {
130
130
  "workspaceRoot": {
131
- "description": "Highest-priority override.",
131
+ "description": "Highest-priority root override.",
132
132
  "type": "string"
133
133
  },
134
134
  "rootIndex": {
135
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
135
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
136
136
  "type": "integer",
137
137
  "minimum": 0,
138
138
  "maximum": 9007199254740991
139
139
  },
140
140
  "allWorkspaceRoots": {
141
141
  "default": false,
142
- "description": "Fan out across all MCP file roots.",
142
+ "description": "Fan out across all MCP roots.",
143
143
  "type": "boolean"
144
144
  },
145
145
  "absoluteGitRoots": {
146
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
146
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
147
147
  "maxItems": 256,
148
148
  "type": "array",
149
149
  "items": {
@@ -200,22 +200,22 @@
200
200
  "type": "object",
201
201
  "properties": {
202
202
  "workspaceRoot": {
203
- "description": "Highest-priority override.",
203
+ "description": "Highest-priority root override.",
204
204
  "type": "string"
205
205
  },
206
206
  "rootIndex": {
207
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
207
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
208
208
  "type": "integer",
209
209
  "minimum": 0,
210
210
  "maximum": 9007199254740991
211
211
  },
212
212
  "allWorkspaceRoots": {
213
213
  "default": false,
214
- "description": "Fan out across all MCP file roots.",
214
+ "description": "Fan out across all MCP roots.",
215
215
  "type": "boolean"
216
216
  },
217
217
  "absoluteGitRoots": {
218
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
218
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
219
219
  "maxItems": 256,
220
220
  "type": "array",
221
221
  "items": {
@@ -242,22 +242,22 @@
242
242
  "type": "object",
243
243
  "properties": {
244
244
  "workspaceRoot": {
245
- "description": "Highest-priority override.",
245
+ "description": "Highest-priority root override.",
246
246
  "type": "string"
247
247
  },
248
248
  "rootIndex": {
249
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
249
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
250
250
  "type": "integer",
251
251
  "minimum": 0,
252
252
  "maximum": 9007199254740991
253
253
  },
254
254
  "allWorkspaceRoots": {
255
255
  "default": false,
256
- "description": "Fan out across all MCP file roots.",
256
+ "description": "Fan out across all MCP roots.",
257
257
  "type": "boolean"
258
258
  },
259
259
  "absoluteGitRoots": {
260
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
260
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
261
261
  "maxItems": 256,
262
262
  "type": "array",
263
263
  "items": {
@@ -266,7 +266,7 @@
266
266
  },
267
267
  "format": {
268
268
  "default": "markdown",
269
- "description": "`markdown` (default): headed sections per root. `json`: structured groups array. `oneline`: `<sha7> <subject>` per line, no headers (single-root) or `### repo (branch)` separator per group (multi-root). Lowest-token option for post-commit verification.",
269
+ "description": "`markdown` (default): headed sections per root. `json`: groups array. `oneline`: `<sha7> <subject>` per line; lowest-token option for post-commit verification.",
270
270
  "type": "string",
271
271
  "enum": [
272
272
  "markdown",
@@ -275,27 +275,27 @@
275
275
  ]
276
276
  },
277
277
  "since": {
278
- "description": "Passed to `git log --since=`. Accepts ISO timestamps or git relative forms like `48.hours` or `2.weeks.ago`. Default: `7.days`.",
278
+ "description": "Passed to `--since=`. ISO timestamp or git relative form (`48.hours`, `2.weeks.ago`). Default: `7.days`.",
279
279
  "type": "string"
280
280
  },
281
281
  "paths": {
282
- "description": "Limit to commits touching these paths (passed as `-- <paths>`).",
282
+ "description": "Limit to commits touching these paths (`-- <paths>`).",
283
283
  "type": "array",
284
284
  "items": {
285
285
  "type": "string"
286
286
  }
287
287
  },
288
288
  "grep": {
289
- "description": "Filter commits whose message matches this regex (git `--grep`, case-insensitive).",
289
+ "description": "Filter by commit message regex (git `--grep`, case-insensitive).",
290
290
  "type": "string"
291
291
  },
292
292
  "author": {
293
- "description": "Filter by author name or email (passed as `--author=`).",
293
+ "description": "Filter by author name or email (`--author=`).",
294
294
  "type": "string"
295
295
  },
296
296
  "maxCommits": {
297
297
  "default": 50,
298
- "description": "Maximum commits to return per root (hard cap 500). Default 50.",
298
+ "description": "Max commits per root (hard cap 500, default 50).",
299
299
  "type": "integer",
300
300
  "minimum": 1,
301
301
  "maximum": 500
@@ -317,17 +317,17 @@
317
317
  "type": "object",
318
318
  "properties": {
319
319
  "workspaceRoot": {
320
- "description": "Highest-priority override.",
320
+ "description": "Highest-priority root override.",
321
321
  "type": "string"
322
322
  },
323
323
  "rootIndex": {
324
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
324
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
325
325
  "type": "integer",
326
326
  "minimum": 0,
327
327
  "maximum": 9007199254740991
328
328
  },
329
329
  "absoluteGitRoots": {
330
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
330
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
331
331
  "maxItems": 256,
332
332
  "type": "array",
333
333
  "items": {
@@ -365,7 +365,7 @@
365
365
  "maximum": 500
366
366
  },
367
367
  "excludePatterns": {
368
- "description": "Glob patterns to exclude. Defaults to common noise: lock files, dist, vendor, etc.",
368
+ "description": "Glob patterns to exclude. Default: lock files, dist, vendor, etc.",
369
369
  "type": "array",
370
370
  "items": {
371
371
  "type": "string"
@@ -384,11 +384,11 @@
384
384
  "type": "object",
385
385
  "properties": {
386
386
  "workspaceRoot": {
387
- "description": "Highest-priority override.",
387
+ "description": "Highest-priority root override.",
388
388
  "type": "string"
389
389
  },
390
390
  "rootIndex": {
391
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
391
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
392
392
  "type": "integer",
393
393
  "minimum": 0,
394
394
  "maximum": 9007199254740991
@@ -402,19 +402,19 @@
402
402
  ]
403
403
  },
404
404
  "base": {
405
- "description": "Base ref (e.g. \"main\", \"HEAD~3\"). Required for range diffs. If omitted and `staged: false`, shows unstaged changes.",
405
+ "description": "Base ref (e.g. \"main\", \"HEAD~3\"). Omit for unstaged changes.",
406
406
  "type": "string"
407
407
  },
408
408
  "head": {
409
- "description": "Head ref (e.g. \"feature-branch\"). If omitted, defaults to HEAD. Only used if `base` is provided.",
409
+ "description": "Head ref (e.g. \"feature-branch\"). Defaults to HEAD. Used only when `base` is set.",
410
410
  "type": "string"
411
411
  },
412
412
  "path": {
413
- "description": "Scope diff to a single file path (e.g. \"src/main.ts\"). For multiple files, prefer `paths`. If both `path` and `paths` are given, they are unioned.",
413
+ "description": "Scope to a single file. Unioned with `paths` if both given.",
414
414
  "type": "string"
415
415
  },
416
416
  "paths": {
417
- "description": "Scope diff to multiple file paths (e.g. [\"src/a.ts\", \"src/b.ts\"]). Each path is validated and must lie within the repository root. If both `path` and `paths` are given, they are unioned.",
417
+ "description": "Scope to multiple files (must be within repo root). Unioned with `path` if both given.",
418
418
  "type": "array",
419
419
  "items": {
420
420
  "type": "string"
@@ -422,11 +422,11 @@
422
422
  },
423
423
  "staged": {
424
424
  "default": false,
425
- "description": "If true, show staged changes (git diff --staged). Ignored if `base` is provided.",
425
+ "description": "Show staged changes (`git diff --staged`). Ignored if `base` is set.",
426
426
  "type": "boolean"
427
427
  },
428
428
  "unified": {
429
- "description": "Number of context lines to show around each change (passed as -U<n> to git diff). Defaults to git's built-in default (3). Use 0 for no context.",
429
+ "description": "Context lines around each change (`-U<n>`). Default: 3. Use 0 for no context.",
430
430
  "type": "integer",
431
431
  "minimum": 0,
432
432
  "maximum": 100
@@ -443,11 +443,11 @@
443
443
  "type": "object",
444
444
  "properties": {
445
445
  "workspaceRoot": {
446
- "description": "Highest-priority override.",
446
+ "description": "Highest-priority root override.",
447
447
  "type": "string"
448
448
  },
449
449
  "rootIndex": {
450
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
450
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
451
451
  "type": "integer",
452
452
  "minimum": 0,
453
453
  "maximum": 9007199254740991
@@ -492,11 +492,11 @@
492
492
  "type": "object",
493
493
  "properties": {
494
494
  "workspaceRoot": {
495
- "description": "Highest-priority override.",
495
+ "description": "Highest-priority root override.",
496
496
  "type": "string"
497
497
  },
498
498
  "rootIndex": {
499
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
499
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
500
500
  "type": "integer",
501
501
  "minimum": 0,
502
502
  "maximum": 9007199254740991
@@ -520,11 +520,11 @@
520
520
  "type": "object",
521
521
  "properties": {
522
522
  "workspaceRoot": {
523
- "description": "Highest-priority override.",
523
+ "description": "Highest-priority root override.",
524
524
  "type": "string"
525
525
  },
526
526
  "rootIndex": {
527
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
527
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
528
528
  "type": "integer",
529
529
  "minimum": 0,
530
530
  "maximum": 9007199254740991
@@ -548,11 +548,11 @@
548
548
  "type": "object",
549
549
  "properties": {
550
550
  "workspaceRoot": {
551
- "description": "Highest-priority override.",
551
+ "description": "Highest-priority root override.",
552
552
  "type": "string"
553
553
  },
554
554
  "rootIndex": {
555
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
555
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
556
556
  "type": "integer",
557
557
  "minimum": 0,
558
558
  "maximum": 9007199254740991
@@ -598,11 +598,11 @@
598
598
  "type": "object",
599
599
  "properties": {
600
600
  "workspaceRoot": {
601
- "description": "Highest-priority override.",
601
+ "description": "Highest-priority root override.",
602
602
  "type": "string"
603
603
  },
604
604
  "rootIndex": {
605
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
605
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
606
606
  "type": "integer",
607
607
  "minimum": 0,
608
608
  "maximum": 9007199254740991
@@ -648,11 +648,11 @@
648
648
  "type": "object",
649
649
  "properties": {
650
650
  "workspaceRoot": {
651
- "description": "Highest-priority override.",
651
+ "description": "Highest-priority root override.",
652
652
  "type": "string"
653
653
  },
654
654
  "rootIndex": {
655
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
655
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
656
656
  "type": "integer",
657
657
  "minimum": 0,
658
658
  "maximum": 9007199254740991
@@ -667,7 +667,7 @@
667
667
  },
668
668
  "includeRemotes": {
669
669
  "default": false,
670
- "description": "When true, also return remote-tracking branches from refs/remotes (excluding symbolic origin/HEAD).",
670
+ "description": "Include remote-tracking branches from refs/remotes (symbolic origin/HEAD excluded).",
671
671
  "type": "boolean"
672
672
  }
673
673
  },
@@ -682,11 +682,11 @@
682
682
  "type": "object",
683
683
  "properties": {
684
684
  "workspaceRoot": {
685
- "description": "Highest-priority override.",
685
+ "description": "Highest-priority root override.",
686
686
  "type": "string"
687
687
  },
688
688
  "rootIndex": {
689
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
689
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
690
690
  "type": "integer",
691
691
  "minimum": 0,
692
692
  "maximum": 9007199254740991
@@ -724,18 +724,18 @@
724
724
  "type": "object",
725
725
  "properties": {
726
726
  "workspaceRoot": {
727
- "description": "Highest-priority override.",
727
+ "description": "Highest-priority root override.",
728
728
  "type": "string"
729
729
  },
730
730
  "rootIndex": {
731
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
731
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
732
732
  "type": "integer",
733
733
  "minimum": 0,
734
734
  "maximum": 9007199254740991
735
735
  },
736
736
  "allWorkspaceRoots": {
737
737
  "default": false,
738
- "description": "Fan out across all MCP file roots.",
738
+ "description": "Fan out across all MCP roots.",
739
739
  "type": "boolean"
740
740
  },
741
741
  "format": {
@@ -807,7 +807,7 @@
807
807
  }
808
808
  ]
809
809
  },
810
- "description": "Paths to stage, relative to the git root. Each can be a string path or { path, lines } for hunk-level staging. Deleted files (missing on disk but tracked in HEAD) are staged as removals via `git rm --cached`. Combining { path, lines } with a deleted file is an error."
810
+ "description": "Paths to stage, relative to git root. String or `{ path, lines }` for hunk-level staging. Deleted tracked files are staged via `git rm --cached`. Cannot combine `lines` with a deleted file."
811
811
  }
812
812
  },
813
813
  "required": [
@@ -816,11 +816,11 @@
816
816
  ],
817
817
  "additionalProperties": false
818
818
  },
819
- "description": "Commits to create, applied in order."
819
+ "description": "Ordered list of commits to create."
820
820
  },
821
821
  "push": {
822
822
  "default": "never",
823
- "description": "`never` (default): no push. `after`: push the current branch to its upstream once all commits succeed; fails with `push_no_upstream` if the branch has no upstream (commits are NOT rolled back). Enum reserved for future modes such as `force-with-lease`.",
823
+ "description": "`never` (default): no push. `after`: push current branch to upstream after all commits succeed; fails with `push_no_upstream` if no upstream (commits are NOT rolled back).",
824
824
  "type": "string",
825
825
  "enum": [
826
826
  "never",
@@ -829,7 +829,7 @@
829
829
  },
830
830
  "dryRun": {
831
831
  "default": false,
832
- "description": "When true, stage files, collect preview (files staged, commit messages), return preview response without writing commits. Unstages any files that were staged for the preview. Response indicates DRY RUN mode.",
832
+ "description": "Stage files and return a preview without writing commits; unstages afterwards. Response is marked DRY RUN.",
833
833
  "type": "boolean"
834
834
  }
835
835
  },
@@ -847,18 +847,18 @@
847
847
  "type": "object",
848
848
  "properties": {
849
849
  "workspaceRoot": {
850
- "description": "Highest-priority override.",
850
+ "description": "Highest-priority root override.",
851
851
  "type": "string"
852
852
  },
853
853
  "rootIndex": {
854
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
854
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
855
855
  "type": "integer",
856
856
  "minimum": 0,
857
857
  "maximum": 9007199254740991
858
858
  },
859
859
  "allWorkspaceRoots": {
860
860
  "default": false,
861
- "description": "Fan out across all MCP file roots.",
861
+ "description": "Fan out across all MCP roots.",
862
862
  "type": "boolean"
863
863
  },
864
864
  "format": {
@@ -895,18 +895,18 @@
895
895
  "type": "object",
896
896
  "properties": {
897
897
  "workspaceRoot": {
898
- "description": "Highest-priority override.",
898
+ "description": "Highest-priority root override.",
899
899
  "type": "string"
900
900
  },
901
901
  "rootIndex": {
902
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
902
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
903
903
  "type": "integer",
904
904
  "minimum": 0,
905
905
  "maximum": 9007199254740991
906
906
  },
907
907
  "allWorkspaceRoots": {
908
908
  "default": false,
909
- "description": "Fan out across all MCP file roots.",
909
+ "description": "Fan out across all MCP roots.",
910
910
  "type": "boolean"
911
911
  },
912
912
  "format": {
@@ -948,12 +948,12 @@
948
948
  },
949
949
  "deleteMergedBranches": {
950
950
  "default": false,
951
- "description": "After all sources merge cleanly, delete each source branch locally (`git branch -d`). Protected names always skipped. Never affects remote branches.",
951
+ "description": "Delete each source branch locally after clean merge (`git branch -d`). Protected names and remote refs unaffected.",
952
952
  "type": "boolean"
953
953
  },
954
954
  "deleteMergedWorktrees": {
955
955
  "default": false,
956
- "description": "After all sources merge cleanly, remove any local worktree currently checked out on a source branch (`git worktree remove`). Protected tails always skipped.",
956
+ "description": "Remove local worktrees on source branches after clean merge (`git worktree remove`). Protected tails skipped.",
957
957
  "type": "boolean"
958
958
  }
959
959
  },
@@ -972,18 +972,18 @@
972
972
  "type": "object",
973
973
  "properties": {
974
974
  "workspaceRoot": {
975
- "description": "Highest-priority override.",
975
+ "description": "Highest-priority root override.",
976
976
  "type": "string"
977
977
  },
978
978
  "rootIndex": {
979
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
979
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
980
980
  "type": "integer",
981
981
  "minimum": 0,
982
982
  "maximum": 9007199254740991
983
983
  },
984
984
  "allWorkspaceRoots": {
985
985
  "default": false,
986
- "description": "Fan out across all MCP file roots.",
986
+ "description": "Fan out across all MCP roots.",
987
987
  "type": "boolean"
988
988
  },
989
989
  "format": {
@@ -1002,7 +1002,7 @@
1002
1002
  "type": "string",
1003
1003
  "minLength": 1
1004
1004
  },
1005
- "description": "Sources to cherry-pick: SHA, `A..B` range, or branch name. Branch sources resolve to `onto..<branch>` (only commits missing from destination)."
1005
+ "description": "Sources: SHA, `A..B` range, or branch name (resolves to `onto..<branch>`)."
1006
1006
  },
1007
1007
  "onto": {
1008
1008
  "description": "Destination branch. Defaults to the currently checked-out branch.",
@@ -1010,17 +1010,17 @@
1010
1010
  },
1011
1011
  "deleteMergedBranches": {
1012
1012
  "default": false,
1013
- "description": "After all commits apply, delete each branch-kind source locally (`git branch -d`) when it is fully merged into the destination. Protected names always skipped; never touches remote refs.",
1013
+ "description": "Delete branch-kind sources locally after success. Protected names and remote refs unaffected.",
1014
1014
  "type": "boolean"
1015
1015
  },
1016
1016
  "deleteMergedWorktrees": {
1017
1017
  "default": false,
1018
- "description": "After success, remove any local worktree attached to a branch-kind source (`git worktree remove`). Protected tails always skipped.",
1018
+ "description": "Remove local worktrees on branch-kind sources after success. Protected tails skipped.",
1019
1019
  "type": "boolean"
1020
1020
  },
1021
1021
  "strictMergedRefEquality": {
1022
1022
  "default": false,
1023
- "description": "When false (default), branch deletion uses patch-id equivalence: a source branch is deleted when every commit it contains has a content-equivalent commit on the destination (same diff, different SHA — the normal cherry-pick outcome). Set to true to require strict ref ancestry (`git branch -d` semantics), which will refuse deletion after a cherry-pick because the SHA differs.",
1023
+ "description": "false (default): delete branch when every commit is content-equivalent on destination (patch-id, normal cherry-pick outcome). true: require strict ref ancestry (`git branch -d` semantics will refuse after cherry-pick due to SHA mismatch).",
1024
1024
  "type": "boolean"
1025
1025
  }
1026
1026
  },
@@ -1039,18 +1039,18 @@
1039
1039
  "type": "object",
1040
1040
  "properties": {
1041
1041
  "workspaceRoot": {
1042
- "description": "Highest-priority override.",
1042
+ "description": "Highest-priority root override.",
1043
1043
  "type": "string"
1044
1044
  },
1045
1045
  "rootIndex": {
1046
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
1046
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
1047
1047
  "type": "integer",
1048
1048
  "minimum": 0,
1049
1049
  "maximum": 9007199254740991
1050
1050
  },
1051
1051
  "allWorkspaceRoots": {
1052
1052
  "default": false,
1053
- "description": "Fan out across all MCP file roots.",
1053
+ "description": "Fan out across all MCP roots.",
1054
1054
  "type": "boolean"
1055
1055
  },
1056
1056
  "format": {
@@ -1064,7 +1064,7 @@
1064
1064
  "ref": {
1065
1065
  "type": "string",
1066
1066
  "minLength": 1,
1067
- "description": "Commit to reset to. Accepts ancestor notation (`HEAD~1`, `HEAD~3`), branch names, or full SHAs."
1067
+ "description": "Commit to reset to: ancestor notation (`HEAD~1`, `HEAD~3`), branch name, or SHA."
1068
1068
  }
1069
1069
  },
1070
1070
  "required": [
@@ -1079,11 +1079,11 @@
1079
1079
  "type": "object",
1080
1080
  "properties": {
1081
1081
  "workspaceRoot": {
1082
- "description": "Highest-priority override.",
1082
+ "description": "Highest-priority root override.",
1083
1083
  "type": "string"
1084
1084
  },
1085
1085
  "rootIndex": {
1086
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
1086
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
1087
1087
  "type": "integer",
1088
1088
  "minimum": 0,
1089
1089
  "maximum": 9007199254740991
@@ -1127,18 +1127,18 @@
1127
1127
  "type": "object",
1128
1128
  "properties": {
1129
1129
  "workspaceRoot": {
1130
- "description": "Highest-priority override.",
1130
+ "description": "Highest-priority root override.",
1131
1131
  "type": "string"
1132
1132
  },
1133
1133
  "rootIndex": {
1134
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
1134
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
1135
1135
  "type": "integer",
1136
1136
  "minimum": 0,
1137
1137
  "maximum": 9007199254740991
1138
1138
  },
1139
1139
  "allWorkspaceRoots": {
1140
1140
  "default": false,
1141
- "description": "Fan out across all MCP file roots.",
1141
+ "description": "Fan out across all MCP roots.",
1142
1142
  "type": "boolean"
1143
1143
  },
1144
1144
  "format": {
@@ -1152,15 +1152,15 @@
1152
1152
  "path": {
1153
1153
  "type": "string",
1154
1154
  "minLength": 1,
1155
- "description": "Filesystem path for the new worktree. Relative paths are resolved from the git toplevel."
1155
+ "description": "Filesystem path for the new worktree (relative paths resolved from git toplevel)."
1156
1156
  },
1157
1157
  "branch": {
1158
1158
  "type": "string",
1159
1159
  "minLength": 1,
1160
- "description": "Branch to check out in the new worktree. Created from `baseRef` if absent."
1160
+ "description": "Branch to check out; created from `baseRef` if it doesn't exist."
1161
1161
  },
1162
1162
  "baseRef": {
1163
- "description": "Commit-ish to base the new branch on when it does not yet exist. Default: HEAD.",
1163
+ "description": "Commit-ish to base the new branch on. Default: HEAD.",
1164
1164
  "type": "string"
1165
1165
  }
1166
1166
  },
@@ -1177,18 +1177,18 @@
1177
1177
  "type": "object",
1178
1178
  "properties": {
1179
1179
  "workspaceRoot": {
1180
- "description": "Highest-priority override.",
1180
+ "description": "Highest-priority root override.",
1181
1181
  "type": "string"
1182
1182
  },
1183
1183
  "rootIndex": {
1184
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
1184
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
1185
1185
  "type": "integer",
1186
1186
  "minimum": 0,
1187
1187
  "maximum": 9007199254740991
1188
1188
  },
1189
1189
  "allWorkspaceRoots": {
1190
1190
  "default": false,
1191
- "description": "Fan out across all MCP file roots.",
1191
+ "description": "Fan out across all MCP roots.",
1192
1192
  "type": "boolean"
1193
1193
  },
1194
1194
  "format": {
@@ -1206,7 +1206,7 @@
1206
1206
  },
1207
1207
  "force": {
1208
1208
  "default": false,
1209
- "description": "Pass `--force` to allow removal of worktrees with uncommitted changes.",
1209
+ "description": "Allow removal of worktrees with uncommitted changes (`--force`).",
1210
1210
  "type": "boolean"
1211
1211
  }
1212
1212
  },
@@ -1223,11 +1223,11 @@
1223
1223
  "type": "object",
1224
1224
  "properties": {
1225
1225
  "workspaceRoot": {
1226
- "description": "Highest-priority override.",
1226
+ "description": "Highest-priority root override.",
1227
1227
  "type": "string"
1228
1228
  },
1229
1229
  "rootIndex": {
1230
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
1230
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
1231
1231
  "type": "integer",
1232
1232
  "minimum": 0,
1233
1233
  "maximum": 9007199254740991
@@ -1249,7 +1249,7 @@
1249
1249
  },
1250
1250
  "pop": {
1251
1251
  "default": false,
1252
- "description": "If true, runs `git stash pop` instead of `git stash apply` (removes stash after applying).",
1252
+ "description": "Run `git stash pop` instead of `git stash apply` (removes stash after applying).",
1253
1253
  "type": "boolean"
1254
1254
  }
1255
1255
  },