tuplet 2.7.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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/agent.d.ts +46 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +393 -0
- package/dist/agent.js.map +1 -0
- package/dist/built-in-agents/explore.d.ts +9 -0
- package/dist/built-in-agents/explore.d.ts.map +1 -0
- package/dist/built-in-agents/explore.js +40 -0
- package/dist/built-in-agents/explore.js.map +1 -0
- package/dist/built-in-agents/index.d.ts +15 -0
- package/dist/built-in-agents/index.d.ts.map +1 -0
- package/dist/built-in-agents/index.js +19 -0
- package/dist/built-in-agents/index.js.map +1 -0
- package/dist/built-in-agents/plan.d.ts +10 -0
- package/dist/built-in-agents/plan.d.ts.map +1 -0
- package/dist/built-in-agents/plan.js +62 -0
- package/dist/built-in-agents/plan.js.map +1 -0
- package/dist/built-in-agents/worker.d.ts +9 -0
- package/dist/built-in-agents/worker.d.ts.map +1 -0
- package/dist/built-in-agents/worker.js +53 -0
- package/dist/built-in-agents/worker.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +18 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-manager.d.ts +65 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +272 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/context-manager.test.d.ts +2 -0
- package/dist/context-manager.test.d.ts.map +1 -0
- package/dist/context-manager.test.js +394 -0
- package/dist/context-manager.test.js.map +1 -0
- package/dist/executor.d.ts +29 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +399 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt/index.d.ts +9 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/index.js +10 -0
- package/dist/prompt/index.js.map +1 -0
- package/dist/prompt/main-agent-builder.d.ts +81 -0
- package/dist/prompt/main-agent-builder.d.ts.map +1 -0
- package/dist/prompt/main-agent-builder.js +287 -0
- package/dist/prompt/main-agent-builder.js.map +1 -0
- package/dist/prompt/sub-agent-builder.d.ts +133 -0
- package/dist/prompt/sub-agent-builder.d.ts.map +1 -0
- package/dist/prompt/sub-agent-builder.js +337 -0
- package/dist/prompt/sub-agent-builder.js.map +1 -0
- package/dist/prompt/templates.d.ts +87 -0
- package/dist/prompt/templates.d.ts.map +1 -0
- package/dist/prompt/templates.js +343 -0
- package/dist/prompt/templates.js.map +1 -0
- package/dist/prompt/types.d.ts +159 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/prompt/types.js +5 -0
- package/dist/prompt/types.js.map +1 -0
- package/dist/prompt.d.ts +32 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +86 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers/dataset/base.d.ts +74 -0
- package/dist/providers/dataset/base.d.ts.map +1 -0
- package/dist/providers/dataset/base.js +7 -0
- package/dist/providers/dataset/base.js.map +1 -0
- package/dist/providers/dataset/index.d.ts +8 -0
- package/dist/providers/dataset/index.d.ts.map +1 -0
- package/dist/providers/dataset/index.js +8 -0
- package/dist/providers/dataset/index.js.map +1 -0
- package/dist/providers/dataset/recorder.d.ts +46 -0
- package/dist/providers/dataset/recorder.d.ts.map +1 -0
- package/dist/providers/dataset/recorder.js +105 -0
- package/dist/providers/dataset/recorder.js.map +1 -0
- package/dist/providers/dataset/replayer.d.ts +46 -0
- package/dist/providers/dataset/replayer.d.ts.map +1 -0
- package/dist/providers/dataset/replayer.js +163 -0
- package/dist/providers/dataset/replayer.js.map +1 -0
- package/dist/providers/dataset/tester.d.ts +89 -0
- package/dist/providers/dataset/tester.d.ts.map +1 -0
- package/dist/providers/dataset/tester.js +143 -0
- package/dist/providers/dataset/tester.js.map +1 -0
- package/dist/providers/env/memory.d.ts +14 -0
- package/dist/providers/env/memory.d.ts.map +1 -0
- package/dist/providers/env/memory.js +19 -0
- package/dist/providers/env/memory.js.map +1 -0
- package/dist/providers/index.d.ts +12 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +10 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/llm/base.d.ts +7 -0
- package/dist/providers/llm/base.d.ts.map +1 -0
- package/dist/providers/llm/base.js +7 -0
- package/dist/providers/llm/base.js.map +1 -0
- package/dist/providers/llm/claude.d.ts +32 -0
- package/dist/providers/llm/claude.d.ts.map +1 -0
- package/dist/providers/llm/claude.js +171 -0
- package/dist/providers/llm/claude.js.map +1 -0
- package/dist/providers/llm/openai.d.ts +26 -0
- package/dist/providers/llm/openai.d.ts.map +1 -0
- package/dist/providers/llm/openai.js +174 -0
- package/dist/providers/llm/openai.js.map +1 -0
- package/dist/providers/llm/openrouter.d.ts +43 -0
- package/dist/providers/llm/openrouter.d.ts.map +1 -0
- package/dist/providers/llm/openrouter.js +288 -0
- package/dist/providers/llm/openrouter.js.map +1 -0
- package/dist/providers/logger/base.d.ts +7 -0
- package/dist/providers/logger/base.d.ts.map +1 -0
- package/dist/providers/logger/base.js +7 -0
- package/dist/providers/logger/base.js.map +1 -0
- package/dist/providers/logger/console.d.ts +29 -0
- package/dist/providers/logger/console.d.ts.map +1 -0
- package/dist/providers/logger/console.js +70 -0
- package/dist/providers/logger/console.js.map +1 -0
- package/dist/providers/repository/base.d.ts +7 -0
- package/dist/providers/repository/base.d.ts.map +1 -0
- package/dist/providers/repository/base.js +7 -0
- package/dist/providers/repository/base.js.map +1 -0
- package/dist/providers/repository/memory.d.ts +21 -0
- package/dist/providers/repository/memory.d.ts.map +1 -0
- package/dist/providers/repository/memory.js +50 -0
- package/dist/providers/repository/memory.js.map +1 -0
- package/dist/providers/workspace/file.d.ts +26 -0
- package/dist/providers/workspace/file.d.ts.map +1 -0
- package/dist/providers/workspace/file.js +151 -0
- package/dist/providers/workspace/file.js.map +1 -0
- package/dist/providers/workspace/index.d.ts +7 -0
- package/dist/providers/workspace/index.d.ts.map +1 -0
- package/dist/providers/workspace/index.js +6 -0
- package/dist/providers/workspace/index.js.map +1 -0
- package/dist/providers/workspace/memory.d.ts +26 -0
- package/dist/providers/workspace/memory.d.ts.map +1 -0
- package/dist/providers/workspace/memory.js +136 -0
- package/dist/providers/workspace/memory.js.map +1 -0
- package/dist/providers/workspace/types.d.ts +27 -0
- package/dist/providers/workspace/types.d.ts.map +1 -0
- package/dist/providers/workspace/types.js +8 -0
- package/dist/providers/workspace/types.js.map +1 -0
- package/dist/providers/workspace/workspace-provider.test.d.ts +2 -0
- package/dist/providers/workspace/workspace-provider.test.d.ts.map +1 -0
- package/dist/providers/workspace/workspace-provider.test.js +250 -0
- package/dist/providers/workspace/workspace-provider.test.js.map +1 -0
- package/dist/shell/commands/browse.d.ts +6 -0
- package/dist/shell/commands/browse.d.ts.map +1 -0
- package/dist/shell/commands/browse.js +158 -0
- package/dist/shell/commands/browse.js.map +1 -0
- package/dist/shell/commands/cat.d.ts +6 -0
- package/dist/shell/commands/cat.d.ts.map +1 -0
- package/dist/shell/commands/cat.js +104 -0
- package/dist/shell/commands/cat.js.map +1 -0
- package/dist/shell/commands/curl.d.ts +6 -0
- package/dist/shell/commands/curl.d.ts.map +1 -0
- package/dist/shell/commands/curl.js +190 -0
- package/dist/shell/commands/curl.js.map +1 -0
- package/dist/shell/commands/date.d.ts +6 -0
- package/dist/shell/commands/date.d.ts.map +1 -0
- package/dist/shell/commands/date.js +151 -0
- package/dist/shell/commands/date.js.map +1 -0
- package/dist/shell/commands/echo.d.ts +6 -0
- package/dist/shell/commands/echo.d.ts.map +1 -0
- package/dist/shell/commands/echo.js +48 -0
- package/dist/shell/commands/echo.js.map +1 -0
- package/dist/shell/commands/env.d.ts +8 -0
- package/dist/shell/commands/env.d.ts.map +1 -0
- package/dist/shell/commands/env.js +41 -0
- package/dist/shell/commands/env.js.map +1 -0
- package/dist/shell/commands/file.d.ts +6 -0
- package/dist/shell/commands/file.d.ts.map +1 -0
- package/dist/shell/commands/file.js +213 -0
- package/dist/shell/commands/file.js.map +1 -0
- package/dist/shell/commands/find.d.ts +6 -0
- package/dist/shell/commands/find.d.ts.map +1 -0
- package/dist/shell/commands/find.js +100 -0
- package/dist/shell/commands/find.js.map +1 -0
- package/dist/shell/commands/grep.d.ts +6 -0
- package/dist/shell/commands/grep.d.ts.map +1 -0
- package/dist/shell/commands/grep.js +229 -0
- package/dist/shell/commands/grep.js.map +1 -0
- package/dist/shell/commands/head.d.ts +6 -0
- package/dist/shell/commands/head.d.ts.map +1 -0
- package/dist/shell/commands/head.js +88 -0
- package/dist/shell/commands/head.js.map +1 -0
- package/dist/shell/commands/index.d.ts +25 -0
- package/dist/shell/commands/index.d.ts.map +1 -0
- package/dist/shell/commands/index.js +43 -0
- package/dist/shell/commands/index.js.map +1 -0
- package/dist/shell/commands/jq.d.ts +8 -0
- package/dist/shell/commands/jq.d.ts.map +1 -0
- package/dist/shell/commands/jq.js +233 -0
- package/dist/shell/commands/jq.js.map +1 -0
- package/dist/shell/commands/ls.d.ts +6 -0
- package/dist/shell/commands/ls.d.ts.map +1 -0
- package/dist/shell/commands/ls.js +88 -0
- package/dist/shell/commands/ls.js.map +1 -0
- package/dist/shell/commands/mkdir.d.ts +6 -0
- package/dist/shell/commands/mkdir.d.ts.map +1 -0
- package/dist/shell/commands/mkdir.js +43 -0
- package/dist/shell/commands/mkdir.js.map +1 -0
- package/dist/shell/commands/rm.d.ts +6 -0
- package/dist/shell/commands/rm.d.ts.map +1 -0
- package/dist/shell/commands/rm.js +64 -0
- package/dist/shell/commands/rm.js.map +1 -0
- package/dist/shell/commands/sed.d.ts +6 -0
- package/dist/shell/commands/sed.d.ts.map +1 -0
- package/dist/shell/commands/sed.js +414 -0
- package/dist/shell/commands/sed.js.map +1 -0
- package/dist/shell/commands/sort.d.ts +6 -0
- package/dist/shell/commands/sort.d.ts.map +1 -0
- package/dist/shell/commands/sort.js +109 -0
- package/dist/shell/commands/sort.js.map +1 -0
- package/dist/shell/commands/tail.d.ts +6 -0
- package/dist/shell/commands/tail.d.ts.map +1 -0
- package/dist/shell/commands/tail.js +68 -0
- package/dist/shell/commands/tail.js.map +1 -0
- package/dist/shell/commands/wc.d.ts +6 -0
- package/dist/shell/commands/wc.d.ts.map +1 -0
- package/dist/shell/commands/wc.js +86 -0
- package/dist/shell/commands/wc.js.map +1 -0
- package/dist/shell/index.d.ts +10 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell/index.js +10 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/limits.d.ts +5 -0
- package/dist/shell/limits.d.ts.map +1 -0
- package/dist/shell/limits.js +5 -0
- package/dist/shell/limits.js.map +1 -0
- package/dist/shell/parser.d.ts +8 -0
- package/dist/shell/parser.d.ts.map +1 -0
- package/dist/shell/parser.js +307 -0
- package/dist/shell/parser.js.map +1 -0
- package/dist/shell/path-validation.d.ts +35 -0
- package/dist/shell/path-validation.d.ts.map +1 -0
- package/dist/shell/path-validation.js +81 -0
- package/dist/shell/path-validation.js.map +1 -0
- package/dist/shell/shell.d.ts +66 -0
- package/dist/shell/shell.d.ts.map +1 -0
- package/dist/shell/shell.js +301 -0
- package/dist/shell/shell.js.map +1 -0
- package/dist/shell/shell.test.d.ts +2 -0
- package/dist/shell/shell.test.d.ts.map +1 -0
- package/dist/shell/shell.test.js +1088 -0
- package/dist/shell/shell.test.js.map +1 -0
- package/dist/shell/types.d.ts +82 -0
- package/dist/shell/types.d.ts.map +1 -0
- package/dist/shell/types.js +5 -0
- package/dist/shell/types.js.map +1 -0
- package/dist/summarizer.d.ts +28 -0
- package/dist/summarizer.d.ts.map +1 -0
- package/dist/summarizer.js +136 -0
- package/dist/summarizer.js.map +1 -0
- package/dist/summarizer.test.d.ts +2 -0
- package/dist/summarizer.test.d.ts.map +1 -0
- package/dist/summarizer.test.js +192 -0
- package/dist/summarizer.test.js.map +1 -0
- package/dist/tools/ask-user.d.ts +11 -0
- package/dist/tools/ask-user.d.ts.map +1 -0
- package/dist/tools/ask-user.js +35 -0
- package/dist/tools/ask-user.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/output.d.ts +15 -0
- package/dist/tools/output.d.ts.map +1 -0
- package/dist/tools/output.js +40 -0
- package/dist/tools/output.js.map +1 -0
- package/dist/tools/shell.d.ts +13 -0
- package/dist/tools/shell.d.ts.map +1 -0
- package/dist/tools/shell.js +166 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +25 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +312 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/tasks.d.ts +170 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +947 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/trace/builder.d.ts +54 -0
- package/dist/trace/builder.d.ts.map +1 -0
- package/dist/trace/builder.js +229 -0
- package/dist/trace/builder.js.map +1 -0
- package/dist/trace/console.d.ts +45 -0
- package/dist/trace/console.d.ts.map +1 -0
- package/dist/trace/console.js +143 -0
- package/dist/trace/console.js.map +1 -0
- package/dist/trace/index.d.ts +11 -0
- package/dist/trace/index.d.ts.map +1 -0
- package/dist/trace/index.js +11 -0
- package/dist/trace/index.js.map +1 -0
- package/dist/trace/openrouter-pricing.d.ts +9 -0
- package/dist/trace/openrouter-pricing.d.ts.map +1 -0
- package/dist/trace/openrouter-pricing.js +321 -0
- package/dist/trace/openrouter-pricing.js.map +1 -0
- package/dist/trace/pricing.d.ts +13 -0
- package/dist/trace/pricing.d.ts.map +1 -0
- package/dist/trace/pricing.js +41 -0
- package/dist/trace/pricing.js.map +1 -0
- package/dist/trace/types.d.ts +142 -0
- package/dist/trace/types.d.ts.map +1 -0
- package/dist/trace/types.js +16 -0
- package/dist/trace/types.js.map +1 -0
- package/dist/trace.d.ts +5 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +5 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +382 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +287 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +560 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Management System (Claude Code 4-Tool Approach)
|
|
3
|
+
*
|
|
4
|
+
* Provides task management functionality for agents to track and execute tasks.
|
|
5
|
+
* Uses 4 separate tools: TaskCreate, TaskUpdate, TaskGet, TaskList
|
|
6
|
+
*/
|
|
7
|
+
/** Path where tasks are persisted in Workspace */
|
|
8
|
+
const TASKS_CONTEXT_PATH = '.tuplet/tasks.json';
|
|
9
|
+
/**
|
|
10
|
+
* Format task list for display
|
|
11
|
+
* @param tasks - Array of tasks to format
|
|
12
|
+
* @param allTasks - Optional full task list (for checking blocker status). If not provided, blockedBy status won't be resolved.
|
|
13
|
+
*/
|
|
14
|
+
export function formatTaskList(tasks, allTasks) {
|
|
15
|
+
if (tasks.length === 0) {
|
|
16
|
+
return "No tasks in the task list.";
|
|
17
|
+
}
|
|
18
|
+
const statusEmoji = {
|
|
19
|
+
pending: "⬜",
|
|
20
|
+
in_progress: "🔄",
|
|
21
|
+
completed: "✅",
|
|
22
|
+
};
|
|
23
|
+
// Create a map for quick task lookup
|
|
24
|
+
const taskMap = new Map();
|
|
25
|
+
for (const t of (allTasks || tasks)) {
|
|
26
|
+
taskMap.set(t.id, t);
|
|
27
|
+
}
|
|
28
|
+
// Helper to check if a task has open blockers
|
|
29
|
+
const hasOpenBlockers = (task) => {
|
|
30
|
+
if (!task.blockedBy || task.blockedBy.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
return task.blockedBy.some(blockerId => {
|
|
33
|
+
const blocker = taskMap.get(blockerId);
|
|
34
|
+
return blocker && blocker.status !== 'completed';
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
// Helper to get open blocker IDs
|
|
38
|
+
const getOpenBlockerIds = (task) => {
|
|
39
|
+
if (!task.blockedBy)
|
|
40
|
+
return [];
|
|
41
|
+
return task.blockedBy.filter(blockerId => {
|
|
42
|
+
const blocker = taskMap.get(blockerId);
|
|
43
|
+
return blocker && blocker.status !== 'completed';
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
return tasks
|
|
47
|
+
.map((task) => {
|
|
48
|
+
const emoji = statusEmoji[task.status];
|
|
49
|
+
const ownerTag = task.owner ? ` [@${task.owner}]` : '';
|
|
50
|
+
// Show blocked indicator if task has open blockers
|
|
51
|
+
let blockedTag = '';
|
|
52
|
+
if (task.status !== 'completed' && hasOpenBlockers(task)) {
|
|
53
|
+
const openBlockers = getOpenBlockerIds(task);
|
|
54
|
+
blockedTag = ` (blocked by: ${openBlockers.join(', ')})`;
|
|
55
|
+
}
|
|
56
|
+
// Show activeForm when in_progress, otherwise show subject
|
|
57
|
+
const label = task.status === "in_progress" && task.activeForm
|
|
58
|
+
? task.activeForm
|
|
59
|
+
: task.subject;
|
|
60
|
+
return `${task.id}. ${emoji} ${label}${ownerTag}${blockedTag}`;
|
|
61
|
+
})
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** @deprecated Use formatTaskList instead */
|
|
65
|
+
export function formatTodoList(todos) {
|
|
66
|
+
// Convert TodoItem to TaskItem format for display
|
|
67
|
+
const tasks = todos.map(todo => ({
|
|
68
|
+
id: todo.id,
|
|
69
|
+
subject: todo.content,
|
|
70
|
+
activeForm: todo.activeForm,
|
|
71
|
+
status: todo.status,
|
|
72
|
+
createdAt: todo.createdAt,
|
|
73
|
+
completedAt: todo.completedAt,
|
|
74
|
+
}));
|
|
75
|
+
return formatTaskList(tasks);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* TaskManager class for tracking tasks during execution
|
|
79
|
+
*/
|
|
80
|
+
export class TaskManager {
|
|
81
|
+
items = new Map();
|
|
82
|
+
currentTaskId;
|
|
83
|
+
nextId = 1;
|
|
84
|
+
/**
|
|
85
|
+
* Get all task items
|
|
86
|
+
*/
|
|
87
|
+
getAll() {
|
|
88
|
+
// Return in ID order (sequential)
|
|
89
|
+
return Array.from(this.items.values()).sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get a task by ID
|
|
93
|
+
*/
|
|
94
|
+
get(id) {
|
|
95
|
+
return this.items.get(id);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get current task being worked on
|
|
99
|
+
*/
|
|
100
|
+
getCurrentTask() {
|
|
101
|
+
if (!this.currentTaskId)
|
|
102
|
+
return undefined;
|
|
103
|
+
return this.items.get(this.currentTaskId);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a task is blocked (has unresolved blockers)
|
|
107
|
+
*/
|
|
108
|
+
isBlocked(task) {
|
|
109
|
+
if (!task.blockedBy || task.blockedBy.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
// Check if any blocker is not completed
|
|
112
|
+
return task.blockedBy.some(blockerId => {
|
|
113
|
+
const blocker = this.items.get(blockerId);
|
|
114
|
+
return blocker && blocker.status !== 'completed';
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get open (unresolved) blockers for a task
|
|
119
|
+
*/
|
|
120
|
+
getOpenBlockers(task) {
|
|
121
|
+
if (!task.blockedBy)
|
|
122
|
+
return [];
|
|
123
|
+
return task.blockedBy.filter(blockerId => {
|
|
124
|
+
const blocker = this.items.get(blockerId);
|
|
125
|
+
return blocker && blocker.status !== 'completed';
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a new task
|
|
130
|
+
* If this is the first task and no tasks are in_progress, auto-starts it (if not blocked)
|
|
131
|
+
*/
|
|
132
|
+
create(subject, description, activeForm, metadata) {
|
|
133
|
+
const id = String(this.nextId++);
|
|
134
|
+
const item = {
|
|
135
|
+
id,
|
|
136
|
+
subject,
|
|
137
|
+
description,
|
|
138
|
+
activeForm,
|
|
139
|
+
status: "pending",
|
|
140
|
+
metadata,
|
|
141
|
+
createdAt: Date.now(),
|
|
142
|
+
};
|
|
143
|
+
this.items.set(id, item);
|
|
144
|
+
// Auto-start if no task is currently in progress and task is not blocked
|
|
145
|
+
const hasInProgress = Array.from(this.items.values()).some(t => t.status === "in_progress");
|
|
146
|
+
if (!hasInProgress && !this.isBlocked(item)) {
|
|
147
|
+
item.status = "in_progress";
|
|
148
|
+
this.currentTaskId = id;
|
|
149
|
+
}
|
|
150
|
+
return item;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Update a task
|
|
154
|
+
* Returns the updated task or undefined if not found
|
|
155
|
+
*/
|
|
156
|
+
update(id, updates) {
|
|
157
|
+
const item = this.items.get(id);
|
|
158
|
+
if (!item)
|
|
159
|
+
return {};
|
|
160
|
+
// Handle deletion
|
|
161
|
+
if (updates.status === 'deleted') {
|
|
162
|
+
// Remove this task from other tasks' blockedBy lists
|
|
163
|
+
this.removeFromBlockedBy(id);
|
|
164
|
+
// Remove this task from other tasks' blocks lists
|
|
165
|
+
this.removeFromBlocks(id);
|
|
166
|
+
this.items.delete(id);
|
|
167
|
+
if (this.currentTaskId === id) {
|
|
168
|
+
this.currentTaskId = undefined;
|
|
169
|
+
}
|
|
170
|
+
// Auto-start next pending task
|
|
171
|
+
const next = this.startNextPending();
|
|
172
|
+
return { deleted: true, next };
|
|
173
|
+
}
|
|
174
|
+
// Apply updates
|
|
175
|
+
if (updates.subject !== undefined)
|
|
176
|
+
item.subject = updates.subject;
|
|
177
|
+
if (updates.description !== undefined)
|
|
178
|
+
item.description = updates.description;
|
|
179
|
+
if (updates.activeForm !== undefined)
|
|
180
|
+
item.activeForm = updates.activeForm;
|
|
181
|
+
if (updates.owner !== undefined)
|
|
182
|
+
item.owner = updates.owner;
|
|
183
|
+
if (updates.metadata !== undefined) {
|
|
184
|
+
// Merge metadata, allowing null values to delete keys
|
|
185
|
+
item.metadata = item.metadata || {};
|
|
186
|
+
for (const [key, value] of Object.entries(updates.metadata)) {
|
|
187
|
+
if (value === null) {
|
|
188
|
+
delete item.metadata[key];
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
item.metadata[key] = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Handle dependency updates
|
|
196
|
+
if (updates.addBlocks) {
|
|
197
|
+
item.blocks = item.blocks || [];
|
|
198
|
+
for (const blockedId of updates.addBlocks) {
|
|
199
|
+
if (!item.blocks.includes(blockedId)) {
|
|
200
|
+
item.blocks.push(blockedId);
|
|
201
|
+
}
|
|
202
|
+
// Also update the blocked task's blockedBy (bidirectional)
|
|
203
|
+
const blockedTask = this.items.get(blockedId);
|
|
204
|
+
if (blockedTask) {
|
|
205
|
+
blockedTask.blockedBy = blockedTask.blockedBy || [];
|
|
206
|
+
if (!blockedTask.blockedBy.includes(id)) {
|
|
207
|
+
blockedTask.blockedBy.push(id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (updates.addBlockedBy) {
|
|
213
|
+
item.blockedBy = item.blockedBy || [];
|
|
214
|
+
for (const blockerId of updates.addBlockedBy) {
|
|
215
|
+
if (!item.blockedBy.includes(blockerId)) {
|
|
216
|
+
item.blockedBy.push(blockerId);
|
|
217
|
+
}
|
|
218
|
+
// Also update the blocker task's blocks (bidirectional)
|
|
219
|
+
const blockerTask = this.items.get(blockerId);
|
|
220
|
+
if (blockerTask) {
|
|
221
|
+
blockerTask.blocks = blockerTask.blocks || [];
|
|
222
|
+
if (!blockerTask.blocks.includes(id)) {
|
|
223
|
+
blockerTask.blocks.push(id);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Handle comment
|
|
229
|
+
if (updates.comment) {
|
|
230
|
+
item.comments = item.comments || [];
|
|
231
|
+
const comment = {
|
|
232
|
+
author: updates.comment.author,
|
|
233
|
+
content: updates.comment.content,
|
|
234
|
+
createdAt: Date.now(),
|
|
235
|
+
};
|
|
236
|
+
item.comments.push(comment);
|
|
237
|
+
}
|
|
238
|
+
// Handle status changes
|
|
239
|
+
if (updates.status) {
|
|
240
|
+
const newStatus = updates.status;
|
|
241
|
+
const oldStatus = item.status;
|
|
242
|
+
item.status = newStatus;
|
|
243
|
+
if (updates.status === 'in_progress') {
|
|
244
|
+
// Pause any other in_progress task
|
|
245
|
+
if (this.currentTaskId && this.currentTaskId !== id) {
|
|
246
|
+
const current = this.items.get(this.currentTaskId);
|
|
247
|
+
if (current && current.status === 'in_progress') {
|
|
248
|
+
current.status = 'pending';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.currentTaskId = id;
|
|
252
|
+
}
|
|
253
|
+
else if (updates.status === 'completed') {
|
|
254
|
+
item.completedAt = Date.now();
|
|
255
|
+
if (this.currentTaskId === id) {
|
|
256
|
+
this.currentTaskId = undefined;
|
|
257
|
+
}
|
|
258
|
+
// Auto-start next pending task (checking for unblocked)
|
|
259
|
+
const next = this.startNextPending();
|
|
260
|
+
return { task: item, next };
|
|
261
|
+
}
|
|
262
|
+
else if (oldStatus === 'in_progress' && updates.status === 'pending') {
|
|
263
|
+
if (this.currentTaskId === id) {
|
|
264
|
+
this.currentTaskId = undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { task: item };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Remove a task ID from all other tasks' blockedBy lists
|
|
272
|
+
*/
|
|
273
|
+
removeFromBlockedBy(taskId) {
|
|
274
|
+
for (const task of this.items.values()) {
|
|
275
|
+
if (task.blockedBy) {
|
|
276
|
+
task.blockedBy = task.blockedBy.filter(id => id !== taskId);
|
|
277
|
+
if (task.blockedBy.length === 0) {
|
|
278
|
+
delete task.blockedBy;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Remove a task ID from all other tasks' blocks lists
|
|
285
|
+
*/
|
|
286
|
+
removeFromBlocks(taskId) {
|
|
287
|
+
for (const task of this.items.values()) {
|
|
288
|
+
if (task.blocks) {
|
|
289
|
+
task.blocks = task.blocks.filter(id => id !== taskId);
|
|
290
|
+
if (task.blocks.length === 0) {
|
|
291
|
+
delete task.blocks;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Find and start the next pending task that is not blocked
|
|
298
|
+
*/
|
|
299
|
+
startNextPending() {
|
|
300
|
+
const pending = this.getAll().find(t => t.status === 'pending' && !this.isBlocked(t));
|
|
301
|
+
if (pending) {
|
|
302
|
+
pending.status = 'in_progress';
|
|
303
|
+
this.currentTaskId = pending.id;
|
|
304
|
+
return pending;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Check if all tasks are completed
|
|
310
|
+
*/
|
|
311
|
+
isAllCompleted() {
|
|
312
|
+
const items = Array.from(this.items.values());
|
|
313
|
+
return items.length > 0 && items.every(i => i.status === "completed");
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Get progress stats
|
|
317
|
+
*/
|
|
318
|
+
getProgress() {
|
|
319
|
+
const items = Array.from(this.items.values());
|
|
320
|
+
return {
|
|
321
|
+
total: items.length,
|
|
322
|
+
completed: items.filter(i => i.status === "completed").length,
|
|
323
|
+
pending: items.filter(i => i.status === "pending").length,
|
|
324
|
+
inProgress: items.filter(i => i.status === "in_progress").length,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Normalize a subject string for duplicate comparison.
|
|
329
|
+
* Lowercases, strips common filler words, and collapses whitespace.
|
|
330
|
+
*/
|
|
331
|
+
normalizeSubject(subject) {
|
|
332
|
+
const fillerWords = /\b(the|a|an|and|or|to|from|for|in|on|of|with|is|are|was|were|be|been|being|that|this|it)\b/g;
|
|
333
|
+
return subject
|
|
334
|
+
.toLowerCase()
|
|
335
|
+
.replace(fillerWords, '')
|
|
336
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
337
|
+
.replace(/\s+/g, ' ')
|
|
338
|
+
.trim();
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Find a duplicate among non-completed tasks.
|
|
342
|
+
* Checks for exact normalized match or substring containment.
|
|
343
|
+
*/
|
|
344
|
+
findDuplicate(subject) {
|
|
345
|
+
const normalized = this.normalizeSubject(subject);
|
|
346
|
+
if (!normalized)
|
|
347
|
+
return undefined;
|
|
348
|
+
for (const task of this.items.values()) {
|
|
349
|
+
if (task.status === 'completed')
|
|
350
|
+
continue;
|
|
351
|
+
const existingNormalized = this.normalizeSubject(task.subject);
|
|
352
|
+
if (!existingNormalized)
|
|
353
|
+
continue;
|
|
354
|
+
// Exact match after normalization
|
|
355
|
+
if (normalized === existingNormalized)
|
|
356
|
+
return task;
|
|
357
|
+
// Substring containment (either direction)
|
|
358
|
+
if (normalized.includes(existingNormalized) || existingNormalized.includes(normalized)) {
|
|
359
|
+
return task;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Clear all tasks (used internally)
|
|
366
|
+
*/
|
|
367
|
+
clear() {
|
|
368
|
+
this.items.clear();
|
|
369
|
+
this.currentTaskId = undefined;
|
|
370
|
+
this.nextId = 1;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Serialize the TaskManager state for persistence
|
|
374
|
+
*/
|
|
375
|
+
serialize() {
|
|
376
|
+
return {
|
|
377
|
+
items: this.getAll(),
|
|
378
|
+
currentTaskId: this.currentTaskId,
|
|
379
|
+
nextId: this.nextId,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Restore TaskManager state from serialized data
|
|
384
|
+
*/
|
|
385
|
+
restore(state) {
|
|
386
|
+
this.items.clear();
|
|
387
|
+
for (const item of state.items) {
|
|
388
|
+
this.items.set(item.id, item);
|
|
389
|
+
}
|
|
390
|
+
this.currentTaskId = state.currentTaskId;
|
|
391
|
+
this.nextId = state.nextId;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Save tasks to Workspace (if provided)
|
|
395
|
+
*/
|
|
396
|
+
saveToWorkspace(workspace, agentName) {
|
|
397
|
+
if (!workspace)
|
|
398
|
+
return;
|
|
399
|
+
try {
|
|
400
|
+
const state = this.serialize();
|
|
401
|
+
workspace.write(TASKS_CONTEXT_PATH, state, agentName || 'task-manager');
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// Silently ignore write errors (workspace may not support this path)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Restore tasks from Workspace (if available)
|
|
409
|
+
* Returns true if tasks were restored
|
|
410
|
+
*/
|
|
411
|
+
async restoreFromWorkspace(workspace) {
|
|
412
|
+
if (!workspace)
|
|
413
|
+
return false;
|
|
414
|
+
try {
|
|
415
|
+
const state = await workspace.read(TASKS_CONTEXT_PATH);
|
|
416
|
+
if (state && state.items && Array.isArray(state.items)) {
|
|
417
|
+
this.restore(state);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Silently ignore read errors
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/** @deprecated Use TaskManager instead */
|
|
428
|
+
export const TodoManager = TaskManager;
|
|
429
|
+
/**
|
|
430
|
+
* Check if the current agent can update a task
|
|
431
|
+
* Returns true if:
|
|
432
|
+
* - Task has no owner
|
|
433
|
+
* - Current agent is the owner
|
|
434
|
+
* - Current agent is a team-lead
|
|
435
|
+
*/
|
|
436
|
+
function canUpdateTask(task, options) {
|
|
437
|
+
// No owner - anyone can update
|
|
438
|
+
if (!task.owner)
|
|
439
|
+
return true;
|
|
440
|
+
const agentId = options.agentId || process.env.CLAUDE_CODE_AGENT_ID;
|
|
441
|
+
const agentType = options.agentType || process.env.CLAUDE_CODE_AGENT_TYPE;
|
|
442
|
+
// Team leads can update any task
|
|
443
|
+
if (agentType === 'team-lead')
|
|
444
|
+
return true;
|
|
445
|
+
// Owner can update their own tasks
|
|
446
|
+
if (agentId && task.owner === agentId)
|
|
447
|
+
return true;
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Notify logger about task list changes and persist to workspace
|
|
452
|
+
*/
|
|
453
|
+
function notifyTaskUpdate(manager, action, options) {
|
|
454
|
+
const { logger, agentName, workspace } = options;
|
|
455
|
+
// Persist to workspace on mutations (not on list)
|
|
456
|
+
if (action !== 'list' && workspace) {
|
|
457
|
+
manager.saveToWorkspace(workspace, agentName);
|
|
458
|
+
}
|
|
459
|
+
if (logger?.onTaskUpdate) {
|
|
460
|
+
const tasks = manager.getAll();
|
|
461
|
+
const current = manager.getCurrentTask();
|
|
462
|
+
const progress = manager.getProgress();
|
|
463
|
+
logger.onTaskUpdate({
|
|
464
|
+
agentName,
|
|
465
|
+
action,
|
|
466
|
+
tasks,
|
|
467
|
+
current: current || undefined,
|
|
468
|
+
progress,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
// Also call deprecated onTodoUpdate for backward compatibility
|
|
472
|
+
if (logger?.onTodoUpdate) {
|
|
473
|
+
const tasks = manager.getAll();
|
|
474
|
+
const current = manager.getCurrentTask();
|
|
475
|
+
const progress = manager.getProgress();
|
|
476
|
+
// Convert to old format
|
|
477
|
+
const todos = tasks.map(t => ({
|
|
478
|
+
id: t.id,
|
|
479
|
+
content: t.subject,
|
|
480
|
+
activeForm: t.activeForm,
|
|
481
|
+
status: t.status,
|
|
482
|
+
createdAt: t.createdAt,
|
|
483
|
+
completedAt: t.completedAt,
|
|
484
|
+
}));
|
|
485
|
+
const todoUpdate = {
|
|
486
|
+
agentName,
|
|
487
|
+
action: action === 'create' ? 'set' : action === 'delete' ? 'update' : action === 'list' ? 'update' : 'update',
|
|
488
|
+
todos,
|
|
489
|
+
current: current ? {
|
|
490
|
+
id: current.id,
|
|
491
|
+
content: current.subject,
|
|
492
|
+
activeForm: current.activeForm,
|
|
493
|
+
status: current.status,
|
|
494
|
+
createdAt: current.createdAt,
|
|
495
|
+
completedAt: current.completedAt,
|
|
496
|
+
} : undefined,
|
|
497
|
+
progress,
|
|
498
|
+
};
|
|
499
|
+
logger.onTodoUpdate(todoUpdate);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Create the TaskCreate tool
|
|
504
|
+
*/
|
|
505
|
+
export function createTaskCreateTool(manager, options = {}) {
|
|
506
|
+
return {
|
|
507
|
+
name: "TaskCreate",
|
|
508
|
+
description: `Create a task in the task list. Duplicates are automatically rejected.
|
|
509
|
+
|
|
510
|
+
## Fields
|
|
511
|
+
|
|
512
|
+
- **subject**: Brief, actionable title in imperative form (e.g., "Fix authentication bug")
|
|
513
|
+
- **description**: What needs to be done, including context and acceptance criteria
|
|
514
|
+
- **activeForm**: Present continuous form shown in spinner when in_progress (e.g., "Fixing authentication bug")
|
|
515
|
+
|
|
516
|
+
Always provide activeForm. Tasks are created with status \`pending\` (first task auto-starts).
|
|
517
|
+
`,
|
|
518
|
+
parameters: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
subject: {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "A brief title for the task",
|
|
524
|
+
},
|
|
525
|
+
description: {
|
|
526
|
+
type: "string",
|
|
527
|
+
description: "A detailed description of what needs to be done",
|
|
528
|
+
},
|
|
529
|
+
activeForm: {
|
|
530
|
+
type: "string",
|
|
531
|
+
description: 'Present continuous form shown in spinner when in_progress (e.g., "Running tests")',
|
|
532
|
+
},
|
|
533
|
+
metadata: {
|
|
534
|
+
type: "string",
|
|
535
|
+
description: "JSON string of arbitrary metadata to attach to the task",
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
required: ["subject", "description"],
|
|
539
|
+
},
|
|
540
|
+
execute: async (params) => {
|
|
541
|
+
const { subject, description, activeForm, metadata: rawMetadata } = params;
|
|
542
|
+
// Check for duplicates before creating
|
|
543
|
+
const duplicate = manager.findDuplicate(subject);
|
|
544
|
+
if (duplicate) {
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: `Duplicate: task #${duplicate.id} "${duplicate.subject}" already covers this. Use the existing task instead.`,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// Parse metadata if provided
|
|
551
|
+
let metadata;
|
|
552
|
+
if (rawMetadata) {
|
|
553
|
+
try {
|
|
554
|
+
metadata = JSON.parse(rawMetadata);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return { success: false, error: "Invalid metadata JSON" };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const task = manager.create(subject, description, activeForm, metadata);
|
|
561
|
+
const progress = manager.getProgress();
|
|
562
|
+
notifyTaskUpdate(manager, 'create', options);
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
data: {
|
|
566
|
+
message: `Created task #${task.id}: "${task.subject}"${task.status === 'in_progress' ? ' (auto-started)' : ''}`,
|
|
567
|
+
task,
|
|
568
|
+
progress,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Create the TaskUpdate tool
|
|
576
|
+
*/
|
|
577
|
+
export function createTaskUpdateTool(manager, options = {}) {
|
|
578
|
+
return {
|
|
579
|
+
name: "TaskUpdate",
|
|
580
|
+
description: `Update a task's status, details, or dependencies.
|
|
581
|
+
|
|
582
|
+
Status: \`pending\` → \`in_progress\` → \`completed\`. Use \`deleted\` to remove.
|
|
583
|
+
|
|
584
|
+
Only mark completed when fully done. If blocked, keep as in_progress.
|
|
585
|
+
|
|
586
|
+
## Examples
|
|
587
|
+
|
|
588
|
+
\`\`\`json
|
|
589
|
+
{"taskId": "1", "status": "completed"}
|
|
590
|
+
{"taskId": "1", "status": "in_progress"}
|
|
591
|
+
{"taskId": "2", "addBlockedBy": ["1"]}
|
|
592
|
+
{"taskId": "1", "owner": "my-name"}
|
|
593
|
+
{"taskId": "1", "comment": "Started implementing"}
|
|
594
|
+
\`\`\`
|
|
595
|
+
`,
|
|
596
|
+
parameters: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
taskId: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "The ID of the task to update",
|
|
602
|
+
},
|
|
603
|
+
status: {
|
|
604
|
+
type: "string",
|
|
605
|
+
enum: ["pending", "in_progress", "completed", "deleted"],
|
|
606
|
+
description: "New status for the task",
|
|
607
|
+
},
|
|
608
|
+
subject: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "New subject for the task",
|
|
611
|
+
},
|
|
612
|
+
description: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "New description for the task",
|
|
615
|
+
},
|
|
616
|
+
activeForm: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: 'Present continuous form shown in spinner when in_progress (e.g., "Running tests")',
|
|
619
|
+
},
|
|
620
|
+
owner: {
|
|
621
|
+
type: "string",
|
|
622
|
+
description: "New owner for the task",
|
|
623
|
+
},
|
|
624
|
+
metadata: {
|
|
625
|
+
type: "string",
|
|
626
|
+
description: "JSON string of metadata keys to merge into the task. Set a key to null to delete it.",
|
|
627
|
+
},
|
|
628
|
+
addBlocks: {
|
|
629
|
+
type: "string",
|
|
630
|
+
description: "JSON array of task IDs that this task blocks (cannot start until this completes)",
|
|
631
|
+
},
|
|
632
|
+
addBlockedBy: {
|
|
633
|
+
type: "string",
|
|
634
|
+
description: "JSON array of task IDs that block this task (must complete before this can start)",
|
|
635
|
+
},
|
|
636
|
+
comment: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Add a progress note or comment to the task",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
required: ["taskId"],
|
|
642
|
+
},
|
|
643
|
+
execute: async (params) => {
|
|
644
|
+
const { taskId, status, subject, description, activeForm, owner, metadata: rawMetadata, addBlocks: rawAddBlocks, addBlockedBy: rawAddBlockedBy, comment, } = params;
|
|
645
|
+
// Check if task exists first
|
|
646
|
+
const existingTask = manager.get(taskId);
|
|
647
|
+
if (!existingTask) {
|
|
648
|
+
return { success: false, error: `Task #${taskId} not found` };
|
|
649
|
+
}
|
|
650
|
+
// Check ownership permissions
|
|
651
|
+
if (!canUpdateTask(existingTask, options)) {
|
|
652
|
+
const agentId = options.agentId || process.env.CLAUDE_CODE_AGENT_ID || 'unknown';
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
error: `Task #${taskId} is owned by "${existingTask.owner}" and cannot be updated by agent "${agentId}"`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
// Parse metadata if provided
|
|
659
|
+
let metadata;
|
|
660
|
+
if (rawMetadata) {
|
|
661
|
+
try {
|
|
662
|
+
metadata = JSON.parse(rawMetadata);
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
return { success: false, error: "Invalid metadata JSON" };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Parse addBlocks if provided
|
|
669
|
+
let addBlocks;
|
|
670
|
+
if (rawAddBlocks) {
|
|
671
|
+
try {
|
|
672
|
+
addBlocks = JSON.parse(rawAddBlocks);
|
|
673
|
+
if (!Array.isArray(addBlocks)) {
|
|
674
|
+
return { success: false, error: "addBlocks must be a JSON array of task IDs" };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return { success: false, error: "Invalid addBlocks JSON" };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Parse addBlockedBy if provided
|
|
682
|
+
let addBlockedBy;
|
|
683
|
+
if (rawAddBlockedBy) {
|
|
684
|
+
try {
|
|
685
|
+
addBlockedBy = JSON.parse(rawAddBlockedBy);
|
|
686
|
+
if (!Array.isArray(addBlockedBy)) {
|
|
687
|
+
return { success: false, error: "addBlockedBy must be a JSON array of task IDs" };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return { success: false, error: "Invalid addBlockedBy JSON" };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Build comment object if provided
|
|
695
|
+
const commentObj = comment ? {
|
|
696
|
+
author: options.agentId || process.env.CLAUDE_CODE_AGENT_ID || options.agentName || 'agent',
|
|
697
|
+
content: comment,
|
|
698
|
+
} : undefined;
|
|
699
|
+
const result = manager.update(taskId, {
|
|
700
|
+
status,
|
|
701
|
+
subject,
|
|
702
|
+
description,
|
|
703
|
+
activeForm,
|
|
704
|
+
owner,
|
|
705
|
+
metadata,
|
|
706
|
+
addBlocks,
|
|
707
|
+
addBlockedBy,
|
|
708
|
+
comment: commentObj,
|
|
709
|
+
});
|
|
710
|
+
if (result.deleted) {
|
|
711
|
+
notifyTaskUpdate(manager, 'delete', options);
|
|
712
|
+
const progress = manager.getProgress();
|
|
713
|
+
let message = `Deleted task #${taskId}`;
|
|
714
|
+
if (result.next) {
|
|
715
|
+
message += `. Next task: #${result.next.id} "${result.next.subject}"`;
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
success: true,
|
|
719
|
+
data: {
|
|
720
|
+
message,
|
|
721
|
+
deleted: true,
|
|
722
|
+
next: result.next,
|
|
723
|
+
progress,
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
if (!result.task) {
|
|
728
|
+
return { success: false, error: `Task #${taskId} not found` };
|
|
729
|
+
}
|
|
730
|
+
notifyTaskUpdate(manager, 'update', options);
|
|
731
|
+
const progress = manager.getProgress();
|
|
732
|
+
let message = `Updated task #${taskId}`;
|
|
733
|
+
if (status === 'completed') {
|
|
734
|
+
message = `Completed task #${taskId}: "${result.task.subject}"`;
|
|
735
|
+
if (result.next) {
|
|
736
|
+
message += `. Next: #${result.next.id} "${result.next.subject}"`;
|
|
737
|
+
}
|
|
738
|
+
else if (manager.isAllCompleted()) {
|
|
739
|
+
message += `. All tasks completed!`;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else if (status === 'in_progress') {
|
|
743
|
+
message = `Started task #${taskId}: "${result.task.subject}"`;
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
success: true,
|
|
747
|
+
data: {
|
|
748
|
+
message,
|
|
749
|
+
task: result.task,
|
|
750
|
+
next: result.next,
|
|
751
|
+
progress,
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Create the TaskGet tool
|
|
759
|
+
*/
|
|
760
|
+
export function createTaskGetTool(manager, _options = {}) {
|
|
761
|
+
return {
|
|
762
|
+
name: "TaskGet",
|
|
763
|
+
description: `Retrieve a task by ID. Returns full details: subject, description, status, dependencies, owner, and metadata.`,
|
|
764
|
+
parameters: {
|
|
765
|
+
type: "object",
|
|
766
|
+
properties: {
|
|
767
|
+
taskId: {
|
|
768
|
+
type: "string",
|
|
769
|
+
description: "The ID of the task to retrieve",
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
required: ["taskId"],
|
|
773
|
+
},
|
|
774
|
+
execute: async (params) => {
|
|
775
|
+
const { taskId } = params;
|
|
776
|
+
const task = manager.get(taskId);
|
|
777
|
+
if (!task) {
|
|
778
|
+
return { success: false, error: `Task #${taskId} not found` };
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
success: true,
|
|
782
|
+
data: {
|
|
783
|
+
task,
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Create the TaskList tool
|
|
791
|
+
*/
|
|
792
|
+
export function createTaskListTool(manager, options = {}) {
|
|
793
|
+
return {
|
|
794
|
+
name: "TaskList",
|
|
795
|
+
description: `List all tasks with their ID, subject, status, and owner. Use after completing a task to find the next one.`,
|
|
796
|
+
parameters: {
|
|
797
|
+
type: "object",
|
|
798
|
+
properties: {},
|
|
799
|
+
required: [],
|
|
800
|
+
},
|
|
801
|
+
execute: async () => {
|
|
802
|
+
const tasks = manager.getAll();
|
|
803
|
+
const current = manager.getCurrentTask();
|
|
804
|
+
const progress = manager.getProgress();
|
|
805
|
+
notifyTaskUpdate(manager, 'list', options);
|
|
806
|
+
return {
|
|
807
|
+
success: true,
|
|
808
|
+
data: {
|
|
809
|
+
message: formatTaskList(tasks, tasks),
|
|
810
|
+
tasks,
|
|
811
|
+
current: current?.id,
|
|
812
|
+
progress,
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Create all 4 task management tools
|
|
820
|
+
*/
|
|
821
|
+
export function createTaskTools(manager, options = {}) {
|
|
822
|
+
return [
|
|
823
|
+
createTaskCreateTool(manager, options),
|
|
824
|
+
createTaskUpdateTool(manager, options),
|
|
825
|
+
createTaskGetTool(manager, options),
|
|
826
|
+
createTaskListTool(manager, options),
|
|
827
|
+
];
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* @deprecated Use createTaskTools instead
|
|
831
|
+
*/
|
|
832
|
+
export function createTodoTool(manager, options = {}) {
|
|
833
|
+
// Return a legacy-compatible single tool that wraps the new system
|
|
834
|
+
return {
|
|
835
|
+
name: "__todo__",
|
|
836
|
+
description: "Deprecated: Use TaskCreate, TaskUpdate, TaskGet, TaskList tools instead.",
|
|
837
|
+
parameters: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
action: {
|
|
841
|
+
type: "string",
|
|
842
|
+
enum: ["set", "complete", "list"],
|
|
843
|
+
description: "The action to perform",
|
|
844
|
+
},
|
|
845
|
+
items: {
|
|
846
|
+
type: "string",
|
|
847
|
+
description: 'Array of task descriptions (for "set" action)',
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
required: ["action"],
|
|
851
|
+
},
|
|
852
|
+
execute: async (params) => {
|
|
853
|
+
const { action, items: rawItems } = params;
|
|
854
|
+
let items;
|
|
855
|
+
if (rawItems) {
|
|
856
|
+
let parsed = rawItems;
|
|
857
|
+
if (typeof rawItems === "string") {
|
|
858
|
+
try {
|
|
859
|
+
parsed = JSON.parse(rawItems);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
parsed = [rawItems];
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (Array.isArray(parsed)) {
|
|
866
|
+
items = parsed.map((item) => {
|
|
867
|
+
if (typeof item === "string") {
|
|
868
|
+
return { content: item };
|
|
869
|
+
}
|
|
870
|
+
if (typeof item === "object" && item !== null && "content" in item) {
|
|
871
|
+
return {
|
|
872
|
+
content: item.content,
|
|
873
|
+
activeForm: item.activeForm,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return { content: String(item) };
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
switch (action) {
|
|
881
|
+
case "set": {
|
|
882
|
+
if (!items || items.length === 0) {
|
|
883
|
+
return { success: false, error: 'Items array is required for "set" action' };
|
|
884
|
+
}
|
|
885
|
+
manager.clear();
|
|
886
|
+
for (const item of items) {
|
|
887
|
+
manager.create(item.content, undefined, item.activeForm);
|
|
888
|
+
}
|
|
889
|
+
const todos = manager.getAll();
|
|
890
|
+
const current = manager.getCurrentTask();
|
|
891
|
+
notifyTaskUpdate(manager, 'create', options);
|
|
892
|
+
return {
|
|
893
|
+
success: true,
|
|
894
|
+
data: {
|
|
895
|
+
message: `Created ${todos.length} tasks. Starting: "${current?.activeForm || current?.subject}"`,
|
|
896
|
+
todos: todos.map(t => ({ ...t, content: t.subject })),
|
|
897
|
+
current: current?.activeForm || current?.subject,
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
case "complete": {
|
|
902
|
+
const current = manager.getCurrentTask();
|
|
903
|
+
if (!current) {
|
|
904
|
+
return { success: true, data: { message: "No tasks to complete.", todos: manager.getAll() } };
|
|
905
|
+
}
|
|
906
|
+
const result = manager.update(current.id, { status: 'completed' });
|
|
907
|
+
const progress = manager.getProgress();
|
|
908
|
+
let message = `Completed: "${current.subject}". `;
|
|
909
|
+
if (result.next) {
|
|
910
|
+
message += `Next: "${result.next.activeForm || result.next.subject}". `;
|
|
911
|
+
}
|
|
912
|
+
else if (manager.isAllCompleted()) {
|
|
913
|
+
message += "All tasks completed!";
|
|
914
|
+
}
|
|
915
|
+
message += `Progress: ${progress.completed}/${progress.total}`;
|
|
916
|
+
notifyTaskUpdate(manager, 'update', options);
|
|
917
|
+
return {
|
|
918
|
+
success: true,
|
|
919
|
+
data: {
|
|
920
|
+
message,
|
|
921
|
+
todos: manager.getAll().map(t => ({ ...t, content: t.subject })),
|
|
922
|
+
current: result.next?.activeForm || result.next?.subject,
|
|
923
|
+
progress,
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
case "list": {
|
|
928
|
+
const tasks = manager.getAll();
|
|
929
|
+
const current = manager.getCurrentTask();
|
|
930
|
+
const progress = manager.getProgress();
|
|
931
|
+
return {
|
|
932
|
+
success: true,
|
|
933
|
+
data: {
|
|
934
|
+
message: formatTaskList(tasks, tasks),
|
|
935
|
+
todos: tasks.map(t => ({ ...t, content: t.subject })),
|
|
936
|
+
current: current?.activeForm || current?.subject,
|
|
937
|
+
progress,
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
default:
|
|
942
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
//# sourceMappingURL=tasks.js.map
|