wave-agent-sdk 0.17.1 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/builtin/skills/deep-research/SKILL.md +90 -0
  2. package/builtin/skills/settings/ENV.md +6 -3
  3. package/dist/agent.d.ts +28 -1
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +128 -34
  6. package/dist/constants/goalPrompts.d.ts +2 -0
  7. package/dist/constants/goalPrompts.d.ts.map +1 -0
  8. package/dist/constants/goalPrompts.js +10 -0
  9. package/dist/constants/tools.d.ts +1 -0
  10. package/dist/constants/tools.d.ts.map +1 -1
  11. package/dist/constants/tools.js +1 -0
  12. package/dist/managers/aiManager.d.ts +7 -0
  13. package/dist/managers/aiManager.d.ts.map +1 -1
  14. package/dist/managers/aiManager.js +77 -41
  15. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  16. package/dist/managers/backgroundTaskManager.js +10 -2
  17. package/dist/managers/goalManager.d.ts +43 -0
  18. package/dist/managers/goalManager.d.ts.map +1 -0
  19. package/dist/managers/goalManager.js +177 -0
  20. package/dist/managers/messageManager.d.ts +2 -2
  21. package/dist/managers/messageManager.d.ts.map +1 -1
  22. package/dist/managers/messageQueue.d.ts +10 -0
  23. package/dist/managers/messageQueue.d.ts.map +1 -1
  24. package/dist/managers/messageQueue.js +53 -1
  25. package/dist/managers/pluginManager.d.ts.map +1 -1
  26. package/dist/managers/pluginManager.js +7 -1
  27. package/dist/managers/skillManager.d.ts +2 -0
  28. package/dist/managers/skillManager.d.ts.map +1 -1
  29. package/dist/managers/skillManager.js +19 -9
  30. package/dist/managers/slashCommandManager.d.ts +6 -0
  31. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  32. package/dist/managers/slashCommandManager.js +105 -0
  33. package/dist/managers/toolManager.d.ts.map +1 -1
  34. package/dist/managers/toolManager.js +5 -0
  35. package/dist/managers/workflowManager.d.ts +65 -0
  36. package/dist/managers/workflowManager.d.ts.map +1 -0
  37. package/dist/managers/workflowManager.js +380 -0
  38. package/dist/prompts/index.d.ts +2 -1
  39. package/dist/prompts/index.d.ts.map +1 -1
  40. package/dist/prompts/index.js +3 -3
  41. package/dist/services/MarketplaceService.d.ts +2 -2
  42. package/dist/services/MarketplaceService.d.ts.map +1 -1
  43. package/dist/services/MarketplaceService.js +11 -32
  44. package/dist/services/aiService.d.ts +23 -0
  45. package/dist/services/aiService.d.ts.map +1 -1
  46. package/dist/services/aiService.js +102 -9
  47. package/dist/services/configurationService.d.ts +1 -1
  48. package/dist/services/configurationService.d.ts.map +1 -1
  49. package/dist/services/configurationService.js +3 -16
  50. package/dist/services/hook.d.ts.map +1 -1
  51. package/dist/services/hook.js +4 -0
  52. package/dist/services/session.d.ts +9 -1
  53. package/dist/services/session.d.ts.map +1 -1
  54. package/dist/services/session.js +28 -1
  55. package/dist/tools/bashTool.d.ts.map +1 -1
  56. package/dist/tools/bashTool.js +49 -7
  57. package/dist/tools/readTool.d.ts.map +1 -1
  58. package/dist/tools/readTool.js +1 -1
  59. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  60. package/dist/tools/taskManagementTools.js +103 -157
  61. package/dist/tools/types.d.ts +2 -0
  62. package/dist/tools/types.d.ts.map +1 -1
  63. package/dist/tools/webFetchTool.d.ts.map +1 -1
  64. package/dist/tools/webFetchTool.js +0 -9
  65. package/dist/tools/workflowTool.d.ts +11 -0
  66. package/dist/tools/workflowTool.d.ts.map +1 -0
  67. package/dist/tools/workflowTool.js +190 -0
  68. package/dist/types/agent.d.ts +2 -0
  69. package/dist/types/agent.d.ts.map +1 -1
  70. package/dist/types/commands.d.ts +4 -0
  71. package/dist/types/commands.d.ts.map +1 -1
  72. package/dist/types/config.d.ts +2 -2
  73. package/dist/types/config.d.ts.map +1 -1
  74. package/dist/types/core.d.ts +1 -1
  75. package/dist/types/core.d.ts.map +1 -1
  76. package/dist/types/hooks.d.ts +2 -0
  77. package/dist/types/hooks.d.ts.map +1 -1
  78. package/dist/types/index.d.ts +1 -0
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/types/index.js +1 -0
  81. package/dist/types/messaging.d.ts +2 -2
  82. package/dist/types/messaging.d.ts.map +1 -1
  83. package/dist/types/processes.d.ts +6 -2
  84. package/dist/types/processes.d.ts.map +1 -1
  85. package/dist/types/workflow.d.ts +2 -0
  86. package/dist/types/workflow.d.ts.map +1 -0
  87. package/dist/types/workflow.js +1 -0
  88. package/dist/utils/cacheControlUtils.d.ts +13 -8
  89. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  90. package/dist/utils/cacheControlUtils.js +73 -102
  91. package/dist/utils/containerSetup.d.ts.map +1 -1
  92. package/dist/utils/containerSetup.js +7 -0
  93. package/dist/utils/markdownParser.d.ts.map +1 -1
  94. package/dist/utils/markdownParser.js +21 -6
  95. package/dist/utils/messageOperations.d.ts +2 -2
  96. package/dist/utils/messageOperations.d.ts.map +1 -1
  97. package/dist/utils/notificationXml.d.ts.map +1 -1
  98. package/dist/workflow/budgetTracker.d.ts +12 -0
  99. package/dist/workflow/budgetTracker.d.ts.map +1 -0
  100. package/dist/workflow/budgetTracker.js +30 -0
  101. package/dist/workflow/concurrencyLimiter.d.ts +14 -0
  102. package/dist/workflow/concurrencyLimiter.d.ts.map +1 -0
  103. package/dist/workflow/concurrencyLimiter.js +39 -0
  104. package/dist/workflow/journal.d.ts +19 -0
  105. package/dist/workflow/journal.d.ts.map +1 -0
  106. package/dist/workflow/journal.js +74 -0
  107. package/dist/workflow/progressReporter.d.ts +21 -0
  108. package/dist/workflow/progressReporter.d.ts.map +1 -0
  109. package/dist/workflow/progressReporter.js +118 -0
  110. package/dist/workflow/runState.d.ts +16 -0
  111. package/dist/workflow/runState.d.ts.map +1 -0
  112. package/dist/workflow/runState.js +57 -0
  113. package/dist/workflow/scriptRuntime.d.ts +35 -0
  114. package/dist/workflow/scriptRuntime.d.ts.map +1 -0
  115. package/dist/workflow/scriptRuntime.js +196 -0
  116. package/dist/workflow/structuredOutput.d.ts +27 -0
  117. package/dist/workflow/structuredOutput.d.ts.map +1 -0
  118. package/dist/workflow/structuredOutput.js +106 -0
  119. package/dist/workflow/types.d.ts +81 -0
  120. package/dist/workflow/types.d.ts.map +1 -0
  121. package/dist/workflow/types.js +1 -0
  122. package/dist/workflow/workflowApis.d.ts +46 -0
  123. package/dist/workflow/workflowApis.d.ts.map +1 -0
  124. package/dist/workflow/workflowApis.js +280 -0
  125. package/package.json +1 -1
  126. package/src/agent.ts +144 -34
  127. package/src/constants/goalPrompts.ts +10 -0
  128. package/src/constants/tools.ts +1 -0
  129. package/src/managers/aiManager.ts +91 -47
  130. package/src/managers/backgroundTaskManager.ts +16 -4
  131. package/src/managers/goalManager.ts +232 -0
  132. package/src/managers/messageManager.ts +2 -2
  133. package/src/managers/messageQueue.ts +59 -1
  134. package/src/managers/pluginManager.ts +8 -1
  135. package/src/managers/skillManager.ts +20 -9
  136. package/src/managers/slashCommandManager.ts +119 -0
  137. package/src/managers/toolManager.ts +7 -0
  138. package/src/managers/workflowManager.ts +491 -0
  139. package/src/prompts/index.ts +4 -2
  140. package/src/services/MarketplaceService.ts +14 -38
  141. package/src/services/aiService.ts +166 -12
  142. package/src/services/configurationService.ts +2 -22
  143. package/src/services/hook.ts +5 -0
  144. package/src/services/session.ts +42 -2
  145. package/src/tools/bashTool.ts +64 -9
  146. package/src/tools/readTool.ts +1 -2
  147. package/src/tools/taskManagementTools.ts +146 -195
  148. package/src/tools/types.ts +2 -0
  149. package/src/tools/webFetchTool.ts +0 -12
  150. package/src/tools/workflowTool.ts +205 -0
  151. package/src/types/agent.ts +6 -0
  152. package/src/types/commands.ts +4 -0
  153. package/src/types/config.ts +2 -2
  154. package/src/types/core.ts +3 -3
  155. package/src/types/hooks.ts +2 -0
  156. package/src/types/index.ts +1 -0
  157. package/src/types/messaging.ts +2 -2
  158. package/src/types/processes.ts +10 -2
  159. package/src/types/workflow.ts +5 -0
  160. package/src/utils/cacheControlUtils.ts +106 -131
  161. package/src/utils/containerSetup.ts +9 -0
  162. package/src/utils/markdownParser.ts +26 -8
  163. package/src/utils/messageOperations.ts +2 -2
  164. package/src/utils/notificationXml.ts +6 -1
  165. package/src/workflow/budgetTracker.ts +34 -0
  166. package/src/workflow/concurrencyLimiter.ts +47 -0
  167. package/src/workflow/journal.ts +95 -0
  168. package/src/workflow/progressReporter.ts +141 -0
  169. package/src/workflow/runState.ts +65 -0
  170. package/src/workflow/scriptRuntime.ts +274 -0
  171. package/src/workflow/structuredOutput.ts +123 -0
  172. package/src/workflow/types.ts +95 -0
  173. package/src/workflow/workflowApis.ts +412 -0
@@ -18,6 +18,17 @@ import {
18
18
 
19
19
  const BASH_DEFAULT_TIMEOUT_MS = 120000;
20
20
 
21
+ // Commands that should not be auto-backgrounded on timeout (e.g. sleep should just be killed)
22
+ const DISALLOWED_AUTO_BACKGROUND_COMMANDS = ["sleep"];
23
+
24
+ function isAutobackgroundingAllowed(command: string): boolean {
25
+ const trimmed = command.trim();
26
+ // Get the first word of the command
27
+ const baseCommand = trimmed.split(/\s+/)[0];
28
+ if (!baseCommand) return true;
29
+ return !DISALLOWED_AUTO_BACKGROUND_COMMANDS.includes(baseCommand);
30
+ }
31
+
21
32
  /**
22
33
  * Bash command execution tool - supports both foreground and background execution
23
34
  */
@@ -125,9 +136,12 @@ The working directory persists between commands. Try to maintain your current wo
125
136
  const runInBackground = args.run_in_background as boolean | undefined;
126
137
  const description = args.description as string | undefined;
127
138
  // Set default timeout: BASH_DEFAULT_TIMEOUT_MS for foreground, no timeout for background
128
- const timeout =
129
- (args.timeout as number | undefined) ??
130
- (runInBackground ? undefined : BASH_DEFAULT_TIMEOUT_MS);
139
+ // When run_in_background is explicitly set, cancel any timeout — the intent is to
140
+ // let the process run to completion (matching Claude Code behavior where background()
141
+ // clears the timeout via cleanupListeners).
142
+ const timeout = runInBackground
143
+ ? undefined
144
+ : ((args.timeout as number | undefined) ?? BASH_DEFAULT_TIMEOUT_MS);
131
145
 
132
146
  if (!command || typeof command !== "string") {
133
147
  return {
@@ -195,7 +209,7 @@ The working directory persists between commands. Try to maintain your current wo
195
209
  };
196
210
  }
197
211
 
198
- const { id: taskId } = backgroundTaskManager.startShell(command, timeout);
212
+ const { id: taskId } = backgroundTaskManager.startShell(command);
199
213
  const task = backgroundTaskManager.getTask(taskId);
200
214
  const outputPath = task?.outputPath;
201
215
  const backgroundMsg = [
@@ -300,12 +314,47 @@ The working directory persists between commands. Try to maintain your current wo
300
314
  });
301
315
  }
302
316
 
303
- // Set up timeout
317
+ // Set up timeout — auto-background if allowed, otherwise kill
304
318
  let timeoutHandle: NodeJS.Timeout | undefined;
319
+ const shouldAutoBackground =
320
+ !!context.backgroundTaskManager && isAutobackgroundingAllowed(command);
305
321
  if (timeout && timeout > 0) {
306
322
  timeoutHandle = setTimeout(() => {
307
- if (!isAborted) {
308
- handleAbort("Command timed out");
323
+ if (!isAborted && !isBackgrounded) {
324
+ if (shouldAutoBackground) {
325
+ // Auto-background: move the process to background task manager instead of killing it
326
+ isBackgrounded = true;
327
+ if (timeoutHandle) {
328
+ clearTimeout(timeoutHandle);
329
+ }
330
+
331
+ // Unregister foreground task since it's now backgrounded
332
+ if (context.foregroundTaskManager) {
333
+ context.foregroundTaskManager.unregisterForegroundTask(
334
+ foregroundTaskId,
335
+ );
336
+ }
337
+
338
+ const backgroundTaskManager = context.backgroundTaskManager!;
339
+ const taskId = backgroundTaskManager.adoptProcess(
340
+ child,
341
+ command,
342
+ outputBuffer,
343
+ errorBuffer,
344
+ );
345
+ const task = backgroundTaskManager.getTask(taskId);
346
+ const outputPath = task?.outputPath;
347
+ logger.info(
348
+ `[Bash] Command timed out after ${timeout}ms, auto-backgrounded as ${taskId}`,
349
+ );
350
+ resolve({
351
+ success: true,
352
+ content: `Command timed out after ${timeout / 1000} seconds and was moved to background with ID: ${taskId}.${outputPath ? ` Real-time output: ${outputPath}` : ""}`,
353
+ shortResult: `Process ${taskId} auto-backgrounded (timeout)`,
354
+ });
355
+ } else {
356
+ handleAbort("Command timed out");
357
+ }
309
358
  }
310
359
  }, timeout);
311
360
  }
@@ -330,8 +379,14 @@ The working directory persists between commands. Try to maintain your current wo
330
379
  if (child.pid && !child.killed) {
331
380
  try {
332
381
  process.kill(-child.pid, "SIGKILL");
333
- } catch (killError) {
334
- logger.error("Failed to force kill process:", killError);
382
+ } catch (killError: unknown) {
383
+ // ESRCH means the process already exited — not an error
384
+ if (
385
+ !(killError instanceof Error) ||
386
+ (killError as NodeJS.ErrnoException).code !== "ESRCH"
387
+ ) {
388
+ logger.error("Failed to force kill process:", killError);
389
+ }
335
390
  }
336
391
  }
337
392
  }, 1000);
@@ -295,8 +295,7 @@ Usage:
295
295
  logger.warn(`File ${filePath} exists but has empty contents`);
296
296
  return {
297
297
  success: true,
298
- content:
299
- "⚠️ System reminder: This file exists but has empty contents.",
298
+ content: "System reminder: This file exists but has empty contents.",
300
299
  shortResult: "Empty file",
301
300
  metadata: {
302
301
  type: "text",
@@ -7,6 +7,42 @@ import {
7
7
  TASK_LIST_TOOL_NAME,
8
8
  } from "../constants/tools.js";
9
9
 
10
+ /**
11
+ * Helper to record and commit a reversion snapshot for a task file.
12
+ */
13
+ async function recordSnapshot(
14
+ context: ToolContext,
15
+ taskManager: { getTaskPath: (id: string) => string },
16
+ taskId: string,
17
+ operation: "create" | "modify" | "delete",
18
+ ): Promise<string | undefined> {
19
+ if (!context.reversionManager || !context.messageId) return undefined;
20
+ const snapshotId = await context.reversionManager.recordSnapshot(
21
+ context.messageId,
22
+ taskManager.getTaskPath(taskId),
23
+ operation,
24
+ );
25
+ await context.reversionManager.commitSnapshot(snapshotId);
26
+ return snapshotId;
27
+ }
28
+
29
+ /**
30
+ * Helper to update a target task's blocks/blockedBy array and record a reversion snapshot.
31
+ */
32
+ async function updateTaskField(
33
+ context: ToolContext,
34
+ taskManager: {
35
+ getTaskPath: (id: string) => string;
36
+ updateTask: (task: Task) => Promise<void>;
37
+ },
38
+ targetTask: Task,
39
+ field: "blocks" | "blockedBy",
40
+ value: string[],
41
+ ): Promise<void> {
42
+ await recordSnapshot(context, taskManager, targetTask.id, "modify");
43
+ await taskManager.updateTask({ ...targetTask, [field]: value });
44
+ }
45
+
10
46
  export const taskCreateTool: ToolPlugin = {
11
47
  name: TASK_CREATE_TOOL_NAME,
12
48
  config: {
@@ -23,32 +59,13 @@ export const taskCreateTool: ToolPlugin = {
23
59
  },
24
60
  description: {
25
61
  type: "string",
26
- description: "A detailed description of what needs to be done",
27
- },
28
- status: {
29
- type: "string",
30
- enum: ["pending", "in_progress", "completed", "deleted"],
31
- description: "Initial status of the task. Defaults to 'pending'.",
62
+ description: "What needs to be done",
32
63
  },
33
64
  activeForm: {
34
65
  type: "string",
35
66
  description:
36
67
  'Present continuous form shown in spinner when in_progress (e.g., "Running tests")',
37
68
  },
38
- owner: {
39
- type: "string",
40
- description: "Optional owner of the task.",
41
- },
42
- blocks: {
43
- type: "array",
44
- items: { type: "string" },
45
- description: "List of task IDs that this task blocks.",
46
- },
47
- blockedBy: {
48
- type: "array",
49
- items: { type: "string" },
50
- description: "List of task IDs that block this task.",
51
- },
52
69
  metadata: {
53
70
  type: "object",
54
71
  description: "Arbitrary metadata to attach to the task",
@@ -88,7 +105,7 @@ NOTE that you should not use this tool if there is only one trivial task to do.
88
105
  ## Task Fields
89
106
 
90
107
  - **subject**: A brief, actionable title in imperative form (e.g., "Fix authentication bug in login flow")
91
- - **description**: Detailed description of what needs to be done, including context and acceptance criteria
108
+ - **description**: What needs to be done, including context and acceptance criteria
92
109
  - **activeForm**: Present continuous form shown in spinner when task is in_progress (e.g., "Fixing authentication bug"). This is displayed to the user while you work on the task.
93
110
 
94
111
  **IMPORTANT**: Always provide activeForm when creating tasks. The subject should be imperative ("Run tests") while activeForm should be present continuous ("Running tests"). All tasks are created with status \`pending\`.
@@ -102,40 +119,23 @@ NOTE that you should not use this tool if there is only one trivial task to do.
102
119
  execute: async (args, context: ToolContext): Promise<ToolResult> => {
103
120
  const taskManager = context.taskManager;
104
121
 
105
- if (args.status === "deleted") {
106
- return {
107
- success: true,
108
- content: `Task creation skipped because status was set to 'deleted'.`,
109
- shortResult: `Skipped deleted task`,
110
- };
111
- }
112
-
113
122
  const task: Omit<Task, "id"> = {
114
123
  subject: args.subject as string,
115
124
  description: args.description as string,
116
- status: (args.status as TaskStatus) || "pending",
125
+ status: "pending",
117
126
  activeForm: args.activeForm as string,
118
- owner: args.owner as string,
119
- blocks: (args.blocks as string[]) || [],
120
- blockedBy: (args.blockedBy as string[]) || [],
127
+ owner: undefined,
128
+ blocks: [],
129
+ blockedBy: [],
121
130
  metadata: (args.metadata as Record<string, unknown>) || {},
122
131
  };
123
132
 
124
133
  const taskId = await taskManager.createTask(task);
125
-
126
- if (context.reversionManager && context.messageId) {
127
- const taskPath = taskManager.getTaskPath(taskId);
128
- const snapshotId = await context.reversionManager.recordSnapshot(
129
- context.messageId,
130
- taskPath,
131
- "create",
132
- );
133
- await context.reversionManager.commitSnapshot(snapshotId);
134
- }
134
+ await recordSnapshot(context, taskManager, taskId, "create");
135
135
 
136
136
  return {
137
137
  success: true,
138
- content: `Task created with ID: ${taskId}`,
138
+ content: `Task #${taskId} created successfully: ${task.subject}`,
139
139
  shortResult: `Created task ${taskId}: ${task.subject}`,
140
140
  };
141
141
  },
@@ -189,13 +189,28 @@ Returns full task details:
189
189
  if (!task) {
190
190
  return {
191
191
  success: false,
192
- content: `Task with ID ${taskId} not found.`,
192
+ content: `Task not found`,
193
193
  };
194
194
  }
195
195
 
196
+ // Format like Claude Code: structured text instead of raw JSON
197
+ const lines = [
198
+ `Task #${task.id}: ${task.subject}`,
199
+ `Status: ${task.status}`,
200
+ `Description: ${task.description}`,
201
+ ];
202
+ if (task.blockedBy.length > 0) {
203
+ lines.push(
204
+ `Blocked by: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`,
205
+ );
206
+ }
207
+ if (task.blocks.length > 0) {
208
+ lines.push(`Blocks: ${task.blocks.map((id) => `#${id}`).join(", ")}`);
209
+ }
210
+
196
211
  return {
197
212
  success: true,
198
- content: JSON.stringify(task, null, 2),
213
+ content: lines.join("\n"),
199
214
  };
200
215
  },
201
216
  };
@@ -339,210 +354,139 @@ Set up task dependencies:
339
354
  if (!existingTask) {
340
355
  return {
341
356
  success: false,
342
- content: `Task with ID ${taskId} not found.`,
357
+ content: `Task #${taskId} not found`,
343
358
  };
344
359
  }
345
360
 
361
+ // Handle deletion — just delete the task file (like Claude Code)
346
362
  if (args.status === "deleted") {
347
- // Reciprocal Dependency Cleanup
348
- // For each task in the deleted task's blocks list, remove the deleted task's ID from their blockedBy list.
349
- for (const targetId of existingTask.blocks) {
350
- const targetTask = await taskManager.getTask(targetId);
351
- if (targetTask && targetTask.blockedBy.includes(taskId)) {
352
- let targetSnapshotId: string | undefined;
353
- if (context.reversionManager && context.messageId) {
354
- const targetPath = taskManager.getTaskPath(targetId);
355
- targetSnapshotId = await context.reversionManager.recordSnapshot(
356
- context.messageId,
357
- targetPath,
358
- "modify",
359
- );
360
- }
361
- await taskManager.updateTask({
362
- ...targetTask,
363
- blockedBy: targetTask.blockedBy.filter((id) => id !== taskId),
364
- });
365
- if (context.reversionManager && targetSnapshotId) {
366
- await context.reversionManager.commitSnapshot(targetSnapshotId);
367
- }
368
- }
369
- }
370
-
371
- // For each task in the deleted task's blockedBy list, remove the deleted task's ID from their blocks list.
372
- for (const targetId of existingTask.blockedBy) {
373
- const targetTask = await taskManager.getTask(targetId);
374
- if (targetTask && targetTask.blocks.includes(taskId)) {
375
- let targetSnapshotId: string | undefined;
376
- if (context.reversionManager && context.messageId) {
377
- const targetPath = taskManager.getTaskPath(targetId);
378
- targetSnapshotId = await context.reversionManager.recordSnapshot(
379
- context.messageId,
380
- targetPath,
381
- "modify",
382
- );
383
- }
384
- await taskManager.updateTask({
385
- ...targetTask,
386
- blocks: targetTask.blocks.filter((id) => id !== taskId),
387
- });
388
- if (context.reversionManager && targetSnapshotId) {
389
- await context.reversionManager.commitSnapshot(targetSnapshotId);
390
- }
391
- }
392
- }
393
-
394
- // Record delete snapshot for the task itself
395
- if (context.reversionManager && context.messageId) {
396
- const taskPath = taskManager.getTaskPath(taskId);
397
- const deleteSnapshotId = await context.reversionManager.recordSnapshot(
398
- context.messageId,
399
- taskPath,
400
- "delete",
401
- );
402
- await context.reversionManager.commitSnapshot(deleteSnapshotId);
403
- }
404
-
363
+ await recordSnapshot(context, taskManager, taskId, "delete");
405
364
  await taskManager.deleteTask(taskId);
406
365
 
407
366
  return {
408
367
  success: true,
409
- content: `Task #${taskId} deleted and removed from disk.`,
368
+ content: `Task #${taskId} deleted`,
410
369
  shortResult: `Deleted task ${taskId}`,
411
370
  };
412
371
  }
413
372
 
414
- let snapshotId: string | undefined;
415
- if (context.reversionManager && context.messageId) {
416
- const taskPath = taskManager.getTaskPath(taskId);
417
- snapshotId = await context.reversionManager.recordSnapshot(
418
- context.messageId,
419
- taskPath,
420
- "modify",
421
- );
422
- }
423
-
373
+ // Build updates object — only include changed fields
424
374
  const updatedFields: string[] = [];
425
-
426
- const updatedTask: Task = {
427
- ...existingTask,
428
- };
375
+ const updates: {
376
+ subject?: string;
377
+ description?: string;
378
+ activeForm?: string;
379
+ status?: TaskStatus;
380
+ owner?: string;
381
+ metadata?: Record<string, unknown>;
382
+ } = {};
429
383
 
430
384
  if (args.subject !== undefined && args.subject !== existingTask.subject) {
431
- updatedTask.subject = args.subject as string;
385
+ updates.subject = args.subject as string;
432
386
  updatedFields.push("subject");
433
387
  }
434
388
  if (
435
389
  args.description !== undefined &&
436
390
  args.description !== existingTask.description
437
391
  ) {
438
- updatedTask.description = args.description as string;
392
+ updates.description = args.description as string;
439
393
  updatedFields.push("description");
440
394
  }
441
- if (args.status !== undefined && args.status !== existingTask.status) {
442
- updatedTask.status = args.status as TaskStatus;
443
- updatedFields.push("status");
444
- }
445
395
  if (
446
396
  args.activeForm !== undefined &&
447
397
  args.activeForm !== existingTask.activeForm
448
398
  ) {
449
- updatedTask.activeForm = args.activeForm as string;
399
+ updates.activeForm = args.activeForm as string;
450
400
  updatedFields.push("activeForm");
451
401
  }
452
402
  if (args.owner !== undefined && args.owner !== existingTask.owner) {
453
- updatedTask.owner = args.owner as string;
403
+ updates.owner = args.owner as string;
454
404
  updatedFields.push("owner");
455
405
  }
406
+ if (args.status !== undefined && args.status !== existingTask.status) {
407
+ updates.status = args.status as TaskStatus;
408
+ updatedFields.push("status");
409
+ }
456
410
 
411
+ // Merge metadata (null values delete keys)
457
412
  if (args.metadata !== undefined) {
458
- const newMetadata = { ...(existingTask.metadata || {}) };
413
+ const merged = { ...(existingTask.metadata ?? {}) };
459
414
  for (const [key, value] of Object.entries(
460
415
  args.metadata as Record<string, unknown>,
461
416
  )) {
462
417
  if (value === null) {
463
- delete newMetadata[key];
418
+ delete merged[key];
464
419
  } else {
465
- newMetadata[key] = value;
420
+ merged[key] = value;
466
421
  }
467
422
  }
468
- updatedTask.metadata = newMetadata;
423
+ updates.metadata = merged;
469
424
  updatedFields.push("metadata");
470
425
  }
471
426
 
427
+ // Apply basic field updates
428
+ if (Object.keys(updates).length > 0) {
429
+ await recordSnapshot(context, taskManager, taskId, "modify");
430
+ await taskManager.updateTask({ ...existingTask, ...updates });
431
+ }
432
+
433
+ // Add blocks: update this task's blocks + reciprocal blockedBy on targets
472
434
  if (args.addBlocks !== undefined) {
473
435
  const blocksToAdd = (args.addBlocks as string[]).filter(
474
- (id) => !updatedTask.blocks.includes(id),
436
+ (id) => !existingTask.blocks.includes(id),
475
437
  );
476
438
  if (blocksToAdd.length > 0) {
477
- updatedTask.blocks = [...updatedTask.blocks, ...blocksToAdd];
439
+ await recordSnapshot(context, taskManager, taskId, "modify");
440
+ await taskManager.updateTask({
441
+ ...existingTask,
442
+ ...updates,
443
+ blocks: [...existingTask.blocks, ...blocksToAdd],
444
+ });
478
445
  updatedFields.push("blocks");
479
446
 
480
- // Also update the blockedBy of the target tasks
481
447
  for (const targetId of blocksToAdd) {
482
448
  const targetTask = await taskManager.getTask(targetId);
483
449
  if (targetTask && !targetTask.blockedBy.includes(taskId)) {
484
- let targetSnapshotId: string | undefined;
485
- if (context.reversionManager && context.messageId) {
486
- const targetPath = taskManager.getTaskPath(targetId);
487
- targetSnapshotId = await context.reversionManager.recordSnapshot(
488
- context.messageId,
489
- targetPath,
490
- "modify",
491
- );
492
- }
493
- await taskManager.updateTask({
494
- ...targetTask,
495
- blockedBy: [...targetTask.blockedBy, taskId],
496
- });
497
- if (context.reversionManager && targetSnapshotId) {
498
- await context.reversionManager.commitSnapshot(targetSnapshotId);
499
- }
450
+ await updateTaskField(
451
+ context,
452
+ taskManager,
453
+ targetTask,
454
+ "blockedBy",
455
+ [...targetTask.blockedBy, taskId],
456
+ );
500
457
  }
501
458
  }
502
459
  }
503
460
  }
504
461
 
462
+ // Add blockedBy: update this task's blockedBy + reciprocal blocks on targets
505
463
  if (args.addBlockedBy !== undefined) {
506
464
  const blockedByToAdd = (args.addBlockedBy as string[]).filter(
507
- (id) => !updatedTask.blockedBy.includes(id),
465
+ (id) => !existingTask.blockedBy.includes(id),
508
466
  );
509
467
  if (blockedByToAdd.length > 0) {
510
- updatedTask.blockedBy = [...updatedTask.blockedBy, ...blockedByToAdd];
468
+ await recordSnapshot(context, taskManager, taskId, "modify");
469
+ await taskManager.updateTask({
470
+ ...existingTask,
471
+ ...updates,
472
+ blockedBy: [...existingTask.blockedBy, ...blockedByToAdd],
473
+ });
511
474
  updatedFields.push("blockedBy");
512
475
 
513
- // Also update the blocks of the target tasks
514
476
  for (const targetId of blockedByToAdd) {
515
477
  const targetTask = await taskManager.getTask(targetId);
516
478
  if (targetTask && !targetTask.blocks.includes(taskId)) {
517
- let targetSnapshotId: string | undefined;
518
- if (context.reversionManager && context.messageId) {
519
- const targetPath = taskManager.getTaskPath(targetId);
520
- targetSnapshotId = await context.reversionManager.recordSnapshot(
521
- context.messageId,
522
- targetPath,
523
- "modify",
524
- );
525
- }
526
- await taskManager.updateTask({
527
- ...targetTask,
528
- blocks: [...targetTask.blocks, taskId],
529
- });
530
- if (context.reversionManager && targetSnapshotId) {
531
- await context.reversionManager.commitSnapshot(targetSnapshotId);
532
- }
479
+ await updateTaskField(context, taskManager, targetTask, "blocks", [
480
+ ...targetTask.blocks,
481
+ taskId,
482
+ ]);
533
483
  }
534
484
  }
535
485
  }
536
486
  }
537
487
 
538
- await taskManager.updateTask(updatedTask);
539
-
540
- if (context.reversionManager && snapshotId) {
541
- await context.reversionManager.commitSnapshot(snapshotId);
542
- }
543
-
544
488
  let content = `Updated task #${taskId} ${updatedFields.join(", ")}`;
545
- if (updatedTask.status === "completed") {
489
+ if (updates.status === "completed") {
546
490
  content += `\n\nTask completed. Call TaskList now to find your next available task or see if your work unblocked others.`;
547
491
  }
548
492
 
@@ -563,13 +507,7 @@ export const taskListTool: ToolPlugin = {
563
507
  description: "List all tasks in the task list",
564
508
  parameters: {
565
509
  type: "object",
566
- properties: {
567
- status: {
568
- type: "string",
569
- enum: ["pending", "in_progress", "completed", "deleted"],
570
- description: "Optional filter by status.",
571
- },
572
- },
510
+ properties: {},
573
511
  },
574
512
  },
575
513
  },
@@ -593,26 +531,39 @@ Returns a summary of each task:
593
531
  - **blockedBy**: List of open task IDs that must be resolved first (tasks with blockedBy cannot be claimed until dependencies resolve)
594
532
 
595
533
  Use TaskGet with a specific task ID to view full details including description and comments.`,
596
- execute: async (args, context: ToolContext): Promise<ToolResult> => {
534
+ execute: async (_args, context: ToolContext): Promise<ToolResult> => {
597
535
  const taskManager = context.taskManager;
598
536
 
599
- let tasks = await taskManager.listTasks();
600
- if (args.status) {
601
- tasks = tasks.filter((t) => t.status === args.status);
602
- }
537
+ // Filter out internal metadata tasks (like Claude Code)
538
+ const allTasks = (await taskManager.listTasks()).filter(
539
+ (t) => !(t.metadata as Record<string, unknown>)?._internal,
540
+ );
603
541
 
604
- if (tasks.length === 0) {
542
+ if (allTasks.length === 0) {
605
543
  return {
606
544
  success: true,
607
- content: "No tasks found.",
545
+ content: "No tasks found",
608
546
  };
609
547
  }
610
548
 
611
549
  // Sort by ID numerically
612
- tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
613
-
614
- const content = tasks
615
- .map((t) => `[${t.id}] ${t.subject} (${t.status})`)
550
+ allTasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
551
+
552
+ // Filter resolved blockers from blockedBy
553
+ const completedIds = new Set(
554
+ allTasks.filter((t) => t.status === "completed").map((t) => t.id),
555
+ );
556
+
557
+ const content = allTasks
558
+ .map((t) => {
559
+ const blockedBy = t.blockedBy.filter((id) => !completedIds.has(id));
560
+ const owner = t.owner ? ` (${t.owner})` : "";
561
+ const blocked =
562
+ blockedBy.length > 0
563
+ ? ` [blocked by ${blockedBy.map((id) => `#${id}`).join(", ")}]`
564
+ : "";
565
+ return `#${t.id} [${t.status}] ${t.subject}${owner}${blocked}`;
566
+ })
616
567
  .join("\n");
617
568
 
618
569
  return {
@@ -113,4 +113,6 @@ export interface ToolContext {
113
113
  originalWorkdir?: string;
114
114
  /** Merged environment variables (process.env + agent env) for child processes */
115
115
  env?: Record<string, string>;
116
+ /** Workflow manager instance for workflow orchestration */
117
+ workflowManager?: import("../managers/workflowManager.js").WorkflowManager;
116
118
  }
@@ -96,9 +96,6 @@ function isPermittedRedirect(
96
96
  }
97
97
  }
98
98
 
99
- const GITHUB_URL_ERROR =
100
- "For GitHub URLs, please use the 'gh' CLI via the Bash tool instead (e.g., 'gh pr view', 'gh issue view', 'gh api').";
101
-
102
99
  // --- Tool ---
103
100
 
104
101
  export const webFetchTool: ToolPlugin = {
@@ -175,15 +172,6 @@ Usage notes:
175
172
  };
176
173
  }
177
174
 
178
- // Check for GitHub URLs
179
- if (url.includes("github.com")) {
180
- return {
181
- success: false,
182
- content: "",
183
- error: GITHUB_URL_ERROR,
184
- };
185
- }
186
-
187
175
  try {
188
176
  const cached = cache.get(url);
189
177
  if (cached) {