@arii/boomtick-mcp 0.6.2 → 0.7.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.
Files changed (109) hide show
  1. package/.mcp/schemas/boomtick.health.json +22 -3
  2. package/.mcp/schemas/boomtick_health.json +22 -3
  3. package/.mcp/schemas/github.checkout_branch.json +16 -3
  4. package/.mcp/schemas/github.comment_triage_summary.json +16 -3
  5. package/.mcp/schemas/github.create_pull_request.json +16 -3
  6. package/.mcp/schemas/github.get_merge_conflict_files.json +55 -5
  7. package/.mcp/schemas/github.get_pr.json +12 -3
  8. package/.mcp/schemas/github.get_pr_commits.json +12 -3
  9. package/.mcp/schemas/github.get_pr_reviews.json +24 -3
  10. package/.mcp/schemas/github.issue_comment.json +19 -3
  11. package/.mcp/schemas/github.issue_view.json +17 -3
  12. package/.mcp/schemas/github.open_replacement_pr.json +16 -3
  13. package/.mcp/schemas/github_checkout_branch.json +16 -3
  14. package/.mcp/schemas/github_comment_triage_summary.json +16 -3
  15. package/.mcp/schemas/github_create_pull_request.json +16 -3
  16. package/.mcp/schemas/github_get_merge_conflict_files.json +55 -5
  17. package/.mcp/schemas/github_get_pr.json +12 -3
  18. package/.mcp/schemas/github_get_pr_commits.json +12 -3
  19. package/.mcp/schemas/github_get_pr_reviews.json +24 -3
  20. package/.mcp/schemas/github_issue_comment.json +19 -3
  21. package/.mcp/schemas/github_issue_view.json +17 -3
  22. package/.mcp/schemas/github_open_replacement_pr.json +16 -3
  23. package/.mcp/schemas/jules.cancel_session.json +38 -3
  24. package/.mcp/schemas/jules.create_session.json +52 -3
  25. package/.mcp/schemas/jules.get_messages.json +19 -3
  26. package/.mcp/schemas/jules.get_pr.json +23 -3
  27. package/.mcp/schemas/jules.get_session.json +38 -3
  28. package/.mcp/schemas/jules.list_sessions.json +53 -3
  29. package/.mcp/schemas/jules.send_message.json +31 -3
  30. package/.mcp/schemas/jules.trigger_feedback.json +28 -3
  31. package/.mcp/schemas/jules_cancel_session.json +38 -3
  32. package/.mcp/schemas/jules_create_session.json +52 -3
  33. package/.mcp/schemas/jules_get_messages.json +19 -3
  34. package/.mcp/schemas/jules_get_pr.json +23 -3
  35. package/.mcp/schemas/jules_get_session.json +38 -3
  36. package/.mcp/schemas/jules_list_sessions.json +53 -3
  37. package/.mcp/schemas/jules_send_message.json +31 -3
  38. package/.mcp/schemas/jules_trigger_feedback.json +28 -3
  39. package/.mcp/schemas/repo.check_jules_status.json +93 -3
  40. package/.mcp/schemas/repo.commit_patch.json +23 -3
  41. package/.mcp/schemas/repo.create_branch.json +91 -3
  42. package/.mcp/schemas/repo.create_repair_branch.json +94 -3
  43. package/.mcp/schemas/repo.get_changed_files.json +50 -3
  44. package/.mcp/schemas/repo.get_command_schema.json +29 -3
  45. package/.mcp/schemas/repo.get_package_scripts.json +11 -3
  46. package/.mcp/schemas/repo.get_workflow_runs.json +12 -3
  47. package/.mcp/schemas/repo.logs.json +14 -3
  48. package/.mcp/schemas/repo.multi_exec.json +89 -0
  49. package/.mcp/schemas/repo.read_agent_context.json +117 -5
  50. package/.mcp/schemas/repo.read_ci_logs.json +46 -3
  51. package/.mcp/schemas/repo_check_jules_status.json +93 -3
  52. package/.mcp/schemas/repo_commit_patch.json +23 -3
  53. package/.mcp/schemas/repo_create_branch.json +91 -3
  54. package/.mcp/schemas/repo_create_repair_branch.json +94 -3
  55. package/.mcp/schemas/repo_get_changed_files.json +50 -3
  56. package/.mcp/schemas/repo_get_command_schema.json +29 -3
  57. package/.mcp/schemas/repo_get_package_scripts.json +11 -3
  58. package/.mcp/schemas/repo_get_workflow_runs.json +12 -3
  59. package/.mcp/schemas/repo_logs.json +14 -3
  60. package/.mcp/schemas/repo_multi_exec.json +89 -0
  61. package/.mcp/schemas/repo_read_agent_context.json +117 -5
  62. package/.mcp/schemas/repo_read_ci_logs.json +46 -3
  63. package/CHANGELOG.md +22 -0
  64. package/README.md +7 -5
  65. package/dist/config/dynamic.js +3 -2
  66. package/dist/config/repo.js +5 -5
  67. package/dist/config.test.js +10 -3
  68. package/dist/lib/git.js +78 -6
  69. package/dist/lib/git.test.js +34 -1
  70. package/dist/mcp/definitions.js +49 -36
  71. package/dist/mcp/http.js +15 -0
  72. package/dist/mcp/http.test.js +8 -0
  73. package/dist/mcp/server.js +34 -0
  74. package/dist/mcp/server.test.js +31 -0
  75. package/dist/tools/contract.js +1264 -92
  76. package/dist/tools/github.get_merge_conflict_files.js +10 -2
  77. package/dist/tools/jules/create-session.js +12 -2
  78. package/dist/tools/jules/create-session.test.js +7 -0
  79. package/dist/tools/repo.create_branch.js +18 -4
  80. package/dist/tools/repo.create_branch.test.js +41 -5
  81. package/dist/tools/repo.create_repair_branch.js +26 -9
  82. package/dist/tools/repo.get_changed_files.js +10 -2
  83. package/dist/tools/repo.multi_exec.js +21 -0
  84. package/dist/tools/repo.multi_exec.test.js +13 -0
  85. package/dist/tools/repo.read_agent_context.js +65 -3
  86. package/dist/tools/repo.read_agent_context.test.js +30 -1
  87. package/package.json +1 -1
  88. package/src/config/dynamic.ts +3 -2
  89. package/src/config/repo.ts +6 -6
  90. package/src/config.test.ts +11 -3
  91. package/src/lib/git.test.ts +45 -1
  92. package/src/lib/git.ts +94 -6
  93. package/src/mcp/definitions.ts +77 -36
  94. package/src/mcp/http.test.ts +10 -0
  95. package/src/mcp/http.ts +20 -0
  96. package/src/mcp/server.test.ts +38 -0
  97. package/src/mcp/server.ts +36 -0
  98. package/src/tools/contract.ts +1355 -67
  99. package/src/tools/github.get_merge_conflict_files.ts +11 -2
  100. package/src/tools/jules/create-session.test.ts +9 -0
  101. package/src/tools/jules/create-session.ts +13 -2
  102. package/src/tools/repo.create_branch.test.ts +73 -5
  103. package/src/tools/repo.create_branch.ts +30 -4
  104. package/src/tools/repo.create_repair_branch.ts +31 -9
  105. package/src/tools/repo.get_changed_files.ts +10 -2
  106. package/src/tools/repo.multi_exec.test.ts +15 -0
  107. package/src/tools/repo.multi_exec.ts +23 -0
  108. package/src/tools/repo.read_agent_context.test.ts +35 -1
  109. package/src/tools/repo.read_agent_context.ts +68 -3
@@ -43,8 +43,19 @@
43
43
  "type": "object"
44
44
  },
45
45
  "outputSchema": {
46
- "type": "object",
47
- "properties": {},
48
- "additionalProperties": true
46
+ "properties": {
47
+ "logs": {
48
+ "anyOf": [
49
+ {},
50
+ {
51
+ "type": "null"
52
+ }
53
+ ],
54
+ "default": null,
55
+ "title": "Logs"
56
+ }
57
+ },
58
+ "title": "RepoLogsResponse",
59
+ "type": "object"
49
60
  }
50
61
  }
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "repo_multi_exec",
3
+ "description": "Execute a shell command sequentially across ecosystem repositories (or a filtered subset).",
4
+ "parameters": {
5
+ "properties": {
6
+ "command": {
7
+ "description": "Shell command to execute sequentially across repositories",
8
+ "title": "Command",
9
+ "type": "string"
10
+ },
11
+ "repos": {
12
+ "anyOf": [
13
+ {
14
+ "items": {
15
+ "type": "string"
16
+ },
17
+ "type": "array"
18
+ },
19
+ {
20
+ "type": "null"
21
+ }
22
+ ],
23
+ "default": null,
24
+ "description": "Optional list of repository names to target",
25
+ "title": "Repos"
26
+ },
27
+ "continueOnError": {
28
+ "anyOf": [
29
+ {
30
+ "type": "boolean"
31
+ },
32
+ {
33
+ "type": "null"
34
+ }
35
+ ],
36
+ "default": true,
37
+ "description": "Whether to continue execution if a repo command fails",
38
+ "title": "Continueonerror"
39
+ }
40
+ },
41
+ "required": [
42
+ "command"
43
+ ],
44
+ "title": "MultiExecInput",
45
+ "type": "object"
46
+ },
47
+ "outputSchema": {
48
+ "properties": {
49
+ "status": {
50
+ "default": "success",
51
+ "description": "Execution status",
52
+ "title": "Status",
53
+ "type": "string"
54
+ },
55
+ "total": {
56
+ "anyOf": [
57
+ {
58
+ "type": "integer"
59
+ },
60
+ {
61
+ "type": "null"
62
+ }
63
+ ],
64
+ "default": null,
65
+ "description": "Total repositories executed against",
66
+ "title": "Total"
67
+ },
68
+ "results": {
69
+ "anyOf": [
70
+ {
71
+ "items": {
72
+ "additionalProperties": true,
73
+ "type": "object"
74
+ },
75
+ "type": "array"
76
+ },
77
+ {
78
+ "type": "null"
79
+ }
80
+ ],
81
+ "default": null,
82
+ "description": "Execution results per repository",
83
+ "title": "Results"
84
+ }
85
+ },
86
+ "title": "MultiExecResponse",
87
+ "type": "object"
88
+ }
89
+ }
@@ -1,14 +1,126 @@
1
1
  {
2
2
  "name": "repo_read_agent_context",
3
- "description": "Read the full repository index including file tree, CLI schema, and MCP tools.",
3
+ "description": "Read the repository index including metadata, model review chains, or multi-repo ecosystem status.",
4
4
  "parameters": {
5
- "properties": {},
5
+ "properties": {
6
+ "path": {
7
+ "anyOf": [
8
+ {
9
+ "type": "string"
10
+ },
11
+ {
12
+ "type": "null"
13
+ }
14
+ ],
15
+ "default": null,
16
+ "description": "Local filesystem path to target repository root",
17
+ "title": "Path"
18
+ },
19
+ "repoPath": {
20
+ "anyOf": [
21
+ {
22
+ "type": "string"
23
+ },
24
+ {
25
+ "type": "null"
26
+ }
27
+ ],
28
+ "default": null,
29
+ "description": "Alias for path (filesystem path to target workspace repository root).",
30
+ "title": "Repopath"
31
+ },
32
+ "repo": {
33
+ "anyOf": [
34
+ {
35
+ "type": "string"
36
+ },
37
+ {
38
+ "type": "null"
39
+ }
40
+ ],
41
+ "default": null,
42
+ "description": "Target GitHub repository string (e.g. org/repo).",
43
+ "title": "Repo"
44
+ },
45
+ "repos": {
46
+ "anyOf": [
47
+ {
48
+ "items": {
49
+ "type": "string"
50
+ },
51
+ "type": "array"
52
+ },
53
+ {
54
+ "type": "null"
55
+ }
56
+ ],
57
+ "default": null,
58
+ "description": "List of repository names to fetch context for",
59
+ "title": "Repos"
60
+ },
61
+ "all": {
62
+ "anyOf": [
63
+ {
64
+ "type": "boolean"
65
+ },
66
+ {
67
+ "type": "null"
68
+ }
69
+ ],
70
+ "default": null,
71
+ "description": "Whether to fetch context across all discovered ecosystem repositories",
72
+ "title": "All"
73
+ }
74
+ },
6
75
  "title": "ReadAgentContextInput",
7
76
  "type": "object"
8
77
  },
9
78
  "outputSchema": {
10
- "type": "object",
11
- "properties": {},
12
- "additionalProperties": true
79
+ "properties": {
80
+ "status": {
81
+ "default": "success",
82
+ "title": "Status",
83
+ "type": "string"
84
+ },
85
+ "packageName": {
86
+ "anyOf": [
87
+ {
88
+ "type": "string"
89
+ },
90
+ {
91
+ "type": "null"
92
+ }
93
+ ],
94
+ "default": null,
95
+ "title": "Packagename"
96
+ },
97
+ "gitCommit": {
98
+ "anyOf": [
99
+ {
100
+ "type": "string"
101
+ },
102
+ {
103
+ "type": "null"
104
+ }
105
+ ],
106
+ "default": null,
107
+ "title": "Gitcommit"
108
+ },
109
+ "config": {
110
+ "anyOf": [
111
+ {
112
+ "additionalProperties": true,
113
+ "type": "object"
114
+ },
115
+ {
116
+ "type": "null"
117
+ }
118
+ ],
119
+ "default": null,
120
+ "title": "Config"
121
+ }
122
+ },
123
+ "title": "ReadAgentContextResponse",
124
+ "type": "object"
13
125
  }
14
126
  }
@@ -36,8 +36,51 @@
36
36
  "type": "object"
37
37
  },
38
38
  "outputSchema": {
39
- "type": "object",
40
- "properties": {},
41
- "additionalProperties": true
39
+ "properties": {
40
+ "checks": {
41
+ "anyOf": [
42
+ {
43
+ "items": {
44
+ "additionalProperties": true,
45
+ "type": "object"
46
+ },
47
+ "type": "array"
48
+ },
49
+ {
50
+ "type": "null"
51
+ }
52
+ ],
53
+ "default": null,
54
+ "title": "Checks"
55
+ },
56
+ "failedChecks": {
57
+ "anyOf": [
58
+ {
59
+ "items": {
60
+ "additionalProperties": true,
61
+ "type": "object"
62
+ },
63
+ "type": "array"
64
+ },
65
+ {
66
+ "type": "null"
67
+ }
68
+ ],
69
+ "default": null,
70
+ "title": "Failedchecks"
71
+ },
72
+ "logs": {
73
+ "anyOf": [
74
+ {},
75
+ {
76
+ "type": "null"
77
+ }
78
+ ],
79
+ "default": null,
80
+ "title": "Logs"
81
+ }
82
+ },
83
+ "title": "ReadCiLogsResponse",
84
+ "type": "object"
42
85
  }
43
86
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.1](https://github.com/arii/boomtick/compare/mcp-v0.7.0...mcp-v0.7.1) (2026-10-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **mcp:** resolve ecosystem_repos without requiring local path existence during isolated test runs ([53ab66a](https://github.com/arii/boomtick/commit/53ab66ab6503c93ff07bfd4e525a5f490df16386))
9
+ * **mcp:** resolve target workspace dynamically in repo.create_branch and worktree tools ([#990](https://github.com/arii/boomtick/issues/990)) ([cd2f34e](https://github.com/arii/boomtick/commit/cd2f34ea8607f037b855d41f899bf5152dc13c1f))
10
+
11
+ ## [0.7.0](https://github.com/arii/boomtick/compare/mcp-v0.6.2...mcp-v0.7.0) (2026-10-29)
12
+
13
+
14
+ ### Features
15
+
16
+ * **aggregate:** agent onboarding, multi-repo context, and sequential orchestration ([#1001](https://github.com/arii/boomtick/issues/1001)) ([7060777](https://github.com/arii/boomtick/commit/7060777195a84d391b5098226ce06a04ddefcd9a))
17
+ * **aggregate:** pure-node container, review runner hardening, mcp modernization, and offline dag engine ([#1002](https://github.com/arii/boomtick/issues/1002)) ([056503c](https://github.com/arii/boomtick/commit/056503c8c21b5bd99f3b9d874ee7fac25972362a))
18
+ * **mcp:** modernize boomtick-mcp with typed outputSchema, resources interface, and streamable HTTP transport ([26401df](https://github.com/arii/boomtick/commit/26401df06774e9de7756a375e0080810cc35926e))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **mcp:** resolve active repository dynamically across workspaces ([#981](https://github.com/arii/boomtick/issues/981)) ([9a757ea](https://github.com/arii/boomtick/commit/9a757ea5d1a878861e69bee4e36201849f7a03e4))
24
+
3
25
  ## [0.6.2](https://github.com/arii/boomtick/compare/mcp-v0.6.1...mcp-v0.6.2) (2026-10-28)
4
26
 
5
27
 
package/README.md CHANGED
@@ -41,19 +41,21 @@ A Model Context Protocol (MCP) server designed to empower AI agents with structu
41
41
 
42
42
  ### Installation
43
43
  ```bash
44
- cd boomtick-mcp
45
- pnpm install
46
- pnpm build
44
+ # From repository root
45
+ pnpm --filter @arii/boomtick-mcp run build
46
+
47
+ # Register boomtick-mcp across installed agent hosts automatically
48
+ td mcp install --execute
47
49
  ```
48
50
 
49
- ### Installation via MCP Client (e.g. Claude Desktop)
51
+ ### Manual Configuration via MCP Client (e.g. Claude Desktop, Cursor)
50
52
  Add the following to your MCP client configuration (e.g. `claude_desktop_config.json`):
51
53
  ```json
52
54
  {
53
55
  "mcpServers": {
54
56
  "boomtick": {
55
57
  "command": "node",
56
- "args": ["/absolute/path/to/boomtick-mcp/dist/index.js"],
58
+ "args": ["/absolute/path/to/mcp/dist/index.js"],
57
59
  "env": {
58
60
  "GITHUB_TOKEN": "your_pat",
59
61
  "GITHUB_OWNER": "your_org",
@@ -1,11 +1,12 @@
1
1
  import { execSync } from "child_process";
2
+ import { findRepoRoot } from "./repo.js";
2
3
  let cachedDynamicConfig = null;
3
- export function initializeConfig() {
4
+ export function initializeConfig(targetCwd) {
4
5
  if (cachedDynamicConfig !== null) {
5
6
  return cachedDynamicConfig;
6
7
  }
7
8
  try {
8
- const cwd = process.env.BOOMTICK_REPO_PATH || process.cwd();
9
+ const cwd = findRepoRoot(targetCwd);
9
10
  const cmd = `td config view`;
10
11
  const output = execSync(cmd, {
11
12
  cwd,
@@ -2,11 +2,8 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { fileURLToPath } from "url";
4
4
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
- export function findRepoRoot() {
6
- if (process.env.BOOMTICK_REPO_PATH) {
7
- return process.env.BOOMTICK_REPO_PATH;
8
- }
9
- let current = process.cwd();
5
+ export function findRepoRoot(startDir) {
6
+ let current = startDir || process.cwd();
10
7
  while (current !== path.parse(current).root) {
11
8
  if (fs.existsSync(path.join(current, "project_config.json")) ||
12
9
  fs.existsSync(path.join(current, "workspace.json")) ||
@@ -15,6 +12,9 @@ export function findRepoRoot() {
15
12
  }
16
13
  current = path.dirname(current);
17
14
  }
15
+ if (process.env.BOOMTICK_REPO_PATH) {
16
+ return process.env.BOOMTICK_REPO_PATH;
17
+ }
18
18
  current = __dirname;
19
19
  while (current !== path.parse(current).root) {
20
20
  if (fs.existsSync(path.join(current, "project_config.json")) ||
@@ -19,9 +19,10 @@ describe("config", () => {
19
19
  const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
20
20
  vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
21
21
  const { initializeConfig } = await import("./config.js");
22
+ const { findRepoRoot } = await import("./config/repo.js");
22
23
  const result = initializeConfig();
23
24
  expect(execSync).toHaveBeenCalledWith("td config view", expect.objectContaining({
24
- cwd: process.cwd()
25
+ cwd: findRepoRoot()
25
26
  }));
26
27
  expect(result).toEqual(mockConfig);
27
28
  });
@@ -103,10 +104,16 @@ describe("config", () => {
103
104
  expect(() => config.githubOwner).toThrow("GITHUB_OWNER must be set via environment variable or project_config.json");
104
105
  expect(() => config.githubRepo).toThrow("GITHUB_REPO must be set via environment variable or project_config.json");
105
106
  });
106
- it("should resolve repoPath from BOOMTICK_REPO_PATH or findRepoRoot fallback", async () => {
107
+ it("should resolve active workspace from process.cwd() over BOOMTICK_REPO_PATH when inside a repo", async () => {
107
108
  process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
108
109
  const { config } = await import("./config.js");
109
- expect(config.repoPath).toBe("/custom/repo/path");
110
+ expect(config.repoPath).toBe(process.cwd().replace(/\/mcp$/, ""));
111
+ });
112
+ it("should fallback to BOOMTICK_REPO_PATH when process.cwd() is outside any repo", async () => {
113
+ process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
114
+ const { findRepoRoot } = await import("./config/repo.js");
115
+ const rootDir = process.platform === "win32" ? "C:\\" : "/";
116
+ expect(findRepoRoot(rootDir)).toBe("/custom/repo/path");
110
117
  });
111
118
  it("should resolve defaultBaseBranch, viteBasePath, and ghPath", async () => {
112
119
  process.env.DEFAULT_BASE_BRANCH = "develop";
package/dist/lib/git.js CHANGED
@@ -2,6 +2,7 @@ import { runCommand } from "./shell.js";
2
2
  import { config } from "../config.js";
3
3
  import path from "path";
4
4
  import fs from "fs/promises";
5
+ import * as syncFs from "fs";
5
6
  import { execSync } from "child_process";
6
7
  const repoCache = new Map();
7
8
  export function resolveRepo(explicitRepo, cwd) {
@@ -31,6 +32,75 @@ export function resolveRepo(explicitRepo, cwd) {
31
32
  repoCache.set(targetCwd, fallback);
32
33
  return fallback;
33
34
  }
35
+ export function resolveRepoPath(options = {}) {
36
+ // 1. Explicit path parameter has highest precedence
37
+ if (options.path && options.path.trim().length > 0) {
38
+ return path.resolve(options.path.trim());
39
+ }
40
+ // 2. repoPath alias parameter
41
+ if (options.repoPath && options.repoPath.trim().length > 0) {
42
+ return path.resolve(options.repoPath.trim());
43
+ }
44
+ // 3. Repository name override (e.g. "arii/tech-dancer" or "tech-dancer")
45
+ if (options.repo && options.repo.trim().length > 0) {
46
+ const rawRepo = options.repo.trim();
47
+ const repoName = rawRepo.includes("/") ? rawRepo.split("/")[1] : rawRepo;
48
+ // First check project_config.json ecosystem_repos if available
49
+ let rootPath = process.cwd();
50
+ try {
51
+ if (options.cwd && options.cwd.trim().length > 0) {
52
+ rootPath = options.cwd.trim();
53
+ }
54
+ else if (config.repoPath) {
55
+ rootPath = config.repoPath;
56
+ }
57
+ }
58
+ catch {
59
+ rootPath = process.cwd();
60
+ }
61
+ const projectConfigPath = path.join(rootPath, "project_config.json");
62
+ if (syncFs.existsSync(projectConfigPath)) {
63
+ try {
64
+ const parsed = JSON.parse(syncFs.readFileSync(projectConfigPath, "utf-8"));
65
+ if (parsed?.ecosystem_repos && typeof parsed.ecosystem_repos === "object") {
66
+ for (const [key, relPath] of Object.entries(parsed.ecosystem_repos)) {
67
+ const keyRepoName = key.includes("/") ? key.split("/")[1] : key;
68
+ if (key === rawRepo || keyRepoName === repoName) {
69
+ return path.resolve(rootPath, relPath);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ // Fallback to directory inspection
76
+ }
77
+ }
78
+ // Check sibling directory relative to cwd or rootPath
79
+ const candidateDirs = [
80
+ path.resolve(process.cwd(), "..", repoName),
81
+ path.resolve(rootPath, "..", repoName),
82
+ ];
83
+ for (const cand of candidateDirs) {
84
+ if (syncFs.existsSync(cand)) {
85
+ return cand;
86
+ }
87
+ }
88
+ }
89
+ // 4. Explicit cwd parameter
90
+ if (options.cwd && options.cwd.trim().length > 0) {
91
+ return path.resolve(options.cwd.trim());
92
+ }
93
+ // 5. Configured repoPath or process.cwd()
94
+ try {
95
+ if (config.repoPath && syncFs.existsSync(config.repoPath)) {
96
+ return config.repoPath;
97
+ }
98
+ }
99
+ catch {
100
+ // Fall back to process.cwd()
101
+ }
102
+ return process.cwd();
103
+ }
34
104
  function validatePrNumber(value) {
35
105
  const prNumber = Number(value);
36
106
  if (!Number.isInteger(prNumber) || prNumber <= 0) {
@@ -38,26 +108,28 @@ function validatePrNumber(value) {
38
108
  }
39
109
  return prNumber;
40
110
  }
41
- export async function createWorktree(branch, prNumber) {
111
+ export async function createWorktree(branch, prNumber, baseRepoPath) {
42
112
  const safePrNumber = validatePrNumber(prNumber);
43
113
  const workspaceRoot = "/tmp/boomtick-worktrees";
44
114
  // nosemgrep
45
115
  const worktreePath = path.join(workspaceRoot, `boomtick-mcp-rescue-${safePrNumber}`);
116
+ const cwdOption = baseRepoPath ? { cwd: baseRepoPath } : undefined;
46
117
  // Clean up if exists
47
118
  try {
48
119
  await fs.rm(worktreePath, { recursive: true, force: true });
49
- await runCommand("git", ["worktree", "prune"]);
120
+ await runCommand("git", ["worktree", "prune"], cwdOption);
50
121
  }
51
122
  catch (e) { /* ignore if no previous worktree */ }
52
- const result = await runCommand("git", ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch]);
123
+ const result = await runCommand("git", ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch], cwdOption);
53
124
  if (result.exitCode !== 0) {
54
125
  throw new Error(`Failed to create worktree: ${result.stderr}`);
55
126
  }
56
127
  return worktreePath;
57
128
  }
58
- export async function removeWorktree(worktreePath) {
59
- await runCommand("git", ["worktree", "remove", "--force", worktreePath]);
60
- await runCommand("git", ["worktree", "prune"]);
129
+ export async function removeWorktree(worktreePath, baseRepoPath) {
130
+ const cwdOption = baseRepoPath ? { cwd: baseRepoPath } : undefined;
131
+ await runCommand("git", ["worktree", "remove", "--force", worktreePath], cwdOption);
132
+ await runCommand("git", ["worktree", "prune"], cwdOption);
61
133
  }
62
134
  export async function getChangedFiles(base = "main", head = "HEAD", cwd) {
63
135
  const result = await runCommand("git", ["diff", "--name-status", "-z", `${base}...${head}`], { cwd });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi, beforeEach } from "vitest";
2
- import { getChangedFiles, getMergeConflictFiles } from "./git.js";
2
+ import { getChangedFiles, getMergeConflictFiles, resolveRepoPath, createWorktree } from "./git.js";
3
3
  import * as shell from "./shell.js";
4
4
  vi.mock("./shell.js", () => ({
5
5
  runCommand: vi.fn(),
@@ -79,4 +79,37 @@ describe("Git Helpers", () => {
79
79
  await expect(getMergeConflictFiles("invalid", "feature")).rejects.toThrow("Git merge-tree failed: fatal: not a valid object name");
80
80
  });
81
81
  });
82
+ describe("resolveRepoPath", () => {
83
+ it("should return explicit path when path is provided", () => {
84
+ const resolved = resolveRepoPath({ path: "/tmp/custom-repo" });
85
+ expect(resolved).toBe("/tmp/custom-repo");
86
+ });
87
+ it("should return explicit repoPath when repoPath is provided", () => {
88
+ const resolved = resolveRepoPath({ repoPath: "/tmp/another-repo" });
89
+ expect(resolved).toBe("/tmp/another-repo");
90
+ });
91
+ it("should resolve repo path via ecosystem_repos when repo is given", () => {
92
+ const resolved = resolveRepoPath({ repo: "arii/tech-dancer" });
93
+ expect(resolved).toContain("tech-dancer");
94
+ });
95
+ it("should fall back to current working directory when empty", () => {
96
+ const resolved = resolveRepoPath({});
97
+ expect(typeof resolved).toBe("string");
98
+ expect(resolved.length).toBeGreaterThan(0);
99
+ });
100
+ });
101
+ describe("createWorktree", () => {
102
+ it("should pass baseRepoPath cwd to runCommand when provided", async () => {
103
+ vi.mocked(shell.runCommand).mockResolvedValue({
104
+ exitCode: 0,
105
+ stdout: "",
106
+ stderr: "",
107
+ durationMs: 5,
108
+ command: "git",
109
+ });
110
+ const worktree = await createWorktree("origin/feature", 123, "/home/ari/tech-dancer");
111
+ expect(worktree).toContain("boomtick-mcp-rescue-123");
112
+ expect(shell.runCommand).toHaveBeenCalledWith("git", expect.arrayContaining(["worktree", "add"]), { cwd: "/home/ari/tech-dancer" });
113
+ });
114
+ });
82
115
  });