ralphctl 0.1.0 → 0.1.2
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/README.md +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { input, select } from '@inquirer/prompts';
|
|
4
|
-
import { editorInput } from '@src/utils/editor-input.ts';
|
|
5
|
-
import { error, muted } from '@src/theme/index.ts';
|
|
6
|
-
import {
|
|
7
|
-
createSpinner,
|
|
8
|
-
field,
|
|
9
|
-
icons,
|
|
10
|
-
log,
|
|
11
|
-
printHeader,
|
|
12
|
-
showError,
|
|
13
|
-
showInfo,
|
|
14
|
-
showNextStep,
|
|
15
|
-
showSuccess,
|
|
16
|
-
showTip,
|
|
17
|
-
showWarning,
|
|
18
|
-
terminalBell,
|
|
19
|
-
} from '@src/theme/ui.ts';
|
|
20
|
-
import { assertSprintStatus, getSprint, resolveSprintId, saveSprint } from '@src/store/sprint.ts';
|
|
21
|
-
import { addTicket } from '@src/store/ticket.ts';
|
|
22
|
-
import { getTasks, validateImportTasks } from '@src/store/task.ts';
|
|
23
|
-
import { getProject, listProjects } from '@src/store/project.ts';
|
|
24
|
-
import { fileExists } from '@src/utils/storage.ts';
|
|
25
|
-
import { getIdeateDir } from '@src/utils/paths.ts';
|
|
26
|
-
import { buildIdeateAutoPrompt, buildIdeatePrompt } from '@src/ai/prompts/index.ts';
|
|
27
|
-
import { spawnHeadless, spawnInteractive } from '@src/ai/session.ts';
|
|
28
|
-
import { IdeateOutputSchema, type Repository } from '@src/schemas/index.ts';
|
|
29
|
-
import { selectProjectPaths } from '@src/interactive/selectors.ts';
|
|
30
|
-
import { extractJsonObject } from '@src/utils/json-extract.ts';
|
|
31
|
-
import { resolveProvider, providerDisplayName } from '@src/utils/provider.ts';
|
|
32
|
-
import { getActiveProvider } from '@src/providers/index.ts';
|
|
33
|
-
import {
|
|
34
|
-
getTaskImportSchema,
|
|
35
|
-
importTasks,
|
|
36
|
-
parsePlanningBlocked,
|
|
37
|
-
parseTasksJson,
|
|
38
|
-
renderParsedTasksTable,
|
|
39
|
-
} from './plan-utils.ts';
|
|
40
|
-
|
|
41
|
-
interface IdeateOptions {
|
|
42
|
-
auto: boolean;
|
|
43
|
-
allPaths: boolean;
|
|
44
|
-
project?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseArgs(args: string[]): { sprintId?: string; options: IdeateOptions } {
|
|
48
|
-
const options: IdeateOptions = {
|
|
49
|
-
auto: false,
|
|
50
|
-
allPaths: false,
|
|
51
|
-
};
|
|
52
|
-
let sprintId: string | undefined;
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < args.length; i++) {
|
|
55
|
-
const arg = args[i];
|
|
56
|
-
const nextArg = args[i + 1];
|
|
57
|
-
|
|
58
|
-
if (arg === '--auto') {
|
|
59
|
-
options.auto = true;
|
|
60
|
-
} else if (arg === '--all-paths') {
|
|
61
|
-
options.allPaths = true;
|
|
62
|
-
} else if (arg === '--project') {
|
|
63
|
-
options.project = nextArg;
|
|
64
|
-
i++;
|
|
65
|
-
} else if (!arg?.startsWith('-')) {
|
|
66
|
-
sprintId = arg;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { sprintId, options };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function invokeAiInteractive(prompt: string, repoPaths: string[], ideateDir: string): Promise<void> {
|
|
74
|
-
// Write full context to the ideation directory for reference
|
|
75
|
-
const contextFile = join(ideateDir, 'ideate-context.md');
|
|
76
|
-
await writeFile(contextFile, prompt, 'utf-8');
|
|
77
|
-
|
|
78
|
-
const provider = await getActiveProvider();
|
|
79
|
-
|
|
80
|
-
// Build initial prompt that tells the AI to start the two-phase process
|
|
81
|
-
const startPrompt = `I have a quick idea I want to implement. The full context is in ideate-context.md. Please read that file and help me refine the idea into requirements and then plan implementation tasks.`;
|
|
82
|
-
|
|
83
|
-
// Build args - pass all repo paths in a single --add-dir
|
|
84
|
-
const args: string[] = ['--add-dir', ...repoPaths];
|
|
85
|
-
|
|
86
|
-
const result = spawnInteractive(
|
|
87
|
-
startPrompt,
|
|
88
|
-
{
|
|
89
|
-
cwd: ideateDir,
|
|
90
|
-
args,
|
|
91
|
-
env: provider.getSpawnEnv(),
|
|
92
|
-
},
|
|
93
|
-
provider
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (result.error) {
|
|
97
|
-
throw new Error(result.error);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function invokeAiAuto(prompt: string, repoPaths: string[], ideateDir: string): Promise<string> {
|
|
102
|
-
const provider = await getActiveProvider();
|
|
103
|
-
|
|
104
|
-
// Build args - all repo paths via --add-dir (neutral CWD in ideation dir)
|
|
105
|
-
const args: string[] = ['--permission-mode', 'plan', '--print'];
|
|
106
|
-
for (const path of repoPaths) {
|
|
107
|
-
args.push('--add-dir', path);
|
|
108
|
-
}
|
|
109
|
-
args.push('-p', prompt);
|
|
110
|
-
|
|
111
|
-
return spawnHeadless(
|
|
112
|
-
{
|
|
113
|
-
cwd: ideateDir,
|
|
114
|
-
args,
|
|
115
|
-
env: provider.getSpawnEnv(),
|
|
116
|
-
},
|
|
117
|
-
provider
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function parseIdeateOutput(output: string): { requirements: string; tasks: unknown[] } {
|
|
122
|
-
// Try to extract a balanced JSON object from the output
|
|
123
|
-
const jsonStr = extractJsonObject(output);
|
|
124
|
-
|
|
125
|
-
let parsed: unknown;
|
|
126
|
-
try {
|
|
127
|
-
parsed = JSON.parse(jsonStr);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`, { cause: err });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Validate against schema
|
|
133
|
-
const result = IdeateOutputSchema.safeParse(parsed);
|
|
134
|
-
if (!result.success) {
|
|
135
|
-
const issues = result.error.issues
|
|
136
|
-
.map((issue) => {
|
|
137
|
-
const path = issue.path.length > 0 ? `[${issue.path.join('.')}]` : '';
|
|
138
|
-
return ` ${path}: ${issue.message}`;
|
|
139
|
-
})
|
|
140
|
-
.join('\n');
|
|
141
|
-
throw new Error(`Invalid ideate output format:\n${issues}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return result.data;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function sprintIdeateCommand(args: string[]): Promise<void> {
|
|
148
|
-
const { sprintId, options } = parseArgs(args);
|
|
149
|
-
|
|
150
|
-
let id: string;
|
|
151
|
-
try {
|
|
152
|
-
id = await resolveSprintId(sprintId);
|
|
153
|
-
} catch {
|
|
154
|
-
showWarning('No sprint specified and no current sprint set.');
|
|
155
|
-
showNextStep('ralphctl sprint create', 'create a new sprint');
|
|
156
|
-
log.newline();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const sprint = await getSprint(id);
|
|
161
|
-
|
|
162
|
-
// Check sprint status - must be draft to ideate
|
|
163
|
-
try {
|
|
164
|
-
assertSprintStatus(sprint, ['draft'], 'ideate');
|
|
165
|
-
} catch (err) {
|
|
166
|
-
if (err instanceof Error) {
|
|
167
|
-
showError(err.message);
|
|
168
|
-
log.newline();
|
|
169
|
-
}
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Check if projects exist
|
|
174
|
-
const projects = await listProjects();
|
|
175
|
-
if (projects.length === 0) {
|
|
176
|
-
showWarning('No projects configured.');
|
|
177
|
-
showNextStep('ralphctl project add', 'add a project first');
|
|
178
|
-
log.newline();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
printHeader('Quick Ideation', icons.ticket);
|
|
183
|
-
console.log(field('Sprint', sprint.name));
|
|
184
|
-
console.log(field('ID', sprint.id));
|
|
185
|
-
console.log(field('Mode', options.auto ? 'Auto (headless)' : 'Interactive'));
|
|
186
|
-
log.newline();
|
|
187
|
-
|
|
188
|
-
// Prompt for project if not specified
|
|
189
|
-
let projectName = options.project;
|
|
190
|
-
if (!projectName) {
|
|
191
|
-
if (projects.length === 1) {
|
|
192
|
-
projectName = projects[0]?.name;
|
|
193
|
-
console.log(field('Project', projectName ?? '(unknown)'));
|
|
194
|
-
} else {
|
|
195
|
-
// Interactive project selection
|
|
196
|
-
projectName = await select({
|
|
197
|
-
message: 'Select project:',
|
|
198
|
-
choices: projects.map((p) => ({ name: p.displayName, value: p.name })),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!projectName) {
|
|
204
|
-
showError('No project selected.');
|
|
205
|
-
log.newline();
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Validate project exists
|
|
210
|
-
let project: Awaited<ReturnType<typeof getProject>>;
|
|
211
|
-
try {
|
|
212
|
-
project = await getProject(projectName);
|
|
213
|
-
} catch {
|
|
214
|
-
showError(`Project '${projectName}' not found.`);
|
|
215
|
-
log.newline();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Prompt for idea title and description
|
|
220
|
-
const ideaTitle = await input({
|
|
221
|
-
message: 'Idea title (short summary):',
|
|
222
|
-
validate: (value) => (value.trim().length > 0 ? true : 'Title is required'),
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const ideaDescription = await editorInput({
|
|
226
|
-
message: 'Idea description (what you want to build):',
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (!ideaDescription.trim()) {
|
|
230
|
-
showError('Description is required.');
|
|
231
|
-
log.newline();
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
log.newline();
|
|
236
|
-
showInfo('Creating ticket...');
|
|
237
|
-
|
|
238
|
-
// Auto-create ticket with pending status
|
|
239
|
-
const ticket = await addTicket(
|
|
240
|
-
{
|
|
241
|
-
title: ideaTitle,
|
|
242
|
-
description: ideaDescription,
|
|
243
|
-
projectName,
|
|
244
|
-
},
|
|
245
|
-
id
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
console.log(field('Ticket ID', ticket.id));
|
|
249
|
-
log.newline();
|
|
250
|
-
|
|
251
|
-
// Resolve AI provider early for display names
|
|
252
|
-
const providerName = providerDisplayName(await resolveProvider());
|
|
253
|
-
|
|
254
|
-
// Select which paths the AI should explore (same as plan.ts)
|
|
255
|
-
let selectedPaths: string[];
|
|
256
|
-
const totalRepos = project.repositories.length;
|
|
257
|
-
|
|
258
|
-
if (options.allPaths) {
|
|
259
|
-
// --all-paths: use all repos
|
|
260
|
-
selectedPaths = project.repositories.map((r) => r.path);
|
|
261
|
-
} else if (options.auto) {
|
|
262
|
-
// --auto: use first repo per project
|
|
263
|
-
selectedPaths = project.repositories.slice(0, 1).map((r) => r.path);
|
|
264
|
-
} else if (totalRepos === 1) {
|
|
265
|
-
// Only one repo - no selection needed
|
|
266
|
-
selectedPaths = [project.repositories[0]?.path ?? ''];
|
|
267
|
-
} else {
|
|
268
|
-
// Multiple repos available - show checkbox
|
|
269
|
-
const reposByProject = new Map<string, Repository[]>();
|
|
270
|
-
reposByProject.set(projectName, project.repositories);
|
|
271
|
-
|
|
272
|
-
selectedPaths = await selectProjectPaths(reposByProject, 'Select paths to explore:');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Save selected paths to ticket.affectedRepositories
|
|
276
|
-
ticket.affectedRepositories = selectedPaths;
|
|
277
|
-
await saveSprint(sprint);
|
|
278
|
-
|
|
279
|
-
if (selectedPaths.length > 1) {
|
|
280
|
-
console.log(muted(`Paths: ${selectedPaths.join(', ')}`));
|
|
281
|
-
} else {
|
|
282
|
-
console.log(muted(`Path: ${selectedPaths[0] ?? process.cwd()}`));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Format repositories for prompt
|
|
286
|
-
const repositoriesText = selectedPaths.map((path) => `- ${path}`).join('\n');
|
|
287
|
-
|
|
288
|
-
// Load schema
|
|
289
|
-
const schema = await getTaskImportSchema();
|
|
290
|
-
|
|
291
|
-
// Create ideation directory
|
|
292
|
-
const ideateDir = getIdeateDir(id, ticket.id);
|
|
293
|
-
await mkdir(ideateDir, { recursive: true });
|
|
294
|
-
|
|
295
|
-
if (options.auto) {
|
|
296
|
-
// Headless mode - AI generates autonomously
|
|
297
|
-
const prompt = buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositoriesText, schema);
|
|
298
|
-
const spinner = createSpinner(`${providerName} is refining idea and planning tasks...`);
|
|
299
|
-
spinner.start();
|
|
300
|
-
|
|
301
|
-
let output: string;
|
|
302
|
-
try {
|
|
303
|
-
output = await invokeAiAuto(prompt, selectedPaths, ideateDir);
|
|
304
|
-
spinner.succeed(`${providerName} finished`);
|
|
305
|
-
} catch (err) {
|
|
306
|
-
spinner.fail(`${providerName} session failed`);
|
|
307
|
-
if (err instanceof Error) {
|
|
308
|
-
showError(`Failed to invoke ${providerName}: ${err.message}`);
|
|
309
|
-
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
310
|
-
log.newline();
|
|
311
|
-
}
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Check for planning-blocked signal before parsing JSON
|
|
316
|
-
const blockedReason = parsePlanningBlocked(output);
|
|
317
|
-
if (blockedReason) {
|
|
318
|
-
showWarning(`Planning blocked: ${blockedReason}`);
|
|
319
|
-
log.newline();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
log.dim('Parsing response...');
|
|
324
|
-
let ideateOutput: { requirements: string; tasks: unknown[] };
|
|
325
|
-
try {
|
|
326
|
-
ideateOutput = parseIdeateOutput(output);
|
|
327
|
-
} catch (err) {
|
|
328
|
-
if (err instanceof Error) {
|
|
329
|
-
showError(`Failed to parse ${providerName} output: ${err.message}`);
|
|
330
|
-
log.dim('Raw output:');
|
|
331
|
-
console.log(output);
|
|
332
|
-
log.newline();
|
|
333
|
-
}
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Update ticket with requirements
|
|
338
|
-
const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
|
|
339
|
-
const ticketToUpdate = sprint.tickets[ticketIdx];
|
|
340
|
-
if (ticketIdx !== -1 && ticketToUpdate) {
|
|
341
|
-
ticketToUpdate.requirements = ideateOutput.requirements;
|
|
342
|
-
ticketToUpdate.requirementStatus = 'approved';
|
|
343
|
-
}
|
|
344
|
-
await saveSprint(sprint);
|
|
345
|
-
|
|
346
|
-
showSuccess('Requirements approved and saved!');
|
|
347
|
-
log.newline();
|
|
348
|
-
|
|
349
|
-
// Parse and validate tasks
|
|
350
|
-
let parsedTasks: ReturnType<typeof parseTasksJson>;
|
|
351
|
-
try {
|
|
352
|
-
parsedTasks = parseTasksJson(JSON.stringify(ideateOutput.tasks));
|
|
353
|
-
} catch (err) {
|
|
354
|
-
if (err instanceof Error) {
|
|
355
|
-
showError(`Failed to parse tasks: ${err.message}`);
|
|
356
|
-
log.newline();
|
|
357
|
-
}
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (parsedTasks.length === 0) {
|
|
362
|
-
showWarning('No tasks generated.');
|
|
363
|
-
log.newline();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
showSuccess(`Generated ${String(parsedTasks.length)} task(s):`);
|
|
368
|
-
log.newline();
|
|
369
|
-
console.log(renderParsedTasksTable(parsedTasks));
|
|
370
|
-
console.log('');
|
|
371
|
-
|
|
372
|
-
// Validate before import
|
|
373
|
-
const existingTasks = await getTasks(id);
|
|
374
|
-
const ticketIds = new Set(sprint.tickets.map((t) => t.id));
|
|
375
|
-
const validationErrors = validateImportTasks(parsedTasks, existingTasks, ticketIds);
|
|
376
|
-
if (validationErrors.length > 0) {
|
|
377
|
-
showError('Validation failed');
|
|
378
|
-
for (const err of validationErrors) {
|
|
379
|
-
log.item(error(err));
|
|
380
|
-
}
|
|
381
|
-
log.newline();
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
showInfo('Importing tasks...');
|
|
386
|
-
const imported = await importTasks(parsedTasks, id);
|
|
387
|
-
terminalBell();
|
|
388
|
-
showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
|
|
389
|
-
log.newline();
|
|
390
|
-
} else {
|
|
391
|
-
// Interactive mode - user iterates with AI
|
|
392
|
-
const outputFile = join(ideateDir, 'output.json');
|
|
393
|
-
const prompt = buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositoriesText, outputFile, schema);
|
|
394
|
-
|
|
395
|
-
showInfo(`Starting interactive ${providerName} session...`);
|
|
396
|
-
console.log(muted(` Exploring: ${selectedPaths.join(', ')}`));
|
|
397
|
-
console.log(muted(`\n ${providerName} will guide you through requirements refinement and task planning.`));
|
|
398
|
-
console.log(muted(` When done, ask ${providerName} to write the output to: ${outputFile}\n`));
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
await invokeAiInteractive(prompt, selectedPaths, ideateDir);
|
|
402
|
-
} catch (err) {
|
|
403
|
-
if (err instanceof Error) {
|
|
404
|
-
showError(`Failed to invoke ${providerName}: ${err.message}`);
|
|
405
|
-
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
406
|
-
log.newline();
|
|
407
|
-
}
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Check if output file was created
|
|
412
|
-
console.log('');
|
|
413
|
-
if (await fileExists(outputFile)) {
|
|
414
|
-
showInfo('Output file found. Processing...');
|
|
415
|
-
|
|
416
|
-
let content: string;
|
|
417
|
-
try {
|
|
418
|
-
content = await readFile(outputFile, 'utf-8');
|
|
419
|
-
} catch {
|
|
420
|
-
showError(`Failed to read output file: ${outputFile}`);
|
|
421
|
-
log.newline();
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
let ideateOutput: { requirements: string; tasks: unknown[] };
|
|
426
|
-
try {
|
|
427
|
-
ideateOutput = parseIdeateOutput(content);
|
|
428
|
-
} catch (err) {
|
|
429
|
-
if (err instanceof Error) {
|
|
430
|
-
showError(`Failed to parse output file: ${err.message}`);
|
|
431
|
-
log.newline();
|
|
432
|
-
}
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Update ticket with requirements
|
|
437
|
-
const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
|
|
438
|
-
const ticketToUpdate = sprint.tickets[ticketIdx];
|
|
439
|
-
if (ticketIdx !== -1 && ticketToUpdate) {
|
|
440
|
-
ticketToUpdate.requirements = ideateOutput.requirements;
|
|
441
|
-
ticketToUpdate.requirementStatus = 'approved';
|
|
442
|
-
}
|
|
443
|
-
await saveSprint(sprint);
|
|
444
|
-
|
|
445
|
-
showSuccess('Requirements approved and saved!');
|
|
446
|
-
log.newline();
|
|
447
|
-
|
|
448
|
-
// Parse and validate tasks
|
|
449
|
-
let parsedTasks: ReturnType<typeof parseTasksJson>;
|
|
450
|
-
try {
|
|
451
|
-
parsedTasks = parseTasksJson(JSON.stringify(ideateOutput.tasks));
|
|
452
|
-
} catch (err) {
|
|
453
|
-
if (err instanceof Error) {
|
|
454
|
-
showError(`Failed to parse tasks: ${err.message}`);
|
|
455
|
-
log.newline();
|
|
456
|
-
}
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (parsedTasks.length === 0) {
|
|
461
|
-
showWarning('No tasks in file.');
|
|
462
|
-
log.newline();
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
showSuccess(`Found ${String(parsedTasks.length)} task(s):`);
|
|
467
|
-
log.newline();
|
|
468
|
-
console.log(renderParsedTasksTable(parsedTasks));
|
|
469
|
-
console.log('');
|
|
470
|
-
|
|
471
|
-
// Validate before import
|
|
472
|
-
const existingTasks = await getTasks(id);
|
|
473
|
-
const ticketIds = new Set(sprint.tickets.map((t) => t.id));
|
|
474
|
-
const validationErrors = validateImportTasks(parsedTasks, existingTasks, ticketIds);
|
|
475
|
-
if (validationErrors.length > 0) {
|
|
476
|
-
showError('Validation failed');
|
|
477
|
-
for (const err of validationErrors) {
|
|
478
|
-
log.item(error(err));
|
|
479
|
-
}
|
|
480
|
-
log.newline();
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
showInfo('Importing tasks...');
|
|
485
|
-
const imported = await importTasks(parsedTasks, id);
|
|
486
|
-
terminalBell();
|
|
487
|
-
showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
|
|
488
|
-
log.newline();
|
|
489
|
-
} else {
|
|
490
|
-
showWarning('No output file found.');
|
|
491
|
-
showTip(`Expected: ${outputFile}`);
|
|
492
|
-
showNextStep('ralphctl sprint ideate', 'run ideation again');
|
|
493
|
-
log.newline();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|