maistro 1.2.3 → 1.2.6
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/app.d.ts.map +1 -1
- package/dist/app.js +17 -4
- package/dist/app.js.map +1 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +52 -1
- package/dist/executor.js.map +1 -1
- package/dist/index.js +11 -6
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +7 -2
- package/dist/orchestrator.js.map +1 -1
- package/dist/planner.d.ts +32 -23
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +219 -310
- package/dist/planner.js.map +1 -1
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +28 -2
- package/dist/validator.js.map +1 -1
- package/package.json +1 -1
package/dist/planner.js
CHANGED
|
@@ -171,14 +171,36 @@ async function callClaudeCode(prompt, cwd, onOutput, abortSignal) {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
const
|
|
174
|
+
const PLANNING_PROMPT = `You are a project planning assistant. Your job is to break down user requests into small, atomic tasks that can be executed sequentially by an AI coding assistant (Claude Code).
|
|
175
|
+
|
|
176
|
+
This is PLANNING MODE - you must NEVER execute actions. When the user says things like "run tests", "add feature X", "fix bug Y", or ANY action-sounding request:
|
|
177
|
+
1. First, check if the request is clear enough to create well-defined tasks
|
|
178
|
+
2. If unclear or ambiguous, return CLARIFYING QUESTIONS as structured JSON
|
|
179
|
+
3. If clear enough, create TASKS and return them as JSON
|
|
180
|
+
4. NEVER actually execute the action yourself
|
|
181
|
+
|
|
182
|
+
DO NOT:
|
|
183
|
+
- Execute tests or builds
|
|
184
|
+
- Make any changes to the codebase
|
|
185
|
+
|
|
186
|
+
WHEN TO ASK QUESTIONS:
|
|
187
|
+
- Request mentions a feature but lacks specifics (e.g., "add authentication" - what type?)
|
|
188
|
+
- Multiple valid approaches exist (e.g., "add a button" - where? what should it do?)
|
|
189
|
+
- Technical choices need to be made (e.g., "add database" - which one?)
|
|
190
|
+
- Requirements are vague (e.g., "make it better" - in what way?)
|
|
191
|
+
|
|
192
|
+
WHEN TO CREATE TASKS DIRECTLY:
|
|
193
|
+
- Request is specific and actionable (e.g., "add a logout button to the header")
|
|
194
|
+
- Simple operations (e.g., "run tests", "fix typo in README")
|
|
195
|
+
- User has already provided enough context
|
|
196
|
+
|
|
197
|
+
## Task Decomposition Guidelines
|
|
175
198
|
|
|
176
|
-
Guidelines for task decomposition:
|
|
177
199
|
1. Each task should be completable in a single coding session (10-30 minutes)
|
|
178
200
|
2. Tasks should be ordered by dependency (tasks that depend on others come later)
|
|
179
201
|
3. Each task should have a clear, specific objective
|
|
180
202
|
4. Include setup tasks if needed (e.g., "Initialize project structure")
|
|
181
|
-
5.
|
|
203
|
+
5. Each task's description must include writing tests for the functionality implemented in that task, unless the user explicitly requests otherwise. If the task involves UI changes, include a UI test; otherwise a unit test is sufficient
|
|
182
204
|
6. Be specific about what files or components are involved
|
|
183
205
|
7. Consider which tasks are critical vs optional - critical tasks should stop execution if they fail
|
|
184
206
|
8. IMPORTANT: Each task MUST have clear, verifiable acceptance criteria
|
|
@@ -208,6 +230,8 @@ Outcome-based criteria allow flexibility while ensuring the desired functionalit
|
|
|
208
230
|
|
|
209
231
|
Only use structural criteria when the structure itself IS the requirement (e.g., "Uses SwiftData" if iOS 17+ SwiftData is a hard requirement from the user).
|
|
210
232
|
|
|
233
|
+
## Task Schema
|
|
234
|
+
|
|
211
235
|
For each task, provide:
|
|
212
236
|
- id: A unique identifier (e.g., "task-1", "task-2")
|
|
213
237
|
- title: A short, descriptive title
|
|
@@ -216,6 +240,7 @@ For each task, provide:
|
|
|
216
240
|
- Focus on observable behavior or testable functionality
|
|
217
241
|
- Be verifiable by running the app or tests (not by inspecting code structure)
|
|
218
242
|
- Avoid specifying exact file names, class names, or method signatures
|
|
243
|
+
- Include at least one criterion about tests passing (e.g., "Unit tests pass for the new functionality")
|
|
219
244
|
- dependencies: Array of task IDs that must complete before this task (empty for first tasks)
|
|
220
245
|
- contextPatterns: Glob patterns for files relevant to this task (e.g., ["src/**/*.ts", "package.json"])
|
|
221
246
|
- ifFailed: What to do if this task fails after retries. IMPORTANT: Default is "skip"
|
|
@@ -223,37 +248,66 @@ For each task, provide:
|
|
|
223
248
|
- "stop": Stop execution entirely - ONLY use for truly critical tasks where nothing else can proceed
|
|
224
249
|
- "retry": Keep retrying indefinitely (use sparingly)
|
|
225
250
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
## Project Context
|
|
252
|
+
|
|
253
|
+
Project: {PROJECT_NAME}
|
|
254
|
+
|
|
255
|
+
{PENDING_SECTION}
|
|
256
|
+
|
|
257
|
+
## Task ID Rules
|
|
258
|
+
- New tasks: Use IDs starting at task-{NEXT_ID}, incrementing for each additional task
|
|
259
|
+
- Modifying pending tasks: Use the EXACT existing task ID from the pending tasks list
|
|
260
|
+
- NEVER reuse IDs from completed tasks
|
|
261
|
+
|
|
262
|
+
## Response Format
|
|
263
|
+
|
|
264
|
+
Create tasks for what the user is asking. Do not add unrelated tasks.
|
|
265
|
+
|
|
266
|
+
You MUST return ONE of these two JSON formats:
|
|
267
|
+
|
|
268
|
+
Option 1: Questions (when clarification needed)
|
|
269
|
+
\`\`\`json
|
|
270
|
+
{
|
|
271
|
+
"type": "questions",
|
|
272
|
+
"questions": [
|
|
273
|
+
{
|
|
274
|
+
"id": "question-1",
|
|
275
|
+
"question": "Where should the speed indicator be displayed?",
|
|
276
|
+
"type": "choice",
|
|
277
|
+
"options": ["Below the tap count", "In the stats section", "As a live updating label", "Other"],
|
|
278
|
+
"reason": "This determines the UI layout approach"
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
\`\`\`
|
|
283
|
+
|
|
284
|
+
Option 2: Tasks (when request is clear)
|
|
285
|
+
\`\`\`json
|
|
286
|
+
{
|
|
287
|
+
"type": "tasks",
|
|
288
|
+
"tasks": [
|
|
289
|
+
{
|
|
290
|
+
"id": "task-{NEXT_ID}",
|
|
291
|
+
"title": "Add speed indicator",
|
|
292
|
+
"description": "Display clicks per minute below the tap count. Write unit tests to verify the CPM calculation logic and UI tests to verify the indicator displays correctly.",
|
|
293
|
+
"acceptanceCriteria": [
|
|
294
|
+
"Speed indicator updates in real-time as user taps",
|
|
295
|
+
"Display shows clicks per minute with proper formatting",
|
|
296
|
+
"Unit tests pass for CPM calculation and display"
|
|
297
|
+
],
|
|
298
|
+
"dependencies": [],
|
|
299
|
+
"contextPatterns": ["src/components/**/*.ts"],
|
|
300
|
+
"ifFailed": "skip"
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
\`\`\`
|
|
305
|
+
|
|
306
|
+
## Rules
|
|
307
|
+
- ALWAYS return valid JSON in a code block
|
|
308
|
+
- For questions: use "type": "questions" and include 1-3 questions with options
|
|
309
|
+
- For tasks: use "type": "tasks" and include the task array
|
|
310
|
+
- Omit "status" field in tasks - system handles it`;
|
|
257
311
|
const DISCOVERY_PROMPT = `You are helping plan a software project. Analyze the user's goal and generate clarifying questions to understand their requirements better.
|
|
258
312
|
|
|
259
313
|
Your task is to:
|
|
@@ -456,252 +510,19 @@ function createFinalizeTask(existingTasks) {
|
|
|
456
510
|
});
|
|
457
511
|
}
|
|
458
512
|
/**
|
|
459
|
-
*
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const userMessage = projectContext
|
|
463
|
-
? `Project Goal: ${goal}\n\nExisting Project Context:\n${projectContext}`
|
|
464
|
-
: `Project Goal: ${goal}`;
|
|
465
|
-
const fullPrompt = `${DECOMPOSITION_PROMPT}\n\n${userMessage}`;
|
|
466
|
-
const result = await callClaudeCode(fullPrompt, options.cwd, undefined, options.abortSignal);
|
|
467
|
-
if (!result.success) {
|
|
468
|
-
return {
|
|
469
|
-
success: false,
|
|
470
|
-
tasks: [],
|
|
471
|
-
error: result.error || 'Claude Code CLI failed',
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
const rawResponse = result.response;
|
|
475
|
-
// Parse JSON response
|
|
476
|
-
let parsedTasks;
|
|
477
|
-
try {
|
|
478
|
-
// Try to extract JSON from the response (in case there's extra text)
|
|
479
|
-
// First, try to extract from markdown code blocks
|
|
480
|
-
let jsonText = null;
|
|
481
|
-
const codeBlockMatch = rawResponse.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
482
|
-
if (codeBlockMatch) {
|
|
483
|
-
jsonText = codeBlockMatch[1];
|
|
484
|
-
}
|
|
485
|
-
// Fallback to raw JSON array match
|
|
486
|
-
if (!jsonText) {
|
|
487
|
-
const jsonMatch = rawResponse.match(/\[[\s\S]*\]/);
|
|
488
|
-
if (jsonMatch) {
|
|
489
|
-
jsonText = jsonMatch[0];
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (!jsonText) {
|
|
493
|
-
return {
|
|
494
|
-
success: false,
|
|
495
|
-
tasks: [],
|
|
496
|
-
error: 'No JSON array found in response',
|
|
497
|
-
rawResponse,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
parsedTasks = JSON.parse(jsonText);
|
|
501
|
-
}
|
|
502
|
-
catch (parseError) {
|
|
503
|
-
return {
|
|
504
|
-
success: false,
|
|
505
|
-
tasks: [],
|
|
506
|
-
error: `Failed to parse JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
507
|
-
rawResponse,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
// Validate and convert to Task objects
|
|
511
|
-
if (!Array.isArray(parsedTasks)) {
|
|
512
|
-
return {
|
|
513
|
-
success: false,
|
|
514
|
-
tasks: [],
|
|
515
|
-
error: 'Response is not an array',
|
|
516
|
-
rawResponse,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
const tasks = parsedTasks.map(pt => {
|
|
520
|
-
// Validate and normalize ifFailed value
|
|
521
|
-
const validFailureActions = ['retry', 'skip', 'stop'];
|
|
522
|
-
const ifFailed = validFailureActions.includes(pt.ifFailed)
|
|
523
|
-
? pt.ifFailed
|
|
524
|
-
: DEFAULT_CONFIG.defaultIfFailed;
|
|
525
|
-
const description = pt.description || '';
|
|
526
|
-
// Extract image references from description
|
|
527
|
-
const imageRefs = extractImagePlaceholders(description);
|
|
528
|
-
return createTask({
|
|
529
|
-
id: pt.id || `task-${Math.random().toString(36).slice(2, 8)}`,
|
|
530
|
-
title: pt.title || 'Untitled Task',
|
|
531
|
-
description,
|
|
532
|
-
acceptanceCriteria: pt.acceptanceCriteria,
|
|
533
|
-
dependencies: pt.dependencies || [],
|
|
534
|
-
contextPatterns: pt.contextPatterns || [],
|
|
535
|
-
ifFailed,
|
|
536
|
-
timeout: pt.timeout ?? DEFAULT_CONFIG.executionTimeout,
|
|
537
|
-
maxRetries: pt.maxRetries ?? DEFAULT_CONFIG.maxRetries,
|
|
538
|
-
imageRefs: imageRefs.length > 0 ? imageRefs : undefined,
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
// Append finalize task if there are tasks to finalize
|
|
542
|
-
if (tasks.length > 0) {
|
|
543
|
-
tasks.push(createFinalizeTask(tasks));
|
|
544
|
-
}
|
|
545
|
-
return {
|
|
546
|
-
success: true,
|
|
547
|
-
tasks,
|
|
548
|
-
rawResponse,
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Validate that task dependencies form a valid DAG (no cycles)
|
|
513
|
+
* Unified planning function - handles both initial decomposition and plan updates.
|
|
514
|
+
* When currentTasks is empty/undefined, creates a fresh plan.
|
|
515
|
+
* When currentTasks is provided, merges new/modified tasks with existing ones.
|
|
553
516
|
*/
|
|
554
|
-
export function
|
|
555
|
-
const
|
|
556
|
-
//
|
|
557
|
-
for (const task of tasks) {
|
|
558
|
-
for (const depId of task.dependencies) {
|
|
559
|
-
if (!taskIds.has(depId)) {
|
|
560
|
-
return {
|
|
561
|
-
valid: false,
|
|
562
|
-
error: `Task "${task.id}" depends on non-existent task "${depId}"`,
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
// Check for cycles using DFS
|
|
568
|
-
const visited = new Set();
|
|
569
|
-
const recursionStack = new Set();
|
|
570
|
-
function hasCycle(taskId) {
|
|
571
|
-
visited.add(taskId);
|
|
572
|
-
recursionStack.add(taskId);
|
|
573
|
-
const task = tasks.find(t => t.id === taskId);
|
|
574
|
-
if (task) {
|
|
575
|
-
for (const depId of task.dependencies) {
|
|
576
|
-
if (!visited.has(depId)) {
|
|
577
|
-
if (hasCycle(depId)) {
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
else if (recursionStack.has(depId)) {
|
|
582
|
-
return true;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
recursionStack.delete(taskId);
|
|
587
|
-
return false;
|
|
588
|
-
}
|
|
589
|
-
for (const task of tasks) {
|
|
590
|
-
if (!visited.has(task.id)) {
|
|
591
|
-
if (hasCycle(task.id)) {
|
|
592
|
-
return {
|
|
593
|
-
valid: false,
|
|
594
|
-
error: 'Circular dependency detected in tasks',
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return { valid: true };
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Sort tasks topologically based on dependencies
|
|
603
|
-
*/
|
|
604
|
-
export function sortTasksByDependency(tasks) {
|
|
605
|
-
const sorted = [];
|
|
606
|
-
const visited = new Set();
|
|
607
|
-
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
|
608
|
-
function visit(taskId) {
|
|
609
|
-
if (visited.has(taskId))
|
|
610
|
-
return;
|
|
611
|
-
visited.add(taskId);
|
|
612
|
-
const task = taskMap.get(taskId);
|
|
613
|
-
if (task) {
|
|
614
|
-
for (const depId of task.dependencies) {
|
|
615
|
-
visit(depId);
|
|
616
|
-
}
|
|
617
|
-
sorted.push(task);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
for (const task of tasks) {
|
|
621
|
-
visit(task.id);
|
|
622
|
-
}
|
|
623
|
-
return sorted;
|
|
624
|
-
}
|
|
625
|
-
const PLAN_REFINEMENT_PROMPT = `You are a PLANNING assistant modifying a task plan. This is PLANNING MODE - you must NEVER execute actions.
|
|
626
|
-
|
|
627
|
-
CRITICAL: When the user says things like "run tests", "add feature X", "fix bug Y", or ANY action-sounding request, you must:
|
|
628
|
-
1. First, check if the request is clear enough to create a well-defined task
|
|
629
|
-
2. If unclear or ambiguous, return CLARIFYING QUESTIONS as structured JSON
|
|
630
|
-
3. If clear enough, create TASKS and return them as JSON
|
|
631
|
-
4. NEVER actually execute the action yourself
|
|
632
|
-
|
|
633
|
-
WHEN TO ASK QUESTIONS:
|
|
634
|
-
- Request mentions a feature but lacks specifics (e.g., "add authentication" - what type?)
|
|
635
|
-
- Multiple valid approaches exist (e.g., "add a button" - where? what should it do?)
|
|
636
|
-
- Technical choices need to be made (e.g., "add database" - which one?)
|
|
637
|
-
- Requirements are vague (e.g., "make it better" - in what way?)
|
|
638
|
-
|
|
639
|
-
WHEN TO CREATE TASKS DIRECTLY:
|
|
640
|
-
- Request is specific and actionable (e.g., "add a logout button to the header")
|
|
641
|
-
- Simple operations (e.g., "run tests", "fix typo in README")
|
|
642
|
-
- User has already provided enough context
|
|
643
|
-
|
|
644
|
-
DO NOT:
|
|
645
|
-
- Run any commands or tools
|
|
646
|
-
- Read files or explore code
|
|
647
|
-
- Execute tests or builds
|
|
648
|
-
- Make any changes to the codebase
|
|
649
|
-
|
|
650
|
-
Project: {PROJECT_NAME}
|
|
651
|
-
|
|
652
|
-
{PENDING_SECTION}
|
|
653
|
-
|
|
654
|
-
IMPORTANT: Create tasks ONLY for what the user is asking in their current message. Do not add unrelated tasks.
|
|
655
|
-
|
|
656
|
-
RESPONSE FORMAT - You MUST return ONE of these two JSON formats:
|
|
657
|
-
|
|
658
|
-
Option 1: Questions (when clarification needed)
|
|
659
|
-
\`\`\`json
|
|
660
|
-
{
|
|
661
|
-
"type": "questions",
|
|
662
|
-
"questions": [
|
|
663
|
-
{
|
|
664
|
-
"id": "question-1",
|
|
665
|
-
"question": "Where should the speed indicator be displayed?",
|
|
666
|
-
"type": "choice",
|
|
667
|
-
"options": ["Below the tap count", "In the stats section", "As a live updating label", "Other"],
|
|
668
|
-
"reason": "This determines the UI layout approach"
|
|
669
|
-
}
|
|
670
|
-
]
|
|
671
|
-
}
|
|
672
|
-
\`\`\`
|
|
673
|
-
|
|
674
|
-
Option 2: Tasks (when request is clear)
|
|
675
|
-
\`\`\`json
|
|
676
|
-
{
|
|
677
|
-
"type": "tasks",
|
|
678
|
-
"tasks": [{"id": "task-{NEXT_ID}", "title": "Add speed indicator", "description": "Display clicks per minute below the tap count"}]
|
|
679
|
-
}
|
|
680
|
-
\`\`\`
|
|
681
|
-
|
|
682
|
-
TASK ID RULES:
|
|
683
|
-
- New tasks: Use IDs starting at task-{NEXT_ID}, incrementing for each additional task
|
|
684
|
-
- Modifying pending tasks: Use the EXACT existing task ID from the pending tasks list
|
|
685
|
-
- NEVER reuse IDs from completed tasks
|
|
686
|
-
|
|
687
|
-
OTHER RULES:
|
|
688
|
-
- ALWAYS return valid JSON in a code block
|
|
689
|
-
- For questions: use "type": "questions" and include 1-3 questions with options
|
|
690
|
-
- For tasks: use "type": "tasks" and include the task array
|
|
691
|
-
- Omit "status" field in tasks - system handles it`;
|
|
692
|
-
/**
|
|
693
|
-
* Refine a plan through conversation with the user
|
|
694
|
-
*/
|
|
695
|
-
export async function refinePlan(goal, currentTasks, userMessage, conversationHistory, options = {}) {
|
|
696
|
-
// Only include non-completed tasks (pending/in-progress/failed) - these are the current plan
|
|
517
|
+
export async function planTasks(options) {
|
|
518
|
+
const { userMessage, currentTasks = [], conversationHistory = [], cwd, abortSignal, onOutput, } = options;
|
|
519
|
+
// Build dynamic sections for the prompt
|
|
697
520
|
const pendingTasks = currentTasks.filter(t => t.status === 'pending' || t.status === 'in_progress' || t.status === 'failed');
|
|
698
|
-
// Calculate next task ID for the prompt
|
|
699
521
|
const maxTaskNum = currentTasks.reduce((max, t) => {
|
|
700
522
|
const match = t.id.match(/task-(\d+)/);
|
|
701
523
|
return match ? Math.max(max, parseInt(match[1], 10)) : max;
|
|
702
524
|
}, 0);
|
|
703
|
-
const nextId = maxTaskNum + 1;
|
|
704
|
-
// Format pending tasks (they may need modification)
|
|
525
|
+
const nextId = maxTaskNum > 0 ? maxTaskNum + 1 : 1;
|
|
705
526
|
const pendingSection = pendingTasks.length > 0
|
|
706
527
|
? `Current plan (pending tasks):\n\`\`\`json\n${JSON.stringify(pendingTasks.map(t => ({
|
|
707
528
|
id: t.id,
|
|
@@ -712,36 +533,34 @@ export async function refinePlan(goal, currentTasks, userMessage, conversationHi
|
|
|
712
533
|
status: t.status,
|
|
713
534
|
})), null, 2)}\n\`\`\``
|
|
714
535
|
: 'No pending tasks - ready for new work.';
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
const systemPrompt = PLAN_REFINEMENT_PROMPT
|
|
536
|
+
const projectName = cwd ? basename(cwd) : 'project';
|
|
537
|
+
const systemPrompt = PLANNING_PROMPT
|
|
718
538
|
.replace('{PROJECT_NAME}', projectName)
|
|
719
539
|
.replace('{PENDING_SECTION}', pendingSection)
|
|
720
540
|
.replace(/{NEXT_ID}/g, String(nextId));
|
|
721
|
-
// Build conversation context
|
|
722
|
-
const recentHistory = conversationHistory.slice(-4);
|
|
541
|
+
// Build conversation context (only last few exchanges to save tokens)
|
|
542
|
+
const recentHistory = conversationHistory.slice(-4);
|
|
723
543
|
const historyContext = recentHistory.length > 0
|
|
724
544
|
? '\n\nRecent conversation:\n' + recentHistory.map(m => `${m.role}: ${m.content}`).join('\n')
|
|
725
545
|
: '';
|
|
726
546
|
const fullPrompt = `${systemPrompt}${historyContext}\n\nUser: ${userMessage}`;
|
|
727
|
-
const result = await callClaudeCode(fullPrompt,
|
|
547
|
+
const result = await callClaudeCode(fullPrompt, cwd, onOutput, abortSignal);
|
|
728
548
|
if (!result.success) {
|
|
729
549
|
return {
|
|
730
550
|
success: false,
|
|
731
|
-
|
|
551
|
+
tasks: [],
|
|
732
552
|
error: result.error || 'Claude Code CLI failed',
|
|
733
553
|
aborted: result.aborted,
|
|
734
554
|
};
|
|
735
555
|
}
|
|
736
556
|
const responseText = result.response;
|
|
737
|
-
//
|
|
557
|
+
// Try to parse structured response (object with type field)
|
|
738
558
|
const objectMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
739
559
|
const arrayMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
740
|
-
// Try to parse structured response first (object with type field)
|
|
741
560
|
if (objectMatch) {
|
|
742
561
|
try {
|
|
743
562
|
const parsed = JSON.parse(objectMatch[1]);
|
|
744
|
-
//
|
|
563
|
+
// Questions response
|
|
745
564
|
if (parsed.type === 'questions' && Array.isArray(parsed.questions)) {
|
|
746
565
|
const questions = parsed.questions.map((q, index) => ({
|
|
747
566
|
id: q.id || `question-${index + 1}`,
|
|
@@ -753,15 +572,15 @@ export async function refinePlan(goal, currentTasks, userMessage, conversationHi
|
|
|
753
572
|
}));
|
|
754
573
|
return {
|
|
755
574
|
success: true,
|
|
575
|
+
tasks: [],
|
|
756
576
|
response: responseText,
|
|
757
577
|
questions,
|
|
578
|
+
rawResponse: responseText,
|
|
758
579
|
};
|
|
759
580
|
}
|
|
760
|
-
//
|
|
581
|
+
// Tasks response
|
|
761
582
|
if (parsed.type === 'tasks' && Array.isArray(parsed.tasks)) {
|
|
762
|
-
|
|
763
|
-
const parsedTasks = parsed.tasks;
|
|
764
|
-
return processTasksResponse(parsedTasks, currentTasks, responseText);
|
|
583
|
+
return processAndMergeTasks(parsed.tasks, currentTasks, responseText);
|
|
765
584
|
}
|
|
766
585
|
}
|
|
767
586
|
catch {
|
|
@@ -772,44 +591,64 @@ export async function refinePlan(goal, currentTasks, userMessage, conversationHi
|
|
|
772
591
|
if (arrayMatch) {
|
|
773
592
|
try {
|
|
774
593
|
const parsedTasks = JSON.parse(arrayMatch[1]);
|
|
775
|
-
return
|
|
594
|
+
return processAndMergeTasks(parsedTasks, currentTasks, responseText);
|
|
776
595
|
}
|
|
777
596
|
catch {
|
|
778
|
-
// JSON parsing failed
|
|
597
|
+
// JSON parsing failed
|
|
779
598
|
}
|
|
780
599
|
}
|
|
600
|
+
// Fallback: try raw JSON array (no code block)
|
|
601
|
+
if (!objectMatch && !arrayMatch) {
|
|
602
|
+
try {
|
|
603
|
+
const jsonMatch = responseText.match(/\[[\s\S]*\]/);
|
|
604
|
+
if (jsonMatch) {
|
|
605
|
+
const parsedTasks = JSON.parse(jsonMatch[0]);
|
|
606
|
+
return processAndMergeTasks(parsedTasks, currentTasks, responseText);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// Not valid JSON
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// No tasks or questions found - return text-only response
|
|
781
614
|
return {
|
|
782
615
|
success: true,
|
|
616
|
+
tasks: [],
|
|
783
617
|
response: responseText,
|
|
618
|
+
rawResponse: responseText,
|
|
784
619
|
};
|
|
785
620
|
}
|
|
786
621
|
/**
|
|
787
|
-
*
|
|
622
|
+
* Process parsed tasks and merge with existing tasks if applicable.
|
|
623
|
+
* Handles both initial planning (no existing tasks) and plan updates (with existing tasks).
|
|
788
624
|
*/
|
|
789
|
-
function
|
|
790
|
-
|
|
625
|
+
function processAndMergeTasks(parsedTasks, currentTasks, responseText) {
|
|
626
|
+
if (!Array.isArray(parsedTasks)) {
|
|
627
|
+
return {
|
|
628
|
+
success: false,
|
|
629
|
+
tasks: [],
|
|
630
|
+
error: 'Response is not an array',
|
|
631
|
+
rawResponse: responseText,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
791
634
|
const existingTaskMap = new Map(currentTasks.map(t => [t.id, t]));
|
|
792
635
|
const modifiedTaskIds = new Set();
|
|
793
|
-
// Process returned tasks (new or modified only)
|
|
794
636
|
const processedTasks = parsedTasks.map((pt) => {
|
|
795
637
|
const taskId = pt.id || `task-${Math.random().toString(36).slice(2, 8)}`;
|
|
796
638
|
modifiedTaskIds.add(taskId);
|
|
797
639
|
const existingTask = existingTaskMap.get(taskId);
|
|
798
|
-
// Validate and normalize ifFailed value
|
|
799
640
|
const validFailureActions = ['retry', 'skip', 'stop'];
|
|
800
641
|
const ifFailed = validFailureActions.includes(pt.ifFailed)
|
|
801
642
|
? pt.ifFailed
|
|
802
643
|
: (existingTask?.ifFailed ?? DEFAULT_CONFIG.defaultIfFailed);
|
|
803
644
|
const description = pt.description || existingTask?.description || '';
|
|
804
|
-
// Extract image references from description
|
|
805
645
|
const imageRefs = extractImagePlaceholders(description);
|
|
806
|
-
//
|
|
646
|
+
// Update existing task
|
|
807
647
|
if (existingTask) {
|
|
808
|
-
|
|
648
|
+
return {
|
|
809
649
|
...existingTask,
|
|
810
650
|
title: pt.title || existingTask.title,
|
|
811
651
|
description,
|
|
812
|
-
// Use ?? instead of || so empty arrays are preserved (not treated as falsy)
|
|
813
652
|
acceptanceCriteria: pt.acceptanceCriteria ?? existingTask.acceptanceCriteria,
|
|
814
653
|
dependencies: pt.dependencies ?? existingTask.dependencies,
|
|
815
654
|
contextPatterns: pt.contextPatterns ?? existingTask.contextPatterns,
|
|
@@ -818,16 +657,14 @@ function processTasksResponse(parsedTasks, currentTasks, responseText) {
|
|
|
818
657
|
maxRetries: pt.maxRetries ?? existingTask.maxRetries,
|
|
819
658
|
imageRefs: imageRefs.length > 0 ? imageRefs : existingTask.imageRefs,
|
|
820
659
|
updatedAt: new Date().toISOString(),
|
|
821
|
-
// Always reset to pending - returned tasks are treated as new work to execute
|
|
822
660
|
status: 'pending',
|
|
823
661
|
retryCount: 0,
|
|
824
662
|
error: undefined,
|
|
825
663
|
failureType: undefined,
|
|
826
664
|
durationMs: undefined,
|
|
827
665
|
};
|
|
828
|
-
return updatedTask;
|
|
829
666
|
}
|
|
830
|
-
// New task
|
|
667
|
+
// New task
|
|
831
668
|
return createTask({
|
|
832
669
|
id: taskId,
|
|
833
670
|
title: pt.title || 'Untitled Task',
|
|
@@ -841,34 +678,106 @@ function processTasksResponse(parsedTasks, currentTasks, responseText) {
|
|
|
841
678
|
imageRefs: imageRefs.length > 0 ? imageRefs : undefined,
|
|
842
679
|
});
|
|
843
680
|
});
|
|
844
|
-
// Merge: keep all existing tasks that weren't modified, then add
|
|
681
|
+
// Merge: keep all existing tasks that weren't modified, then add modified/new ones
|
|
845
682
|
const unchangedTasks = currentTasks.filter(t => !modifiedTaskIds.has(t.id) && t.id !== FINALIZE_TASK_ID);
|
|
846
683
|
const tasks = [...unchangedTasks, ...processedTasks];
|
|
847
|
-
// Handle finalize task
|
|
684
|
+
// Handle finalize task
|
|
848
685
|
const nonFinalizeTasks = tasks.filter(t => t.id !== FINALIZE_TASK_ID);
|
|
849
686
|
const existingFinalizeTask = currentTasks.find(t => t.id === FINALIZE_TASK_ID);
|
|
850
687
|
if (nonFinalizeTasks.length > 0) {
|
|
851
688
|
if (existingFinalizeTask) {
|
|
852
|
-
|
|
853
|
-
const updatedFinalizeTask = {
|
|
689
|
+
tasks.push({
|
|
854
690
|
...existingFinalizeTask,
|
|
855
691
|
dependencies: nonFinalizeTasks.map(t => t.id),
|
|
856
692
|
status: 'pending',
|
|
857
693
|
updatedAt: new Date().toISOString(),
|
|
858
|
-
};
|
|
859
|
-
tasks.push(updatedFinalizeTask);
|
|
694
|
+
});
|
|
860
695
|
}
|
|
861
696
|
else {
|
|
862
|
-
// Re-add finalize task if it was removed
|
|
863
697
|
tasks.push(createFinalizeTask(nonFinalizeTasks));
|
|
864
698
|
}
|
|
865
699
|
}
|
|
866
700
|
return {
|
|
867
701
|
success: true,
|
|
702
|
+
tasks,
|
|
868
703
|
response: responseText,
|
|
869
|
-
|
|
704
|
+
rawResponse: responseText,
|
|
870
705
|
};
|
|
871
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Validate that task dependencies form a valid DAG (no cycles)
|
|
709
|
+
*/
|
|
710
|
+
export function validateTaskDependencies(tasks) {
|
|
711
|
+
const taskIds = new Set(tasks.map(t => t.id));
|
|
712
|
+
// Check all dependencies reference existing tasks
|
|
713
|
+
for (const task of tasks) {
|
|
714
|
+
for (const depId of task.dependencies) {
|
|
715
|
+
if (!taskIds.has(depId)) {
|
|
716
|
+
return {
|
|
717
|
+
valid: false,
|
|
718
|
+
error: `Task "${task.id}" depends on non-existent task "${depId}"`,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Check for cycles using DFS
|
|
724
|
+
const visited = new Set();
|
|
725
|
+
const recursionStack = new Set();
|
|
726
|
+
function hasCycle(taskId) {
|
|
727
|
+
visited.add(taskId);
|
|
728
|
+
recursionStack.add(taskId);
|
|
729
|
+
const task = tasks.find(t => t.id === taskId);
|
|
730
|
+
if (task) {
|
|
731
|
+
for (const depId of task.dependencies) {
|
|
732
|
+
if (!visited.has(depId)) {
|
|
733
|
+
if (hasCycle(depId)) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else if (recursionStack.has(depId)) {
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
recursionStack.delete(taskId);
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
for (const task of tasks) {
|
|
746
|
+
if (!visited.has(task.id)) {
|
|
747
|
+
if (hasCycle(task.id)) {
|
|
748
|
+
return {
|
|
749
|
+
valid: false,
|
|
750
|
+
error: 'Circular dependency detected in tasks',
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return { valid: true };
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Sort tasks topologically based on dependencies
|
|
759
|
+
*/
|
|
760
|
+
export function sortTasksByDependency(tasks) {
|
|
761
|
+
const sorted = [];
|
|
762
|
+
const visited = new Set();
|
|
763
|
+
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
|
764
|
+
function visit(taskId) {
|
|
765
|
+
if (visited.has(taskId))
|
|
766
|
+
return;
|
|
767
|
+
visited.add(taskId);
|
|
768
|
+
const task = taskMap.get(taskId);
|
|
769
|
+
if (task) {
|
|
770
|
+
for (const depId of task.dependencies) {
|
|
771
|
+
visit(depId);
|
|
772
|
+
}
|
|
773
|
+
sorted.push(task);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
for (const task of tasks) {
|
|
777
|
+
visit(task.id);
|
|
778
|
+
}
|
|
779
|
+
return sorted;
|
|
780
|
+
}
|
|
872
781
|
const README_GENERATION_PROMPT = `You are a technical documentation assistant. Generate a README.md file for a new project.
|
|
873
782
|
|
|
874
783
|
The README should include:
|