super-subagents 1.0.1

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 (149) hide show
  1. package/.windsurf/plans/persist-tasks-by-cwd.md +113 -0
  2. package/CHANGELOG.md +10 -0
  3. package/CLAUDE.md +67 -0
  4. package/README.md +124 -0
  5. package/build/config/timeouts.d.ts +12 -0
  6. package/build/config/timeouts.d.ts.map +1 -0
  7. package/build/config/timeouts.js +21 -0
  8. package/build/config/timeouts.js.map +1 -0
  9. package/build/index.d.ts +3 -0
  10. package/build/index.d.ts.map +1 -0
  11. package/build/index.js +116 -0
  12. package/build/index.js.map +1 -0
  13. package/build/models.d.ts +11 -0
  14. package/build/models.d.ts.map +1 -0
  15. package/build/models.js +22 -0
  16. package/build/models.js.map +1 -0
  17. package/build/services/client-context.d.ts +31 -0
  18. package/build/services/client-context.d.ts.map +1 -0
  19. package/build/services/client-context.js +44 -0
  20. package/build/services/client-context.js.map +1 -0
  21. package/build/services/copilot-switch.d.ts +36 -0
  22. package/build/services/copilot-switch.d.ts.map +1 -0
  23. package/build/services/copilot-switch.js +226 -0
  24. package/build/services/copilot-switch.js.map +1 -0
  25. package/build/services/process-spawner.d.ts +9 -0
  26. package/build/services/process-spawner.d.ts.map +1 -0
  27. package/build/services/process-spawner.js +475 -0
  28. package/build/services/process-spawner.js.map +1 -0
  29. package/build/services/retry-queue.d.ts +35 -0
  30. package/build/services/retry-queue.d.ts.map +1 -0
  31. package/build/services/retry-queue.js +125 -0
  32. package/build/services/retry-queue.js.map +1 -0
  33. package/build/services/task-manager.d.ts +124 -0
  34. package/build/services/task-manager.d.ts.map +1 -0
  35. package/build/services/task-manager.js +584 -0
  36. package/build/services/task-manager.js.map +1 -0
  37. package/build/services/task-persistence.d.ts +29 -0
  38. package/build/services/task-persistence.d.ts.map +1 -0
  39. package/build/services/task-persistence.js +158 -0
  40. package/build/services/task-persistence.js.map +1 -0
  41. package/build/templates/index.d.ts +11 -0
  42. package/build/templates/index.d.ts.map +1 -0
  43. package/build/templates/index.js +30 -0
  44. package/build/templates/index.js.map +1 -0
  45. package/build/tools/batch-spawn.d.ts +69 -0
  46. package/build/tools/batch-spawn.d.ts.map +1 -0
  47. package/build/tools/batch-spawn.js +150 -0
  48. package/build/tools/batch-spawn.js.map +1 -0
  49. package/build/tools/cancel-task.d.ts +21 -0
  50. package/build/tools/cancel-task.d.ts.map +1 -0
  51. package/build/tools/cancel-task.js +44 -0
  52. package/build/tools/cancel-task.js.map +1 -0
  53. package/build/tools/clear-tasks.d.ts +21 -0
  54. package/build/tools/clear-tasks.d.ts.map +1 -0
  55. package/build/tools/clear-tasks.js +43 -0
  56. package/build/tools/clear-tasks.js.map +1 -0
  57. package/build/tools/force-start.d.ts +21 -0
  58. package/build/tools/force-start.d.ts.map +1 -0
  59. package/build/tools/force-start.js +38 -0
  60. package/build/tools/force-start.js.map +1 -0
  61. package/build/tools/get-status.d.ts +31 -0
  62. package/build/tools/get-status.d.ts.map +1 -0
  63. package/build/tools/get-status.js +384 -0
  64. package/build/tools/get-status.js.map +1 -0
  65. package/build/tools/list-tasks.d.ts +26 -0
  66. package/build/tools/list-tasks.d.ts.map +1 -0
  67. package/build/tools/list-tasks.js +74 -0
  68. package/build/tools/list-tasks.js.map +1 -0
  69. package/build/tools/recover-task.d.ts +29 -0
  70. package/build/tools/recover-task.d.ts.map +1 -0
  71. package/build/tools/recover-task.js +91 -0
  72. package/build/tools/recover-task.js.map +1 -0
  73. package/build/tools/resume-task.d.ts +29 -0
  74. package/build/tools/resume-task.d.ts.map +1 -0
  75. package/build/tools/resume-task.js +43 -0
  76. package/build/tools/resume-task.js.map +1 -0
  77. package/build/tools/retry-task.d.ts +21 -0
  78. package/build/tools/retry-task.d.ts.map +1 -0
  79. package/build/tools/retry-task.js +52 -0
  80. package/build/tools/retry-task.js.map +1 -0
  81. package/build/tools/simulate-rate-limit.d.ts +25 -0
  82. package/build/tools/simulate-rate-limit.d.ts.map +1 -0
  83. package/build/tools/simulate-rate-limit.js +69 -0
  84. package/build/tools/simulate-rate-limit.js.map +1 -0
  85. package/build/tools/spawn-task.d.ts +57 -0
  86. package/build/tools/spawn-task.d.ts.map +1 -0
  87. package/build/tools/spawn-task.js +113 -0
  88. package/build/tools/spawn-task.js.map +1 -0
  89. package/build/tools/stream-output.d.ts +29 -0
  90. package/build/tools/stream-output.d.ts.map +1 -0
  91. package/build/tools/stream-output.js +96 -0
  92. package/build/tools/stream-output.js.map +1 -0
  93. package/build/types.d.ts +75 -0
  94. package/build/types.d.ts.map +1 -0
  95. package/build/types.js +12 -0
  96. package/build/types.js.map +1 -0
  97. package/build/utils/format.d.ts +29 -0
  98. package/build/utils/format.d.ts.map +1 -0
  99. package/build/utils/format.js +81 -0
  100. package/build/utils/format.js.map +1 -0
  101. package/build/utils/sanitize.d.ts +63 -0
  102. package/build/utils/sanitize.d.ts.map +1 -0
  103. package/build/utils/sanitize.js +28 -0
  104. package/build/utils/sanitize.js.map +1 -0
  105. package/build/utils/task-id-generator.d.ts +10 -0
  106. package/build/utils/task-id-generator.d.ts.map +1 -0
  107. package/build/utils/task-id-generator.js +22 -0
  108. package/build/utils/task-id-generator.js.map +1 -0
  109. package/docs/timeout-durability.md +28 -0
  110. package/package.json +39 -0
  111. package/plans/timeout-durability/00-overview.md +38 -0
  112. package/plans/timeout-durability/01-analysis.md +37 -0
  113. package/plans/timeout-durability/decisions.md +22 -0
  114. package/plans/timeout-durability/resources.md +24 -0
  115. package/plans/timeout-durability/step-01-timeout-flow.md +27 -0
  116. package/plans/timeout-durability/step-02-root-cause-map.md +26 -0
  117. package/plans/timeout-durability/step-03-state-schema.md +26 -0
  118. package/plans/timeout-durability/step-04-messaging-recovery.md +27 -0
  119. package/src/config/timeouts.ts +22 -0
  120. package/src/index.ts +129 -0
  121. package/src/models.ts +23 -0
  122. package/src/services/client-context.ts +49 -0
  123. package/src/services/copilot-switch.ts +269 -0
  124. package/src/services/process-spawner.ts +548 -0
  125. package/src/services/retry-queue.ts +151 -0
  126. package/src/services/task-manager.ts +667 -0
  127. package/src/services/task-persistence.ts +175 -0
  128. package/src/templates/index.ts +35 -0
  129. package/src/templates/super-coder.mdx +519 -0
  130. package/src/templates/super-planner.mdx +558 -0
  131. package/src/templates/super-researcher.mdx +394 -0
  132. package/src/templates/super-tester.mdx +688 -0
  133. package/src/tools/batch-spawn.ts +179 -0
  134. package/src/tools/cancel-task.ts +58 -0
  135. package/src/tools/clear-tasks.ts +52 -0
  136. package/src/tools/force-start.ts +48 -0
  137. package/src/tools/get-status.ts +480 -0
  138. package/src/tools/list-tasks.ts +83 -0
  139. package/src/tools/recover-task.ts +112 -0
  140. package/src/tools/resume-task.ts +51 -0
  141. package/src/tools/retry-task.ts +72 -0
  142. package/src/tools/simulate-rate-limit.ts +84 -0
  143. package/src/tools/spawn-task.ts +135 -0
  144. package/src/tools/stream-output.ts +101 -0
  145. package/src/types.ts +86 -0
  146. package/src/utils/format.ts +83 -0
  147. package/src/utils/sanitize.ts +35 -0
  148. package/src/utils/task-id-generator.ts +25 -0
  149. package/tsconfig.json +20 -0
@@ -0,0 +1,51 @@
1
+ import { ResumeTaskSchema } from '../utils/sanitize.js';
2
+ import { spawnCopilotProcess } from '../services/process-spawner.js';
3
+ import { mcpText, formatError, join } from '../utils/format.js';
4
+
5
+ export const resumeTaskTool = {
6
+ name: 'resume_task',
7
+ description: `Resume an interrupted Copilot session. Get session_id from get_status response.`,
8
+ inputSchema: {
9
+ type: 'object' as const,
10
+ properties: {
11
+ session_id: {
12
+ type: 'string',
13
+ description: 'Session ID from previous task.'
14
+ },
15
+ cwd: {
16
+ type: 'string',
17
+ description: 'Working directory. Auto-detected if omitted.'
18
+ },
19
+ timeout: {
20
+ type: 'number',
21
+ description: 'Max execution time in ms. Default: 600000 (configurable via MCP_TASK_TIMEOUT_MS).'
22
+ },
23
+ },
24
+ required: ['session_id'],
25
+ },
26
+ };
27
+
28
+ export async function handleResumeTask(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
29
+ try {
30
+ const input = args as any;
31
+ const parsed = ResumeTaskSchema.parse({ sessionId: input?.session_id || input?.sessionId, ...input });
32
+
33
+ const taskId = await spawnCopilotProcess({
34
+ prompt: '',
35
+ timeout: parsed.timeout,
36
+ cwd: parsed.cwd,
37
+ autonomous: parsed.autonomous,
38
+ resumeSessionId: parsed.sessionId,
39
+ });
40
+
41
+ return mcpText(join(
42
+ `Session \`${parsed.sessionId}\` resumed as task **${taskId}**.`,
43
+ 'Check status with `get_status`.'
44
+ ));
45
+ } catch (error) {
46
+ return mcpText(formatError(
47
+ error instanceof Error ? error.message : 'Unknown',
48
+ 'Get `session_id` from a completed or failed task using `get_status` before resuming.'
49
+ ));
50
+ }
51
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { taskManager } from '../services/task-manager.js';
3
+ import { TaskStatus } from '../types.js';
4
+ import { mcpText, formatError, join, displayStatus } from '../utils/format.js';
5
+
6
+ const RetryTaskSchema = z.object({
7
+ task_id: z.string().min(1).describe('Task ID to retry'),
8
+ });
9
+
10
+ export const retryTaskTool = {
11
+ name: 'retry_task',
12
+ description: `Immediately retry a rate-limited task. Creates new task with same prompt.`,
13
+ inputSchema: {
14
+ type: 'object' as const,
15
+ properties: {
16
+ task_id: {
17
+ type: 'string',
18
+ description: 'Rate-limited task ID to retry.',
19
+ },
20
+ },
21
+ required: ['task_id'],
22
+ },
23
+ };
24
+
25
+ export async function handleRetryTask(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
26
+ try {
27
+ const parsed = RetryTaskSchema.parse(args || {});
28
+ const taskId = parsed.task_id.toLowerCase().trim();
29
+
30
+ const task = taskManager.getTask(taskId);
31
+
32
+ if (!task) {
33
+ return mcpText(formatError('Task not found', 'Use `list_tasks` to find valid task IDs.'));
34
+ }
35
+
36
+ if (task.status !== TaskStatus.RATE_LIMITED) {
37
+ const hint = task.status === TaskStatus.FAILED
38
+ ? 'Task has already failed. Use `spawn_task` to create a new task with the same prompt.'
39
+ : 'Only `rate_limited` tasks can be retried with this tool.';
40
+ return mcpText(formatError(
41
+ `Task is not rate-limited (status: ${displayStatus(task.status)})`,
42
+ hint
43
+ ));
44
+ }
45
+
46
+ // Check if max retries exceeded
47
+ if (task.retryInfo && task.retryInfo.retryCount >= task.retryInfo.maxRetries) {
48
+ return mcpText(formatError(
49
+ `Max retries exceeded (${task.retryInfo.retryCount}/${task.retryInfo.maxRetries})`,
50
+ 'Use `spawn_task` to create a new task with the same prompt.'
51
+ ));
52
+ }
53
+
54
+ // Trigger the retry via TaskManager
55
+ const result = await taskManager.triggerManualRetry(taskId);
56
+
57
+ if (!result.success) {
58
+ return mcpText(formatError(result.error || 'Retry failed'));
59
+ }
60
+
61
+ const attempt = (task.retryInfo?.retryCount ?? 0) + 1;
62
+ return mcpText(join(
63
+ `Retried **${task.id}** as **${result.newTaskId}** (attempt ${attempt}).`,
64
+ 'Check status with `get_status`.'
65
+ ));
66
+ } catch (error) {
67
+ return mcpText(formatError(
68
+ error instanceof Error ? error.message : 'Unknown error',
69
+ 'Ensure `task_id` is provided.'
70
+ ));
71
+ }
72
+ }
@@ -0,0 +1,84 @@
1
+ import { z } from 'zod';
2
+ import { taskManager } from '../services/task-manager.js';
3
+ import { TaskStatus } from '../types.js';
4
+ import { createRetryInfo } from '../services/retry-queue.js';
5
+ import { mcpText, formatError, join } from '../utils/format.js';
6
+
7
+ const SimulateRateLimitSchema = z.object({
8
+ prompt: z.string().min(1).max(50000).optional().default('Test task for rate limit simulation'),
9
+ skip_fallback: z.boolean().optional().default(false),
10
+ });
11
+
12
+ export const simulateRateLimitTool = {
13
+ name: 'simulate_rate_limit',
14
+ description: `[DEBUG] Simulate a rate-limited task to test Claude CLI fallback behavior. Creates a task, marks it as rate-limited, and optionally triggers the fallback flow via manual retry.`,
15
+ inputSchema: {
16
+ type: 'object' as const,
17
+ properties: {
18
+ prompt: {
19
+ type: 'string',
20
+ description: 'Task prompt to use for the simulated task. Default: test prompt.',
21
+ },
22
+ skip_fallback: {
23
+ type: 'boolean',
24
+ description: 'If true, skip triggering retry/fallback and leave task in RATE_LIMITED state for inspection. Default: false.',
25
+ },
26
+ },
27
+ required: [],
28
+ },
29
+ };
30
+
31
+ export async function handleSimulateRateLimit(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
32
+ try {
33
+ const parsed = SimulateRateLimitSchema.parse(args || {});
34
+
35
+ // Create a task marked as coming from copilot
36
+ const task = taskManager.createTask(parsed.prompt, undefined, undefined, {
37
+ provider: 'copilot',
38
+ fallbackAttempted: parsed.skip_fallback,
39
+ });
40
+
41
+ // Simulate some output
42
+ taskManager.appendOutput(task.id, 'Starting task...');
43
+ taskManager.appendOutput(task.id, '[simulated] Error: too many requests, rate limit exceeded');
44
+
45
+ // Mark as rate-limited
46
+ const retryInfo = createRetryInfo(task, 'Simulated rate limit');
47
+ taskManager.updateTask(task.id, {
48
+ status: TaskStatus.RATE_LIMITED,
49
+ exitCode: 1,
50
+ endTime: new Date().toISOString(),
51
+ error: 'Simulated: too many requests, rate limit exceeded',
52
+ retryInfo,
53
+ });
54
+
55
+ // If not skipping fallback, trigger manual retry which will exercise the fallback path
56
+ let fallbackResult: { success: boolean; newTaskId?: string; error?: string } | undefined;
57
+ if (!parsed.skip_fallback) {
58
+ fallbackResult = await taskManager.triggerManualRetry(task.id);
59
+ }
60
+
61
+ let message: string;
62
+ if (fallbackResult?.success) {
63
+ message = join(
64
+ `[Debug] Rate limit simulated for **${task.id}**.`,
65
+ `Fallback retry triggered as **${fallbackResult.newTaskId}**.`,
66
+ 'Check status with `get_status`.'
67
+ );
68
+ } else if (parsed.skip_fallback) {
69
+ message = join(
70
+ `[Debug] Rate limit simulated for **${task.id}**.`,
71
+ 'Task left in `rate_limited` state for inspection.'
72
+ );
73
+ } else {
74
+ message = join(
75
+ `[Debug] Rate limit simulated for **${task.id}**.`,
76
+ `Fallback trigger failed: ${fallbackResult?.error || 'unknown error'}`
77
+ );
78
+ }
79
+
80
+ return mcpText(message);
81
+ } catch (error) {
82
+ return mcpText(formatError(error instanceof Error ? error.message : 'Unknown error'));
83
+ }
84
+ }
@@ -0,0 +1,135 @@
1
+ import { SpawnTaskSchema } from '../utils/sanitize.js';
2
+ import { spawnCopilotProcess } from '../services/process-spawner.js';
3
+ import { taskManager } from '../services/task-manager.js';
4
+ import { MODEL_IDS, MODELS, DEFAULT_MODEL } from '../models.js';
5
+ import { TASK_TYPE_IDS, TASK_TYPES, applyTemplate, isValidTaskType, type TaskType } from '../templates/index.js';
6
+ import { mcpText, formatError, join, formatLabels } from '../utils/format.js';
7
+
8
+ export const spawnTaskTool = {
9
+ name: 'spawn_task',
10
+ description: `Spawn an autonomous agent task. Each task runs as a completely isolated agent with NO shared memory or conversation history -- the prompt you provide is the ONLY context the agent receives.
11
+
12
+ IMPORTANT: Prefer spawning tasks one at a time with spawn_task. Only use batch_spawn when you have multiple small tasks with explicit cross-dependencies that must be set up atomically. For most work, individual spawn_task calls give you better control and let you write more detailed, context-rich prompts for each task.
13
+
14
+ Task types: super-coder (implementation), super-planner (architecture), super-researcher (investigation), super-tester (QA).
15
+ Models: ${MODEL_IDS.join(', ')}. Default: ${DEFAULT_MODEL}.`,
16
+ inputSchema: {
17
+ type: 'object' as const,
18
+ properties: {
19
+ prompt: {
20
+ type: 'string',
21
+ description: `The complete, self-contained instructions for the spawned agent. This is the ONLY context the agent will have -- it cannot see your conversation history, previous tool calls, or any other context.
22
+
23
+ Your prompt MUST include:
24
+ - WHAT to do: Clear, specific objective (not vague like "fix the bug" -- say exactly which bug, which file, what the expected behavior is)
25
+ - WHERE to do it: All relevant file paths as absolute paths (e.g. /Users/dev/project/src/auth.ts, not just "auth.ts")
26
+ - HOW to verify: What does "done" look like? What tests to run? What to check?
27
+ - CONTEXT: Any background the agent needs -- error messages, stack traces, related code snippets, architectural decisions
28
+
29
+ BAD prompt: "Fix the login bug"
30
+ GOOD prompt: "In /Users/dev/myapp/src/services/auth.ts, the login() function on line 45 throws 'TypeError: Cannot read property email of undefined' when the user object is null. Fix the null check, ensure the function returns a proper error response for missing users, and verify by running: npm test -- --grep login"
31
+
32
+ The more detailed your prompt, the better the agent performs. Treat it as a complete brief for a developer who has never seen the codebase before.`,
33
+ },
34
+ task_type: {
35
+ type: 'string',
36
+ enum: TASK_TYPE_IDS,
37
+ description: `Agent template that prepends specialized system instructions to your prompt.
38
+ - super-coder: For implementation tasks -- writing, editing, refactoring code
39
+ - super-planner: For architecture and design -- planning implementations, evaluating tradeoffs
40
+ - super-researcher: For investigation -- answering questions, finding code patterns, analyzing behavior
41
+ - super-tester: For QA -- writing tests, running test suites, verifying behavior`,
42
+ },
43
+ model: {
44
+ type: 'string',
45
+ enum: MODEL_IDS,
46
+ description: `Model to use. Default: ${DEFAULT_MODEL}.
47
+ - claude-sonnet-4.5: Best balance of speed and capability (default, recommended for most tasks)
48
+ - claude-haiku-4.5: Fastest -- use for simple, well-defined tasks like running a single command or small edits
49
+ - claude-opus-4.5: Most capable -- use for complex reasoning, large refactors, or tasks requiring deep analysis`,
50
+ },
51
+ cwd: {
52
+ type: 'string',
53
+ description: `The absolute path to the working directory where the agent will execute. You should detect your current working directory and pass it here as a full absolute path. This is especially important when working in git worktrees -- pass the actual worktree path (e.g. /Users/dev/project/worktrees/feature-branch), NOT the main repository root. Do not create a new worktree just for this -- simply pass the directory you are currently working in.`,
54
+ },
55
+ timeout: {
56
+ type: 'number',
57
+ description: 'Max execution time in ms. Default: 600000 (10 min, configurable via MCP_TASK_TIMEOUT_MS). Max: 3600000 (configurable via MCP_TASK_TIMEOUT_MAX_MS). Increase for large tasks.',
58
+ },
59
+ autonomous: {
60
+ type: 'boolean',
61
+ description: 'Run without interactive prompts. Default: true. Almost always leave this as true.',
62
+ },
63
+ depends_on: {
64
+ type: 'array',
65
+ items: { type: 'string' },
66
+ description: 'Task IDs that must complete before this task starts. The task will wait in "waiting" status until all dependencies finish.',
67
+ },
68
+ labels: {
69
+ type: 'array',
70
+ items: { type: 'string' },
71
+ description: 'Labels for grouping and filtering tasks (max 10). Useful when managing multiple related tasks -- e.g. "auth-migration", "phase-1".',
72
+ },
73
+ },
74
+ required: ['prompt'],
75
+ },
76
+ };
77
+
78
+ export async function handleSpawnTask(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
79
+ try {
80
+ const parsed = SpawnTaskSchema.parse(args);
81
+
82
+ // Validate dependencies if provided
83
+ const dependsOn = parsed.depends_on?.filter((d: string) => d.trim()) || [];
84
+ if (dependsOn.length > 0) {
85
+ const validationError = taskManager.validateDependencies(dependsOn);
86
+ if (validationError) {
87
+ return mcpText(formatError(
88
+ validationError,
89
+ 'Ensure all dependency task IDs exist.\nUse `list_tasks` to find valid task IDs.'
90
+ ));
91
+ }
92
+ }
93
+
94
+ let finalPrompt = parsed.prompt;
95
+ if (parsed.task_type && isValidTaskType(parsed.task_type)) {
96
+ finalPrompt = applyTemplate(parsed.task_type as TaskType, parsed.prompt);
97
+ }
98
+
99
+ const labels = parsed.labels?.filter((l: string) => l.trim()) || [];
100
+
101
+ const taskId = await spawnCopilotProcess({
102
+ prompt: finalPrompt,
103
+ timeout: parsed.timeout,
104
+ cwd: parsed.cwd,
105
+ model: parsed.model,
106
+ autonomous: parsed.autonomous,
107
+ dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
108
+ labels: labels.length > 0 ? labels : undefined,
109
+ });
110
+
111
+ const task = taskManager.getTask(taskId);
112
+ const isWaiting = task?.status === 'waiting';
113
+
114
+ if (isWaiting) {
115
+ const depsList = dependsOn.map(d => `\`${d}\``).join(', ');
116
+ return mcpText(join(
117
+ `Task **${taskId}** spawned (waiting).`,
118
+ `Depends on: ${depsList}`,
119
+ '',
120
+ 'Dependencies must complete before this task runs.',
121
+ 'Check status with `get_status`.'
122
+ ));
123
+ }
124
+
125
+ return mcpText(join(
126
+ `Task **${taskId}** spawned (${task?.status || 'pending'}).`,
127
+ 'Check status with `get_status`.'
128
+ ));
129
+ } catch (error) {
130
+ return mcpText(formatError(
131
+ error instanceof Error ? error.message : 'Unknown error',
132
+ 'Check that the `prompt` parameter is provided and valid.'
133
+ ));
134
+ }
135
+ }
@@ -0,0 +1,101 @@
1
+ import { z } from 'zod';
2
+ import { taskManager } from '../services/task-manager.js';
3
+ import { TaskStatus } from '../types.js';
4
+ import { mcpText, formatError, displayStatus, formatDuration, join } from '../utils/format.js';
5
+ import { TASK_STALL_WARN_MS } from '../config/timeouts.js';
6
+
7
+ const StreamOutputSchema = z.object({
8
+ task_id: z.string().min(1),
9
+ offset: z.number().int().min(0).optional().default(0),
10
+ limit: z.number().int().min(1).max(500).optional().default(100),
11
+ });
12
+
13
+ export const streamOutputTool = {
14
+ name: 'stream_output',
15
+ description: `Get incremental output from a task. Use offset to get new lines since last call. This is the preferred way to monitor task output -- unlike get_status, this tool is not throttled and returns only the new output lines you haven't seen yet.`,
16
+ inputSchema: {
17
+ type: 'object' as const,
18
+ properties: {
19
+ task_id: {
20
+ type: 'string',
21
+ description: 'Task ID to stream output from.',
22
+ },
23
+ offset: {
24
+ type: 'number',
25
+ description: 'Line offset to start from. Default: 0. Use next_offset from response.',
26
+ },
27
+ limit: {
28
+ type: 'number',
29
+ description: 'Max lines to return. Default: 100. Max: 500.',
30
+ },
31
+ },
32
+ required: ['task_id'],
33
+ },
34
+ };
35
+
36
+ export async function handleStreamOutput(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
37
+ try {
38
+ const parsed = StreamOutputSchema.parse(args || {});
39
+ const taskId = parsed.task_id.toLowerCase().trim();
40
+
41
+ const task = taskManager.getTask(taskId);
42
+
43
+ if (!task) {
44
+ return mcpText(formatError('Task not found', 'Use `list_tasks` to find valid task IDs.'));
45
+ }
46
+
47
+ const totalLines = task.output.length;
48
+ const offset = Math.min(parsed.offset, totalLines);
49
+ const outputLines = task.output.slice(offset, offset + parsed.limit);
50
+ const nextOffset = offset + outputLines.length;
51
+ const hasMore = nextOffset < totalLines;
52
+ const now = Date.now();
53
+ const lastOutputAgeMs = task.lastOutputAt ? now - new Date(task.lastOutputAt).getTime() : undefined;
54
+
55
+ const isTerminal = [
56
+ TaskStatus.COMPLETED,
57
+ TaskStatus.FAILED,
58
+ TaskStatus.CANCELLED,
59
+ TaskStatus.TIMED_OUT,
60
+ ].includes(task.status);
61
+
62
+ // Build headline
63
+ const statusStr = displayStatus(task.status);
64
+ let rangeStr: string;
65
+ if (outputLines.length > 0) {
66
+ rangeStr = `lines ${offset}-${nextOffset - 1} of ${totalLines}`;
67
+ } else {
68
+ rangeStr = `${totalLines} lines`;
69
+ }
70
+ const exitInfo = isTerminal && task.exitCode !== undefined ? `, exit code: ${task.exitCode}` : '';
71
+ const headline = `**${task.id}** -- ${statusStr} (${rangeStr}${exitInfo})`;
72
+
73
+ // Build body
74
+ let body: string;
75
+ if (outputLines.length > 0) {
76
+ body = outputLines.map(l => `> ${l}`).join('\n');
77
+ } else {
78
+ body = 'No output yet.';
79
+ }
80
+
81
+ // Build footer
82
+ let footer: string;
83
+ if (hasMore) {
84
+ const remaining = totalLines - nextOffset;
85
+ footer = `${remaining} more lines available. Use offset \`${nextOffset}\` to continue.`;
86
+ } else if (isTerminal) {
87
+ footer = 'All output retrieved.';
88
+ } else if (lastOutputAgeMs !== undefined && lastOutputAgeMs >= TASK_STALL_WARN_MS) {
89
+ footer = `No output for ${formatDuration(lastOutputAgeMs)}. Task may be stalled.`;
90
+ } else {
91
+ footer = 'Waiting for more output...';
92
+ }
93
+
94
+ // Add error if terminal and has error
95
+ const errorLine = task.error && isTerminal ? `**Error:** ${task.error}` : undefined;
96
+
97
+ return mcpText(join(headline, '', body, '', errorLine, footer));
98
+ } catch (error) {
99
+ return mcpText(formatError(error instanceof Error ? error.message : 'Unknown error'));
100
+ }
101
+ }
package/src/types.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { ResultPromise } from 'execa';
2
+
3
+ export type Provider = 'copilot' | 'claude-cli';
4
+
5
+ export enum TaskStatus {
6
+ PENDING = 'pending',
7
+ WAITING = 'waiting',
8
+ RUNNING = 'running',
9
+ COMPLETED = 'completed',
10
+ FAILED = 'failed',
11
+ CANCELLED = 'cancelled',
12
+ RATE_LIMITED = 'rate_limited',
13
+ TIMED_OUT = 'timed_out',
14
+ }
15
+
16
+ export type TimeoutReason =
17
+ | 'hard_timeout'
18
+ | 'stall'
19
+ | 'process_dead'
20
+ | 'server_restart'
21
+ | 'unknown';
22
+
23
+ export interface TimeoutContext {
24
+ timeoutMs?: number;
25
+ timeoutAt?: string;
26
+ elapsedMs?: number;
27
+ lastOutputAt?: string;
28
+ lastOutputAgeMs?: number;
29
+ lastHeartbeatAt?: string;
30
+ pidAlive?: boolean;
31
+ detectedBy?: 'execa' | 'health_check' | 'startup_recovery' | 'manual';
32
+ }
33
+
34
+ export interface RetryInfo {
35
+ reason: string;
36
+ retryCount: number;
37
+ nextRetryTime: string;
38
+ maxRetries: number;
39
+ originalTaskId?: string;
40
+ }
41
+
42
+ export interface TaskState {
43
+ id: string;
44
+ status: TaskStatus;
45
+ prompt: string;
46
+ output: string[];
47
+ pid?: number;
48
+ sessionId?: string;
49
+ startTime: string;
50
+ lastOutputAt?: string;
51
+ lastHeartbeatAt?: string;
52
+ endTime?: string;
53
+ exitCode?: number;
54
+ error?: string;
55
+ cwd?: string;
56
+ model?: string;
57
+ autonomous?: boolean;
58
+ isResume?: boolean;
59
+ process?: ResultPromise;
60
+ retryInfo?: RetryInfo;
61
+ dependsOn?: string[];
62
+ timeout?: number;
63
+ timeoutAt?: string;
64
+ timeoutReason?: TimeoutReason;
65
+ timeoutContext?: TimeoutContext;
66
+ labels?: string[];
67
+ provider?: Provider;
68
+ fallbackAttempted?: boolean;
69
+ switchAttempted?: boolean;
70
+ recoveryAttempted?: boolean;
71
+ }
72
+
73
+ export interface SpawnOptions {
74
+ prompt: string;
75
+ timeout?: number;
76
+ cwd?: string;
77
+ model?: string;
78
+ autonomous?: boolean;
79
+ resumeSessionId?: string;
80
+ retryInfo?: RetryInfo;
81
+ dependsOn?: string[];
82
+ labels?: string[];
83
+ provider?: Provider;
84
+ fallbackAttempted?: boolean;
85
+ switchAttempted?: boolean;
86
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Shared formatting helpers for converting MCP tool responses to markdown.
3
+ */
4
+
5
+ /** Wraps a markdown string in the MCP response shape. */
6
+ export function mcpText(markdown: string): { content: Array<{ type: string; text: string }> } {
7
+ return { content: [{ type: 'text', text: markdown }] };
8
+ }
9
+
10
+ /** Display status in human-readable form: "rate_limited" -> "rate limited" */
11
+ export function displayStatus(status: string): string {
12
+ return status.replace(/_/g, ' ');
13
+ }
14
+
15
+ /** Format labels as inline code: "`label-a`, `label-b`". Returns empty string if none. */
16
+ export function formatLabels(labels?: string[]): string {
17
+ if (!labels || labels.length === 0) return '';
18
+ return labels.map(l => `\`${l}\``).join(', ');
19
+ }
20
+
21
+ /** "Labels: `a`, `b`" or empty string. */
22
+ export function formatLabelsLine(labels?: string[]): string {
23
+ const f = formatLabels(labels);
24
+ return f ? `Labels: ${f}` : '';
25
+ }
26
+
27
+ /** Milliseconds to human duration: 541364 -> "~9m" */
28
+ export function formatDuration(ms: number): string {
29
+ if (ms <= 0) return '0s';
30
+ const totalSec = Math.round(ms / 1000);
31
+ if (totalSec < 60) return `~${totalSec}s`;
32
+ const min = Math.round(totalSec / 60);
33
+ if (min < 60) return `~${min}m`;
34
+ const h = Math.floor(min / 60);
35
+ const rm = min % 60;
36
+ return rm === 0 ? `~${h}h` : `~${h}h ${rm}m`;
37
+ }
38
+
39
+ /** Format output as a blockquote with optional truncation. Returns empty string for empty output. */
40
+ export function formatOutputBlock(output: string, label = 'Output', maxLen = 2000): string {
41
+ if (!output || !output.trim()) return '';
42
+ let text = output;
43
+ let truncated = false;
44
+ if (text.length > maxLen) {
45
+ text = text.slice(-maxLen);
46
+ truncated = true;
47
+ }
48
+ const quoted = text.split('\n').map(line => `> ${line}`).join('\n');
49
+ const parts = [`**${label}:**`, quoted];
50
+ if (truncated) parts.push('*(truncated -- use `stream_output` for full output)*');
51
+ return parts.join('\n');
52
+ }
53
+
54
+ /** Standard error block with optional actionable hint. */
55
+ export function formatError(error: string, hint?: string): string {
56
+ const parts = [`**Error:** ${error}`];
57
+ if (hint) parts.push('', hint);
58
+ return parts.join('\n');
59
+ }
60
+
61
+ /** "Run `sleep 120` then check again." or empty string. */
62
+ export function formatRetryHint(retryCommand?: string): string {
63
+ if (!retryCommand) return '';
64
+ return `Run \`${retryCommand}\` then check again.`;
65
+ }
66
+
67
+ /** Escape pipe characters in table cell content. */
68
+ function escapeCell(value: string): string {
69
+ return value.replace(/\|/g, '\\|');
70
+ }
71
+
72
+ /** Render a markdown table from headers and rows. */
73
+ export function formatTable(headers: string[], rows: string[][]): string {
74
+ const headerRow = `| ${headers.map(escapeCell).join(' | ')} |`;
75
+ const separator = `|${headers.map(() => '------').join('|')}|`;
76
+ const dataRows = rows.map(r => `| ${r.map(escapeCell).join(' | ')} |`);
77
+ return [headerRow, separator, ...dataRows].join('\n');
78
+ }
79
+
80
+ /** Join non-empty strings with newlines, filtering out falsy/empty values. */
81
+ export function join(...parts: (string | undefined | false | null)[]): string {
82
+ return parts.filter(p => typeof p === 'string' && p.length > 0).join('\n');
83
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { MODEL_IDS } from '../models.js';
3
+ import { TASK_TYPE_IDS } from '../templates/index.js';
4
+ import {
5
+ TASK_TIMEOUT_DEFAULT_MS,
6
+ TASK_TIMEOUT_MAX_MS,
7
+ TASK_TIMEOUT_MIN_MS,
8
+ } from '../config/timeouts.js';
9
+
10
+ export const SpawnTaskSchema = z.object({
11
+ prompt: z.string().min(1).max(50000),
12
+ timeout: z.number().int().min(TASK_TIMEOUT_MIN_MS).max(TASK_TIMEOUT_MAX_MS).optional().default(TASK_TIMEOUT_DEFAULT_MS), // 10 minutes default
13
+ cwd: z.string().optional(),
14
+ model: z.enum(MODEL_IDS as [string, ...string[]]).optional(),
15
+ task_type: z.enum(TASK_TYPE_IDS as [string, ...string[]]).optional(),
16
+ autonomous: z.boolean().optional().default(true),
17
+ depends_on: z.array(z.string().min(1)).optional(),
18
+ labels: z.array(z.string().min(1).max(50)).max(10).optional(),
19
+ });
20
+
21
+ export const ResumeTaskSchema = z.object({
22
+ sessionId: z.string().min(1),
23
+ timeout: z.number().int().min(TASK_TIMEOUT_MIN_MS).max(TASK_TIMEOUT_MAX_MS).optional().default(TASK_TIMEOUT_DEFAULT_MS), // 10 minutes default
24
+ cwd: z.string().optional(),
25
+ autonomous: z.boolean().optional().default(true),
26
+ });
27
+
28
+ export const GetTaskStatusSchema = z.object({
29
+ taskId: z.union([z.string().min(1), z.array(z.string().min(1))]),
30
+ });
31
+
32
+ export const ListTasksSchema = z.object({
33
+ status: z.enum(['pending', 'waiting', 'running', 'completed', 'failed', 'cancelled', 'rate_limited', 'timed_out']).optional(),
34
+ label: z.string().min(1).optional(),
35
+ });
@@ -0,0 +1,25 @@
1
+ import { uniqueNamesGenerator, adjectives, colors, animals, names } from 'unique-names-generator';
2
+
3
+ /**
4
+ * Generates human-readable task IDs like "brave-tiger-42" or "cosmic-falcon-17"
5
+ * Much easier to remember and communicate than UUIDs or random strings
6
+ */
7
+ export function generateTaskId(): string {
8
+ const randomNumber = Math.floor(Math.random() * 100);
9
+
10
+ const name = uniqueNamesGenerator({
11
+ dictionaries: [adjectives, animals],
12
+ separator: '-',
13
+ length: 2,
14
+ style: 'lowerCase',
15
+ });
16
+
17
+ return `${name}-${randomNumber}`;
18
+ }
19
+
20
+ /**
21
+ * Normalizes task ID for case-insensitive lookups
22
+ */
23
+ export function normalizeTaskId(taskId: string): string {
24
+ return taskId.toLowerCase().trim();
25
+ }