@claudetools/tools 0.3.9 → 0.5.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.
Files changed (47) hide show
  1. package/README.md +60 -4
  2. package/dist/cli.js +0 -0
  3. package/dist/codedna/generators/base.d.ts +41 -0
  4. package/dist/codedna/generators/base.js +102 -0
  5. package/dist/codedna/generators/express-api.d.ts +12 -0
  6. package/dist/codedna/generators/express-api.js +61 -0
  7. package/dist/codedna/index.d.ts +4 -0
  8. package/dist/codedna/index.js +7 -0
  9. package/dist/codedna/parser.d.ts +117 -0
  10. package/dist/codedna/parser.js +233 -0
  11. package/dist/codedna/registry.d.ts +60 -0
  12. package/dist/codedna/registry.js +217 -0
  13. package/dist/codedna/template-engine.d.ts +17 -0
  14. package/dist/codedna/template-engine.js +183 -0
  15. package/dist/codedna/types.d.ts +64 -0
  16. package/dist/codedna/types.js +4 -0
  17. package/dist/handlers/codedna-handlers.d.ts +122 -0
  18. package/dist/handlers/codedna-handlers.js +194 -0
  19. package/dist/handlers/tool-handlers.js +593 -14
  20. package/dist/helpers/api-client.d.ts +37 -0
  21. package/dist/helpers/api-client.js +70 -0
  22. package/dist/helpers/codedna-monitoring.d.ts +34 -0
  23. package/dist/helpers/codedna-monitoring.js +159 -0
  24. package/dist/helpers/error-tracking.d.ts +73 -0
  25. package/dist/helpers/error-tracking.js +164 -0
  26. package/dist/helpers/library-detection.d.ts +26 -0
  27. package/dist/helpers/library-detection.js +145 -0
  28. package/dist/helpers/tasks-retry.d.ts +49 -0
  29. package/dist/helpers/tasks-retry.js +168 -0
  30. package/dist/helpers/tasks.d.ts +24 -1
  31. package/dist/helpers/tasks.js +146 -50
  32. package/dist/helpers/usage-analytics.d.ts +91 -0
  33. package/dist/helpers/usage-analytics.js +256 -0
  34. package/dist/helpers/workers.d.ts +25 -0
  35. package/dist/helpers/workers.js +80 -0
  36. package/dist/templates/claude-md.d.ts +1 -1
  37. package/dist/templates/claude-md.js +16 -5
  38. package/dist/tools.js +314 -0
  39. package/docs/AUTO-REGISTRATION.md +353 -0
  40. package/docs/CLAUDE4_PROMPT_ANALYSIS.md +589 -0
  41. package/docs/ENTITY_DSL_REFERENCE.md +685 -0
  42. package/docs/MODERN_STACK_COMPLETE_GUIDE.md +706 -0
  43. package/docs/PROMPT_STANDARDIZATION_RESULTS.md +324 -0
  44. package/docs/PROMPT_TIER_TEMPLATES.md +787 -0
  45. package/docs/RESEARCH_METHODOLOGY_EXTRACTION.md +336 -0
  46. package/package.json +14 -3
  47. package/scripts/verify-prompt-compliance.sh +197 -0
@@ -0,0 +1,145 @@
1
+ // =============================================================================
2
+ // Library/Framework Detection for Task Planning
3
+ // =============================================================================
4
+ /**
5
+ * Common libraries and frameworks to detect in task descriptions
6
+ * Maps lowercase keywords to canonical library names
7
+ */
8
+ const LIBRARY_KEYWORDS = {
9
+ // Frontend frameworks
10
+ 'react': 'React',
11
+ 'reactjs': 'React',
12
+ 'react.js': 'React',
13
+ 'next': 'Next.js',
14
+ 'nextjs': 'Next.js',
15
+ 'next.js': 'Next.js',
16
+ 'vue': 'Vue.js',
17
+ 'vuejs': 'Vue.js',
18
+ 'vue.js': 'Vue.js',
19
+ 'angular': 'Angular',
20
+ 'svelte': 'Svelte',
21
+ 'sveltekit': 'SvelteKit',
22
+ // Backend frameworks
23
+ 'express': 'Express',
24
+ 'expressjs': 'Express',
25
+ 'fastify': 'Fastify',
26
+ 'hono': 'Hono',
27
+ 'koa': 'Koa',
28
+ 'nest': 'NestJS',
29
+ 'nestjs': 'NestJS',
30
+ // Databases & ORMs
31
+ 'supabase': 'Supabase',
32
+ 'firebase': 'Firebase',
33
+ 'prisma': 'Prisma',
34
+ 'drizzle': 'Drizzle',
35
+ 'mongoose': 'Mongoose',
36
+ 'mongodb': 'MongoDB',
37
+ 'postgres': 'PostgreSQL',
38
+ 'postgresql': 'PostgreSQL',
39
+ 'mysql': 'MySQL',
40
+ 'redis': 'Redis',
41
+ // State management
42
+ 'redux': 'Redux',
43
+ 'zustand': 'Zustand',
44
+ 'jotai': 'Jotai',
45
+ 'recoil': 'Recoil',
46
+ 'tanstack': 'TanStack Query',
47
+ 'react-query': 'TanStack Query',
48
+ 'swr': 'SWR',
49
+ // UI libraries
50
+ 'tailwind': 'Tailwind CSS',
51
+ 'tailwindcss': 'Tailwind CSS',
52
+ 'shadcn': 'shadcn/ui',
53
+ 'radix': 'Radix UI',
54
+ 'chakra': 'Chakra UI',
55
+ 'mantine': 'Mantine',
56
+ 'mui': 'Material UI',
57
+ 'material-ui': 'Material UI',
58
+ // Testing
59
+ 'jest': 'Jest',
60
+ 'vitest': 'Vitest',
61
+ 'playwright': 'Playwright',
62
+ 'cypress': 'Cypress',
63
+ 'testing-library': 'Testing Library',
64
+ // Build tools
65
+ 'vite': 'Vite',
66
+ 'webpack': 'Webpack',
67
+ 'esbuild': 'esbuild',
68
+ 'turbopack': 'Turbopack',
69
+ 'turborepo': 'Turborepo',
70
+ // Auth & APIs
71
+ 'auth': 'Authentication',
72
+ 'oauth': 'OAuth',
73
+ 'jwt': 'JWT',
74
+ 'stripe': 'Stripe',
75
+ 'openai': 'OpenAI',
76
+ 'langchain': 'LangChain',
77
+ 'vercel': 'Vercel',
78
+ // Other
79
+ 'typescript': 'TypeScript',
80
+ 'graphql': 'GraphQL',
81
+ 'trpc': 'tRPC',
82
+ 'zod': 'Zod',
83
+ 'socket.io': 'Socket.IO',
84
+ 'websocket': 'WebSocket',
85
+ };
86
+ /**
87
+ * Context7-compatible library ID mappings
88
+ * Maps canonical names to context7 library IDs
89
+ */
90
+ export const CONTEXT7_LIBRARY_IDS = {
91
+ 'React': '/facebook/react',
92
+ 'Next.js': '/vercel/next.js',
93
+ 'Vue.js': '/vuejs/vue',
94
+ 'Supabase': '/supabase/supabase',
95
+ 'Prisma': '/prisma/prisma',
96
+ 'Tailwind CSS': '/tailwindlabs/tailwindcss',
97
+ 'TypeScript': '/microsoft/typescript',
98
+ 'Vitest': '/vitest-dev/vitest',
99
+ 'Zod': '/colinhacks/zod',
100
+ 'tRPC': '/trpc/trpc',
101
+ 'TanStack Query': '/tanstack/query',
102
+ };
103
+ /**
104
+ * Detect libraries mentioned in text
105
+ * @param text - Text to scan for library mentions
106
+ * @returns Array of detected libraries with mention counts
107
+ */
108
+ export function detectLibraries(text) {
109
+ const lowerText = text.toLowerCase();
110
+ const detected = new Map();
111
+ // Scan for each keyword
112
+ for (const [keyword, libraryName] of Object.entries(LIBRARY_KEYWORDS)) {
113
+ // Use word boundary matching to avoid partial matches
114
+ const regex = new RegExp(`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi');
115
+ const matches = lowerText.match(regex);
116
+ if (matches) {
117
+ const current = detected.get(libraryName) || 0;
118
+ detected.set(libraryName, current + matches.length);
119
+ }
120
+ }
121
+ // Convert to array and sort by mention count
122
+ const results = [];
123
+ for (const [name, mentions] of detected) {
124
+ results.push({
125
+ name,
126
+ context7Id: CONTEXT7_LIBRARY_IDS[name],
127
+ mentions,
128
+ });
129
+ }
130
+ return results.sort((a, b) => b.mentions - a.mentions);
131
+ }
132
+ /**
133
+ * Detect libraries from goal and task descriptions
134
+ * @param goal - The epic/plan goal
135
+ * @param tasks - Array of task objects with title and description
136
+ * @returns Deduplicated list of detected libraries
137
+ */
138
+ export function detectLibrariesFromPlan(goal, tasks) {
139
+ // Combine all text
140
+ const allText = [
141
+ goal,
142
+ ...tasks.map(t => `${t.title} ${t.description || ''}`),
143
+ ].join(' ');
144
+ return detectLibraries(allText);
145
+ }
@@ -0,0 +1,49 @@
1
+ import { type Task } from './tasks.js';
2
+ export interface TimedOutTask {
3
+ task: Task;
4
+ lockExpiredAt: string;
5
+ timeSinceExpiry: number;
6
+ }
7
+ export interface RetryMetadata {
8
+ retryCount: number;
9
+ lastFailedAt: string;
10
+ lastError?: string;
11
+ failureHistory: Array<{
12
+ timestamp: string;
13
+ error?: string;
14
+ }>;
15
+ }
16
+ /**
17
+ * Detect tasks that have timed out (lock expired while in_progress)
18
+ * These are likely abandoned or failed tasks
19
+ */
20
+ export declare function detectTimedOutTasks(userId: string, projectId: string): Promise<TimedOutTask[]>;
21
+ /**
22
+ * Retry a failed or timed-out task
23
+ * Returns success: false if retry limit exceeded
24
+ */
25
+ export declare function retryTask(userId: string, projectId: string, taskId: string, maxRetries?: number, errorContext?: string): Promise<{
26
+ success: boolean;
27
+ data?: {
28
+ task: Task;
29
+ retryCount: number;
30
+ retriesRemaining: number;
31
+ };
32
+ error?: string;
33
+ }>;
34
+ /**
35
+ * Mark a task as failed with context
36
+ */
37
+ export declare function failTask(userId: string, projectId: string, taskId: string, errorContext: string, agentId?: string): Promise<{
38
+ success: boolean;
39
+ data: Task;
40
+ }>;
41
+ /**
42
+ * Auto-detect and handle timed-out tasks
43
+ * Returns tasks that were auto-retried
44
+ */
45
+ export declare function autoRetryTimedOutTasks(userId: string, projectId: string, maxRetries?: number): Promise<{
46
+ timedOut: TimedOutTask[];
47
+ retried: Task[];
48
+ failed: Task[];
49
+ }>;
@@ -0,0 +1,168 @@
1
+ // =============================================================================
2
+ // Task Failure Handling & Retry System
3
+ // =============================================================================
4
+ import { apiRequest } from './api-client.js';
5
+ import { getTask, listTasks, updateTaskStatus, addTaskContext } from './tasks.js';
6
+ /**
7
+ * Get retry metadata from task metadata
8
+ */
9
+ function getRetryMetadata(task) {
10
+ const metadata = task.metadata || {};
11
+ return {
12
+ retryCount: metadata.retryCount || 0,
13
+ lastFailedAt: metadata.lastFailedAt || '',
14
+ lastError: metadata.lastError,
15
+ failureHistory: metadata.failureHistory || [],
16
+ };
17
+ }
18
+ /**
19
+ * Update retry metadata
20
+ */
21
+ function updateRetryMetadata(metadata, error) {
22
+ const current = metadata;
23
+ const retryCount = (current.retryCount || 0) + 1;
24
+ const timestamp = new Date().toISOString();
25
+ const failureHistory = (current.failureHistory || []);
26
+ return {
27
+ ...metadata,
28
+ retryCount,
29
+ lastFailedAt: timestamp,
30
+ lastError: error,
31
+ failureHistory: [
32
+ ...failureHistory,
33
+ { timestamp, error },
34
+ ],
35
+ };
36
+ }
37
+ /**
38
+ * Detect tasks that have timed out (lock expired while in_progress)
39
+ * These are likely abandoned or failed tasks
40
+ */
41
+ export async function detectTimedOutTasks(userId, projectId) {
42
+ // Get all in_progress tasks
43
+ const result = await listTasks(userId, projectId, {
44
+ status: 'in_progress',
45
+ limit: 100,
46
+ });
47
+ if (!result.success || !result.data.length) {
48
+ return [];
49
+ }
50
+ const now = new Date();
51
+ const timedOut = [];
52
+ for (const task of result.data) {
53
+ // Check if lock has expired
54
+ if (task.lock_expires_at) {
55
+ const lockExpires = new Date(task.lock_expires_at);
56
+ if (lockExpires < now) {
57
+ timedOut.push({
58
+ task,
59
+ lockExpiredAt: task.lock_expires_at,
60
+ timeSinceExpiry: now.getTime() - lockExpires.getTime(),
61
+ });
62
+ }
63
+ }
64
+ }
65
+ return timedOut;
66
+ }
67
+ /**
68
+ * Retry a failed or timed-out task
69
+ * Returns success: false if retry limit exceeded
70
+ */
71
+ export async function retryTask(userId, projectId, taskId, maxRetries = 3, errorContext) {
72
+ // Get current task
73
+ const taskResult = await getTask(userId, projectId, taskId);
74
+ if (!taskResult.success) {
75
+ return {
76
+ success: false,
77
+ error: 'Task not found',
78
+ };
79
+ }
80
+ const task = taskResult.data;
81
+ const retryMetadata = getRetryMetadata(task);
82
+ // Check retry limit
83
+ if (retryMetadata.retryCount >= maxRetries) {
84
+ // Mark as failed permanently
85
+ await updateTaskStatus(userId, projectId, taskId, 'failed');
86
+ // Add failure context
87
+ await addTaskContext(userId, projectId, taskId, 'work_log', `Task failed permanently after ${retryMetadata.retryCount} retries. Last error: ${errorContext || 'Unknown'}`, 'system');
88
+ return {
89
+ success: false,
90
+ error: `Retry limit exceeded (${maxRetries} retries)`,
91
+ };
92
+ }
93
+ // Update metadata with failure info
94
+ const updatedMetadata = updateRetryMetadata(task.metadata || {}, errorContext);
95
+ // Update task with retry metadata and reset status
96
+ // Using the /api/v1/tasks/:userId/:projectId/:taskId endpoint with POST
97
+ await apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}`, 'POST', {
98
+ status: 'ready',
99
+ assigned_to: null,
100
+ locked_at: null,
101
+ lock_expires_at: null,
102
+ metadata: updatedMetadata,
103
+ });
104
+ // Add retry context
105
+ const newRetryCount = retryMetadata.retryCount + 1;
106
+ await addTaskContext(userId, projectId, taskId, 'work_log', `Task retry ${newRetryCount}/${maxRetries}. Previous failure: ${errorContext || 'Lock timeout'}`, 'system');
107
+ // Get updated task
108
+ const updatedResult = await getTask(userId, projectId, taskId);
109
+ const updatedTask = updatedResult.data;
110
+ return {
111
+ success: true,
112
+ data: {
113
+ task: updatedTask,
114
+ retryCount: newRetryCount,
115
+ retriesRemaining: maxRetries - newRetryCount,
116
+ },
117
+ };
118
+ }
119
+ /**
120
+ * Mark a task as failed with context
121
+ */
122
+ export async function failTask(userId, projectId, taskId, errorContext, agentId) {
123
+ // Get current task to update metadata
124
+ const taskResult = await getTask(userId, projectId, taskId);
125
+ if (!taskResult.success) {
126
+ throw new Error('Task not found');
127
+ }
128
+ const task = taskResult.data;
129
+ const updatedMetadata = updateRetryMetadata(task.metadata || {}, errorContext);
130
+ // Update to failed status with metadata
131
+ await apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}`, 'POST', {
132
+ status: 'failed',
133
+ metadata: updatedMetadata,
134
+ assigned_to: null,
135
+ locked_at: null,
136
+ lock_expires_at: null,
137
+ });
138
+ // Add failure context
139
+ await addTaskContext(userId, projectId, taskId, 'work_log', `Task failed: ${errorContext}`, agentId || 'system');
140
+ // Get updated task
141
+ const result = await getTask(userId, projectId, taskId);
142
+ return result;
143
+ }
144
+ /**
145
+ * Auto-detect and handle timed-out tasks
146
+ * Returns tasks that were auto-retried
147
+ */
148
+ export async function autoRetryTimedOutTasks(userId, projectId, maxRetries = 3) {
149
+ const timedOut = await detectTimedOutTasks(userId, projectId);
150
+ const retried = [];
151
+ const failed = [];
152
+ for (const { task, timeSinceExpiry } of timedOut) {
153
+ const minutesSinceExpiry = Math.floor(timeSinceExpiry / 1000 / 60);
154
+ const errorContext = `Lock timeout: expired ${minutesSinceExpiry} minutes ago`;
155
+ const retryResult = await retryTask(userId, projectId, task.id, maxRetries, errorContext);
156
+ if (retryResult.success && retryResult.data) {
157
+ retried.push(retryResult.data.task);
158
+ }
159
+ else {
160
+ // Task failed permanently
161
+ const failResult = await getTask(userId, projectId, task.id);
162
+ if (failResult.success) {
163
+ failed.push(failResult.data);
164
+ }
165
+ }
166
+ }
167
+ return { timedOut, retried, failed };
168
+ }
@@ -127,12 +127,23 @@ export declare function heartbeatTask(userId: string, projectId: string, taskId:
127
127
  new_expires_at: string;
128
128
  };
129
129
  }>;
130
+ export declare const DEFAULT_MAX_PARALLEL = 5;
131
+ /**
132
+ * Get count of currently active tasks
133
+ * Returns both in_progress and claimed (locked) task counts
134
+ */
135
+ export declare function getActiveTaskCount(userId: string, projectId: string, epicId?: string): Promise<{
136
+ inProgress: number;
137
+ claimed: number;
138
+ total: number;
139
+ }>;
130
140
  /**
131
141
  * Get all tasks ready for parallel dispatch
132
142
  * - Filters for 'ready' status
133
143
  * - Excludes already claimed tasks
134
144
  * - Resolves dependencies (only returns unblocked tasks)
135
145
  * - Matches each to appropriate expert worker
146
+ * - Respects max parallel limit by considering currently active tasks
136
147
  */
137
148
  export declare function getDispatchableTasks(userId: string, projectId: string, epicId?: string, maxParallel?: number): Promise<DispatchableTask[]>;
138
149
  /**
@@ -147,6 +158,18 @@ export declare function getExecutionContext(userId: string, projectId: string, t
147
158
  siblingTasks?: Task[];
148
159
  }>;
149
160
  /**
150
- * Find newly unblocked tasks after a completion
161
+ * Find newly unblocked tasks after a completion and update their status to ready
151
162
  */
152
163
  export declare function resolveTaskDependencies(userId: string, projectId: string, completedTaskId: string, epicId?: string): Promise<Task[]>;
164
+ /**
165
+ * Get epic status with progress tracking
166
+ * Auto-completes epic if all child tasks are done
167
+ */
168
+ export declare function getEpicStatus(userId: string, projectId: string, epicId: string): Promise<{
169
+ epic: Task;
170
+ totalTasks: number;
171
+ byStatus: Record<string, number>;
172
+ percentComplete: number;
173
+ allComplete: boolean;
174
+ autoCompleted: boolean;
175
+ }>;
@@ -2,7 +2,7 @@
2
2
  // Task Management System
3
3
  // =============================================================================
4
4
  import { apiRequest } from './api-client.js';
5
- import { matchTaskToWorker } from './workers.js';
5
+ import { matchTaskToWorker, buildWorkerPrompt } from './workers.js';
6
6
  export function parseJsonArray(value) {
7
7
  if (!value)
8
8
  return [];
@@ -90,14 +90,57 @@ export async function heartbeatTask(userId, projectId, taskId, agentId, extendMi
90
90
  // -----------------------------------------------------------------------------
91
91
  // Orchestration Functions
92
92
  // -----------------------------------------------------------------------------
93
+ // Configuration: Default maximum parallel tasks
94
+ export const DEFAULT_MAX_PARALLEL = 5;
95
+ /**
96
+ * Get count of currently active tasks
97
+ * Returns both in_progress and claimed (locked) task counts
98
+ */
99
+ export async function getActiveTaskCount(userId, projectId, epicId) {
100
+ // Get all in_progress tasks
101
+ const inProgressResult = await listTasks(userId, projectId, {
102
+ status: 'in_progress',
103
+ parent_id: epicId,
104
+ limit: 100,
105
+ });
106
+ let inProgressCount = 0;
107
+ let claimedCount = 0;
108
+ if (inProgressResult.success && inProgressResult.data.length > 0) {
109
+ const now = new Date();
110
+ for (const task of inProgressResult.data) {
111
+ inProgressCount++;
112
+ // Check if task is also claimed (has valid lock)
113
+ if (task.assigned_to && task.lock_expires_at) {
114
+ const lockExpires = new Date(task.lock_expires_at);
115
+ if (lockExpires > now) {
116
+ claimedCount++;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return {
122
+ inProgress: inProgressCount,
123
+ claimed: claimedCount,
124
+ total: inProgressCount,
125
+ };
126
+ }
93
127
  /**
94
128
  * Get all tasks ready for parallel dispatch
95
129
  * - Filters for 'ready' status
96
130
  * - Excludes already claimed tasks
97
131
  * - Resolves dependencies (only returns unblocked tasks)
98
132
  * - Matches each to appropriate expert worker
133
+ * - Respects max parallel limit by considering currently active tasks
99
134
  */
100
- export async function getDispatchableTasks(userId, projectId, epicId, maxParallel = 5) {
135
+ export async function getDispatchableTasks(userId, projectId, epicId, maxParallel = DEFAULT_MAX_PARALLEL) {
136
+ // Get current active task count
137
+ const activeCount = await getActiveTaskCount(userId, projectId, epicId);
138
+ // Calculate remaining capacity
139
+ const remainingCapacity = maxParallel - activeCount.total;
140
+ if (remainingCapacity <= 0) {
141
+ // No capacity available, return empty array
142
+ return [];
143
+ }
101
144
  // Get all tasks with 'ready' status
102
145
  const result = await listTasks(userId, projectId, {
103
146
  status: 'ready',
@@ -152,7 +195,8 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
152
195
  parentContext: taskData.parent,
153
196
  dependencies: [],
154
197
  });
155
- if (dispatchable.length >= maxParallel) {
198
+ // Only dispatch tasks that fit within remaining capacity
199
+ if (dispatchable.length >= remainingCapacity) {
156
200
  break;
157
201
  }
158
202
  }
@@ -184,46 +228,29 @@ export async function getExecutionContext(userId, projectId, taskId) {
184
228
  siblingTasks = siblingResult.data.filter(t => t.id !== taskId);
185
229
  }
186
230
  }
187
- // Build system prompt
188
- let systemPrompt = worker.promptTemplate + '\n\n';
189
- systemPrompt += `# Task: ${task.title}\n\n`;
190
- if (task.description) {
191
- systemPrompt += `## Description\n${task.description}\n\n`;
192
- }
193
- // Add acceptance criteria
194
- const criteria = parseJsonArray(task.acceptance_criteria);
195
- if (criteria.length > 0) {
196
- systemPrompt += `## Acceptance Criteria\n`;
197
- criteria.forEach((c, i) => {
198
- systemPrompt += `${i + 1}. ${c}\n`;
199
- });
200
- systemPrompt += '\n';
201
- }
202
- // Add parent context
203
- if (taskData.parent) {
204
- systemPrompt += `## Epic Context\n`;
205
- systemPrompt += `**Epic:** ${taskData.parent.title}\n`;
206
- if (taskData.parent.description) {
207
- systemPrompt += `**Goal:** ${taskData.parent.description}\n`;
208
- }
209
- systemPrompt += '\n';
210
- }
211
- // Add attached context
212
- if (taskData.context.length > 0) {
213
- systemPrompt += `## Attached Context\n`;
214
- for (const ctx of taskData.context) {
215
- systemPrompt += `### ${ctx.context_type.toUpperCase()}${ctx.source ? ` (${ctx.source})` : ''}\n`;
216
- systemPrompt += `${ctx.content}\n\n`;
217
- }
218
- }
219
- // Add sibling task awareness
220
- if (siblingTasks && siblingTasks.length > 0) {
221
- systemPrompt += `## Related Tasks (for awareness)\n`;
222
- for (const sibling of siblingTasks.slice(0, 5)) {
223
- systemPrompt += `- ${sibling.title} [${sibling.status}]\n`;
224
- }
225
- systemPrompt += '\n';
226
- }
231
+ // Build structured system prompt using the template system
232
+ const systemPrompt = buildWorkerPrompt({
233
+ task: {
234
+ id: task.id,
235
+ title: task.title,
236
+ description: task.description || undefined,
237
+ acceptance_criteria: task.acceptance_criteria,
238
+ },
239
+ worker,
240
+ epicContext: taskData.parent ? {
241
+ title: taskData.parent.title,
242
+ description: taskData.parent.description || undefined,
243
+ } : undefined,
244
+ attachedContext: taskData.context.map(ctx => ({
245
+ type: ctx.context_type,
246
+ content: ctx.content,
247
+ source: ctx.source || undefined,
248
+ })),
249
+ siblingTasks: siblingTasks?.slice(0, 5).map(s => ({
250
+ title: s.title,
251
+ status: s.status,
252
+ })),
253
+ });
227
254
  return {
228
255
  task,
229
256
  worker,
@@ -234,20 +261,20 @@ export async function getExecutionContext(userId, projectId, taskId) {
234
261
  };
235
262
  }
236
263
  /**
237
- * Find newly unblocked tasks after a completion
264
+ * Find newly unblocked tasks after a completion and update their status to ready
238
265
  */
239
266
  export async function resolveTaskDependencies(userId, projectId, completedTaskId, epicId) {
240
- // Get all tasks that might be blocked by the completed task
241
- const result = await listTasks(userId, projectId, {
242
- status: 'ready', // or 'blocked'
267
+ // Get all blocked tasks that might be unblocked by the completed task
268
+ const blockedResult = await listTasks(userId, projectId, {
269
+ status: 'blocked',
243
270
  parent_id: epicId,
244
271
  limit: 50,
245
272
  });
246
- if (!result.success) {
273
+ if (!blockedResult.success) {
247
274
  return [];
248
275
  }
249
276
  const newlyUnblocked = [];
250
- for (const task of result.data) {
277
+ for (const task of blockedResult.data) {
251
278
  const blockedBy = parseJsonArray(task.blocked_by);
252
279
  // Check if this task was blocked by the completed task
253
280
  if (blockedBy.includes(completedTaskId)) {
@@ -265,10 +292,79 @@ export async function resolveTaskDependencies(userId, projectId, completedTaskId
265
292
  }
266
293
  }
267
294
  }
295
+ // If all blockers are complete, update task status to ready
268
296
  if (allBlockersComplete) {
269
- newlyUnblocked.push(task);
297
+ const updateResult = await updateTaskStatus(userId, projectId, task.id, 'ready');
298
+ if (updateResult.success) {
299
+ newlyUnblocked.push(updateResult.data);
300
+ }
270
301
  }
271
302
  }
272
303
  }
273
304
  return newlyUnblocked;
274
305
  }
306
+ /**
307
+ * Get epic status with progress tracking
308
+ * Auto-completes epic if all child tasks are done
309
+ */
310
+ export async function getEpicStatus(userId, projectId, epicId) {
311
+ // Get the epic
312
+ const epicResult = await getTask(userId, projectId, epicId);
313
+ if (!epicResult.success) {
314
+ throw new Error(`Epic not found: ${epicId}`);
315
+ }
316
+ const epic = epicResult.data;
317
+ // Verify it's an epic
318
+ if (epic.type !== 'epic') {
319
+ throw new Error(`Task ${epicId} is not an epic (type: ${epic.type})`);
320
+ }
321
+ // Get all child tasks
322
+ const tasksResult = await listTasks(userId, projectId, {
323
+ parent_id: epicId,
324
+ limit: 100,
325
+ });
326
+ if (!tasksResult.success) {
327
+ return {
328
+ epic,
329
+ totalTasks: 0,
330
+ byStatus: {},
331
+ percentComplete: 0,
332
+ allComplete: false,
333
+ autoCompleted: false,
334
+ };
335
+ }
336
+ const tasks = tasksResult.data;
337
+ const totalTasks = tasks.length;
338
+ // Count by status
339
+ const byStatus = {
340
+ backlog: 0,
341
+ ready: 0,
342
+ in_progress: 0,
343
+ blocked: 0,
344
+ review: 0,
345
+ done: 0,
346
+ cancelled: 0,
347
+ };
348
+ for (const task of tasks) {
349
+ byStatus[task.status] = (byStatus[task.status] || 0) + 1;
350
+ }
351
+ // Calculate progress
352
+ const doneCount = byStatus.done || 0;
353
+ const percentComplete = totalTasks > 0 ? Math.round((doneCount / totalTasks) * 100) : 0;
354
+ const allComplete = totalTasks > 0 && doneCount === totalTasks;
355
+ // Auto-complete epic if all tasks done and epic not already done
356
+ let autoCompleted = false;
357
+ if (allComplete && epic.status !== 'done') {
358
+ await updateTaskStatus(userId, projectId, epicId, 'done', 'system:auto-complete');
359
+ epic.status = 'done';
360
+ autoCompleted = true;
361
+ }
362
+ return {
363
+ epic,
364
+ totalTasks,
365
+ byStatus,
366
+ percentComplete,
367
+ allComplete,
368
+ autoCompleted,
369
+ };
370
+ }