@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.
- package/dist/mipham +0 -0
- package/package.json +1 -1
- package/src/agent/background-registry.ts +192 -0
- package/src/agent/message-bus.ts +113 -0
- package/src/agent/sub-agent.ts +101 -3
- package/src/agent/types.ts +4 -0
- package/src/agent-view/agent-view-manager.ts +10 -0
- package/src/agent-view/dashboard.tsx +20 -1
- package/src/core/engine.ts +52 -0
- package/src/core/hooks.ts +48 -0
- package/src/shared/types.ts +16 -1
- package/src/tools/agent/agent.ts +41 -2
- package/src/tools/agent/enter-plan.ts +92 -0
- package/src/tools/agent/exit-plan.ts +54 -0
- package/src/tools/agent/report-findings.ts +186 -0
- package/src/tools/agent/send-message.ts +67 -0
- package/src/tools/agent/workflow.ts +25 -5
- package/src/tools/exec/enter-worktree.ts +142 -0
- package/src/tools/exec/exit-worktree.ts +162 -0
- package/src/tools/exec/task.ts +215 -22
- package/src/tools/index.ts +12 -0
- package/src/ui/app.tsx +49 -19
- package/src/ui/commands.ts +10 -12
- package/src/workflow/primitives/agent.ts +77 -15
- package/src/workflow/runtime.ts +84 -7
- package/src/workflow/schema-validator.ts +162 -0
|
@@ -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
|
+
}
|
package/src/tools/exec/task.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -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),
|