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.
Files changed (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
@@ -1,549 +0,0 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { confirm } from '@inquirer/prompts';
4
- import { error, muted } from '@src/theme/index.ts';
5
- import {
6
- createSpinner,
7
- emoji,
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 { getTasks, listTasks, reorderByDependencies, validateImportTasks } from '@src/store/task.ts';
22
- import {
23
- allRequirementsApproved,
24
- formatTicketDisplay,
25
- getPendingRequirements,
26
- groupTicketsByProject,
27
- } from '@src/store/ticket.ts';
28
- import { getProject } from '@src/store/project.ts';
29
- import { fileExists } from '@src/utils/storage.ts';
30
- import { getPlanningDir } from '@src/utils/paths.ts';
31
- import { buildAutoPrompt, buildInteractivePrompt } from '@src/ai/prompts/index.ts';
32
- import { spawnHeadless, spawnInteractive } from '@src/ai/session.ts';
33
- import { type ImportTask, type Repository, type Ticket } from '@src/schemas/index.ts';
34
- import { selectProjectPaths } from '@src/interactive/selectors.ts';
35
- import { resolveProvider, providerDisplayName } from '@src/utils/provider.ts';
36
- import { getActiveProvider } from '@src/providers/index.ts';
37
- import {
38
- getTaskImportSchema,
39
- importTasks,
40
- parsePlanningBlocked,
41
- parseTasksJson,
42
- renderParsedTasksTable,
43
- } from './plan-utils.ts';
44
-
45
- interface PlanOptions {
46
- auto: boolean;
47
- allPaths: boolean;
48
- }
49
-
50
- function parseArgs(args: string[]): { sprintId?: string; options: PlanOptions } {
51
- const options: PlanOptions = {
52
- auto: false,
53
- allPaths: false,
54
- };
55
- let sprintId: string | undefined;
56
-
57
- for (const arg of args) {
58
- if (arg === '--auto') {
59
- options.auto = true;
60
- } else if (arg === '--all-paths') {
61
- options.allPaths = true;
62
- } else if (!arg.startsWith('-')) {
63
- sprintId = arg;
64
- }
65
- }
66
-
67
- return { sprintId, options };
68
- }
69
-
70
- async function getSprintContext(
71
- sprintName: string,
72
- ticketsByProject: Map<string, Ticket[]>,
73
- existingTasks: {
74
- id: string;
75
- name: string;
76
- description?: string;
77
- status: string;
78
- ticketId?: string;
79
- projectPath: string;
80
- }[]
81
- ): Promise<string> {
82
- const lines: string[] = [];
83
- lines.push(`# Sprint: ${sprintName}`);
84
-
85
- // Group tickets by project in context
86
- for (const [projectName, tickets] of ticketsByProject) {
87
- lines.push('');
88
- lines.push(`## Project: ${projectName}`);
89
-
90
- // Get project repositories
91
- try {
92
- const project = await getProject(projectName);
93
- lines.push('');
94
- lines.push('### Repositories');
95
- for (const repo of project.repositories) {
96
- lines.push(`- **${repo.name}**: ${repo.path}`);
97
- if (repo.checkScript) {
98
- lines.push(` - Check: \`${repo.checkScript}\``);
99
- }
100
- }
101
- } catch {
102
- lines.push('Repositories: (project not found)');
103
- }
104
- lines.push('');
105
- lines.push('### Tickets');
106
-
107
- for (const ticket of tickets) {
108
- lines.push('');
109
- lines.push(`#### ${formatTicketDisplay(ticket)}`);
110
-
111
- if (ticket.description) {
112
- lines.push('');
113
- lines.push('**Original Description:**');
114
- lines.push(ticket.description);
115
- }
116
- if (ticket.link) {
117
- lines.push('');
118
- lines.push(`Link: ${ticket.link}`);
119
- }
120
- // Include refined requirements if available
121
- if (ticket.requirements) {
122
- lines.push('');
123
- lines.push('**Refined Requirements:**');
124
- lines.push('');
125
- lines.push(ticket.requirements);
126
- }
127
- }
128
- }
129
-
130
- if (existingTasks.length > 0) {
131
- lines.push('');
132
- lines.push('## Existing Tasks');
133
- lines.push('');
134
- lines.push(
135
- '> These are tasks from a previous planning run. Your output will replace all existing tasks entirely. You may reuse, modify, or drop existing tasks, and add new ones. Generate a complete task set covering ALL tickets.'
136
- );
137
- lines.push('');
138
- for (const task of existingTasks) {
139
- const desc = task.description ? ` — ${task.description}` : '';
140
- const ticket = task.ticketId ? ` ticket:${task.ticketId}` : '';
141
- lines.push(`- ${task.id}: ${task.name} [${task.status}] (${task.projectPath})${ticket}${desc}`);
142
- }
143
- }
144
-
145
- return lines.join('\n');
146
- }
147
-
148
- async function invokeAiInteractive(prompt: string, repoPaths: string[], planDir: string): Promise<void> {
149
- // Write full context to the planning directory for reference
150
- const contextFile = join(planDir, 'planning-context.md');
151
- await writeFile(contextFile, prompt, 'utf-8');
152
-
153
- const provider = await getActiveProvider();
154
-
155
- // Count tickets in the prompt for the summary
156
- const ticketCount = (prompt.match(/^####/gm) ?? []).length;
157
-
158
- // Build initial prompt that tells the AI to read the context file
159
- const startPrompt = `I need help planning tasks for a sprint. The full planning context is in planning-context.md (${String(ticketCount)} tickets). Please read that file now and follow the instructions to help me plan implementation tasks.`;
160
-
161
- // Build args - pass all repo paths in a single --add-dir to avoid variadic option
162
- // consuming the positional prompt argument
163
- const args: string[] = ['--add-dir', ...repoPaths];
164
-
165
- const result = spawnInteractive(
166
- startPrompt,
167
- {
168
- cwd: planDir,
169
- args,
170
- env: provider.getSpawnEnv(),
171
- },
172
- provider
173
- );
174
-
175
- if (result.error) {
176
- throw new Error(result.error);
177
- }
178
- }
179
-
180
- async function invokeAiAuto(prompt: string, repoPaths: string[], planDir: string): Promise<string> {
181
- const provider = await getActiveProvider();
182
-
183
- // Build args - all repo paths via --add-dir (neutral CWD in planning dir)
184
- const args: string[] = ['--permission-mode', 'plan', '--print'];
185
- for (const path of repoPaths) {
186
- args.push('--add-dir', path);
187
- }
188
- args.push('-p', prompt);
189
-
190
- return spawnHeadless(
191
- {
192
- cwd: planDir,
193
- args,
194
- env: provider.getSpawnEnv(),
195
- },
196
- provider
197
- );
198
- }
199
-
200
- export async function sprintPlanCommand(args: string[]): Promise<void> {
201
- const { sprintId, options } = parseArgs(args);
202
-
203
- let id: string;
204
- try {
205
- id = await resolveSprintId(sprintId);
206
- } catch {
207
- showWarning('No sprint specified and no current sprint set.');
208
- showNextStep('ralphctl sprint create', 'create a new sprint');
209
- log.newline();
210
- return;
211
- }
212
-
213
- const sprint = await getSprint(id);
214
-
215
- // Check sprint status — draft only
216
- try {
217
- assertSprintStatus(sprint, ['draft'], 'plan');
218
- } catch (err) {
219
- if (err instanceof Error) {
220
- showError(err.message);
221
- log.newline();
222
- }
223
- return;
224
- }
225
-
226
- if (sprint.tickets.length === 0) {
227
- showWarning('No tickets in sprint.');
228
- showNextStep('ralphctl ticket add --project <project-name>', 'add tickets first');
229
- log.newline();
230
- return;
231
- }
232
-
233
- // Always process ALL tickets
234
- const ticketsToProcess = sprint.tickets;
235
-
236
- // Check if all tickets have approved requirements
237
- if (!allRequirementsApproved(ticketsToProcess)) {
238
- const pendingTickets = getPendingRequirements(ticketsToProcess);
239
- showWarning('Not all tickets have approved requirements.');
240
- log.dim(`Pending: ${String(pendingTickets.length)} ticket(s)`);
241
- for (const ticket of pendingTickets) {
242
- log.item(muted(formatTicketDisplay(ticket)));
243
- }
244
- showNextStep('ralphctl sprint refine', 'refine requirements first');
245
- log.newline();
246
- return;
247
- }
248
-
249
- // Check for existing tasks (re-plan scenario)
250
- const existingTasks = await listTasks(id);
251
- const isReplan = existingTasks.length > 0;
252
-
253
- if (isReplan) {
254
- if (options.auto) {
255
- showInfo(`Re-plan: ${String(existingTasks.length)} existing task(s) will be replaced with a fresh plan.`);
256
- log.newline();
257
- } else {
258
- const proceed = await confirm({
259
- message: `${emoji.donut} ${String(existingTasks.length)} task(s) already exist. Re-planning will replace all tasks. Continue?`,
260
- default: true,
261
- });
262
-
263
- if (!proceed) {
264
- log.dim('Cancelled.');
265
- log.newline();
266
- return;
267
- }
268
- }
269
- }
270
-
271
- // Group tickets to process by project
272
- const ticketsByProject = groupTicketsByProject(ticketsToProcess);
273
-
274
- // Resolve AI provider early for display names
275
- const providerName = providerDisplayName(await resolveProvider());
276
-
277
- // Determine mode label
278
- const modeLabel = options.auto ? 'Auto (headless)' : 'Interactive';
279
-
280
- printHeader('Sprint Planning', icons.sprint);
281
- console.log(field('Sprint', sprint.name));
282
- console.log(field('ID', sprint.id));
283
- console.log(field('Tickets', String(ticketsToProcess.length)));
284
- console.log(field('Projects', String(ticketsByProject.size)));
285
- console.log(field('Mode', modeLabel));
286
- console.log(field('Provider', providerName));
287
-
288
- for (const [proj, tickets] of ticketsByProject) {
289
- console.log(muted(` - ${proj}: ${String(tickets.length)} ticket(s)`));
290
- }
291
- console.log('');
292
-
293
- // Collect repositories by project for selection UI (from tickets being planned)
294
- const reposByProject = new Map<string, Repository[]>();
295
- const defaultPaths: string[] = []; // First repo path per project
296
-
297
- for (const ticket of ticketsToProcess) {
298
- if (reposByProject.has(ticket.projectName)) continue; // Already processed
299
- try {
300
- const project = await getProject(ticket.projectName);
301
- reposByProject.set(ticket.projectName, project.repositories);
302
- if (project.repositories[0]) defaultPaths.push(project.repositories[0].path);
303
- } catch {
304
- // Project not found, skip
305
- }
306
- }
307
-
308
- // Collect previously saved affected repos from tickets being planned (for resumability)
309
- const savedPaths = new Set<string>();
310
- for (const ticket of ticketsToProcess) {
311
- if (ticket.affectedRepositories) {
312
- for (const path of ticket.affectedRepositories) {
313
- savedPaths.add(path);
314
- }
315
- }
316
- }
317
- const hasSavedSelection = savedPaths.size > 0;
318
-
319
- // Select which paths the AI should explore
320
- let selectedPaths: string[];
321
- const totalRepos = [...reposByProject.values()].reduce((n, repos) => n + repos.length, 0);
322
-
323
- if (options.allPaths) {
324
- // --all-paths: use all (opt-in to current slow behavior)
325
- selectedPaths = [...reposByProject.values()].flatMap((repos) => repos.map((r) => r.path));
326
- } else if (options.auto) {
327
- // --auto: use saved selection or first repo per project
328
- selectedPaths = hasSavedSelection ? [...savedPaths] : defaultPaths;
329
- } else if (totalRepos === defaultPaths.length) {
330
- // Only one repo per project - no selection needed
331
- selectedPaths = defaultPaths;
332
- } else {
333
- // Multiple repos available - show checkbox (pre-select saved paths if any)
334
- selectedPaths = await selectProjectPaths(
335
- reposByProject,
336
- 'Select paths to explore:',
337
- hasSavedSelection ? [...savedPaths] : undefined
338
- );
339
- }
340
-
341
- // Persist selected paths to ticket.affectedRepositories (only for planned tickets)
342
- for (const ticket of ticketsToProcess) {
343
- const projectRepos = reposByProject.get(ticket.projectName);
344
- if (projectRepos) {
345
- const projectRepoPaths = new Set(projectRepos.map((r) => r.path));
346
- ticket.affectedRepositories = selectedPaths.filter((p) => projectRepoPaths.has(p));
347
- } else {
348
- ticket.affectedRepositories = [];
349
- }
350
- }
351
- await saveSprint(sprint);
352
-
353
- if (selectedPaths.length > 1) {
354
- console.log(muted(`Paths: ${selectedPaths.join(', ')}`));
355
- } else {
356
- console.log(muted(`Path: ${selectedPaths[0] ?? process.cwd()}`));
357
- }
358
-
359
- const context = await getSprintContext(
360
- sprint.name,
361
- ticketsByProject,
362
- existingTasks.map((t) => ({
363
- id: t.id,
364
- name: t.name,
365
- description: t.description,
366
- status: t.status,
367
- ticketId: t.ticketId,
368
- projectPath: t.projectPath,
369
- }))
370
- );
371
- const schema = await getTaskImportSchema();
372
-
373
- // Debug: show context size to verify content is being generated
374
- const contextLines = context.split('\n').length;
375
- const contextChars = context.length;
376
- console.log(muted(`Context: ${String(contextLines)} lines, ${String(contextChars)} chars`));
377
-
378
- // Create planning directory in the sprint's data folder
379
- const planDir = getPlanningDir(id);
380
- await mkdir(planDir, { recursive: true });
381
-
382
- // Build ticket ID set for validating ticketId references during import
383
- const ticketIds = new Set(sprint.tickets.map((t) => t.id));
384
-
385
- if (options.auto) {
386
- // Headless mode - AI generates and we import
387
- const prompt = buildAutoPrompt(context, schema);
388
- const spinner = createSpinner(`${providerName} is planning tasks...`);
389
- spinner.start();
390
-
391
- let output: string;
392
- try {
393
- output = await invokeAiAuto(prompt, selectedPaths, planDir);
394
- spinner.succeed(`${providerName} finished planning`);
395
- } catch (err) {
396
- spinner.fail(`${providerName} planning failed`);
397
- if (err instanceof Error) {
398
- showError(`Failed to invoke ${providerName}: ${err.message}`);
399
- showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
400
- log.newline();
401
- }
402
- return;
403
- }
404
-
405
- // Check for planning-blocked signal before parsing JSON
406
- const blockedReason = parsePlanningBlocked(output);
407
- if (blockedReason) {
408
- showWarning(`Planning blocked: ${blockedReason}`);
409
- log.newline();
410
- return;
411
- }
412
-
413
- console.log(muted('Parsing response...'));
414
- let parsedTasks: ImportTask[];
415
- try {
416
- parsedTasks = parseTasksJson(output);
417
- } catch (err) {
418
- if (err instanceof Error) {
419
- showError(`Failed to parse ${providerName} output: ${err.message}`);
420
- log.dim('Raw output:');
421
- console.log(output);
422
- log.newline();
423
- }
424
- return;
425
- }
426
-
427
- if (parsedTasks.length === 0) {
428
- showWarning('No tasks generated.');
429
- log.newline();
430
- return;
431
- }
432
-
433
- showSuccess(`Generated ${String(parsedTasks.length)} task(s):`);
434
- log.newline();
435
- console.log(renderParsedTasksTable(parsedTasks));
436
- console.log('');
437
-
438
- // Validate before import — when replacing, pass empty existingTasks since new set is self-contained
439
- const validationExistingTasks = isReplan ? [] : await getTasks(id);
440
- const validationErrors = validateImportTasks(parsedTasks, validationExistingTasks, ticketIds);
441
- if (validationErrors.length > 0) {
442
- showError('Validation failed');
443
- for (const err of validationErrors) {
444
- log.item(error(err));
445
- }
446
- log.newline();
447
- return;
448
- }
449
-
450
- showInfo('Importing tasks...');
451
- const imported = await importTasks(parsedTasks, id, isReplan ? { replace: true } : undefined);
452
-
453
- await reorderByDependencies(id);
454
- log.dim('Tasks reordered by dependencies.');
455
-
456
- terminalBell();
457
- showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
458
- log.newline();
459
- } else {
460
- // Interactive mode - user iterates with AI
461
- const outputFile = join(planDir, 'tasks.json');
462
- const prompt = buildInteractivePrompt(context, outputFile, schema);
463
-
464
- showInfo(`Starting interactive ${providerName} session...`);
465
- console.log(
466
- muted(
467
- ` Planning ${String(ticketsToProcess.length)} ticket(s) across ${String(ticketsByProject.size)} project(s)`
468
- )
469
- );
470
- console.log(muted(` Exploring: ${selectedPaths.join(', ')}`));
471
- console.log(muted(`\n ${providerName} will read planning-context.md and explore the repos.`));
472
- console.log(muted(` When done, ask ${providerName} to write tasks to: ${outputFile}\n`));
473
-
474
- try {
475
- await invokeAiInteractive(prompt, selectedPaths, planDir);
476
- } catch (err) {
477
- if (err instanceof Error) {
478
- showError(`Failed to invoke ${providerName}: ${err.message}`);
479
- showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
480
- log.newline();
481
- }
482
- return;
483
- }
484
-
485
- // Check if output file was created
486
- console.log('');
487
- if (await fileExists(outputFile)) {
488
- showInfo('Task file found. Processing...');
489
-
490
- let content: string;
491
- try {
492
- content = await readFile(outputFile, 'utf-8');
493
- } catch {
494
- showError(`Failed to read task file: ${outputFile}`);
495
- log.newline();
496
- return;
497
- }
498
-
499
- let parsedTasks: ImportTask[];
500
- try {
501
- parsedTasks = parseTasksJson(content);
502
- } catch (err) {
503
- if (err instanceof Error) {
504
- showError(`Failed to parse task file: ${err.message}`);
505
- log.newline();
506
- }
507
- return;
508
- }
509
-
510
- if (parsedTasks.length === 0) {
511
- showWarning('No tasks in file.');
512
- log.newline();
513
- return;
514
- }
515
-
516
- showSuccess(`Found ${String(parsedTasks.length)} task(s):`);
517
- log.newline();
518
- console.log(renderParsedTasksTable(parsedTasks));
519
- console.log('');
520
-
521
- // Validate before import — when replacing, pass empty existingTasks since new set is self-contained
522
- const validationExistingTasks = isReplan ? [] : await getTasks(id);
523
- const validationErrors = validateImportTasks(parsedTasks, validationExistingTasks, ticketIds);
524
- if (validationErrors.length > 0) {
525
- showError('Validation failed');
526
- for (const err of validationErrors) {
527
- log.item(error(err));
528
- }
529
- log.newline();
530
- return;
531
- }
532
-
533
- showInfo('Importing tasks...');
534
- const imported = await importTasks(parsedTasks, id, isReplan ? { replace: true } : undefined);
535
-
536
- await reorderByDependencies(id);
537
- log.dim('Tasks reordered by dependencies.');
538
-
539
- terminalBell();
540
- showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
541
- log.newline();
542
- } else {
543
- showWarning('No task file found.');
544
- showTip(`Expected: ${outputFile}`);
545
- showNextStep('ralphctl sprint plan', 'run planning again to create tasks');
546
- log.newline();
547
- }
548
- }
549
- }