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/planner.js CHANGED
@@ -171,14 +171,36 @@ async function callClaudeCode(prompt, cwd, onOutput, abortSignal) {
171
171
  };
172
172
  }
173
173
  }
174
- const DECOMPOSITION_PROMPT = `You are a project planning assistant. Your job is to break down a project goal into a series of small, atomic tasks that can be executed sequentially by an AI coding assistant (Claude Code).
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. Include tests for each task by default, unless the user explicitly requests otherwise
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
- Respond with a JSON array of tasks. Example format:
227
- [
228
- {
229
- "id": "task-1",
230
- "title": "Initialize project structure",
231
- "description": "Create the basic project structure with package.json, tsconfig.json, and src directory",
232
- "acceptanceCriteria": [
233
- "Project can be installed with npm install",
234
- "TypeScript compilation succeeds with strict mode",
235
- "Project structure follows standard conventions"
236
- ],
237
- "dependencies": [],
238
- "contextPatterns": ["package.json", "tsconfig.json"],
239
- "ifFailed": "skip"
240
- },
241
- {
242
- "id": "task-2",
243
- "title": "Implement user model",
244
- "description": "Create a User type and model with fields for id, name, email. The model should be exportable for use in other modules.",
245
- "acceptanceCriteria": [
246
- "User type can be imported from the models module",
247
- "User objects can be created with id, name, and email fields",
248
- "TypeScript provides proper type checking for User objects"
249
- ],
250
- "dependencies": ["task-1"],
251
- "contextPatterns": ["src/models/**/*.ts"],
252
- "ifFailed": "skip"
253
- }
254
- ]
255
-
256
- IMPORTANT: Return ONLY the JSON array, no other text or markdown formatting.`;
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
- * Decompose a project goal into tasks using Claude Code CLI
460
- */
461
- export async function decompose(goal, projectContext = '', options = {}) {
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 validateTaskDependencies(tasks) {
555
- const taskIds = new Set(tasks.map(t => t.id));
556
- // Check all dependencies reference existing tasks
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
- // Extract project name from cwd or use generic fallback
716
- const projectName = options.cwd ? basename(options.cwd) : 'project';
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 for CLI (only last few exchanges to save tokens)
722
- const recentHistory = conversationHistory.slice(-4); // Keep last 2 exchanges
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, options.cwd, options.onOutput, options.abortSignal);
547
+ const result = await callClaudeCode(fullPrompt, cwd, onOutput, abortSignal);
728
548
  if (!result.success) {
729
549
  return {
730
550
  success: false,
731
- response: '',
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
- // Check if the response contains JSON (either object or array)
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
- // Check if this is a questions response
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
- // Check if this is a tasks response
581
+ // Tasks response
761
582
  if (parsed.type === 'tasks' && Array.isArray(parsed.tasks)) {
762
- // Process the tasks array
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 processTasksResponse(parsedTasks, currentTasks, responseText);
594
+ return processAndMergeTasks(parsedTasks, currentTasks, responseText);
776
595
  }
777
596
  catch {
778
- // JSON parsing failed, just return the response text
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
- * Helper function to process task array from refinePlan response
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 processTasksResponse(parsedTasks, currentTasks, responseText) {
790
- // Create a map of existing tasks for merging
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
- // If task already exists, update it and reset to pending for re-execution
646
+ // Update existing task
807
647
  if (existingTask) {
808
- const updatedTask = {
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 - create with default status
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/update modified ones
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: ensure it exists and has correct dependencies
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
- // Update finalize task dependencies to include all other tasks
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
- updatedTasks: tasks,
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: