@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.
- package/README.md +60 -4
- package/dist/cli.js +0 -0
- package/dist/codedna/generators/base.d.ts +41 -0
- package/dist/codedna/generators/base.js +102 -0
- package/dist/codedna/generators/express-api.d.ts +12 -0
- package/dist/codedna/generators/express-api.js +61 -0
- package/dist/codedna/index.d.ts +4 -0
- package/dist/codedna/index.js +7 -0
- package/dist/codedna/parser.d.ts +117 -0
- package/dist/codedna/parser.js +233 -0
- package/dist/codedna/registry.d.ts +60 -0
- package/dist/codedna/registry.js +217 -0
- package/dist/codedna/template-engine.d.ts +17 -0
- package/dist/codedna/template-engine.js +183 -0
- package/dist/codedna/types.d.ts +64 -0
- package/dist/codedna/types.js +4 -0
- package/dist/handlers/codedna-handlers.d.ts +122 -0
- package/dist/handlers/codedna-handlers.js +194 -0
- package/dist/handlers/tool-handlers.js +593 -14
- package/dist/helpers/api-client.d.ts +37 -0
- package/dist/helpers/api-client.js +70 -0
- package/dist/helpers/codedna-monitoring.d.ts +34 -0
- package/dist/helpers/codedna-monitoring.js +159 -0
- package/dist/helpers/error-tracking.d.ts +73 -0
- package/dist/helpers/error-tracking.js +164 -0
- package/dist/helpers/library-detection.d.ts +26 -0
- package/dist/helpers/library-detection.js +145 -0
- package/dist/helpers/tasks-retry.d.ts +49 -0
- package/dist/helpers/tasks-retry.js +168 -0
- package/dist/helpers/tasks.d.ts +24 -1
- package/dist/helpers/tasks.js +146 -50
- package/dist/helpers/usage-analytics.d.ts +91 -0
- package/dist/helpers/usage-analytics.js +256 -0
- package/dist/helpers/workers.d.ts +25 -0
- package/dist/helpers/workers.js +80 -0
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +16 -5
- package/dist/tools.js +314 -0
- package/docs/AUTO-REGISTRATION.md +353 -0
- package/docs/CLAUDE4_PROMPT_ANALYSIS.md +589 -0
- package/docs/ENTITY_DSL_REFERENCE.md +685 -0
- package/docs/MODERN_STACK_COMPLETE_GUIDE.md +706 -0
- package/docs/PROMPT_STANDARDIZATION_RESULTS.md +324 -0
- package/docs/PROMPT_TIER_TEMPLATES.md +787 -0
- package/docs/RESEARCH_METHODOLOGY_EXTRACTION.md +336 -0
- package/package.json +14 -3
- 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
|
+
}
|
package/dist/helpers/tasks.d.ts
CHANGED
|
@@ -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
|
+
}>;
|
package/dist/helpers/tasks.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
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
|
|
241
|
-
const
|
|
242
|
-
status: '
|
|
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 (!
|
|
273
|
+
if (!blockedResult.success) {
|
|
247
274
|
return [];
|
|
248
275
|
}
|
|
249
276
|
const newlyUnblocked = [];
|
|
250
|
-
for (const task of
|
|
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
|
-
|
|
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
|
+
}
|