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.
- package/LICENSE +15 -0
- package/README.md +107 -0
- package/dist/app.d.ts +247 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +4971 -0
- package/dist/app.js.map +1 -0
- package/dist/buildInfo.d.ts +5 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +2 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/caffeinate.d.ts +72 -0
- package/dist/caffeinate.d.ts.map +1 -0
- package/dist/caffeinate.js +258 -0
- package/dist/caffeinate.js.map +1 -0
- package/dist/claudePath.d.ts +10 -0
- package/dist/claudePath.d.ts.map +1 -0
- package/dist/claudePath.js +34 -0
- package/dist/claudePath.js.map +1 -0
- package/dist/clipboard.d.ts +44 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +442 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/config.d.ts +211 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +933 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -0
- package/dist/contextBuilder.d.ts +38 -0
- package/dist/contextBuilder.d.ts.map +1 -0
- package/dist/contextBuilder.js +113 -0
- package/dist/contextBuilder.js.map +1 -0
- package/dist/dependencyDetector.d.ts +57 -0
- package/dist/dependencyDetector.d.ts.map +1 -0
- package/dist/dependencyDetector.js +505 -0
- package/dist/dependencyDetector.js.map +1 -0
- package/dist/executor.d.ts +83 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +583 -0
- package/dist/executor.js.map +1 -0
- package/dist/git.d.ts +85 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +283 -0
- package/dist/git.js.map +1 -0
- package/dist/imageManager.d.ts +161 -0
- package/dist/imageManager.d.ts.map +1 -0
- package/dist/imageManager.js +674 -0
- package/dist/imageManager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +437 -0
- package/dist/index.js.map +1 -0
- package/dist/input-visual-test.d.ts +9 -0
- package/dist/input-visual-test.d.ts.map +1 -0
- package/dist/input-visual-test.js +108 -0
- package/dist/input-visual-test.js.map +1 -0
- package/dist/inputBox.d.ts +228 -0
- package/dist/inputBox.d.ts.map +1 -0
- package/dist/inputBox.js +966 -0
- package/dist/inputBox.js.map +1 -0
- package/dist/logger.d.ts +136 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/orchestrator.d.ts +149 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +821 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/planner.d.ts +86 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +830 -0
- package/dist/planner.js.map +1 -0
- package/dist/pty-test-runner.d.ts +87 -0
- package/dist/pty-test-runner.d.ts.map +1 -0
- package/dist/pty-test-runner.js +721 -0
- package/dist/pty-test-runner.js.map +1 -0
- package/dist/screen.d.ts +44 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +152 -0
- package/dist/screen.js.map +1 -0
- package/dist/taskQueue.d.ts +70 -0
- package/dist/taskQueue.d.ts.map +1 -0
- package/dist/taskQueue.js +282 -0
- package/dist/taskQueue.js.map +1 -0
- package/dist/tui-test-harness.d.ts +216 -0
- package/dist/tui-test-harness.d.ts.map +1 -0
- package/dist/tui-test-harness.js +527 -0
- package/dist/tui-test-harness.js.map +1 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-visual-test.d.ts +15 -0
- package/dist/ui-visual-test.d.ts.map +1 -0
- package/dist/ui-visual-test.js +141 -0
- package/dist/ui-visual-test.js.map +1 -0
- package/dist/ui.d.ts +272 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1531 -0
- package/dist/ui.js.map +1 -0
- package/dist/validator.d.ts +53 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +491 -0
- package/dist/validator.js.map +1 -0
- package/dist/versionCheck.d.ts +63 -0
- package/dist/versionCheck.d.ts.map +1 -0
- package/dist/versionCheck.js +261 -0
- package/dist/versionCheck.js.map +1 -0
- package/package.json +62 -0
package/dist/planner.js
ADDED
|
@@ -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
|