infra-kit 0.1.99 → 0.1.101

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 (47) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-d367c3be-9c2a-48e7-bcea-b45861af568c.jsonl +2 -0
  3. package/.omc/state/agent-replay-f2846d8f-974c-486c-b16f-4bdaa28ca45f.jsonl +2 -0
  4. package/.omc/state/last-tool-error.json +7 -0
  5. package/.omc/state/subagent-tracking.json +7 -0
  6. package/.turbo/turbo-eslint-check.log +1 -4
  7. package/.turbo/turbo-eslint-fix.log +1 -0
  8. package/.turbo/turbo-prettier-check.log +1 -4
  9. package/.turbo/turbo-prettier-fix.log +1 -10
  10. package/.turbo/turbo-test.log +5 -11
  11. package/.turbo/turbo-ts-check.log +1 -4
  12. package/dist/cli.js +45 -36
  13. package/dist/cli.js.map +4 -4
  14. package/dist/mcp.js +42 -34
  15. package/dist/mcp.js.map +4 -4
  16. package/package.json +12 -12
  17. package/src/commands/doctor/doctor.ts +62 -12
  18. package/src/commands/env-clear/env-clear.ts +5 -10
  19. package/src/commands/env-list/env-list.ts +5 -10
  20. package/src/commands/env-load/env-load.ts +5 -10
  21. package/src/commands/env-status/env-status.ts +5 -10
  22. package/src/commands/gh-merge-dev/gh-merge-dev.ts +17 -18
  23. package/src/commands/gh-release-deliver/gh-release-deliver.ts +290 -89
  24. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +15 -14
  25. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +30 -23
  26. package/src/commands/gh-release-list/gh-release-list.ts +5 -10
  27. package/src/commands/release-create/release-create.ts +22 -18
  28. package/src/commands/release-desc-edit/index.ts +1 -0
  29. package/src/commands/release-desc-edit/release-desc-edit.ts +207 -0
  30. package/src/commands/version/version.ts +5 -10
  31. package/src/commands/worktrees-add/worktrees-add.ts +18 -14
  32. package/src/commands/worktrees-list/worktrees-list.ts +6 -11
  33. package/src/commands/worktrees-open/worktrees-open.ts +10 -6
  34. package/src/commands/worktrees-remove/worktrees-remove.ts +18 -14
  35. package/src/commands/worktrees-sync/worktrees-sync.ts +17 -12
  36. package/src/entry/cli.ts +16 -0
  37. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +21 -0
  38. package/src/integrations/gh/gh-release-prs/index.ts +1 -1
  39. package/src/integrations/gh/index.ts +1 -1
  40. package/src/integrations/jira/api.ts +8 -17
  41. package/src/integrations/jira/index.ts +2 -0
  42. package/src/lib/errors/__tests__/operation-error.test.ts +62 -0
  43. package/src/lib/errors/format-zx-error.ts +54 -0
  44. package/src/lib/errors/operation-error.ts +80 -0
  45. package/src/mcp/tools/index.ts +2 -0
  46. package/src/types.ts +56 -2
  47. package/tsconfig.tsbuildinfo +1 -1
@@ -3,12 +3,12 @@ import { z } from 'zod/v4'
3
3
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
4
4
  import { logger } from 'src/lib/logger'
5
5
  import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
6
- import type { ToolsExecutionResult } from 'src/types'
6
+ import { defineMcpTool, textContent } from 'src/types'
7
7
 
8
8
  /**
9
9
  * List all open release branches
10
10
  */
11
- export const ghReleaseList = async (): Promise<ToolsExecutionResult> => {
11
+ export const ghReleaseList = async () => {
12
12
  const releasePRs = await getReleasePRsWithInfo()
13
13
 
14
14
  const releases = releasePRs.map((pr) => {
@@ -52,18 +52,13 @@ export const ghReleaseList = async (): Promise<ToolsExecutionResult> => {
52
52
  }
53
53
 
54
54
  return {
55
- content: [
56
- {
57
- type: 'text',
58
- text: JSON.stringify(structuredContent, null, 2),
59
- },
60
- ],
55
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
61
56
  structuredContent,
62
57
  }
63
58
  }
64
59
 
65
60
  // MCP Tool Registration
66
- export const ghReleaseListMcpTool = {
61
+ export const ghReleaseListMcpTool = defineMcpTool({
67
62
  name: 'gh-release-list',
68
63
  description:
69
64
  'List every open release PR with its version, type (regular / hotfix), and associated Jira fix-version description. Read-only; sourced from GitHub and Jira.',
@@ -81,4 +76,4 @@ export const ghReleaseListMcpTool = {
81
76
  count: z.number().describe('Number of release branches'),
82
77
  },
83
78
  handler: ghReleaseList,
84
- }
79
+ })
@@ -6,6 +6,7 @@ import { question } from 'zx'
6
6
 
7
7
  import { loadJiraConfig } from 'src/integrations/jira'
8
8
  import { commandEcho } from 'src/lib/command-echo'
9
+ import { OperationError } from 'src/lib/errors/operation-error'
9
10
  import { logger } from 'src/lib/logger'
10
11
  import { createSingleRelease, prepareGitForRelease } from 'src/lib/release-utils'
11
12
  import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
@@ -18,7 +19,8 @@ import {
18
19
  resolveReleaseEntries,
19
20
  } from 'src/lib/version-utils'
20
21
  import type { ReleaseEntry, SemVer } from 'src/lib/version-utils'
21
- import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
22
+ import { defineMcpTool, textContent } from 'src/types'
23
+ import type { RequiredConfirmedOptionArg } from 'src/types'
22
24
 
23
25
  interface ReleaseCreateArgs extends RequiredConfirmedOptionArg {
24
26
  releases?: ReleaseEntry[]
@@ -41,8 +43,10 @@ const resolveOrExit = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[]
41
43
  return resolveReleaseEntries(entries, known)
42
44
  } catch (err) {
43
45
  if (err instanceof NoPriorVersionsError) {
44
- logger.error(err.message)
45
- process.exit(1)
46
+ throw new OperationError(err, {
47
+ operation: 'resolve release version',
48
+ remediation: 'pass an explicit version (e.g. "1.2.5") instead of "next" when there are no prior versions',
49
+ })
46
50
  }
47
51
 
48
52
  throw err
@@ -181,12 +185,14 @@ const executeOne = async (
181
185
 
182
186
  return { result }
183
187
  } catch (error) {
184
- const errorMessage = error instanceof Error ? error.message : String(error)
188
+ const err = new OperationError(error, {
189
+ operation: `create release v${entry.version} (${entry.type})`,
190
+ remediation: 'verify the version is unique and the base branch is clean',
191
+ })
185
192
 
186
- logger.error(`❌ Failed to create release: v${entry.version}`)
187
- logger.error(` Error: ${errorMessage}\n`)
193
+ logger.error(`❌ ${err.message}\n`)
188
194
 
189
- return { failure: { version: entry.version, error: errorMessage } }
195
+ return { failure: { version: entry.version, error: err.message } }
190
196
  }
191
197
  }
192
198
 
@@ -206,7 +212,7 @@ const logFinalSummary = (total: number, successCount: number, failureCount: numb
206
212
  * (regular/hotfix) and optional Jira description, so a single invocation
207
213
  * may mix regular and hotfix releases off their respective base branches.
208
214
  */
209
- export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecutionResult> => {
215
+ export const releaseCreate = async (args: ReleaseCreateArgs) => {
210
216
  const { releases: inputReleases, confirmedCommand } = args
211
217
 
212
218
  commandEcho.start('release-create')
@@ -223,8 +229,11 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
223
229
  const entries = await collectEntries(inputReleases, ensureKnown)
224
230
 
225
231
  if (entries.length === 0) {
226
- logger.error('No releases provided. Exiting...')
227
- process.exit(1)
232
+ throw new OperationError(undefined, {
233
+ operation: 'create release',
234
+ remediation: 'pass at least one entry in "releases" (e.g. [{ version: "1.2.5", type: "regular" }])',
235
+ stderrExcerpt: 'no releases provided',
236
+ })
228
237
  }
229
238
 
230
239
  await confirmReleases(entries, Boolean(confirmedCommand))
@@ -254,18 +263,13 @@ export const releaseCreate = async (args: ReleaseCreateArgs): Promise<ToolsExecu
254
263
  }
255
264
 
256
265
  return {
257
- content: [
258
- {
259
- type: 'text',
260
- text: JSON.stringify(structuredContent, null, 2),
261
- },
262
- ],
266
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
263
267
  structuredContent,
264
268
  }
265
269
  }
266
270
 
267
271
  // MCP Tool Registration
268
- export const releaseCreateMcpTool = {
272
+ export const releaseCreateMcpTool = defineMcpTool({
269
273
  name: 'release-create',
270
274
  description:
271
275
  'Create one or more releases in a single call. Each entry in "releases" carries its own version, type (regular|hotfix, default regular), and optional description, so regular and hotfix releases can be mixed in the same invocation. For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch, opens a GitHub release PR, and creates the matching Jira fix version. The literal token "next" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple "next" tokens advance sequentially across mixed types. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',
@@ -312,4 +316,4 @@ export const releaseCreateMcpTool = {
312
316
  .describe('List of releases that failed with error messages'),
313
317
  },
314
318
  handler: releaseCreate,
315
- }
319
+ })
@@ -0,0 +1 @@
1
+ export { releaseDescEdit, releaseDescEditMcpTool } from './release-desc-edit'
@@ -0,0 +1,207 @@
1
+ import confirm from '@inquirer/confirm'
2
+ import select from '@inquirer/select'
3
+ import process from 'node:process'
4
+ import { z } from 'zod/v4'
5
+ import { question } from 'zx'
6
+
7
+ import { getReleasePRsWithInfo, updateReleasePRBody } from 'src/integrations/gh'
8
+ import { findVersionByName, loadJiraConfig, updateJiraVersion } from 'src/integrations/jira'
9
+ import type { JiraConfig, JiraVersion } from 'src/integrations/jira'
10
+ import { commandEcho } from 'src/lib/command-echo'
11
+ import { OperationError } from 'src/lib/errors/operation-error'
12
+ import { logger } from 'src/lib/logger'
13
+ import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
14
+ import type { ReleaseType } from 'src/lib/release-utils'
15
+ import { defineMcpTool, textContent } from 'src/types'
16
+ import type { RequiredConfirmedOptionArg } from 'src/types'
17
+
18
+ interface ReleaseDescEditArgs extends RequiredConfirmedOptionArg {
19
+ version?: string
20
+ description?: string
21
+ }
22
+
23
+ const buildJiraVersionUrl = (jiraConfig: JiraConfig, version: JiraVersion): string => {
24
+ return `${jiraConfig.baseUrl}/projects/${version.projectId}/versions/${version.id}/tab/release-report-all-issues`
25
+ }
26
+
27
+ const buildPRBody = (jiraVersionUrl: string, description: string): string => {
28
+ return description.trim() !== '' ? `${jiraVersionUrl}\n\n${description}` : `${jiraVersionUrl} \n`
29
+ }
30
+
31
+ const pickReleaseBranch = async (): Promise<{ branch: string; type: ReleaseType }> => {
32
+ const releasePRsInfo = await getReleasePRsWithInfo()
33
+ const branches = releasePRsInfo.map((pr) => {
34
+ return pr.branch
35
+ })
36
+ const types = new Map<string, ReleaseType>(
37
+ releasePRsInfo.map((pr) => {
38
+ return [pr.branch, detectReleaseType(pr.title)]
39
+ }),
40
+ )
41
+ const descriptions = await getJiraDescriptions()
42
+
43
+ const branch = await select({
44
+ message: '🌿 Select release branch',
45
+ choices: formatBranchChoices({ branches, descriptions, types }),
46
+ })
47
+
48
+ return { branch, type: types.get(branch) || 'regular' }
49
+ }
50
+
51
+ const verifyReleasePRExists = async (selectedBranch: string): Promise<ReleaseType> => {
52
+ const releasePRsInfo = await getReleasePRsWithInfo()
53
+ const prInfo = releasePRsInfo.find((pr) => {
54
+ return pr.branch === selectedBranch
55
+ })
56
+
57
+ if (!prInfo) {
58
+ throw new OperationError(undefined, {
59
+ operation: `edit description for ${selectedBranch}`,
60
+ remediation: `confirm an open PR exists for ${selectedBranch} ('gh pr list')`,
61
+ })
62
+ }
63
+
64
+ return detectReleaseType(prInfo.title)
65
+ }
66
+
67
+ const promptDescription = async (current: string): Promise<string> => {
68
+ const hint = current === '' ? '(no current description)' : `current: "${current}"`
69
+ const answer = await question(` New description ${hint}\n (press Enter to keep current): `)
70
+ const trimmed = answer.replace(/\n$/, '')
71
+
72
+ return trimmed === '' ? current : trimmed
73
+ }
74
+
75
+ /**
76
+ * Edit a release's description in Jira (fix version) and in the matching
77
+ * GitHub release PR body. The PR body is rewritten canonically to
78
+ * `<jiraVersionUrl>\n\n<description>` (matching `release-create`).
79
+ */
80
+ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
81
+ const { version: versionArg, description: descriptionArg, confirmedCommand } = args
82
+
83
+ commandEcho.start('release-desc-edit')
84
+
85
+ const jiraConfig = await loadJiraConfig()
86
+
87
+ let selectedBranch: string
88
+
89
+ if (versionArg) {
90
+ selectedBranch = `release/v${versionArg}`
91
+ await verifyReleasePRExists(selectedBranch)
92
+ } else {
93
+ commandEcho.setInteractive()
94
+ const picked = await pickReleaseBranch()
95
+
96
+ selectedBranch = picked.branch
97
+ }
98
+
99
+ const selectedVersion = selectedBranch.replace('release/v', '')
100
+
101
+ commandEcho.addOption('--version', selectedVersion)
102
+
103
+ const versionName = `v${selectedVersion}`
104
+ const jiraVersion = await findVersionByName(versionName, jiraConfig)
105
+
106
+ if (!jiraVersion) {
107
+ throw new OperationError(undefined, {
108
+ operation: `edit description for ${versionName}`,
109
+ remediation: `create the Jira fix version "${versionName}" first or pick a different release`,
110
+ })
111
+ }
112
+
113
+ const previousDescription = jiraVersion.description ?? ''
114
+
115
+ let newDescription: string
116
+
117
+ if (descriptionArg !== undefined) {
118
+ newDescription = descriptionArg
119
+ commandEcho.addOption('--description', newDescription)
120
+ } else {
121
+ commandEcho.setInteractive()
122
+ newDescription = await promptDescription(previousDescription)
123
+ }
124
+
125
+ if (newDescription === previousDescription) {
126
+ logger.info(`No change — description for ${versionName} is already: "${previousDescription}"`)
127
+ commandEcho.print()
128
+
129
+ const structuredContent = {
130
+ version: selectedVersion,
131
+ branch: selectedBranch,
132
+ jiraVersionUrl: buildJiraVersionUrl(jiraConfig, jiraVersion),
133
+ previousDescription,
134
+ newDescription,
135
+ changed: false,
136
+ }
137
+
138
+ return {
139
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
140
+ structuredContent,
141
+ }
142
+ }
143
+
144
+ const answer = confirmedCommand
145
+ ? true
146
+ : await confirm({
147
+ message: `Update description for ${versionName}?\n from: "${previousDescription}"\n to: "${newDescription}"\n`,
148
+ })
149
+
150
+ if (!confirmedCommand) {
151
+ commandEcho.setInteractive()
152
+ }
153
+
154
+ if (!answer) {
155
+ logger.info('Operation cancelled. Exiting...')
156
+ process.exit(0)
157
+ }
158
+
159
+ commandEcho.addOption('--yes', true)
160
+
161
+ await updateJiraVersion({ versionId: jiraVersion.id, description: newDescription }, jiraConfig)
162
+
163
+ const jiraVersionUrl = buildJiraVersionUrl(jiraConfig, jiraVersion)
164
+ const body = buildPRBody(jiraVersionUrl, newDescription)
165
+
166
+ await updateReleasePRBody({ branch: selectedBranch, body })
167
+
168
+ logger.info(`✅ Updated description for ${versionName}`)
169
+ logger.info(`🔗 Jira Version: ${jiraVersionUrl}`)
170
+ logger.info(`🔗 PR branch: ${selectedBranch}\n`)
171
+
172
+ commandEcho.print()
173
+
174
+ const structuredContent = {
175
+ version: selectedVersion,
176
+ branch: selectedBranch,
177
+ jiraVersionUrl,
178
+ previousDescription,
179
+ newDescription,
180
+ changed: true,
181
+ }
182
+
183
+ return {
184
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
185
+ structuredContent,
186
+ }
187
+ }
188
+
189
+ // MCP Tool Registration
190
+ export const releaseDescEditMcpTool = defineMcpTool({
191
+ name: 'release-desc-edit',
192
+ description:
193
+ "Edit a release's description in Jira and in the matching GitHub release PR body. Targets the Jira fix version named `v<version>` and the open PR on branch `release/v<version>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\n\\n<description>` — any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.",
194
+ inputSchema: {
195
+ version: z.string().describe('Release version, e.g. "1.2.5".'),
196
+ description: z.string().describe('New description. Empty string clears the description.'),
197
+ },
198
+ outputSchema: {
199
+ version: z.string().describe('Release version'),
200
+ branch: z.string().describe('Release branch name (e.g. "release/v1.2.5")'),
201
+ jiraVersionUrl: z.string().describe('Jira fix version URL'),
202
+ previousDescription: z.string().describe('The description before the update'),
203
+ newDescription: z.string().describe('The description after the update'),
204
+ changed: z.boolean().describe('Whether the description actually changed'),
205
+ },
206
+ handler: releaseDescEdit,
207
+ })
@@ -1,14 +1,14 @@
1
1
  import { z } from 'zod/v4'
2
2
 
3
3
  import { logger } from 'src/lib/logger'
4
- import type { ToolsExecutionResult } from 'src/types'
4
+ import { defineMcpTool, textContent } from 'src/types'
5
5
 
6
6
  import packageJson from '../../../package.json' with { type: 'json' }
7
7
 
8
8
  /**
9
9
  * Print the infra-kit CLI version
10
10
  */
11
- export const version = async (): Promise<ToolsExecutionResult> => {
11
+ export const version = async () => {
12
12
  const cliVersion = packageJson.version
13
13
 
14
14
  logger.info(cliVersion)
@@ -16,18 +16,13 @@ export const version = async (): Promise<ToolsExecutionResult> => {
16
16
  const structuredContent = { version: cliVersion }
17
17
 
18
18
  return {
19
- content: [
20
- {
21
- type: 'text',
22
- text: JSON.stringify(structuredContent, null, 2),
23
- },
24
- ],
19
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
25
20
  structuredContent,
26
21
  }
27
22
  }
28
23
 
29
24
  // MCP Tool Registration
30
- export const versionMcpTool = {
25
+ export const versionMcpTool = defineMcpTool({
31
26
  name: 'version',
32
27
  description: 'Print the installed infra-kit CLI version',
33
28
  inputSchema: {},
@@ -35,4 +30,4 @@ export const versionMcpTool = {
35
30
  version: z.string().describe('Installed infra-kit CLI version (from package.json)'),
36
31
  },
37
32
  handler: version,
38
- }
33
+ })
@@ -11,12 +11,14 @@ import { addFoldersToCursorWorkspace, resolveCursorWorkspacePath } from 'src/int
11
11
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
12
12
  import { commandEcho } from 'src/lib/command-echo'
13
13
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
14
+ import { OperationError } from 'src/lib/errors/operation-error'
14
15
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
15
16
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
16
17
  import { logger } from 'src/lib/logger'
17
18
  import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
18
19
  import type { ReleaseType } from 'src/lib/release-utils'
19
- import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
20
+ import { defineMcpTool, textContent } from 'src/types'
21
+ import type { RequiredConfirmedOptionArg } from 'src/types'
20
22
 
21
23
  // Constants
22
24
  const FEATURE_DIR = 'feature'
@@ -27,7 +29,7 @@ export const CURSOR_MODES = ['workspace', 'windows', 'none'] as const
27
29
  export type CursorMode = (typeof CURSOR_MODES)[number]
28
30
 
29
31
  interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
30
- all: boolean
32
+ all?: boolean
31
33
  versions?: string
32
34
  cursor?: CursorMode
33
35
  githubDesktop?: boolean
@@ -38,7 +40,7 @@ interface WorktreeManagementArgs extends RequiredConfirmedOptionArg {
38
40
  * Manage git worktrees for release branches
39
41
  * Creates worktrees for active release branches and removes unused ones
40
42
  */
41
- export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<ToolsExecutionResult> => {
43
+ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
42
44
  const { confirmedCommand, all, versions, cursor, githubDesktop, cmux } = options
43
45
 
44
46
  commandEcho.start('worktrees-add')
@@ -71,7 +73,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
71
73
  commandEcho.print()
72
74
 
73
75
  return {
74
- content: [{ type: 'text', text: JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2) }],
76
+ content: textContent(JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2)),
75
77
  structuredContent: { createdWorktrees: [], count: 0 },
76
78
  }
77
79
  }
@@ -249,17 +251,15 @@ export const worktreesAdd = async (options: WorktreeManagementArgs): Promise<Too
249
251
  }
250
252
 
251
253
  return {
252
- content: [
253
- {
254
- type: 'text',
255
- text: JSON.stringify(structuredContent, null, 2),
256
- },
257
- ],
254
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
258
255
  structuredContent,
259
256
  }
260
257
  } catch (error) {
261
258
  logger.error({ error }, '❌ Error managing worktrees')
262
- throw error
259
+ throw new OperationError(error, {
260
+ operation: 'create worktrees',
261
+ remediation: "verify branches don't already exist as worktrees: 'git worktree list'",
262
+ })
263
263
  }
264
264
  }
265
265
 
@@ -314,8 +314,12 @@ const createWorktrees = async (branches: string[], worktreeDir: string): Promise
314
314
  created.push(result.value)
315
315
  } else {
316
316
  const branch = branches[index]
317
+ const err = new OperationError(result.reason, {
318
+ operation: `git worktree add for ${branch}`,
319
+ remediation: 'check the branch name and that the parent dir is writable',
320
+ })
317
321
 
318
- logger.error({ error: result.reason }, `❌ Failed to create worktree for ${branch}`)
322
+ logger.error({ error: result.reason, msg: err.message })
319
323
  }
320
324
  }
321
325
 
@@ -338,7 +342,7 @@ const logResults = (created: string[]): void => {
338
342
  }
339
343
 
340
344
  // MCP Tool Registration
341
- export const worktreesAddMcpTool = {
345
+ export const worktreesAddMcpTool = defineMcpTool({
342
346
  name: 'worktrees-add',
343
347
  description:
344
348
  'Create local git worktrees for release branches under the worktrees directory and run "pnpm install" in each. Mutates the local filesystem. When invoked via MCP, pass either "versions" (comma-separated) or all=true — the branch picker and "open in Cursor / GitHub Desktop / cmux" follow-up prompts are unreachable without a TTY, and the CLI confirmation is auto-skipped for MCP calls.',
@@ -379,4 +383,4 @@ export const worktreesAddMcpTool = {
379
383
  count: z.number().describe('Number of git worktrees created'),
380
384
  },
381
385
  handler: worktreesAdd,
382
- }
386
+ })
@@ -5,7 +5,7 @@ import { getCurrentWorktrees } from 'src/lib/git-utils'
5
5
  import { logger } from 'src/lib/logger'
6
6
  import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
7
7
  import type { ReleaseType } from 'src/lib/release-utils'
8
- import type { ToolsExecutionResult } from 'src/types'
8
+ import { defineMcpTool, textContent } from 'src/types'
9
9
 
10
10
  interface WorktreeInfo {
11
11
  version: string
@@ -16,14 +16,14 @@ interface WorktreeInfo {
16
16
  /**
17
17
  * List all release git worktrees with version, type, and Jira description
18
18
  */
19
- export const worktreesList = async (): Promise<ToolsExecutionResult> => {
19
+ export const worktreesList = async () => {
20
20
  const currentWorktrees = await getCurrentWorktrees('release')
21
21
 
22
22
  if (currentWorktrees.length === 0) {
23
23
  logger.info('ℹ️ No active worktrees found')
24
24
 
25
25
  return {
26
- content: [{ type: 'text', text: JSON.stringify({ worktrees: [], count: 0 }, null, 2) }],
26
+ content: textContent(JSON.stringify({ worktrees: [], count: 0 }, null, 2)),
27
27
  structuredContent: { worktrees: [], count: 0 },
28
28
  }
29
29
  }
@@ -70,18 +70,13 @@ export const worktreesList = async (): Promise<ToolsExecutionResult> => {
70
70
  }
71
71
 
72
72
  return {
73
- content: [
74
- {
75
- type: 'text',
76
- text: JSON.stringify(structuredContent, null, 2),
77
- },
78
- ],
73
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
79
74
  structuredContent,
80
75
  }
81
76
  }
82
77
 
83
78
  // MCP Tool Registration
84
- export const worktreesListMcpTool = {
79
+ export const worktreesListMcpTool = defineMcpTool({
85
80
  name: 'worktrees-list',
86
81
  description:
87
82
  'List existing release-branch worktrees with version, release type (regular / hotfix), and Jira fix-version description. Read-only.',
@@ -99,4 +94,4 @@ export const worktreesListMcpTool = {
99
94
  count: z.number().describe('Number of worktrees'),
100
95
  },
101
96
  handler: worktreesList,
102
- }
97
+ })
@@ -5,10 +5,11 @@ import { buildCmuxWorkspaceTitle, listCmuxWorkspaceTitles, openCmuxWorkspaceWith
5
5
  import { reconcileCursorWorkspaceFolders, resolveCursorWorkspacePath } from 'src/integrations/cursor'
6
6
  import { commandEcho } from 'src/lib/command-echo'
7
7
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
8
+ import { OperationError } from 'src/lib/errors/operation-error'
8
9
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
9
10
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
10
11
  import { logger } from 'src/lib/logger'
11
- import type { ToolsExecutionResult } from 'src/types'
12
+ import { defineMcpTool, textContent } from 'src/types'
12
13
 
13
14
  interface WorktreesOpenResult {
14
15
  openedCmux: string[]
@@ -23,7 +24,7 @@ interface WorktreesOpenResult {
23
24
  * workspace exists per worktree. Idempotent and additive — never removes
24
25
  * worktrees, never recreates running cmux workspaces.
25
26
  */
26
- export const worktreesOpen = async (): Promise<ToolsExecutionResult> => {
27
+ export const worktreesOpen = async () => {
27
28
  commandEcho.start('worktrees-open')
28
29
 
29
30
  try {
@@ -46,12 +47,15 @@ export const worktreesOpen = async (): Promise<ToolsExecutionResult> => {
46
47
  commandEcho.print()
47
48
 
48
49
  return {
49
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
50
+ content: textContent(JSON.stringify(result, null, 2)),
50
51
  structuredContent: { ...result },
51
52
  }
52
53
  } catch (error) {
53
54
  logger.error({ error }, '❌ Error opening worktrees')
54
- throw error
55
+ throw new OperationError(error, {
56
+ operation: 'open worktrees',
57
+ remediation: "run 'worktrees-list' to confirm the branches exist",
58
+ })
55
59
  }
56
60
  }
57
61
 
@@ -180,7 +184,7 @@ const logResults = (result: WorktreesOpenResult, context: LogResultsContext): vo
180
184
  }
181
185
 
182
186
  // MCP Tool Registration
183
- export const worktreesOpenMcpTool = {
187
+ export const worktreesOpenMcpTool = defineMcpTool({
184
188
  name: 'worktrees-open',
185
189
  description:
186
190
  'Open Cursor against the configured workspace file and ensure a cmux workspace exists for each existing release worktree. Idempotent and additive — never removes worktrees, never recreates running cmux workspaces. Use after a cold start (Cursor + cmux closed). For stale-worktree cleanup, use worktrees-sync.',
@@ -194,4 +198,4 @@ export const worktreesOpenMcpTool = {
194
198
  .describe('Number of dangling worktree folders removed from the Cursor workspace file'),
195
199
  },
196
200
  handler: worktreesOpen,
197
- }
201
+ })