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,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
|
-
}
|