maistro 1.0.390

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +107 -0
  3. package/dist/app.d.ts +247 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +4971 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/buildInfo.d.ts +5 -0
  8. package/dist/buildInfo.d.ts.map +1 -0
  9. package/dist/buildInfo.js +2 -0
  10. package/dist/buildInfo.js.map +1 -0
  11. package/dist/caffeinate.d.ts +72 -0
  12. package/dist/caffeinate.d.ts.map +1 -0
  13. package/dist/caffeinate.js +258 -0
  14. package/dist/caffeinate.js.map +1 -0
  15. package/dist/claudePath.d.ts +10 -0
  16. package/dist/claudePath.d.ts.map +1 -0
  17. package/dist/claudePath.js +34 -0
  18. package/dist/claudePath.js.map +1 -0
  19. package/dist/clipboard.d.ts +44 -0
  20. package/dist/clipboard.d.ts.map +1 -0
  21. package/dist/clipboard.js +442 -0
  22. package/dist/clipboard.js.map +1 -0
  23. package/dist/config.d.ts +211 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +933 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/constants.d.ts +50 -0
  28. package/dist/constants.d.ts.map +1 -0
  29. package/dist/constants.js +81 -0
  30. package/dist/constants.js.map +1 -0
  31. package/dist/contextBuilder.d.ts +38 -0
  32. package/dist/contextBuilder.d.ts.map +1 -0
  33. package/dist/contextBuilder.js +113 -0
  34. package/dist/contextBuilder.js.map +1 -0
  35. package/dist/dependencyDetector.d.ts +57 -0
  36. package/dist/dependencyDetector.d.ts.map +1 -0
  37. package/dist/dependencyDetector.js +505 -0
  38. package/dist/dependencyDetector.js.map +1 -0
  39. package/dist/executor.d.ts +83 -0
  40. package/dist/executor.d.ts.map +1 -0
  41. package/dist/executor.js +583 -0
  42. package/dist/executor.js.map +1 -0
  43. package/dist/git.d.ts +85 -0
  44. package/dist/git.d.ts.map +1 -0
  45. package/dist/git.js +283 -0
  46. package/dist/git.js.map +1 -0
  47. package/dist/imageManager.d.ts +161 -0
  48. package/dist/imageManager.d.ts.map +1 -0
  49. package/dist/imageManager.js +674 -0
  50. package/dist/imageManager.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +437 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/input-visual-test.d.ts +9 -0
  56. package/dist/input-visual-test.d.ts.map +1 -0
  57. package/dist/input-visual-test.js +108 -0
  58. package/dist/input-visual-test.js.map +1 -0
  59. package/dist/inputBox.d.ts +228 -0
  60. package/dist/inputBox.d.ts.map +1 -0
  61. package/dist/inputBox.js +966 -0
  62. package/dist/inputBox.js.map +1 -0
  63. package/dist/logger.d.ts +136 -0
  64. package/dist/logger.d.ts.map +1 -0
  65. package/dist/logger.js +347 -0
  66. package/dist/logger.js.map +1 -0
  67. package/dist/orchestrator.d.ts +149 -0
  68. package/dist/orchestrator.d.ts.map +1 -0
  69. package/dist/orchestrator.js +821 -0
  70. package/dist/orchestrator.js.map +1 -0
  71. package/dist/planner.d.ts +86 -0
  72. package/dist/planner.d.ts.map +1 -0
  73. package/dist/planner.js +830 -0
  74. package/dist/planner.js.map +1 -0
  75. package/dist/pty-test-runner.d.ts +87 -0
  76. package/dist/pty-test-runner.d.ts.map +1 -0
  77. package/dist/pty-test-runner.js +721 -0
  78. package/dist/pty-test-runner.js.map +1 -0
  79. package/dist/screen.d.ts +44 -0
  80. package/dist/screen.d.ts.map +1 -0
  81. package/dist/screen.js +152 -0
  82. package/dist/screen.js.map +1 -0
  83. package/dist/taskQueue.d.ts +70 -0
  84. package/dist/taskQueue.d.ts.map +1 -0
  85. package/dist/taskQueue.js +282 -0
  86. package/dist/taskQueue.js.map +1 -0
  87. package/dist/tui-test-harness.d.ts +216 -0
  88. package/dist/tui-test-harness.d.ts.map +1 -0
  89. package/dist/tui-test-harness.js +527 -0
  90. package/dist/tui-test-harness.js.map +1 -0
  91. package/dist/types.d.ts +257 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +46 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/ui-visual-test.d.ts +15 -0
  96. package/dist/ui-visual-test.d.ts.map +1 -0
  97. package/dist/ui-visual-test.js +141 -0
  98. package/dist/ui-visual-test.js.map +1 -0
  99. package/dist/ui.d.ts +272 -0
  100. package/dist/ui.d.ts.map +1 -0
  101. package/dist/ui.js +1531 -0
  102. package/dist/ui.js.map +1 -0
  103. package/dist/validator.d.ts +53 -0
  104. package/dist/validator.d.ts.map +1 -0
  105. package/dist/validator.js +491 -0
  106. package/dist/validator.js.map +1 -0
  107. package/dist/versionCheck.d.ts +63 -0
  108. package/dist/versionCheck.d.ts.map +1 -0
  109. package/dist/versionCheck.js +261 -0
  110. package/dist/versionCheck.js.map +1 -0
  111. package/package.json +62 -0
@@ -0,0 +1,830 @@
1
+ import { execa } from 'execa';
2
+ import { createTask, DEFAULT_CONFIG } from './types.js';
3
+ import { extractImagePlaceholders } from './imageManager.js';
4
+ import { getClaudePath } from './claudePath.js';
5
+ /**
6
+ * Call Claude Code CLI with a prompt and return the response
7
+ * Supports optional streaming output via onOutput callback
8
+ */
9
+ async function callClaudeCode(prompt, cwd, onOutput, abortSignal) {
10
+ // Exclude ANTHROPIC_API_KEY to use subscription auth
11
+ const { ANTHROPIC_API_KEY: _excluded, ...cleanEnv } = process.env;
12
+ try {
13
+ // Use streaming JSON output if onOutput callback provided
14
+ // Note: --output-format stream-json requires --verbose when used with --print
15
+ const args = onOutput
16
+ ? ['--print', '--verbose', '--output-format', 'stream-json', '-p', prompt]
17
+ : ['--print', '-p', prompt];
18
+ const subprocess = execa(getClaudePath(), args, {
19
+ cwd: cwd || process.cwd(),
20
+ timeout: 600000, // 10 minutes for planning (long prompts may need time)
21
+ reject: false,
22
+ stdin: 'ignore',
23
+ stdout: 'pipe',
24
+ stderr: 'pipe',
25
+ buffer: !onOutput, // Disable buffering for streaming
26
+ env: cleanEnv,
27
+ cancelSignal: abortSignal,
28
+ });
29
+ let stdout = '';
30
+ let stderr = '';
31
+ let lineBuffer = '';
32
+ // Stream stdout if callback provided
33
+ if (onOutput && subprocess.stdout) {
34
+ subprocess.stdout.on('data', (chunk) => {
35
+ const text = chunk.toString();
36
+ stdout += text;
37
+ // Process line by line
38
+ lineBuffer += text;
39
+ const lines = lineBuffer.split('\n');
40
+ lineBuffer = lines.pop() || '';
41
+ for (const line of lines) {
42
+ if (!line.trim())
43
+ continue;
44
+ try {
45
+ const parsed = JSON.parse(line);
46
+ // Handle stream-json format - extract text content
47
+ if (parsed.type === 'assistant' && parsed.message?.content) {
48
+ for (const block of parsed.message.content) {
49
+ if (block.type === 'text' && block.text) {
50
+ onOutput(block.text);
51
+ }
52
+ }
53
+ }
54
+ else if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
55
+ onOutput(parsed.delta.text);
56
+ }
57
+ }
58
+ catch {
59
+ // Not JSON, output raw line
60
+ onOutput(line);
61
+ }
62
+ }
63
+ });
64
+ }
65
+ if (subprocess.stderr) {
66
+ subprocess.stderr.on('data', (chunk) => {
67
+ stderr += chunk.toString();
68
+ });
69
+ }
70
+ const result = await subprocess;
71
+ // If not streaming, get stdout from result
72
+ if (!onOutput) {
73
+ stdout = typeof result.stdout === 'string' ? result.stdout : '';
74
+ stderr = typeof result.stderr === 'string' ? result.stderr : '';
75
+ }
76
+ if (result.exitCode !== 0) {
77
+ // Provide user-friendly error messages for common exit codes
78
+ let errorMsg = stderr || `Claude Code exited with code ${result.exitCode}`;
79
+ if (result.exitCode === 143) {
80
+ // SIGTERM (128 + 15) - process was terminated
81
+ // Check if this was an intentional abort via the abort signal
82
+ if (abortSignal?.aborted) {
83
+ return {
84
+ success: false,
85
+ response: '',
86
+ error: 'Request cancelled',
87
+ aborted: true,
88
+ };
89
+ }
90
+ // Otherwise it was a timeout or external termination
91
+ errorMsg = 'Request timed out or was interrupted. Try breaking your request into smaller parts.';
92
+ }
93
+ else if (result.exitCode === 137) {
94
+ // SIGKILL (128 + 9) - process was killed, possibly OOM
95
+ errorMsg = 'Process was killed (possibly out of memory). Try a simpler request.';
96
+ }
97
+ else if (result.exitCode === 1 && stderr.includes('rate limit')) {
98
+ errorMsg = 'Rate limit reached. Please wait a moment and try again.';
99
+ }
100
+ return {
101
+ success: false,
102
+ response: '',
103
+ error: errorMsg,
104
+ };
105
+ }
106
+ // For streaming, extract the final result from the accumulated JSON
107
+ let finalResponse = stdout.trim();
108
+ if (onOutput) {
109
+ // Parse the accumulated output to get the final text
110
+ const textParts = [];
111
+ for (const line of stdout.split('\n')) {
112
+ if (!line.trim())
113
+ continue;
114
+ try {
115
+ const parsed = JSON.parse(line);
116
+ if (parsed.type === 'result' && parsed.result) {
117
+ finalResponse = parsed.result;
118
+ break;
119
+ }
120
+ }
121
+ catch {
122
+ // Ignore parse errors
123
+ }
124
+ }
125
+ if (!finalResponse || finalResponse === stdout.trim()) {
126
+ // Fallback: try to extract text from content blocks
127
+ for (const line of stdout.split('\n')) {
128
+ if (!line.trim())
129
+ continue;
130
+ try {
131
+ const parsed = JSON.parse(line);
132
+ if (parsed.type === 'assistant' && parsed.message?.content) {
133
+ for (const block of parsed.message.content) {
134
+ if (block.type === 'text' && block.text) {
135
+ textParts.push(block.text);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ catch {
141
+ // Ignore
142
+ }
143
+ }
144
+ if (textParts.length > 0) {
145
+ finalResponse = textParts.join('');
146
+ }
147
+ }
148
+ }
149
+ return {
150
+ success: true,
151
+ response: finalResponse,
152
+ };
153
+ }
154
+ catch (error) {
155
+ // Check if this was an abort
156
+ const isAborted = error instanceof Error && (error.name === 'AbortError' ||
157
+ error.message.includes('aborted') ||
158
+ error.message.includes('canceled') ||
159
+ error.code === 'ERR_CANCELED');
160
+ if (isAborted) {
161
+ return {
162
+ success: false,
163
+ response: '',
164
+ error: 'Request cancelled',
165
+ aborted: true,
166
+ };
167
+ }
168
+ return {
169
+ success: false,
170
+ response: '',
171
+ error: error instanceof Error ? error.message : String(error),
172
+ };
173
+ }
174
+ }
175
+ 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).
176
+
177
+ Guidelines for task decomposition:
178
+ 1. Each task should be completable in a single coding session (10-30 minutes)
179
+ 2. Tasks should be ordered by dependency (tasks that depend on others come later)
180
+ 3. Each task should have a clear, specific objective
181
+ 4. Include setup tasks if needed (e.g., "Initialize project structure")
182
+ 5. Include testing tasks where appropriate
183
+ 6. Be specific about what files or components are involved
184
+ 7. Consider which tasks are critical vs optional - critical tasks should stop execution if they fail
185
+ 8. IMPORTANT: Each task MUST have clear, verifiable acceptance criteria
186
+
187
+ ## Acceptance Criteria Guidelines - CRITICAL
188
+
189
+ Generate OUTCOME-BASED criteria, not structural criteria. Focus on WHAT should work, not HOW it should be implemented.
190
+
191
+ GOOD (outcome-based, flexible):
192
+ - "App can persist data locally and retrieve it after restart"
193
+ - "User can view entries filtered by date range"
194
+ - "Form validates required fields and shows error messages"
195
+ - "API endpoint returns user data in JSON format"
196
+ - "Project builds without errors"
197
+
198
+ AVOID (structural, fragile):
199
+ - "DataPersistenceService.swift exists in Services/ folder"
200
+ - "Uses @Model macro for SwiftData"
201
+ - "fetchByDateRange() method accepts two Date parameters"
202
+ - "modelContainer property exists on the service class"
203
+
204
+ WHY: Implementation details may change during development as the AI finds better approaches.
205
+ Outcome-based criteria allow flexibility while ensuring the desired functionality works.
206
+
207
+ 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).
208
+
209
+ For each task, provide:
210
+ - id: A unique identifier (e.g., "task-1", "task-2")
211
+ - title: A short, descriptive title
212
+ - description: Detailed instructions for what needs to be done
213
+ - acceptanceCriteria: Array of 2-4 short, specific, OUTCOME-BASED conditions. Each criterion should:
214
+ - Focus on observable behavior or testable functionality
215
+ - Be verifiable by running the app or tests (not by inspecting code structure)
216
+ - Avoid specifying exact file names, class names, or method signatures
217
+ - dependencies: Array of task IDs that must complete before this task (empty for first tasks)
218
+ - contextPatterns: Glob patterns for files relevant to this task (e.g., ["src/**/*.ts", "package.json"])
219
+ - ifFailed: What to do if this task fails after retries. IMPORTANT: Default is "skip"
220
+ - "skip" (DEFAULT - use this unless there's a specific reason not to): Skip this task and continue with others
221
+ - "stop": Stop execution entirely - ONLY use for truly critical tasks where nothing else can proceed
222
+ - "retry": Keep retrying indefinitely (use sparingly)
223
+
224
+ Respond with a JSON array of tasks. Example format:
225
+ [
226
+ {
227
+ "id": "task-1",
228
+ "title": "Initialize project structure",
229
+ "description": "Create the basic project structure with package.json, tsconfig.json, and src directory",
230
+ "acceptanceCriteria": [
231
+ "Project can be installed with npm install",
232
+ "TypeScript compilation succeeds with strict mode",
233
+ "Project structure follows standard conventions"
234
+ ],
235
+ "dependencies": [],
236
+ "contextPatterns": ["package.json", "tsconfig.json"],
237
+ "ifFailed": "skip"
238
+ },
239
+ {
240
+ "id": "task-2",
241
+ "title": "Implement user model",
242
+ "description": "Create a User type and model with fields for id, name, email. The model should be exportable for use in other modules.",
243
+ "acceptanceCriteria": [
244
+ "User type can be imported from the models module",
245
+ "User objects can be created with id, name, and email fields",
246
+ "TypeScript provides proper type checking for User objects"
247
+ ],
248
+ "dependencies": ["task-1"],
249
+ "contextPatterns": ["src/models/**/*.ts"],
250
+ "ifFailed": "skip"
251
+ }
252
+ ]
253
+
254
+ IMPORTANT: Return ONLY the JSON array, no other text or markdown formatting.`;
255
+ const DISCOVERY_PROMPT = `You are helping plan a software project. Analyze the user's goal and generate clarifying questions to understand their requirements better.
256
+
257
+ Your task is to:
258
+ 1. Briefly summarize what you understand the user wants to build
259
+ 2. Generate 2-4 targeted questions about key decisions that will affect the implementation
260
+ 3. List assumptions you're making that might need confirmation
261
+
262
+ Focus questions on:
263
+ - Technology preferences (frameworks, languages, databases)
264
+ - Scope clarification (MVP features vs full feature set)
265
+ - Architecture decisions (monolith vs services, deployment target)
266
+ - Integration requirements (external APIs, existing systems)
267
+
268
+ Return your response as JSON in this exact format:
269
+ {
270
+ "summary": "Brief description of what the user wants to build",
271
+ "questions": [
272
+ {
273
+ "id": "tech-stack",
274
+ "question": "What tech stack would you prefer?",
275
+ "type": "choice",
276
+ "options": ["Option 1 (Recommended)", "Option 2", "Option 3", "Other"],
277
+ "default": "Option 1 (Recommended)",
278
+ "reason": "Why this question matters for the project"
279
+ }
280
+ ],
281
+ "assumptions": [
282
+ "Assumption 1 that can be confirmed or corrected",
283
+ "Assumption 2"
284
+ ]
285
+ }
286
+
287
+ Question types:
288
+ - "choice": Single selection from options (most common)
289
+ - "text": Free-form text input
290
+ - "multi-choice": Multiple selections allowed
291
+
292
+ Guidelines:
293
+ - Keep questions actionable and relevant to implementation
294
+ - Provide sensible defaults where possible
295
+ - Mark recommended options with "(Recommended)"
296
+ - Limit to 2-4 questions to avoid fatigue
297
+ - Keep the summary concise (1-2 sentences)
298
+
299
+ IMPORTANT: Return ONLY the JSON object, no other text or markdown formatting.`;
300
+ /**
301
+ * Analyze a project goal and generate discovery questions
302
+ */
303
+ export async function discoverRequirements(goal, options = {}) {
304
+ const fullPrompt = `${DISCOVERY_PROMPT}\n\nProject Goal: ${goal}`;
305
+ const result = await callClaudeCode(fullPrompt, options.cwd, options.onOutput);
306
+ if (!result.success) {
307
+ return {
308
+ success: false,
309
+ summary: '',
310
+ questions: [],
311
+ assumptions: [],
312
+ error: result.error || 'Claude Code CLI failed',
313
+ };
314
+ }
315
+ const rawResponse = result.response;
316
+ // Parse JSON response
317
+ try {
318
+ // Try to extract JSON object from the response
319
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
320
+ if (!jsonMatch) {
321
+ return {
322
+ success: false,
323
+ summary: '',
324
+ questions: [],
325
+ assumptions: [],
326
+ error: 'No JSON object found in response',
327
+ rawResponse,
328
+ };
329
+ }
330
+ const parsed = JSON.parse(jsonMatch[0]);
331
+ // Validate required fields
332
+ if (!parsed.summary || !Array.isArray(parsed.questions)) {
333
+ return {
334
+ success: false,
335
+ summary: '',
336
+ questions: [],
337
+ assumptions: [],
338
+ error: 'Invalid response format: missing summary or questions',
339
+ rawResponse,
340
+ };
341
+ }
342
+ // Validate and normalize questions
343
+ const questions = parsed.questions.map((q, index) => ({
344
+ id: q.id || `question-${index + 1}`,
345
+ question: q.question || 'Unknown question',
346
+ type: (q.type === 'text' || q.type === 'multi-choice') ? q.type : 'choice',
347
+ options: q.options || [],
348
+ default: q.default,
349
+ reason: q.reason,
350
+ }));
351
+ return {
352
+ success: true,
353
+ summary: parsed.summary,
354
+ questions,
355
+ assumptions: Array.isArray(parsed.assumptions) ? parsed.assumptions : [],
356
+ rawResponse,
357
+ };
358
+ }
359
+ catch (parseError) {
360
+ return {
361
+ success: false,
362
+ summary: '',
363
+ questions: [],
364
+ assumptions: [],
365
+ error: `Failed to parse JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
366
+ rawResponse,
367
+ };
368
+ }
369
+ }
370
+ /**
371
+ * Build project context from discovery results for decomposition
372
+ */
373
+ export function buildProjectContext(goal, discoveryResult) {
374
+ return {
375
+ goal,
376
+ summary: discoveryResult.summary,
377
+ preferences: discoveryResult.answers,
378
+ assumptions: discoveryResult.assumptions,
379
+ };
380
+ }
381
+ /**
382
+ * Format project context as a string for the decomposition prompt
383
+ */
384
+ export function formatContextForDecomposition(context) {
385
+ const parts = [];
386
+ if (context.summary) {
387
+ parts.push(`Project Summary: ${context.summary}`);
388
+ }
389
+ if (Object.keys(context.preferences).length > 0) {
390
+ parts.push('\nUser Preferences:');
391
+ for (const [key, value] of Object.entries(context.preferences)) {
392
+ parts.push(`- ${key}: ${value}`);
393
+ }
394
+ }
395
+ if (context.assumptions.length > 0) {
396
+ parts.push('\nConfirmed Assumptions:');
397
+ for (const assumption of context.assumptions) {
398
+ parts.push(`- ${assumption}`);
399
+ }
400
+ }
401
+ return parts.join('\n');
402
+ }
403
+ /** Reserved ID for the finalize task */
404
+ export const FINALIZE_TASK_ID = 'task-finalize';
405
+ /**
406
+ * Create a finalize task that depends on all existing tasks
407
+ * This task runs last and updates CLAUDE.md with project knowledge
408
+ */
409
+ function createFinalizeTask(existingTasks) {
410
+ const allTaskIds = existingTasks.map(t => t.id);
411
+ return createTask({
412
+ id: FINALIZE_TASK_ID,
413
+ title: 'Finalize project and update documentation',
414
+ description: `Complete the project with cleanup and documentation:
415
+
416
+ 1. **Cleanup Tasks:**
417
+ - Remove any temporary files, debug logs, or test artifacts created during development
418
+ - Clean up any commented-out code or TODO markers that are no longer relevant
419
+ - Ensure no sensitive data (API keys, credentials) is left in the codebase
420
+
421
+ 2. **Update CLAUDE.md Documentation:**
422
+ Read the existing CLAUDE.md (if present) and update it with:
423
+ - Project overview and purpose
424
+ - Architecture summary (key modules, their responsibilities, and how they interact)
425
+ - Build and development commands (install, build, test, run)
426
+ - Key design patterns and conventions used in this codebase
427
+ - Important file locations and their purposes
428
+ - Any gotchas or non-obvious behavior future developers should know
429
+
430
+ If CLAUDE.md does not exist, create it with the above sections.
431
+
432
+ 3. **Verification:**
433
+ - Ensure the project builds successfully
434
+ - Ensure tests pass (if applicable)
435
+ - Review that documentation accurately reflects the implemented code`,
436
+ dependencies: allTaskIds,
437
+ contextPatterns: ['CLAUDE.md', 'README.md', 'package.json', 'tsconfig.json', 'src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js', '*.config.js', '*.config.ts'],
438
+ ifFailed: 'skip', // Non-critical - don't block if this fails
439
+ maxRetries: 2, // Fewer retries since it's a documentation task
440
+ });
441
+ }
442
+ /**
443
+ * Decompose a project goal into tasks using Claude Code CLI
444
+ */
445
+ export async function decompose(goal, projectContext = '', options = {}) {
446
+ const userMessage = projectContext
447
+ ? `Project Goal: ${goal}\n\nExisting Project Context:\n${projectContext}`
448
+ : `Project Goal: ${goal}`;
449
+ const fullPrompt = `${DECOMPOSITION_PROMPT}\n\n${userMessage}`;
450
+ const result = await callClaudeCode(fullPrompt, options.cwd);
451
+ if (!result.success) {
452
+ return {
453
+ success: false,
454
+ tasks: [],
455
+ error: result.error || 'Claude Code CLI failed',
456
+ };
457
+ }
458
+ const rawResponse = result.response;
459
+ // Parse JSON response
460
+ let parsedTasks;
461
+ try {
462
+ // Try to extract JSON from the response (in case there's extra text)
463
+ const jsonMatch = rawResponse.match(/\[[\s\S]*\]/);
464
+ if (!jsonMatch) {
465
+ return {
466
+ success: false,
467
+ tasks: [],
468
+ error: 'No JSON array found in response',
469
+ rawResponse,
470
+ };
471
+ }
472
+ parsedTasks = JSON.parse(jsonMatch[0]);
473
+ }
474
+ catch (parseError) {
475
+ return {
476
+ success: false,
477
+ tasks: [],
478
+ error: `Failed to parse JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
479
+ rawResponse,
480
+ };
481
+ }
482
+ // Validate and convert to Task objects
483
+ if (!Array.isArray(parsedTasks)) {
484
+ return {
485
+ success: false,
486
+ tasks: [],
487
+ error: 'Response is not an array',
488
+ rawResponse,
489
+ };
490
+ }
491
+ const tasks = parsedTasks.map(pt => {
492
+ // Validate and normalize ifFailed value
493
+ const validFailureActions = ['retry', 'skip', 'stop'];
494
+ const ifFailed = validFailureActions.includes(pt.ifFailed)
495
+ ? pt.ifFailed
496
+ : DEFAULT_CONFIG.defaultIfFailed;
497
+ const description = pt.description || '';
498
+ // Extract image references from description
499
+ const imageRefs = extractImagePlaceholders(description);
500
+ return createTask({
501
+ id: pt.id || `task-${Math.random().toString(36).slice(2, 8)}`,
502
+ title: pt.title || 'Untitled Task',
503
+ description,
504
+ acceptanceCriteria: pt.acceptanceCriteria,
505
+ dependencies: pt.dependencies || [],
506
+ contextPatterns: pt.contextPatterns || [],
507
+ testCommand: pt.testCommand,
508
+ ifFailed,
509
+ timeout: pt.timeout ?? DEFAULT_CONFIG.executionTimeout,
510
+ maxRetries: pt.maxRetries ?? DEFAULT_CONFIG.maxRetries,
511
+ imageRefs: imageRefs.length > 0 ? imageRefs : undefined,
512
+ });
513
+ });
514
+ // Append finalize task if there are tasks to finalize
515
+ if (tasks.length > 0) {
516
+ tasks.push(createFinalizeTask(tasks));
517
+ }
518
+ return {
519
+ success: true,
520
+ tasks,
521
+ rawResponse,
522
+ };
523
+ }
524
+ /**
525
+ * Validate that task dependencies form a valid DAG (no cycles)
526
+ */
527
+ export function validateTaskDependencies(tasks) {
528
+ const taskIds = new Set(tasks.map(t => t.id));
529
+ // Check all dependencies reference existing tasks
530
+ for (const task of tasks) {
531
+ for (const depId of task.dependencies) {
532
+ if (!taskIds.has(depId)) {
533
+ return {
534
+ valid: false,
535
+ error: `Task "${task.id}" depends on non-existent task "${depId}"`,
536
+ };
537
+ }
538
+ }
539
+ }
540
+ // Check for cycles using DFS
541
+ const visited = new Set();
542
+ const recursionStack = new Set();
543
+ function hasCycle(taskId) {
544
+ visited.add(taskId);
545
+ recursionStack.add(taskId);
546
+ const task = tasks.find(t => t.id === taskId);
547
+ if (task) {
548
+ for (const depId of task.dependencies) {
549
+ if (!visited.has(depId)) {
550
+ if (hasCycle(depId)) {
551
+ return true;
552
+ }
553
+ }
554
+ else if (recursionStack.has(depId)) {
555
+ return true;
556
+ }
557
+ }
558
+ }
559
+ recursionStack.delete(taskId);
560
+ return false;
561
+ }
562
+ for (const task of tasks) {
563
+ if (!visited.has(task.id)) {
564
+ if (hasCycle(task.id)) {
565
+ return {
566
+ valid: false,
567
+ error: 'Circular dependency detected in tasks',
568
+ };
569
+ }
570
+ }
571
+ }
572
+ return { valid: true };
573
+ }
574
+ /**
575
+ * Sort tasks topologically based on dependencies
576
+ */
577
+ export function sortTasksByDependency(tasks) {
578
+ const sorted = [];
579
+ const visited = new Set();
580
+ const taskMap = new Map(tasks.map(t => [t.id, t]));
581
+ function visit(taskId) {
582
+ if (visited.has(taskId))
583
+ return;
584
+ visited.add(taskId);
585
+ const task = taskMap.get(taskId);
586
+ if (task) {
587
+ for (const depId of task.dependencies) {
588
+ visit(depId);
589
+ }
590
+ sorted.push(task);
591
+ }
592
+ }
593
+ for (const task of tasks) {
594
+ visit(task.id);
595
+ }
596
+ return sorted;
597
+ }
598
+ const PLAN_REFINEMENT_PROMPT = `You are a PLANNING assistant modifying a task plan. This is PLANNING MODE - you must NEVER execute actions.
599
+
600
+ CRITICAL: When the user says things like "run tests", "add feature X", "fix bug Y", or ANY action-sounding request, you must:
601
+ 1. Create a TASK that describes this work
602
+ 2. Return it as JSON so it can be ADDED TO THE PLAN
603
+ 3. NEVER actually execute the action yourself
604
+
605
+ DO NOT:
606
+ - Run any commands or tools
607
+ - Read files or explore code
608
+ - Execute tests or builds
609
+ - Make any changes to the codebase
610
+
611
+ ONLY:
612
+ - Return new/modified tasks as JSON
613
+ - Discuss the plan with the user
614
+
615
+ Project: {PROJECT_NAME}
616
+ Goal summary: {GOAL_SUMMARY}
617
+ Progress: {COMPLETED_COUNT} completed, {PENDING_COUNT} pending
618
+
619
+ {COMPLETED_SECTION}
620
+ {PENDING_SECTION}
621
+
622
+ RULES:
623
+ - Return ONLY the tasks you're adding/modifying as a JSON array in a code block
624
+ - Use existing task IDs when modifying, new IDs (e.g., "task-{NEXT_ID}") when adding
625
+ - NEVER return completed tasks - they are preserved automatically
626
+ - Omit "status" field - system handles it
627
+ - Keep responses brief
628
+
629
+ Example: If user says "run ui tests", return:
630
+ \`\`\`json
631
+ [{"id": "task-{NEXT_ID}", "title": "Run UI tests", "description": "Execute the UI test suite to verify functionality"}]
632
+ \`\`\``;
633
+ /**
634
+ * Refine a plan through conversation with the user
635
+ */
636
+ export async function refinePlan(goal, currentTasks, userMessage, conversationHistory, options = {}) {
637
+ // Separate completed and pending tasks for compact context
638
+ const completedTasks = currentTasks.filter(t => t.status === 'completed' || t.status === 'skipped');
639
+ const pendingTasks = currentTasks.filter(t => t.status === 'pending' || t.status === 'in_progress' || t.status === 'failed');
640
+ // Calculate next task ID for the prompt
641
+ const maxTaskNum = currentTasks.reduce((max, t) => {
642
+ const match = t.id.match(/task-(\d+)/);
643
+ return match ? Math.max(max, parseInt(match[1], 10)) : max;
644
+ }, 0);
645
+ const nextId = maxTaskNum + 1;
646
+ // Compact format for completed tasks (just id and title)
647
+ const completedSection = completedTasks.length > 0
648
+ ? `Completed tasks (${completedTasks.length}):\n${completedTasks.map(t => `- ${t.id}: ${t.title}`).join('\n')}`
649
+ : '';
650
+ // Full format for pending tasks (they may need modification)
651
+ const pendingSection = pendingTasks.length > 0
652
+ ? `Pending tasks:\n\`\`\`json\n${JSON.stringify(pendingTasks.map(t => ({
653
+ id: t.id,
654
+ title: t.title,
655
+ description: t.description,
656
+ acceptanceCriteria: t.acceptanceCriteria,
657
+ dependencies: t.dependencies,
658
+ status: t.status,
659
+ })), null, 2)}\n\`\`\``
660
+ : 'No pending tasks.';
661
+ // Truncate goal if too long (keep first 500 chars as summary)
662
+ const goalSummary = goal.length > 500
663
+ ? goal.slice(0, 500) + '...'
664
+ : goal;
665
+ // Extract project name from goal (first line or first sentence)
666
+ const projectName = goal.split(/[.\n]/)[0].slice(0, 100);
667
+ const systemPrompt = PLAN_REFINEMENT_PROMPT
668
+ .replace('{PROJECT_NAME}', projectName)
669
+ .replace('{GOAL_SUMMARY}', goalSummary)
670
+ .replace('{COMPLETED_COUNT}', String(completedTasks.length))
671
+ .replace('{PENDING_COUNT}', String(pendingTasks.length))
672
+ .replace('{COMPLETED_SECTION}', completedSection)
673
+ .replace('{PENDING_SECTION}', pendingSection)
674
+ .replace('{NEXT_ID}', String(nextId));
675
+ // Build conversation context for CLI (only last few exchanges to save tokens)
676
+ const recentHistory = conversationHistory.slice(-4); // Keep last 2 exchanges
677
+ const historyContext = recentHistory.length > 0
678
+ ? '\n\nRecent conversation:\n' + recentHistory.map(m => `${m.role}: ${m.content}`).join('\n')
679
+ : '';
680
+ const fullPrompt = `${systemPrompt}${historyContext}\n\nUser: ${userMessage}`;
681
+ const result = await callClaudeCode(fullPrompt, options.cwd, options.onOutput, options.abortSignal);
682
+ if (!result.success) {
683
+ return {
684
+ success: false,
685
+ response: '',
686
+ error: result.error || 'Claude Code CLI failed',
687
+ aborted: result.aborted,
688
+ };
689
+ }
690
+ const responseText = result.response;
691
+ // Check if the response contains a new task list
692
+ const jsonMatch = responseText.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
693
+ if (jsonMatch) {
694
+ try {
695
+ const parsedTasks = JSON.parse(jsonMatch[1]);
696
+ // Create a map of existing tasks for merging
697
+ const existingTaskMap = new Map(currentTasks.map(t => [t.id, t]));
698
+ const modifiedTaskIds = new Set();
699
+ // Process returned tasks (new or modified only)
700
+ const processedTasks = parsedTasks.map((pt) => {
701
+ const taskId = pt.id || `task-${Math.random().toString(36).slice(2, 8)}`;
702
+ modifiedTaskIds.add(taskId);
703
+ const existingTask = existingTaskMap.get(taskId);
704
+ // Validate and normalize ifFailed value
705
+ const validFailureActions = ['retry', 'skip', 'stop'];
706
+ const ifFailed = validFailureActions.includes(pt.ifFailed)
707
+ ? pt.ifFailed
708
+ : (existingTask?.ifFailed ?? DEFAULT_CONFIG.defaultIfFailed);
709
+ const description = pt.description || existingTask?.description || '';
710
+ // Extract image references from description
711
+ const imageRefs = extractImagePlaceholders(description);
712
+ // If task already exists, update it
713
+ if (existingTask) {
714
+ // Allow status change to 'pending' for retry (clears error state)
715
+ const shouldResetStatus = pt.status === 'pending' && existingTask.status !== 'pending';
716
+ const updatedTask = {
717
+ ...existingTask,
718
+ title: pt.title || existingTask.title,
719
+ description,
720
+ // Use ?? instead of || so empty arrays are preserved (not treated as falsy)
721
+ acceptanceCriteria: pt.acceptanceCriteria ?? existingTask.acceptanceCriteria,
722
+ dependencies: pt.dependencies ?? existingTask.dependencies,
723
+ contextPatterns: pt.contextPatterns ?? existingTask.contextPatterns,
724
+ testCommand: pt.testCommand ?? existingTask.testCommand,
725
+ ifFailed,
726
+ timeout: pt.timeout ?? existingTask.timeout,
727
+ maxRetries: pt.maxRetries ?? existingTask.maxRetries,
728
+ imageRefs: imageRefs.length > 0 ? imageRefs : existingTask.imageRefs,
729
+ updatedAt: new Date().toISOString(),
730
+ };
731
+ // Reset task to pending if Claude explicitly requested it
732
+ if (shouldResetStatus) {
733
+ updatedTask.status = 'pending';
734
+ updatedTask.retryCount = 0;
735
+ updatedTask.error = undefined;
736
+ updatedTask.failureType = undefined;
737
+ }
738
+ return updatedTask;
739
+ }
740
+ // New task - create with default status
741
+ return createTask({
742
+ id: taskId,
743
+ title: pt.title || 'Untitled Task',
744
+ description,
745
+ acceptanceCriteria: pt.acceptanceCriteria,
746
+ dependencies: pt.dependencies || [],
747
+ contextPatterns: pt.contextPatterns || [],
748
+ testCommand: pt.testCommand,
749
+ ifFailed,
750
+ timeout: pt.timeout ?? DEFAULT_CONFIG.executionTimeout,
751
+ maxRetries: pt.maxRetries ?? DEFAULT_CONFIG.maxRetries,
752
+ imageRefs: imageRefs.length > 0 ? imageRefs : undefined,
753
+ });
754
+ });
755
+ // Merge: keep all existing tasks that weren't modified, then add/update modified ones
756
+ const unchangedTasks = currentTasks.filter(t => !modifiedTaskIds.has(t.id) && t.id !== FINALIZE_TASK_ID);
757
+ const tasks = [...unchangedTasks, ...processedTasks];
758
+ // Handle finalize task: ensure it exists and has correct dependencies
759
+ const nonFinalizeTasks = tasks.filter(t => t.id !== FINALIZE_TASK_ID);
760
+ const existingFinalizeTask = currentTasks.find(t => t.id === FINALIZE_TASK_ID);
761
+ if (nonFinalizeTasks.length > 0) {
762
+ if (existingFinalizeTask) {
763
+ // Update finalize task dependencies to include all other tasks
764
+ const updatedFinalizeTask = {
765
+ ...existingFinalizeTask,
766
+ dependencies: nonFinalizeTasks.map(t => t.id),
767
+ status: 'pending',
768
+ updatedAt: new Date().toISOString(),
769
+ };
770
+ tasks.push(updatedFinalizeTask);
771
+ }
772
+ else {
773
+ // Re-add finalize task if it was removed
774
+ tasks.push(createFinalizeTask(nonFinalizeTasks));
775
+ }
776
+ }
777
+ return {
778
+ success: true,
779
+ response: responseText,
780
+ updatedTasks: tasks,
781
+ };
782
+ }
783
+ catch {
784
+ // JSON parsing failed, just return the response text
785
+ }
786
+ }
787
+ return {
788
+ success: true,
789
+ response: responseText,
790
+ };
791
+ }
792
+ const README_GENERATION_PROMPT = `You are a technical documentation assistant. Generate a README.md file for a new project.
793
+
794
+ The README should include:
795
+ 1. Project title and description
796
+ 2. High-level architecture overview
797
+ 3. Components/modules that will be built
798
+ 4. Technology stack (inferred from the goal and tasks)
799
+ 5. Getting started placeholder section
800
+
801
+ Use proper markdown formatting. Keep it concise but informative.
802
+
803
+ IMPORTANT: Return ONLY the markdown content, no code blocks wrapping it.`;
804
+ /**
805
+ * Generate a README.md content based on the project goal and tasks
806
+ */
807
+ export async function generateReadme(projectName, goal, tasks, options = {}) {
808
+ const taskList = tasks.map((t, i) => `${i + 1}. ${t.title}: ${t.description}`).join('\n');
809
+ const userMessage = `Project Name: ${projectName}
810
+ Project Goal: ${goal}
811
+
812
+ Planned Tasks:
813
+ ${taskList}`;
814
+ const fullPrompt = `${README_GENERATION_PROMPT}\n\n${userMessage}`;
815
+ const result = await callClaudeCode(fullPrompt, options.cwd);
816
+ if (!result.success) {
817
+ return {
818
+ success: false,
819
+ content: '',
820
+ error: result.error || 'Claude Code CLI failed',
821
+ };
822
+ }
823
+ return {
824
+ success: true,
825
+ content: result.response,
826
+ };
827
+ }
828
+ // Re-export dependency detection for use during discovery phase
829
+ export { detectDependencies } from './dependencyDetector.js';
830
+ //# sourceMappingURL=planner.js.map