@miphamai/cli 0.8.0 → 0.10.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,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
+ }
@@ -4,13 +4,17 @@ interface Task {
4
4
  id: string
5
5
  subject: string
6
6
  description: string
7
- status: 'pending' | 'in_progress' | 'completed' | 'deleted'
7
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted' | 'failed'
8
8
  activeForm?: string
9
9
  metadata?: Record<string, unknown>
10
10
  blocks: string[]
11
11
  blockedBy: string[]
12
12
  owner?: string
13
13
  createdAt: string
14
+ /** Background task output content (set when a background agent completes). */
15
+ output?: string
16
+ /** Background task error message (set when a background agent fails). */
17
+ outputError?: string
14
18
  }
15
19
 
16
20
  const tasks = new Map<string, Task>()
@@ -20,14 +24,33 @@ function formatTask(t: Task): string {
20
24
  const meta = t.metadata ? ` ${JSON.stringify(t.metadata)}` : ''
21
25
  const blocks = t.blocks.length ? ` blocks:[${t.blocks.join(',')}]` : ''
22
26
  const blocked = t.blockedBy.length ? ` waits:[${t.blockedBy.join(',')}]` : ''
23
- return `[${t.status}] #${t.id}: ${t.subject}${blocks}${blocked}${meta}`
27
+ const owner = t.owner ? ` @${t.owner}` : ''
28
+ return `[${t.status}] #${t.id}: ${t.subject}${owner}${blocks}${blocked}${meta}`
29
+ }
30
+
31
+ /** Check if a task is blocked — has unresolved dependencies. */
32
+ function isBlocked(task: Task): boolean {
33
+ if (task.blockedBy.length === 0) return false
34
+ return task.blockedBy.some((depId) => {
35
+ const dep = tasks.get(depId)
36
+ return dep && dep.status !== 'completed' && dep.status !== 'deleted'
37
+ })
38
+ }
39
+
40
+ /** Get the list of blocking task IDs (unresolved dependencies). */
41
+ function getBlockingIds(task: Task): string[] {
42
+ return task.blockedBy.filter((depId) => {
43
+ const dep = tasks.get(depId)
44
+ return dep && dep.status !== 'completed' && dep.status !== 'deleted'
45
+ })
24
46
  }
25
47
 
26
48
  export const taskTool: ToolDefinition = {
27
49
  name: 'Task',
28
50
  description:
29
51
  'Create and manage structured task lists for tracking progress. ' +
30
- 'Supports CRUD, dependencies (blocks/blockedBy), metadata, owner assignment, and status workflow: pending → in_progress → completed. ' +
52
+ 'Supports CRUD, dependencies (blocks/blockedBy), metadata, owner assignment, ' +
53
+ 'background task output/stop, and status workflow: pending → in_progress → completed. ' +
31
54
  'Use for complex multi-step tasks, session tracking, and organizing work.',
32
55
  category: 'exec',
33
56
  permission: 'auto',
@@ -36,9 +59,9 @@ export const taskTool: ToolDefinition = {
36
59
  properties: {
37
60
  action: {
38
61
  type: 'string',
39
- enum: ['create', 'list', 'update', 'get', 'delete'],
62
+ enum: ['create', 'list', 'update', 'get', 'delete', 'output', 'stop'],
40
63
  description:
41
- 'Action: create a task, list all, update status/fields, get one by ID, or delete.',
64
+ 'Action: create a task, list all, update status/fields, get one by ID, delete, view output of background task, or stop a running task.',
42
65
  },
43
66
  subject: { type: 'string', description: 'A brief, actionable title (for create/update).' },
44
67
  description: { type: 'string', description: 'What needs to be done (for create/update).' },
@@ -46,21 +69,24 @@ export const taskTool: ToolDefinition = {
46
69
  type: 'string',
47
70
  description: 'Present continuous form shown during work (for create/update).',
48
71
  },
49
- taskId: { type: 'string', description: 'Task ID (for update/get/delete).' },
72
+ taskId: { type: 'string', description: 'Task ID (for update/get/delete/output/stop).' },
50
73
  status: {
51
74
  type: 'string',
52
- enum: ['pending', 'in_progress', 'completed', 'deleted'],
53
- description: 'New status. "deleted" permanently removes the task.',
75
+ enum: ['pending', 'in_progress', 'completed', 'deleted', 'failed'],
76
+ description:
77
+ 'New status. "deleted" permanently removes the task, "failed" marks as failed.',
54
78
  },
55
79
  addBlocks: {
56
80
  type: 'array',
57
81
  items: { type: 'string' },
58
- description: 'Task IDs that this task blocks (they depend on this one).',
82
+ description:
83
+ 'Task IDs that this task blocks (they depend on this one). Use for create or update.',
59
84
  },
60
85
  addBlockedBy: {
61
86
  type: 'array',
62
87
  items: { type: 'string' },
63
- description: 'Task IDs that block this task (this one depends on them).',
88
+ description:
89
+ 'Task IDs that block this task (this one depends on them). Use for create or update.',
64
90
  },
65
91
  owner: { type: 'string', description: 'Agent or user name assigned to this task.' },
66
92
  metadata: {
@@ -90,8 +116,24 @@ export const taskTool: ToolDefinition = {
90
116
  owner: (params.owner as string) || undefined,
91
117
  createdAt: new Date().toISOString(),
92
118
  }
119
+
120
+ // Accept initial dependencies at creation time
121
+ if (Array.isArray(params.addBlocks)) {
122
+ for (const bid of params.addBlocks as string[]) {
123
+ if (!task.blocks.includes(bid)) task.blocks.push(bid)
124
+ }
125
+ }
126
+ if (Array.isArray(params.addBlockedBy)) {
127
+ for (const bid of params.addBlockedBy as string[]) {
128
+ if (!task.blockedBy.includes(bid)) task.blockedBy.push(bid)
129
+ }
130
+ }
131
+
93
132
  tasks.set(id, task)
94
- return { success: true, content: `Task #${id} created: ${subject}` }
133
+ const blockedNote = isBlocked(task)
134
+ ? ` (blocked — waiting on: ${getBlockingIds(task).join(', ')})`
135
+ : ''
136
+ return { success: true, content: `Task #${id} created: ${subject}${blockedNote}` }
95
137
  }
96
138
 
97
139
  // ── LIST ──
@@ -99,18 +141,63 @@ export const taskTool: ToolDefinition = {
99
141
  const all = Array.from(tasks.values()).filter((t) => t.status !== 'deleted')
100
142
  if (all.length === 0) return { success: true, content: '(no tasks)' }
101
143
 
102
- // Group by status
103
- const groups: Record<string, Task[]> = { pending: [], in_progress: [], completed: [] }
104
- for (const t of all) groups[t.status]?.push(t)
144
+ // Sort by availability: in_progress → pending(available) → pending(blocked) → completed
145
+ const sortOrder: Record<string, number> = {
146
+ in_progress: 0,
147
+ pending: 1,
148
+ completed: 2,
149
+ failed: 3,
150
+ }
151
+
152
+ const sorted = [...all].sort((a, b) => {
153
+ const orderA = sortOrder[a.status] ?? 3
154
+ const orderB = sortOrder[b.status] ?? 3
155
+ if (orderA !== orderB) return orderA - orderB
156
+ // Within same status: available before blocked
157
+ if (a.status === 'pending' && b.status === 'pending') {
158
+ const aBlocked = isBlocked(a)
159
+ const bBlocked = isBlocked(b)
160
+ if (aBlocked !== bBlocked) return aBlocked ? 1 : -1
161
+ }
162
+ return 0
163
+ })
105
164
 
106
165
  const lines: string[] = []
107
- for (const status of ['in_progress', 'pending', 'completed'] as const) {
108
- const group = groups[status]!
109
- if (group.length === 0) continue
110
- const emoji = status === 'in_progress' ? '🔄' : status === 'pending' ? '📋' : '✅'
111
- lines.push(`${emoji} ${status.replace('_', ' ')} (${group.length}):`)
112
- for (const t of group) lines.push(` ${formatTask(t)}`)
166
+ const statusLabels: Record<string, { emoji: string; label: string }> = {
167
+ in_progress: { emoji: '🔄', label: 'In Progress' },
168
+ pending: { emoji: '📋', label: 'Pending' },
169
+ completed: { emoji: '✅', label: 'Completed' },
170
+ failed: { emoji: '❌', label: 'Failed' },
171
+ }
172
+
173
+ let currentStatus = ''
174
+ for (const t of sorted) {
175
+ const statusInfo = statusLabels[t.status]
176
+ if (!statusInfo) continue
177
+
178
+ if (statusInfo.label !== currentStatus) {
179
+ currentStatus = statusInfo.label
180
+ // Count tasks in this status group
181
+ const count = sorted.filter((s) => statusLabels[s.status]?.label === currentStatus).length
182
+ lines.push(`${statusInfo.emoji} ${currentStatus} (${count}):`)
183
+ }
184
+
185
+ const blocked = isBlocked(t)
186
+ const prefix = blocked && t.status === 'pending' ? '🚫 ' : ' '
187
+ const blockingNote = blocked ? ` 🚫 阻塞中 (等待: ${getBlockingIds(t).join(', ')})` : ''
188
+ lines.push(`${prefix}${formatTask(t)}${blockingNote}`)
113
189
  }
190
+
191
+ // Summary footer
192
+ const available = all.filter((t) => t.status === 'pending' && !isBlocked(t)).length
193
+ const blockedCount = all.filter((t) => t.status === 'pending' && isBlocked(t)).length
194
+ if (blockedCount > 0) {
195
+ lines.push('')
196
+ lines.push(
197
+ `📊 ${available} available · ${blockedCount} blocked · ${all.filter((t) => t.status === 'completed').length} done`,
198
+ )
199
+ }
200
+
114
201
  return { success: true, content: lines.join('\n') }
115
202
  }
116
203
 
@@ -129,8 +216,27 @@ export const taskTool: ToolDefinition = {
129
216
  if (task.activeForm) lines.push(`Active form: ${task.activeForm}`)
130
217
  if (task.owner) lines.push(`Owner: ${task.owner}`)
131
218
  if (task.blocks.length) lines.push(`Blocks: ${task.blocks.join(', ')}`)
132
- if (task.blockedBy.length) lines.push(`Blocked by: ${task.blockedBy.join(', ')}`)
219
+ if (task.blockedBy.length) {
220
+ const blocking = getBlockingIds(task)
221
+ if (blocking.length > 0) {
222
+ lines.push(
223
+ `Blocked by: ${task.blockedBy.join(', ')} (active blockers: ${blocking.join(', ')})`,
224
+ )
225
+ } else {
226
+ lines.push(`Blocked by: ${task.blockedBy.join(', ')} (all resolved ✓)`)
227
+ }
228
+ }
133
229
  if (task.metadata) lines.push(`Metadata: ${JSON.stringify(task.metadata)}`)
230
+ if (task.output) {
231
+ lines.push('')
232
+ lines.push('── Output ──')
233
+ lines.push(task.output.slice(0, 2000))
234
+ }
235
+ if (task.outputError) {
236
+ lines.push('')
237
+ lines.push(`── Error ──`)
238
+ lines.push(task.outputError)
239
+ }
134
240
 
135
241
  return { success: true, content: lines.join('\n') }
136
242
  }
@@ -167,7 +273,14 @@ export const taskTool: ToolDefinition = {
167
273
  }
168
274
  }
169
275
 
170
- return { success: true, content: `Task #${taskId} updated.\n${formatTask(task)}` }
276
+ const blockedNote = isBlocked(task)
277
+ ? ` (blocked — waiting on: ${getBlockingIds(task).join(', ')})`
278
+ : ''
279
+
280
+ return {
281
+ success: true,
282
+ content: `Task #${taskId} updated.\n${formatTask(task)}${blockedNote}`,
283
+ }
171
284
  }
172
285
 
173
286
  // ── DELETE ──
@@ -179,6 +292,86 @@ export const taskTool: ToolDefinition = {
179
292
  return { success: true, content: `Task #${taskId} deleted.` }
180
293
  }
181
294
 
295
+ // ── OUTPUT ──
296
+ if (action === 'output') {
297
+ const taskId = params.taskId as string
298
+ const task = tasks.get(taskId)
299
+ if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
300
+
301
+ if (task.status === 'pending') {
302
+ return {
303
+ success: true,
304
+ content: `Task #${taskId} is still pending — no output yet.\nStatus: ${task.status}\nSubject: ${task.subject}`,
305
+ }
306
+ }
307
+
308
+ if (task.status === 'in_progress') {
309
+ return {
310
+ success: true,
311
+ content: `Task #${taskId} is still running — output not yet available.\nStatus: in_progress\nSubject: ${task.subject}\n\nUse Task output again once the task completes.`,
312
+ }
313
+ }
314
+
315
+ if (task.status === 'completed' && task.output) {
316
+ return {
317
+ success: true,
318
+ content: `── Task #${taskId} Output ──\nStatus: completed\nSubject: ${task.subject}\n\n${task.output.slice(0, 5000)}`,
319
+ }
320
+ }
321
+
322
+ if (task.status === 'failed' && task.outputError) {
323
+ return {
324
+ success: false,
325
+ content: '',
326
+ error: `Task #${taskId} failed: ${task.outputError}`,
327
+ }
328
+ }
329
+
330
+ return {
331
+ success: true,
332
+ content: `Task #${taskId} — Status: ${task.status}\nSubject: ${task.subject}\n\n(no output recorded)`,
333
+ }
334
+ }
335
+
336
+ // ── STOP ──
337
+ if (action === 'stop') {
338
+ const taskId = params.taskId as string
339
+ const task = tasks.get(taskId)
340
+ if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
341
+
342
+ if (task.status === 'completed' || task.status === 'deleted') {
343
+ return {
344
+ success: true,
345
+ content: `Task #${taskId} is already ${task.status}. Nothing to stop.`,
346
+ }
347
+ }
348
+
349
+ if (task.status === 'pending') {
350
+ // Cancel a pending task
351
+ task.status = 'deleted'
352
+ return { success: true, content: `Task #${taskId} cancelled (was pending).` }
353
+ }
354
+
355
+ // For in_progress tasks: mark as failed — actual process termination is handled
356
+ // by BackgroundAgentRegistry.stop() when wired via the engine context
357
+ task.status = 'failed'
358
+ task.outputError = 'Task stopped by user.'
359
+
360
+ // If BackgroundAgentRegistry is available in context, try to abort
361
+ if (_ctx.registry) {
362
+ // Signal to any background registry that this task should stop
363
+ // The background registry will be injected via the engine context
364
+ const bgRegistry = (_ctx as unknown as Record<string, unknown>).backgroundAgentRegistry as
365
+ | { stop(id: string): boolean }
366
+ | undefined
367
+ if (bgRegistry) {
368
+ bgRegistry.stop(taskId)
369
+ }
370
+ }
371
+
372
+ return { success: true, content: `Task #${taskId} stopped.` }
373
+ }
374
+
182
375
  return { success: false, content: '', error: `Unknown action: ${action}` }
183
376
  },
184
377
  }
@@ -7,9 +7,13 @@ import { grepTool } from './file/grep'
7
7
  import { bashTool } from './exec/bash'
8
8
  import { gitTool } from './exec/git'
9
9
  import { taskTool } from './exec/task'
10
+ import { enterWorktreeTool } from './exec/enter-worktree'
11
+ import { exitWorktreeTool } from './exec/exit-worktree'
10
12
  import { agentTool } from './agent/agent'
11
13
  import { skillTool } from './agent/skill'
12
14
  import { planTool } from './agent/plan'
15
+ import { enterPlanModeTool } from './agent/enter-plan'
16
+ import { exitPlanModeTool } from './agent/exit-plan'
13
17
  import { memoryTool } from './agent/memory'
14
18
  import { workflowTool } from './agent/workflow'
15
19
  import { webFetchTool } from './network/web-fetch'
@@ -17,6 +21,8 @@ import { webSearchTool } from './network/web-search'
17
21
  import { configTool } from './system/config'
18
22
  import { mcpTool } from './system/mcp'
19
23
  import { artifactTool } from './artifact/artifact'
24
+ import { reportFindingsTool } from './agent/report-findings'
25
+ import { sendMessageTool } from './agent/send-message'
20
26
  import { computerUseTool } from './computer/computer-use'
21
27
  import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
22
28
  import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
@@ -110,12 +116,18 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
110
116
  withValidation(bashTool),
111
117
  withValidation(gitTool),
112
118
  withValidation(taskTool),
119
+ withValidation(enterWorktreeTool),
120
+ withValidation(exitWorktreeTool),
113
121
  // Agent tools
114
122
  withValidation(agentTool),
115
123
  withValidation(skillTool),
116
124
  withValidation(planTool),
125
+ withValidation(enterPlanModeTool),
126
+ withValidation(exitPlanModeTool),
117
127
  withValidation(memoryTool),
118
128
  withValidation(workflowTool),
129
+ withValidation(reportFindingsTool),
130
+ withValidation(sendMessageTool),
119
131
  // Network tools
120
132
  withValidation(webFetchTool),
121
133
  withValidation(webSearchTool),