@claudetools/tools 0.3.8 → 0.4.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/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 +80 -0
- package/dist/codedna/parser.js +176 -0
- package/dist/codedna/registry.d.ts +60 -0
- package/dist/codedna/registry.js +214 -0
- package/dist/codedna/template-engine.d.ts +17 -0
- package/dist/codedna/template-engine.js +149 -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 +167 -0
- package/dist/handlers/tool-handlers.js +593 -14
- package/dist/helpers/api-client.d.ts +37 -0
- package/dist/helpers/api-client.js +63 -0
- package/dist/helpers/config-manager.js +1 -1
- 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/workers.d.ts +25 -0
- package/dist/helpers/workers.js +80 -0
- package/dist/setup.js +19 -3
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +16 -5
- package/dist/tools.js +314 -0
- package/package.json +3 -1
|
@@ -48,3 +48,40 @@ export declare function injectContext(projectId: string, query: string, userId?:
|
|
|
48
48
|
reason?: string;
|
|
49
49
|
};
|
|
50
50
|
}>;
|
|
51
|
+
export interface CachedDoc {
|
|
52
|
+
library: string;
|
|
53
|
+
context7Id: string | null;
|
|
54
|
+
version: string | null;
|
|
55
|
+
fetchedAt: string;
|
|
56
|
+
size: number;
|
|
57
|
+
mode: 'code' | 'info';
|
|
58
|
+
content: string;
|
|
59
|
+
library_name?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface DocsCacheListResponse {
|
|
62
|
+
libraries: {
|
|
63
|
+
library_id: string;
|
|
64
|
+
library_name: string;
|
|
65
|
+
topics?: string[];
|
|
66
|
+
cached_at: string;
|
|
67
|
+
}[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* List all cached documentation libraries (global cache)
|
|
71
|
+
*/
|
|
72
|
+
export declare function listCachedDocs(_projectId?: string): Promise<DocsCacheListResponse>;
|
|
73
|
+
/**
|
|
74
|
+
* Get cached documentation for a specific library
|
|
75
|
+
*/
|
|
76
|
+
export declare function getCachedDocs(_projectId: string, // Unused - docs cache is global
|
|
77
|
+
libraryId: string, topic?: string): Promise<CachedDoc | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Ensure documentation is cached (fetches from Context7 if needed)
|
|
80
|
+
*/
|
|
81
|
+
export declare function cacheDocs(_projectId: string, // Unused - docs cache is global
|
|
82
|
+
libraryId: string, libraryName: string, _content?: string, // Unused - backend fetches from Context7
|
|
83
|
+
topic?: string): Promise<{
|
|
84
|
+
success: boolean;
|
|
85
|
+
status?: string;
|
|
86
|
+
size?: number;
|
|
87
|
+
}>;
|
|
@@ -57,3 +57,66 @@ export async function injectContext(projectId, query, userId = DEFAULT_USER_ID)
|
|
|
57
57
|
});
|
|
58
58
|
return response.data;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* List all cached documentation libraries (global cache)
|
|
62
|
+
*/
|
|
63
|
+
export async function listCachedDocs(_projectId // Unused - docs cache is global
|
|
64
|
+
) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await apiRequest('/api/v1/docs');
|
|
67
|
+
// Transform to expected format
|
|
68
|
+
return {
|
|
69
|
+
libraries: response.data.libraries.map(lib => ({
|
|
70
|
+
library_id: lib.library,
|
|
71
|
+
library_name: lib.library,
|
|
72
|
+
cached_at: new Date().toISOString(), // Not provided by list endpoint
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { libraries: [] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get cached documentation for a specific library
|
|
82
|
+
*/
|
|
83
|
+
export async function getCachedDocs(_projectId, // Unused - docs cache is global
|
|
84
|
+
libraryId, topic) {
|
|
85
|
+
try {
|
|
86
|
+
const modeParam = topic ? `?mode=info` : '';
|
|
87
|
+
const response = await apiRequest(`/api/v1/docs/${encodeURIComponent(libraryId)}${modeParam}`);
|
|
88
|
+
if (!response.success || !response.data) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Add compatibility alias
|
|
92
|
+
return {
|
|
93
|
+
...response.data,
|
|
94
|
+
library_name: response.data.library,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Ensure documentation is cached (fetches from Context7 if needed)
|
|
103
|
+
*/
|
|
104
|
+
export async function cacheDocs(_projectId, // Unused - docs cache is global
|
|
105
|
+
libraryId, libraryName, _content, // Unused - backend fetches from Context7
|
|
106
|
+
topic) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await apiRequest('/api/v1/docs/ensure', 'POST', {
|
|
109
|
+
library: libraryName || libraryId,
|
|
110
|
+
mode: topic ? 'info' : 'code',
|
|
111
|
+
topic,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
success: response.success,
|
|
115
|
+
status: response.data?.status,
|
|
116
|
+
size: response.data?.size,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { success: false };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -21,7 +21,7 @@ export const DEFAULT_CONFIG = {
|
|
|
21
21
|
defaultProjectId: undefined,
|
|
22
22
|
defaultUserId: 'default',
|
|
23
23
|
// Context Injection
|
|
24
|
-
autoInjectContext:
|
|
24
|
+
autoInjectContext: true, // Enabled by default - this is the core value prop
|
|
25
25
|
contextRelevanceThreshold: 0.5,
|
|
26
26
|
maxContextFacts: 10,
|
|
27
27
|
// Logging & Verbosity
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context7-compatible library ID mappings
|
|
3
|
+
* Maps canonical names to context7 library IDs
|
|
4
|
+
*/
|
|
5
|
+
export declare const CONTEXT7_LIBRARY_IDS: Record<string, string>;
|
|
6
|
+
export interface DetectedLibrary {
|
|
7
|
+
name: string;
|
|
8
|
+
context7Id?: string;
|
|
9
|
+
mentions: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Detect libraries mentioned in text
|
|
13
|
+
* @param text - Text to scan for library mentions
|
|
14
|
+
* @returns Array of detected libraries with mention counts
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectLibraries(text: string): DetectedLibrary[];
|
|
17
|
+
/**
|
|
18
|
+
* Detect libraries from goal and task descriptions
|
|
19
|
+
* @param goal - The epic/plan goal
|
|
20
|
+
* @param tasks - Array of task objects with title and description
|
|
21
|
+
* @returns Deduplicated list of detected libraries
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectLibrariesFromPlan(goal: string, tasks: {
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
}[]): DetectedLibrary[];
|
|
@@ -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
|
+
}>;
|