@rethunk/mcp-multi-root-git 2.8.1 → 2.9.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
@@ -23,7 +23,7 @@ IDEs injecting this as context: do not re-link from rules.
23
23
  | [`src/server/schemas.ts`](src/server/schemas.ts) | `WorkspacePickSchema`, `MAX_INVENTORY_ROOTS_DEFAULT`, **`MAX_ABSOLUTE_GIT_ROOTS`** (256), optional **`absoluteGitRoots`** on workspace pick |
24
24
  | [`src/server/inventory.ts`](src/server/inventory.ts) | `InventoryEntryJson`, `validateRepoPath`, `makeSkipEntry`, `buildInventorySectionMarkdown`, `collectInventoryEntry` |
25
25
  | [`src/server/git-refs.ts`](src/server/git-refs.ts) | `isProtectedBranch`, `isSafeGitRefToken`, `isSafeGitRangeToken`, `isSafeGitAncestorRef`; `getCurrentBranch`, `resolveRef`, `isWorkingTreeClean`, `isFullyMergedInto`, `isContentEquivalentlyMergedInto`, `commitListBetween`; `listWorktrees`, `worktreeForBranch`; `inferRemoteFromUpstream`; `conflictPaths` |
26
- | [`src/server/tools.ts`](src/server/tools.ts) | `registerRethunkGitTools` — dispatches to `register*` below |
26
+ | [`src/server/tools.ts`](src/server/tools.ts) | `registerRethunkGitTools` — dispatches to `register*` below; `selectToolRegistrars(envValue, registrars)` — pure parse/filter fn for `RETHUNK_GIT_TOOLS` (reads env inside `registerRethunkGitTools`, not at module scope); `TOOL_REGISTRARS` ordered array |
27
27
  | [`src/server/git-status-tool.ts`](src/server/git-status-tool.ts) | `git_status` |
28
28
  | [`src/server/git-inventory-tool.ts`](src/server/git-inventory-tool.ts) | `git_inventory` |
29
29
  | [`src/server/git-parity-tool.ts`](src/server/git-parity-tool.ts) | `git_parity` |
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  All notable changes to `@rethunk/mcp-multi-root-git` are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com); the project uses [Semantic Versioning](https://semver.org).
4
4
 
5
+ ## [2.9.1] — 2026-06-11
6
+
7
+ Patch release: security hardening, schema refresh, and developer-tooling correctness. No JSON-format change.
8
+
9
+ ### Security
10
+
11
+ - **Insecure temp-file creation eliminated.** `scripts/publish-preflight.ts` and `src/server/git-merge-tool.test.ts` now use `fs.mkdtemp` instead of predictable paths (2 high `js/insecure-temporary-file` alerts resolved).
12
+ - **TOCTOU race in `.gitmodules` parsing fixed.** `parseGitSubmodulePaths` in `git.ts` previously called `existsSync` then `lstatSync` separately, creating a file-system race window. The redundant `existsSync` check is removed; `lstatSync` + `readFileSync` is now wrapped in a single `try/catch` (1 high `js/file-system-race` alert resolved).
13
+ - **GitHub Actions workflow permissions hardened.** `ci.yml` and `release.yml` gain `permissions: contents: read` blocks at workflow and job level. All third-party action tags are pinned to full commit SHAs (5 medium workflow-permission alerts resolved).
14
+
15
+ ### Changed
16
+
17
+ - **Schema artifacts regenerated.** `schemas/*.json` (23 files) were stale relative to tool-description changes in the 2.9.0 token-trim and allowlist commits; regenerated so `bun run schema:individual:check` passes in CI.
18
+
19
+ ### Fixed
20
+
21
+ - **Test files are now type-checked.** `tsconfig.json` includes `**/*.test.ts` with `@types/bun`, and a new `tsconfig.build.json` keeps tests out of the published `dist`. This surfaced and fixed latent type errors in the `tools`, `git-merge`, `list-presets`, and `tool-parameter-schemas` tests that the prior build never caught.
22
+ - **`coverage:check` is robust to ANSI color.** `parseAllFilesLineCoverage` strips ANSI escape codes before matching the summary rows, so coverage parsing no longer fails when `bun test --coverage` emits color (e.g. under `FORCE_COLOR`).
23
+
24
+ ## [2.9.0] — 2026-06-05
25
+
26
+ Feature release: `RETHUNK_GIT_TOOLS` allowlist env var. Additive — no JSON format-version bump.
27
+
28
+ ### Added
29
+
30
+ - **`RETHUNK_GIT_TOOLS`** — comma-separated allowlist of exact tool names. When set to a non-empty value, only the listed tools are registered; unknown names are warned to stderr and ignored. When unset or empty (default), all 23 tools are registered — zero behavioral change for existing consumers. If every name in the list is unrecognized, zero tools are registered and a loud warning is emitted (the restriction is honored literally rather than falling back to all tools). The presets resource (`rethunk-git://presets`) is always registered regardless of this setting. Cuts MCP token cost for agents that only need a small subset of tools. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`.
31
+
32
+ ## [2.8.1] — 2026-06-05
33
+
34
+ Patch release: token-cost reduction. No behavior or JSON-format change.
35
+
36
+ ### Changed
37
+
38
+ - Tightened verbose tool parameter descriptions (shared `WorkspacePickSchema` plus several per-tool schemas) to cut the serialized MCP tool payload (~7% of the real `tools/list` size). Descriptions only — no parameter, type, enum, or default changes.
39
+
5
40
  ## [2.8.0] — 2026-05-29
6
41
 
7
42
  Feature release: three new read-only inspection tools (tool count 20 → 23). Additive — no JSON format-version bump.
@@ -252,6 +287,13 @@ Mutating git operations: merge, cherry-pick, and optional push-after for `batch_
252
287
 
253
288
  - Initial release: `git_status`, `git_inventory`, `git_parity`, `list_presets`.
254
289
 
290
+ [2.9.1]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.9.1
291
+ [2.9.0]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.9.0
292
+ [2.8.1]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.8.1
293
+ [2.8.0]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.8.0
294
+ [2.7.0]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.7.0
295
+ [2.6.0]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.6.0
296
+ [2.5.0]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.5.0
255
297
  [2.4.0]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.4.0
256
298
  [2.3.4]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.3.4
257
299
  [2.3.3]: https://github.com/Rethunk-AI/mcp-multi-root-git/releases/tag/v2.3.3
@@ -1,7 +1,11 @@
1
1
  export function parseAllFilesLineCoverage(output) {
2
+ // Strip ANSI color codes (bun emits them when FORCE_COLOR is set, which would
3
+ // otherwise prefix the "All files"/"File" rows and defeat the matchers).
4
+ // Built via fromCharCode to keep a literal control character out of the source.
5
+ const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
2
6
  const rows = output
3
7
  .split(/\r?\n/)
4
- .map((line) => line.trim())
8
+ .map((line) => line.replace(ansi, "").trim())
5
9
  .filter((line) => line.includes("|"));
6
10
  const header = rows.find((line) => /^File\s*\|/i.test(line));
7
11
  if (!header)
@@ -95,19 +95,13 @@ export function gitRevParseHead(cwd) {
95
95
  }
96
96
  export function parseGitSubmodulePaths(gitRoot) {
97
97
  const f = join(gitRoot, ".gitmodules");
98
- if (!existsSync(f))
99
- return [];
100
98
  // Skip non-regular files (character devices, sockets, etc.) — common in
101
99
  // Claude Code sandbox environments where stub device files shadow paths.
100
+ // Use a single try/catch to avoid TOCTOU between existence check and open.
101
+ let text;
102
102
  try {
103
103
  if (!lstatSync(f).isFile())
104
104
  return [];
105
- }
106
- catch {
107
- return [];
108
- }
109
- let text;
110
- try {
111
105
  text = readFileSync(f, "utf8");
112
106
  }
113
107
  catch {
@@ -19,32 +19,73 @@ import { registerGitTagTool } from "./git-tag-tool.js";
19
19
  import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
20
20
  import { registerListPresetsTool } from "./list-presets-tool.js";
21
21
  import { registerPresetsResource } from "./presets-resource.js";
22
- export function registerRethunkGitTools(server) {
22
+ /**
23
+ * Ordered registry of all 23 MCP tools. Registration order is preserved for
24
+ * both full and filtered (RETHUNK_GIT_TOOLS) subsets.
25
+ */
26
+ const TOOL_REGISTRARS = [
23
27
  // Read-only tools
24
- registerGitStatusTool(server);
25
- registerGitInventoryTool(server);
26
- registerGitParityTool(server);
27
- registerListPresetsTool(server);
28
- registerGitLogTool(server);
29
- registerGitDiffSummaryTool(server);
30
- registerGitDiffTool(server);
31
- registerGitShowTool(server);
32
- registerGitWorktreeListTool(server);
33
- registerGitStashListTool(server);
34
- registerGitFetchTool(server);
35
- registerGitBlameTool(server);
36
- registerGitBranchListTool(server);
37
- registerGitReflogTool(server);
28
+ { name: "git_status", register: registerGitStatusTool },
29
+ { name: "git_inventory", register: registerGitInventoryTool },
30
+ { name: "git_parity", register: registerGitParityTool },
31
+ { name: "list_presets", register: registerListPresetsTool },
32
+ { name: "git_log", register: registerGitLogTool },
33
+ { name: "git_diff_summary", register: registerGitDiffSummaryTool },
34
+ { name: "git_diff", register: registerGitDiffTool },
35
+ { name: "git_show", register: registerGitShowTool },
36
+ { name: "git_worktree_list", register: registerGitWorktreeListTool },
37
+ { name: "git_stash_list", register: registerGitStashListTool },
38
+ { name: "git_fetch", register: registerGitFetchTool },
39
+ { name: "git_blame", register: registerGitBlameTool },
40
+ { name: "git_branch_list", register: registerGitBranchListTool },
41
+ { name: "git_reflog", register: registerGitReflogTool },
38
42
  // Mutating tools
39
- registerBatchCommitTool(server);
40
- registerGitPushTool(server);
41
- registerGitMergeTool(server);
42
- registerGitCherryPickTool(server);
43
- registerGitResetSoftTool(server);
44
- registerGitTagTool(server);
45
- registerGitWorktreeAddTool(server);
46
- registerGitWorktreeRemoveTool(server);
47
- registerGitStashApplyTool(server);
48
- // Resources
43
+ { name: "batch_commit", register: registerBatchCommitTool },
44
+ { name: "git_push", register: registerGitPushTool },
45
+ { name: "git_merge", register: registerGitMergeTool },
46
+ { name: "git_cherry_pick", register: registerGitCherryPickTool },
47
+ { name: "git_reset_soft", register: registerGitResetSoftTool },
48
+ { name: "git_tag", register: registerGitTagTool },
49
+ { name: "git_worktree_add", register: registerGitWorktreeAddTool },
50
+ { name: "git_worktree_remove", register: registerGitWorktreeRemoveTool },
51
+ { name: "git_stash_apply", register: registerGitStashApplyTool },
52
+ ];
53
+ /**
54
+ * Parse the RETHUNK_GIT_TOOLS env var and return the matching subset of
55
+ * registrars plus any unrecognized token names.
56
+ *
57
+ * @param envValue Raw value of process.env.RETHUNK_GIT_TOOLS (may be undefined).
58
+ * @param registrars Full ordered registrar list (injectable for tests).
59
+ */
60
+ export function selectToolRegistrars(envValue, registrars) {
61
+ const tokens = (envValue ?? "")
62
+ .split(",")
63
+ .map((t) => t.trim())
64
+ .filter((t) => t.length > 0);
65
+ // Unset, empty, or whitespace-only → register all tools.
66
+ if (tokens.length === 0) {
67
+ return { selected: registrars, unknown: [] };
68
+ }
69
+ const knownNames = new Set(registrars.map((r) => r.name));
70
+ const requested = new Set(tokens);
71
+ const unknown = tokens.filter((t) => !knownNames.has(t));
72
+ // Preserve canonical order; deduplicate duplicate tokens automatically.
73
+ const selected = registrars.filter((r) => requested.has(r.name));
74
+ return { selected, unknown };
75
+ }
76
+ export function registerRethunkGitTools(server) {
77
+ const env = process.env.RETHUNK_GIT_TOOLS;
78
+ const { selected, unknown } = selectToolRegistrars(env, TOOL_REGISTRARS);
79
+ if (unknown.length > 0) {
80
+ process.stderr.write(`[rethunk-git] RETHUNK_GIT_TOOLS: unknown tool name(s) ignored: ${unknown.map((n) => JSON.stringify(n)).join(", ")}\n`);
81
+ }
82
+ if (selected.length === 0 && (env ?? "").trim().length > 0) {
83
+ process.stderr.write(`[rethunk-git] RETHUNK_GIT_TOOLS: every listed name was unrecognized — registering NO tools. ` +
84
+ `Set RETHUNK_GIT_TOOLS to a comma-separated list of valid tool names, or unset it to register all tools.\n`);
85
+ }
86
+ for (const { register } of selected) {
87
+ register(server);
88
+ }
89
+ // The presets RESOURCE is always registered, regardless of the tool allowlist.
49
90
  registerPresetsResource(server);
50
91
  }
package/docs/install.md CHANGED
@@ -92,6 +92,7 @@ Omit any `cwd` / `workingDirectory` field unless your client requires it for unr
92
92
  | Variable | Default | Purpose |
93
93
  |----------|---------|---------|
94
94
  | `GIT_SUBPROCESS_PARALLELISM` | `4` | Number of parallel git subprocesses for inventory operations and `git_status` submodule rows. Valid range: 1 to 2×CPU count (auto-clamped to prevent runaway spawning). Increase on high-core machines to accelerate large fleet scans; decrease if system resources are constrained. |
95
+ | `RETHUNK_GIT_TOOLS` | _(unset)_ | Comma-separated list of tool names to register. When unset or empty, all 23 tools are registered (default). When set, only the listed tools are exposed — unknown names are warned and ignored. If every name is unknown, **zero** tools are registered and a loud warning is emitted (the restriction is honored literally). The presets resource (`rethunk-git://presets`) is always registered regardless of this setting. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`. Full tool-name list: `git_status`, `git_inventory`, `git_parity`, `list_presets`, `git_log`, `git_diff_summary`, `git_diff`, `git_show`, `git_worktree_list`, `git_stash_list`, `git_fetch`, `git_blame`, `git_branch_list`, `git_reflog`, `batch_commit`, `git_push`, `git_merge`, `git_cherry_pick`, `git_reset_soft`, `git_tag`, `git_worktree_add`, `git_worktree_remove`, `git_stash_apply`. |
95
96
 
96
97
  Set these in the environment where the MCP client launches the server (e.g. in your shell, in the MCP client config as `env`, or in a startup script).
97
98
 
package/docs/mcp-tools.md CHANGED
@@ -1029,6 +1029,7 @@ For deletions, `type` is `"deleted"` and `sha` is an empty string.
1029
1029
  |----------|---------|-------|
1030
1030
  | `GIT_SUBPROCESS_PARALLELISM` | CPU-based | Max concurrent git subprocesses for multi-root fan-out (`git_inventory`, `git_parity`, multi-root `git_log`). |
1031
1031
  | `GIT_SUBPROCESS_TIMEOUT_MS` | `120000` | Per-subprocess timeout in ms; on expiry the child is killed (SIGTERM) and the call resolves as failed. Set `0` (or negative) to disable (unbounded). |
1032
+ | `RETHUNK_GIT_TOOLS` | _(unset — all)_ | Comma-separated allowlist of exact tool names. Unset or empty → all 23 tools registered. Non-empty → only the listed tools; unknown names warned to stderr. If every listed name is unknown, zero tools are registered (restriction honored literally). The presets resource is always available. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`. |
1032
1033
 
1033
1034
  ## Resource
1034
1035
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethunk/mcp-multi-root-git",
3
- "version": "2.8.1",
3
+ "version": "2.9.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,
@@ -30,7 +30,7 @@
30
30
  }
31
31
  },
32
32
  "scripts": {
33
- "build": "rimraf dist && tsc",
33
+ "build": "rimraf dist && tsc -p tsconfig.build.json",
34
34
  "coverage:check": "bun scripts/check-coverage.ts",
35
35
  "schema:tools": "bun scripts/generate-tool-parameters-schema.ts",
36
36
  "schema:tools:check": "bun scripts/generate-tool-parameters-schema.ts --check",
@@ -67,12 +67,13 @@
67
67
  "access": "public"
68
68
  },
69
69
  "dependencies": {
70
- "fastmcp": "^4.0.1",
70
+ "fastmcp": "^4.0.2",
71
71
  "zod": "^4.4.3"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@biomejs/biome": "^2.4.16",
75
- "@types/node": "^25.9.1",
75
+ "@types/bun": "^1.3.14",
76
+ "@types/node": "^25.9.2",
76
77
  "rimraf": "^6.1.3",
77
78
  "typescript": "^6.0.3"
78
79
  },
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -88,7 +88,7 @@
88
88
  }
89
89
  ]
90
90
  },
91
- "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."
91
+ "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."
92
92
  }
93
93
  },
94
94
  "required": [
@@ -97,11 +97,11 @@
97
97
  ],
98
98
  "additionalProperties": false
99
99
  },
100
- "description": "Commits to create, applied in order."
100
+ "description": "Ordered list of commits to create."
101
101
  },
102
102
  "push": {
103
103
  "default": "never",
104
- "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`.",
104
+ "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).",
105
105
  "type": "string",
106
106
  "enum": [
107
107
  "never",
@@ -110,7 +110,7 @@
110
110
  },
111
111
  "dryRun": {
112
112
  "default": false,
113
- "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.",
113
+ "description": "Stage files and return a preview without writing commits; unstages afterwards. Response is marked DRY RUN.",
114
114
  "type": "boolean"
115
115
  }
116
116
  },
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "includeRemotes": {
26
26
  "default": false,
27
- "description": "When true, also return remote-tracking branches from refs/remotes (excluding symbolic origin/HEAD).",
27
+ "description": "Include remote-tracking branches from refs/remotes (symbolic origin/HEAD excluded).",
28
28
  "type": "boolean"
29
29
  }
30
30
  },
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -35,7 +35,7 @@
35
35
  "type": "string",
36
36
  "minLength": 1
37
37
  },
38
- "description": "Sources to cherry-pick: SHA, `A..B` range, or branch name. Branch sources resolve to `onto..<branch>` (only commits missing from destination)."
38
+ "description": "Sources: SHA, `A..B` range, or branch name (resolves to `onto..<branch>`)."
39
39
  },
40
40
  "onto": {
41
41
  "description": "Destination branch. Defaults to the currently checked-out branch.",
@@ -43,17 +43,17 @@
43
43
  },
44
44
  "deleteMergedBranches": {
45
45
  "default": false,
46
- "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.",
46
+ "description": "Delete branch-kind sources locally after success. Protected names and remote refs unaffected.",
47
47
  "type": "boolean"
48
48
  },
49
49
  "deleteMergedWorktrees": {
50
50
  "default": false,
51
- "description": "After success, remove any local worktree attached to a branch-kind source (`git worktree remove`). Protected tails always skipped.",
51
+ "description": "Remove local worktrees on branch-kind sources after success. Protected tails skipped.",
52
52
  "type": "boolean"
53
53
  },
54
54
  "strictMergedRefEquality": {
55
55
  "default": false,
56
- "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.",
56
+ "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).",
57
57
  "type": "boolean"
58
58
  }
59
59
  },
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -23,19 +23,19 @@
23
23
  ]
24
24
  },
25
25
  "base": {
26
- "description": "Base ref (e.g. \"main\", \"HEAD~3\"). Required for range diffs. If omitted and `staged: false`, shows unstaged changes.",
26
+ "description": "Base ref (e.g. \"main\", \"HEAD~3\"). Omit for unstaged changes.",
27
27
  "type": "string"
28
28
  },
29
29
  "head": {
30
- "description": "Head ref (e.g. \"feature-branch\"). If omitted, defaults to HEAD. Only used if `base` is provided.",
30
+ "description": "Head ref (e.g. \"feature-branch\"). Defaults to HEAD. Used only when `base` is set.",
31
31
  "type": "string"
32
32
  },
33
33
  "path": {
34
- "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.",
34
+ "description": "Scope to a single file. Unioned with `paths` if both given.",
35
35
  "type": "string"
36
36
  },
37
37
  "paths": {
38
- "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.",
38
+ "description": "Scope to multiple files (must be within repo root). Unioned with `path` if both given.",
39
39
  "type": "array",
40
40
  "items": {
41
41
  "type": "string"
@@ -43,11 +43,11 @@
43
43
  },
44
44
  "staged": {
45
45
  "default": false,
46
- "description": "If true, show staged changes (git diff --staged). Ignored if `base` is provided.",
46
+ "description": "Show staged changes (`git diff --staged`). Ignored if `base` is set.",
47
47
  "type": "boolean"
48
48
  },
49
49
  "unified": {
50
- "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.",
50
+ "description": "Context lines around each change (`-U<n>`). Default: 3. Use 0 for no context.",
51
51
  "type": "integer",
52
52
  "minimum": 0,
53
53
  "maximum": 100
@@ -5,17 +5,17 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "absoluteGitRoots": {
18
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
18
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
19
19
  "maxItems": 256,
20
20
  "type": "array",
21
21
  "items": {
@@ -53,7 +53,7 @@
53
53
  "maximum": 500
54
54
  },
55
55
  "excludePatterns": {
56
- "description": "Glob patterns to exclude. Defaults to common noise: lock files, dist, vendor, etc.",
56
+ "description": "Glob patterns to exclude. Default: lock files, dist, vendor, etc.",
57
57
  "type": "array",
58
58
  "items": {
59
59
  "type": "string"
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,22 +5,22 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "absoluteGitRoots": {
23
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
23
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
24
24
  "maxItems": 256,
25
25
  "type": "array",
26
26
  "items": {
@@ -5,22 +5,22 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "absoluteGitRoots": {
23
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
23
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
24
24
  "maxItems": 256,
25
25
  "type": "array",
26
26
  "items": {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "format": {
31
31
  "default": "markdown",
32
- "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.",
32
+ "description": "`markdown` (default): headed sections per root. `json`: groups array. `oneline`: `<sha7> <subject>` per line; lowest-token option for post-commit verification.",
33
33
  "type": "string",
34
34
  "enum": [
35
35
  "markdown",
@@ -38,27 +38,27 @@
38
38
  ]
39
39
  },
40
40
  "since": {
41
- "description": "Passed to `git log --since=`. Accepts ISO timestamps or git relative forms like `48.hours` or `2.weeks.ago`. Default: `7.days`.",
41
+ "description": "Passed to `--since=`. ISO timestamp or git relative form (`48.hours`, `2.weeks.ago`). Default: `7.days`.",
42
42
  "type": "string"
43
43
  },
44
44
  "paths": {
45
- "description": "Limit to commits touching these paths (passed as `-- <paths>`).",
45
+ "description": "Limit to commits touching these paths (`-- <paths>`).",
46
46
  "type": "array",
47
47
  "items": {
48
48
  "type": "string"
49
49
  }
50
50
  },
51
51
  "grep": {
52
- "description": "Filter commits whose message matches this regex (git `--grep`, case-insensitive).",
52
+ "description": "Filter by commit message regex (git `--grep`, case-insensitive).",
53
53
  "type": "string"
54
54
  },
55
55
  "author": {
56
- "description": "Filter by author name or email (passed as `--author=`).",
56
+ "description": "Filter by author name or email (`--author=`).",
57
57
  "type": "string"
58
58
  },
59
59
  "maxCommits": {
60
60
  "default": 50,
61
- "description": "Maximum commits to return per root (hard cap 500). Default 50.",
61
+ "description": "Max commits per root (hard cap 500, default 50).",
62
62
  "type": "integer",
63
63
  "minimum": 1,
64
64
  "maximum": 500
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -58,12 +58,12 @@
58
58
  },
59
59
  "deleteMergedBranches": {
60
60
  "default": false,
61
- "description": "After all sources merge cleanly, delete each source branch locally (`git branch -d`). Protected names always skipped. Never affects remote branches.",
61
+ "description": "Delete each source branch locally after clean merge (`git branch -d`). Protected names and remote refs unaffected.",
62
62
  "type": "boolean"
63
63
  },
64
64
  "deleteMergedWorktrees": {
65
65
  "default": false,
66
- "description": "After all sources merge cleanly, remove any local worktree currently checked out on a source branch (`git worktree remove`). Protected tails always skipped.",
66
+ "description": "Remove local worktrees on source branches after clean merge (`git worktree remove`). Protected tails skipped.",
67
67
  "type": "boolean"
68
68
  }
69
69
  },
@@ -5,22 +5,22 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "absoluteGitRoots": {
23
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
23
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
24
24
  "maxItems": 256,
25
25
  "type": "array",
26
26
  "items": {
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -30,7 +30,7 @@
30
30
  "ref": {
31
31
  "type": "string",
32
32
  "minLength": 1,
33
- "description": "Commit to reset to. Accepts ancestor notation (`HEAD~1`, `HEAD~3`), branch names, or full SHAs."
33
+ "description": "Commit to reset to: ancestor notation (`HEAD~1`, `HEAD~3`), branch name, or SHA."
34
34
  }
35
35
  },
36
36
  "required": [
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "pop": {
33
33
  "default": false,
34
- "description": "If true, runs `git stash pop` instead of `git stash apply` (removes stash after applying).",
34
+ "description": "Run `git stash pop` instead of `git stash apply` (removes stash after applying).",
35
35
  "type": "boolean"
36
36
  }
37
37
  },
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,22 +5,22 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "absoluteGitRoots": {
23
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
23
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
24
24
  "maxItems": 256,
25
25
  "type": "array",
26
26
  "items": {
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -30,15 +30,15 @@
30
30
  "path": {
31
31
  "type": "string",
32
32
  "minLength": 1,
33
- "description": "Filesystem path for the new worktree. Relative paths are resolved from the git toplevel."
33
+ "description": "Filesystem path for the new worktree (relative paths resolved from git toplevel)."
34
34
  },
35
35
  "branch": {
36
36
  "type": "string",
37
37
  "minLength": 1,
38
- "description": "Branch to check out in the new worktree. Created from `baseRef` if absent."
38
+ "description": "Branch to check out; created from `baseRef` if it doesn't exist."
39
39
  },
40
40
  "baseRef": {
41
- "description": "Commit-ish to base the new branch on when it does not yet exist. Default: HEAD.",
41
+ "description": "Commit-ish to base the new branch on. Default: HEAD.",
42
42
  "type": "string"
43
43
  }
44
44
  },
@@ -5,11 +5,11 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
@@ -5,18 +5,18 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "format": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "force": {
36
36
  "default": false,
37
- "description": "Pass `--force` to allow removal of worktrees with uncommitted changes.",
37
+ "description": "Allow removal of worktrees with uncommitted changes (`--force`).",
38
38
  "type": "boolean"
39
39
  }
40
40
  },
@@ -5,22 +5,22 @@
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "workspaceRoot": {
8
- "description": "Highest-priority override.",
8
+ "description": "Highest-priority root override.",
9
9
  "type": "string"
10
10
  },
11
11
  "rootIndex": {
12
- "description": "0-based index into the MCP file roots list; ignored when workspaceRoot is set.",
12
+ "description": "0-based index into MCP roots; ignored if workspaceRoot set.",
13
13
  "type": "integer",
14
14
  "minimum": 0,
15
15
  "maximum": 9007199254740991
16
16
  },
17
17
  "allWorkspaceRoots": {
18
18
  "default": false,
19
- "description": "Fan out across all MCP file roots.",
19
+ "description": "Fan out across all MCP roots.",
20
20
  "type": "boolean"
21
21
  },
22
22
  "absoluteGitRoots": {
23
- "description": "Absolute paths to git repo roots. Use for many sibling clones under a non-git parent directory.",
23
+ "description": "Absolute paths to git repo roots; use for sibling clones under a non-git parent.",
24
24
  "maxItems": 256,
25
25
  "type": "array",
26
26
  "items": {