@miphamai/cli 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const exitPlanModeTool: ToolDefinition = {
4
+ name: 'ExitPlanMode',
5
+ description:
6
+ 'Exit plan mode and submit your plan for approval. ' +
7
+ 'Set approved: true to switch to acceptEdits mode (code changes allowed). ' +
8
+ 'Set approved: false to revert to default mode. ' +
9
+ 'Use this after you have finished designing your approach in plan mode.',
10
+ category: 'agent',
11
+ permission: 'auto',
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ approved: {
16
+ type: 'boolean',
17
+ description:
18
+ 'Whether the user approved the plan. true → switch to acceptEdits mode. false → revert to default.',
19
+ },
20
+ },
21
+ required: ['approved'],
22
+ },
23
+ async execute(params, _ctx) {
24
+ const approved = params.approved === true
25
+
26
+ if (approved) {
27
+ return {
28
+ success: true,
29
+ content: [
30
+ '── Plan Approved ──',
31
+ '',
32
+ '✓ Exiting plan mode.',
33
+ '✓ Switching to acceptEdits mode — reads and file edits are auto-approved.',
34
+ '',
35
+ 'You can now implement the plan. The plan file is in .mipham/plans/.',
36
+ '',
37
+ 'Use Shift+Tab to cycle permission modes if you need to change.',
38
+ ].join('\n'),
39
+ }
40
+ }
41
+
42
+ return {
43
+ success: true,
44
+ content: [
45
+ '── Plan Mode Exited ──',
46
+ '',
47
+ '✓ Returning to default permission mode.',
48
+ '',
49
+ 'No code changes were made. The plan file is preserved in .mipham/plans/.',
50
+ 'Use EnterPlanMode again when ready to resume planning.',
51
+ ].join('\n'),
52
+ }
53
+ },
54
+ }
@@ -0,0 +1,186 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const reportFindingsTool: ToolDefinition = {
4
+ name: 'ReportFindings',
5
+ description:
6
+ 'Report code-review findings as a typed list. ' +
7
+ 'Use this to output structured review results with file, line, summary, ' +
8
+ 'failure_scenario, and category. Findings are ranked most-severe first.',
9
+ category: 'agent',
10
+ permission: 'auto',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ level: {
15
+ type: 'string',
16
+ enum: ['low', 'medium', 'high', 'xhigh', 'max'],
17
+ description: 'Effort level the review ran at.',
18
+ },
19
+ findings: {
20
+ type: 'array',
21
+ description: 'Verified findings, most-severe first; empty if none survived.',
22
+ minItems: 0,
23
+ maxItems: 32,
24
+ items: {
25
+ type: 'object',
26
+ properties: {
27
+ file: {
28
+ type: 'string',
29
+ description: 'Repo-relative path of the file the finding is in.',
30
+ },
31
+ line: {
32
+ type: 'integer',
33
+ description: '1-indexed line the finding anchors to.',
34
+ },
35
+ summary: {
36
+ type: 'string',
37
+ description: 'One-sentence statement of the defect.',
38
+ },
39
+ short_summary: {
40
+ type: 'string',
41
+ maxLength: 60,
42
+ description: 'Compressed label for compact UI (≤60 chars).',
43
+ },
44
+ failure_scenario: {
45
+ type: 'string',
46
+ description: 'Concrete inputs/state → wrong output/crash.',
47
+ },
48
+ category: {
49
+ type: 'string',
50
+ maxLength: 40,
51
+ description:
52
+ 'Short kebab-case slug: correctness, security, performance, simplification, test-coverage, etc.',
53
+ },
54
+ verdict: {
55
+ type: 'string',
56
+ enum: ['CONFIRMED', 'PLAUSIBLE'],
57
+ description: 'Set when a verify pass ran; absent on inline-only reviews.',
58
+ },
59
+ outcome: {
60
+ type: 'string',
61
+ enum: ['fixed', 'skipped', 'no_change_needed'],
62
+ description:
63
+ 'Set ONLY when re-reporting after applying fixes: what happened to this finding.',
64
+ },
65
+ },
66
+ required: ['file', 'summary', 'failure_scenario'],
67
+ },
68
+ },
69
+ },
70
+ required: ['findings'],
71
+ },
72
+
73
+ async execute(params, _ctx) {
74
+ const findings = params.findings as Array<Record<string, unknown>> | undefined
75
+ const level = (params.level as string) || 'medium'
76
+
77
+ if (!Array.isArray(findings)) {
78
+ return {
79
+ success: false,
80
+ content: '',
81
+ error: 'findings must be an array',
82
+ }
83
+ }
84
+
85
+ // Validate each finding has required fields
86
+ const errors: string[] = []
87
+ for (let i = 0; i < findings.length; i++) {
88
+ const f = findings[i]!
89
+ if (!f.file || typeof f.file !== 'string') {
90
+ errors.push(`findings[${i}]: "file" is required and must be a string`)
91
+ }
92
+ if (!f.summary || typeof f.summary !== 'string') {
93
+ errors.push(`findings[${i}]: "summary" is required and must be a string`)
94
+ }
95
+ if (!f.failure_scenario || typeof f.failure_scenario !== 'string') {
96
+ errors.push(`findings[${i}]: "failure_scenario" is required and must be a string`)
97
+ }
98
+ if (f.line !== undefined && typeof f.line !== 'number') {
99
+ errors.push(`findings[${i}]: "line" must be an integer if provided`)
100
+ }
101
+ if (f.verdict && !['CONFIRMED', 'PLAUSIBLE'].includes(f.verdict as string)) {
102
+ errors.push(`findings[${i}]: "verdict" must be CONFIRMED or PLAUSIBLE`)
103
+ }
104
+ if (f.outcome && !['fixed', 'skipped', 'no_change_needed'].includes(f.outcome as string)) {
105
+ errors.push(`findings[${i}]: "outcome" must be fixed, skipped, or no_change_needed`)
106
+ }
107
+ }
108
+
109
+ if (errors.length > 0) {
110
+ return {
111
+ success: false,
112
+ content: '',
113
+ error: `Validation errors:\n${errors.map((e) => ` • ${e}`).join('\n')}`,
114
+ }
115
+ }
116
+
117
+ if (findings.length === 0) {
118
+ return {
119
+ success: true,
120
+ content: `── Review Findings (${level} effort) ──\n\n✅ No findings to report.\n\nAll checks passed at ${level} effort level.`,
121
+ }
122
+ }
123
+
124
+ // Format findings as a structured report
125
+ const lines: string[] = [
126
+ `── Review Findings (${level} effort) ──`,
127
+ '',
128
+ `${findings.length} finding${findings.length === 1 ? '' : 's'}:`,
129
+ '',
130
+ ]
131
+
132
+ // Group by category for readability
133
+ const byCategory = new Map<string, Array<Record<string, unknown>>>()
134
+ for (const f of findings) {
135
+ const cat = (f.category as string) || 'uncategorized'
136
+ const list = byCategory.get(cat) || []
137
+ list.push(f)
138
+ byCategory.set(cat, list)
139
+ }
140
+
141
+ const CATEGORY_ICONS: Record<string, string> = {
142
+ correctness: '🔴',
143
+ security: '🔒',
144
+ performance: '⚡',
145
+ simplification: '🧹',
146
+ efficiency: '⏱️',
147
+ 'test-coverage': '🧪',
148
+ architecture: '🏗️',
149
+ maintainability: '🔧',
150
+ }
151
+
152
+ for (const [category, items] of byCategory) {
153
+ const icon = CATEGORY_ICONS[category] || '📌'
154
+ lines.push(`${icon} ${category} (${items.length}):`)
155
+ for (const f of items) {
156
+ const verdict = f.verdict ? ` [${f.verdict}]` : ''
157
+ const outcome = f.outcome ? ` → ${f.outcome}` : ''
158
+ const lineRef = f.line ? `:${f.line}` : ''
159
+ lines.push(` • ${f.file}${lineRef}${verdict}${outcome}`)
160
+ lines.push(` ${f.summary}`)
161
+ if (f.failure_scenario) {
162
+ const scenario =
163
+ (f.failure_scenario as string).length > 120
164
+ ? (f.failure_scenario as string).slice(0, 120) + '...'
165
+ : (f.failure_scenario as string)
166
+ lines.push(` 💥 ${scenario}`)
167
+ }
168
+ }
169
+ lines.push('')
170
+ }
171
+
172
+ // Severity summary
173
+ const confirmed = findings.filter((f) => f.verdict === 'CONFIRMED').length
174
+ const plausible = findings.filter((f) => f.verdict === 'PLAUSIBLE').length
175
+ if (confirmed > 0 || plausible > 0) {
176
+ lines.push(
177
+ `📊 ${confirmed} CONFIRMED · ${plausible} PLAUSIBLE · ${findings.length - confirmed - plausible} unverified`,
178
+ )
179
+ }
180
+
181
+ return {
182
+ success: true,
183
+ content: lines.join('\n'),
184
+ }
185
+ },
186
+ }
@@ -0,0 +1,67 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+ import { getMessageBus } from '../../agent/message-bus'
3
+
4
+ export const sendMessageTool: ToolDefinition = {
5
+ name: 'SendMessage',
6
+ description:
7
+ 'Send a message to another agent or the main conversation. ' +
8
+ 'Use "main" as the recipient to message the parent session, ' +
9
+ 'or a background task ID (e.g., "bg-1-xxx") to message a specific agent. ' +
10
+ 'Messages are stored in the AgentMessageBus and can be polled by the recipient.',
11
+ category: 'agent',
12
+ permission: 'auto',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ to: {
17
+ type: 'string',
18
+ description:
19
+ 'Recipient: "main" for the parent conversation, a background task ID, or an agent name.',
20
+ },
21
+ summary: {
22
+ type: 'string',
23
+ description: 'A 5-10 word summary shown as a one-line preview (max 200 chars).',
24
+ },
25
+ message: {
26
+ type: 'string',
27
+ description: 'Plain text message content.',
28
+ },
29
+ },
30
+ required: ['to', 'message'],
31
+ },
32
+ async execute(params, ctx) {
33
+ const to = params.to as string
34
+ const summary = (params.summary as string) || '(no subject)'
35
+ const message = params.message as string
36
+
37
+ // Determine sender: use sessionId or 'main'
38
+ const from =
39
+ ctx.sessionId === 'sub-agent'
40
+ ? `sub-agent-${Date.now().toString(36)}`
41
+ : ctx.sessionId || 'main'
42
+
43
+ try {
44
+ const bus = getMessageBus()
45
+ const msgId = bus.post(from, to, summary, message)
46
+
47
+ const unreadForRecipient = bus.unreadCount(to)
48
+
49
+ return {
50
+ success: true,
51
+ content:
52
+ `── Message Sent ──\n\n` +
53
+ `ID: ${msgId}\n` +
54
+ `From: ${from}\n` +
55
+ `To: ${to}\n` +
56
+ `Summary: ${summary.slice(0, 100)}\n\n` +
57
+ `The recipient has ${unreadForRecipient} unread message(s).`,
58
+ }
59
+ } catch (err) {
60
+ return {
61
+ success: false,
62
+ content: '',
63
+ error: `Failed to send message: ${String(err)}`,
64
+ }
65
+ }
66
+ },
67
+ }
@@ -5,7 +5,9 @@ import type { QueryEngine } from '../../core/engine'
5
5
  export const workflowTool: ToolDefinition = {
6
6
  name: 'Workflow',
7
7
  description:
8
- 'Execute a multi-agent workflow script. The script uses agent(), parallel(), pipeline(), phase(), log(), args, budget primitives. Use for complex orchestrated tasks.',
8
+ 'Execute a multi-agent workflow script. The script uses agent(), parallel(), pipeline(), phase(), log(), args, budget primitives. ' +
9
+ 'Pass resumeFromRunId to resume a prior run — cached agent() calls are replayed from the journal, ' +
10
+ 'and only new/changed calls execute live.',
9
11
  category: 'agent',
10
12
  permission: 'ask',
11
13
  parameters: {
@@ -20,12 +22,20 @@ export const workflowTool: ToolDefinition = {
20
22
  type: 'object',
21
23
  description: 'Arguments to pass to the workflow script as the `args` global',
22
24
  },
25
+ resumeFromRunId: {
26
+ type: 'string',
27
+ description:
28
+ 'Run ID of a prior Workflow invocation to resume from. ' +
29
+ 'Completed agent() calls with unchanged (prompt, opts) return cached results; ' +
30
+ 'only edited or new calls re-run.',
31
+ },
23
32
  },
24
33
  required: ['script'],
25
34
  },
26
35
  async execute(params, ctx) {
27
36
  const script = params.script as string
28
37
  const args = (params.args as Record<string, unknown>) || {}
38
+ const resumeFromRunId = params.resumeFromRunId as string | undefined
29
39
 
30
40
  if (!ctx.registry || !ctx.toolRegistry) {
31
41
  return {
@@ -42,11 +52,21 @@ export const workflowTool: ToolDefinition = {
42
52
  } as QueryEngine
43
53
 
44
54
  try {
45
- const { runId, result } = await runWorkflow(script, engineStub, args)
46
- return {
47
- success: true,
48
- content: `Workflow ${runId} completed.\n\nResult:\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}`,
55
+ const { runId, result, cacheHits, cacheMisses } = await runWorkflow(
56
+ script,
57
+ engineStub,
58
+ args,
59
+ null,
60
+ resumeFromRunId,
61
+ )
62
+
63
+ let content = `Workflow ${runId} completed.\n\n`
64
+ if (resumeFromRunId) {
65
+ content += `Cache: ${cacheHits} hits · ${cacheMisses} live\n\n`
49
66
  }
67
+ content += `Result:\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}`
68
+
69
+ return { success: true, content }
50
70
  } catch (err) {
51
71
  return {
52
72
  success: false,
@@ -0,0 +1,142 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const enterWorktreeTool: ToolDefinition = {
4
+ name: 'EnterWorktree',
5
+ description:
6
+ 'Create an isolated git worktree for parallel development. ' +
7
+ 'Creates a new worktree at .claude/worktrees/<name> on its own branch. ' +
8
+ 'Use this when you need to work on a separate task without affecting the main workspace. ' +
9
+ 'Pair with ExitWorktree to clean up when done.',
10
+ category: 'exec',
11
+ permission: 'ask',
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ name: {
16
+ type: 'string',
17
+ description:
18
+ 'Name for the new worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total.',
19
+ },
20
+ baseRef: {
21
+ type: 'string',
22
+ description:
23
+ 'Base branch or ref to create from. Default: the current HEAD. Use "origin/main" or a branch name to start from a specific point.',
24
+ },
25
+ },
26
+ required: ['name'],
27
+ },
28
+ async execute(params, ctx) {
29
+ const name = params.name as string
30
+ const baseRef = (params.baseRef as string) || 'HEAD'
31
+
32
+ // Validate name
33
+ if (name.length > 64) {
34
+ return {
35
+ success: false,
36
+ content: '',
37
+ error: 'Worktree name must be 64 characters or fewer.',
38
+ }
39
+ }
40
+ if (!/^[a-zA-Z0-9._/-]+$/.test(name)) {
41
+ return {
42
+ success: false,
43
+ content: '',
44
+ error:
45
+ 'Worktree name may only contain letters, digits, dots, underscores, dashes, and slashes.',
46
+ }
47
+ }
48
+
49
+ const cwd = ctx.cwd
50
+ const worktreePath = `${cwd}/.claude/worktrees/${name}`
51
+
52
+ try {
53
+ // Validate we're in a git repo
54
+ const gitCheck = Bun.spawn(['git', 'rev-parse', '--git-dir'], {
55
+ cwd,
56
+ stdout: 'pipe',
57
+ stderr: 'pipe',
58
+ })
59
+ const gitCheckExit = await gitCheck.exited
60
+ if (gitCheckExit !== 0) {
61
+ return {
62
+ success: false,
63
+ content: '',
64
+ error: 'Not in a git repository. EnterWorktree requires a git working directory.',
65
+ }
66
+ }
67
+
68
+ // Check if worktree already exists
69
+ const checkProc = Bun.spawn(['git', 'worktree', 'list', '--porcelain'], {
70
+ cwd,
71
+ stdout: 'pipe',
72
+ stderr: 'pipe',
73
+ })
74
+ const existingWorktrees = await new Response(checkProc.stdout).text()
75
+ if (existingWorktrees.includes(worktreePath)) {
76
+ return {
77
+ success: true,
78
+ content:
79
+ `── Worktree Already Exists ──\n\n` +
80
+ `Path: ${worktreePath}\n` +
81
+ `Name: ${name}\n\n` +
82
+ `This worktree already exists. To use it:\n` +
83
+ ` cd ${worktreePath}\n\n` +
84
+ `To remove it, use ExitWorktree.`,
85
+ }
86
+ }
87
+
88
+ // Determine base ref
89
+ let resolvedBaseRef = baseRef
90
+ if (baseRef === 'HEAD') {
91
+ try {
92
+ const branchProc = Bun.spawn(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
93
+ cwd,
94
+ stdout: 'pipe',
95
+ stderr: 'pipe',
96
+ })
97
+ const branch = (await new Response(branchProc.stdout).text()).trim()
98
+ resolvedBaseRef = branch || 'HEAD'
99
+ } catch {
100
+ resolvedBaseRef = 'HEAD'
101
+ }
102
+ }
103
+
104
+ // Create worktree with new branch
105
+ const branchName = `worktree/${name}`
106
+ const proc = Bun.spawn(
107
+ ['git', 'worktree', 'add', '-b', branchName, worktreePath, resolvedBaseRef],
108
+ { cwd, stdout: 'pipe', stderr: 'pipe' },
109
+ )
110
+ const stdout = await new Response(proc.stdout).text()
111
+ const exitCode = await proc.exited
112
+
113
+ if (exitCode !== 0) {
114
+ const stderr = await new Response(proc.stderr).text()
115
+ return {
116
+ success: false,
117
+ content: '',
118
+ error: `Failed to create worktree: ${stderr.slice(0, 1000)}`,
119
+ }
120
+ }
121
+
122
+ return {
123
+ success: true,
124
+ content:
125
+ `── Worktree Created ──\n\n` +
126
+ `Path: ${worktreePath}\n` +
127
+ `Branch: ${branchName}\n` +
128
+ `Base: ${resolvedBaseRef}\n\n` +
129
+ `The worktree is ready. To switch to it:\n` +
130
+ ` cd ${worktreePath}\n\n` +
131
+ `Work in the worktree is isolated — changes on "${branchName}" won't affect your main branch.\n` +
132
+ `When done, use ExitWorktree to clean up.`,
133
+ }
134
+ } catch (err) {
135
+ return {
136
+ success: false,
137
+ content: '',
138
+ error: `Worktree creation failed: ${String(err)}`,
139
+ }
140
+ }
141
+ },
142
+ }
@@ -0,0 +1,162 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const exitWorktreeTool: ToolDefinition = {
4
+ name: 'ExitWorktree',
5
+ description:
6
+ 'Remove a git worktree created by EnterWorktree. ' +
7
+ 'Use action "remove" to delete the worktree and its branch, ' +
8
+ 'or "keep" to leave it intact on disk.',
9
+ category: 'exec',
10
+ permission: 'ask',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ path: {
15
+ type: 'string',
16
+ description: 'Absolute path to the worktree to exit. Must be under .claude/worktrees/.',
17
+ },
18
+ action: {
19
+ type: 'string',
20
+ enum: ['keep', 'remove'],
21
+ description:
22
+ '"keep" leaves the worktree and branch on disk. "remove" deletes both the worktree directory and its branch.',
23
+ },
24
+ discard_changes: {
25
+ type: 'boolean',
26
+ description:
27
+ 'Required true when action is "remove" and the worktree has uncommitted files or unmerged commits. The tool will refuse and list them otherwise.',
28
+ },
29
+ },
30
+ required: ['path', 'action'],
31
+ },
32
+ async execute(params, ctx) {
33
+ const worktreePath = params.path as string
34
+ const action = params.action as string
35
+ const discardChanges = params.discard_changes === true
36
+
37
+ // Validate the path is under .claude/worktrees
38
+ const cwd = ctx.cwd
39
+
40
+ try {
41
+ // Verify the worktree exists in git's worktree list
42
+ const listProc = Bun.spawn(['git', 'worktree', 'list', '--porcelain'], {
43
+ cwd,
44
+ stdout: 'pipe',
45
+ stderr: 'pipe',
46
+ })
47
+ const listOutput = await new Response(listProc.stdout).text()
48
+
49
+ if (!listOutput.includes(worktreePath)) {
50
+ return {
51
+ success: false,
52
+ content: '',
53
+ error:
54
+ `Worktree not found: ${worktreePath}\n\n` +
55
+ `This path is not in git's worktree list. It may have already been removed, ` +
56
+ `or it was created outside of EnterWorktree.\n\n` +
57
+ `Use "git worktree list" to see active worktrees.`,
58
+ }
59
+ }
60
+
61
+ if (action === 'keep') {
62
+ return {
63
+ success: true,
64
+ content:
65
+ `── Worktree Kept ──\n\n` +
66
+ `Path: ${worktreePath}\n\n` +
67
+ `The worktree and its branch are preserved on disk.\n` +
68
+ `To remove later: ExitWorktree with action="remove".`,
69
+ }
70
+ }
71
+
72
+ // action === 'remove' — check for uncommitted changes
73
+ if (!discardChanges) {
74
+ const statusProc = Bun.spawn(['git', '-C', worktreePath, 'status', '--porcelain'], {
75
+ cwd,
76
+ stdout: 'pipe',
77
+ stderr: 'pipe',
78
+ })
79
+ const statusOutput = await new Response(statusProc.stdout).text()
80
+ const changedFiles = statusOutput.trim().split('\n').filter(Boolean)
81
+
82
+ if (changedFiles.length > 0) {
83
+ const preview = changedFiles.slice(0, 10).join('\n')
84
+ const extra =
85
+ changedFiles.length > 10 ? `\n... and ${changedFiles.length - 10} more files` : ''
86
+ return {
87
+ success: false,
88
+ content: '',
89
+ error:
90
+ `Worktree has ${changedFiles.length} uncommitted file(s):\n\n` +
91
+ `${preview}${extra}\n\n` +
92
+ `Set discard_changes: true to force removal and discard these changes.\n` +
93
+ `Or commit the changes first, then retry.`,
94
+ }
95
+ }
96
+ }
97
+
98
+ // Get the branch name from worktree metadata
99
+ let branchName = ''
100
+ try {
101
+ const branchProc = Bun.spawn(
102
+ ['git', '-C', worktreePath, 'rev-parse', '--abbrev-ref', 'HEAD'],
103
+ { cwd, stdout: 'pipe', stderr: 'pipe' },
104
+ )
105
+ branchName = (await new Response(branchProc.stdout).text()).trim()
106
+ } catch {
107
+ // Branch detection is best-effort
108
+ }
109
+
110
+ // Remove the worktree
111
+ const removeArgs = discardChanges
112
+ ? ['git', 'worktree', 'remove', '--force', worktreePath]
113
+ : ['git', 'worktree', 'remove', worktreePath]
114
+
115
+ const removeProc = Bun.spawn(removeArgs, {
116
+ cwd,
117
+ stdout: 'pipe',
118
+ stderr: 'pipe',
119
+ })
120
+ const removeExit = await removeProc.exited
121
+
122
+ if (removeExit !== 0) {
123
+ const stderr = await new Response(removeProc.stderr).text()
124
+ return {
125
+ success: false,
126
+ content: '',
127
+ error: `Failed to remove worktree: ${stderr.slice(0, 1000)}`,
128
+ }
129
+ }
130
+
131
+ // Clean up the branch if we have its name
132
+ if (branchName && branchName !== 'HEAD') {
133
+ try {
134
+ const branchProc = Bun.spawn(['git', 'branch', '-D', branchName], {
135
+ cwd,
136
+ stdout: 'pipe',
137
+ stderr: 'pipe',
138
+ })
139
+ await branchProc.exited
140
+ // Best-effort — branch cleanup failure is not fatal
141
+ } catch {
142
+ // Ignore branch cleanup failures
143
+ }
144
+ }
145
+
146
+ return {
147
+ success: true,
148
+ content:
149
+ `── Worktree Removed ──\n\n` +
150
+ `Path: ${worktreePath}\n` +
151
+ (branchName ? `Branch: ${branchName} (deleted)\n` : '') +
152
+ `\nThe worktree has been removed.`,
153
+ }
154
+ } catch (err) {
155
+ return {
156
+ success: false,
157
+ content: '',
158
+ error: `Worktree removal failed: ${String(err)}`,
159
+ }
160
+ }
161
+ },
162
+ }