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,359 +0,0 @@
1
- import { mkdir, readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { confirm } from '@inquirer/prompts';
4
- import { colors, info } from '@src/theme/index.ts';
5
- import {
6
- createSpinner,
7
- emoji,
8
- field,
9
- fieldMultiline,
10
- icons,
11
- log,
12
- printHeader,
13
- printSeparator,
14
- progressBar,
15
- renderCard,
16
- showError,
17
- showSuccess,
18
- showTip,
19
- showWarning,
20
- } from '@src/theme/ui.ts';
21
- import { assertSprintStatus, getSprint, resolveSprintId, saveSprint } from '@src/store/sprint.ts';
22
- import { getPendingRequirements } from '@src/store/ticket.ts';
23
- import { getProject } from '@src/store/project.ts';
24
- import { buildTicketRefinePrompt } from '@src/ai/prompts/index.ts';
25
- import { fileExists } from '@src/utils/storage.ts';
26
- import { getRefinementDir, getSchemaPath, getSprintDir } from '@src/utils/paths.ts';
27
- import { IssueFetchError, fetchIssueFromUrl, formatIssueContext } from '@src/utils/issue-fetch.ts';
28
- import { type RefinedRequirement } from '@src/schemas/index.ts';
29
- import { exportRequirementsToMarkdown } from '@src/utils/requirements-export.ts';
30
- import { resolveProvider, providerDisplayName } from '@src/utils/provider.ts';
31
- import { formatTicketForPrompt, parseRequirementsFile, runAiSession } from '@src/commands/ticket/refine-utils.ts';
32
-
33
- interface RefineOptions {
34
- project?: string;
35
- }
36
-
37
- function parseArgs(args: string[]): { sprintId?: string; options: RefineOptions } {
38
- const options: RefineOptions = {};
39
- let sprintId: string | undefined;
40
-
41
- for (let i = 0; i < args.length; i++) {
42
- const arg = args[i];
43
- const nextArg = args[i + 1];
44
-
45
- if (arg === '--project') {
46
- options.project = nextArg;
47
- i++;
48
- } else if (!arg?.startsWith('-')) {
49
- sprintId = arg;
50
- }
51
- }
52
-
53
- return { sprintId, options };
54
- }
55
-
56
- export async function sprintRefineCommand(args: string[]): Promise<void> {
57
- const { sprintId, options } = parseArgs(args);
58
-
59
- let id: string;
60
- try {
61
- id = await resolveSprintId(sprintId);
62
- } catch {
63
- showWarning('No sprint specified and no current sprint set.');
64
- showTip('Specify a sprint ID or create one first.');
65
- log.newline();
66
- return;
67
- }
68
-
69
- const sprint = await getSprint(id);
70
-
71
- // Check sprint status - must be draft to refine
72
- try {
73
- assertSprintStatus(sprint, ['draft'], 'refine');
74
- } catch (err) {
75
- if (err instanceof Error) {
76
- showError(err.message);
77
- log.newline();
78
- }
79
- return;
80
- }
81
-
82
- if (sprint.tickets.length === 0) {
83
- showWarning('No tickets in sprint.');
84
- showTip('Add tickets first: ralphctl ticket add --project <project-name>');
85
- log.newline();
86
- return;
87
- }
88
-
89
- // Get pending tickets (filter by project if specified)
90
- let pendingTickets = getPendingRequirements(sprint.tickets);
91
- if (options.project) {
92
- pendingTickets = pendingTickets.filter((t) => t.projectName === options.project);
93
- if (pendingTickets.length === 0) {
94
- showWarning(`No pending tickets for project: ${options.project}`);
95
- log.newline();
96
- return;
97
- }
98
- }
99
-
100
- if (pendingTickets.length === 0) {
101
- showSuccess('All tickets already have approved requirements!');
102
- showTip('Run "ralphctl sprint plan" to generate tasks.');
103
- log.newline();
104
- return;
105
- }
106
-
107
- // Show initial summary
108
- printHeader('Requirements Refinement', icons.ticket);
109
- console.log(field('Sprint', sprint.name));
110
- console.log(field('ID', sprint.id));
111
- console.log(field('Pending', `${String(pendingTickets.length)} ticket(s)`));
112
- log.newline();
113
-
114
- // Load schema once before processing tickets
115
- const schemaPath = getSchemaPath('requirements-output.schema.json');
116
- const schema = await readFile(schemaPath, 'utf-8');
117
-
118
- // Resolve AI provider for display names
119
- const providerName = providerDisplayName(await resolveProvider());
120
-
121
- // Process tickets one by one
122
- let approved = 0;
123
- let skipped = 0;
124
-
125
- for (let i = 0; i < pendingTickets.length; i++) {
126
- const ticket = pendingTickets[i];
127
- if (!ticket) continue;
128
-
129
- const ticketNum = i + 1;
130
- const totalTickets = pendingTickets.length;
131
-
132
- // Show ticket card
133
- printSeparator(60);
134
- console.log('');
135
- console.log(` ${icons.ticket} ${info(`Ticket ${String(ticketNum)} of ${String(totalTickets)}`)}`);
136
- console.log(
137
- ` ${progressBar(i, totalTickets, {
138
- width: 15,
139
- showPercent: false,
140
- })} ${colors.muted(`${String(ticketNum)}/${String(totalTickets)}`)}`
141
- );
142
- console.log('');
143
- console.log(field('Title', ticket.title, 14));
144
- console.log(field('Project', ticket.projectName, 14));
145
- if (ticket.link) {
146
- console.log(field('Link', ticket.link, 14));
147
- }
148
- if (ticket.description) {
149
- console.log(fieldMultiline('Description', ticket.description, 14));
150
- }
151
- log.newline();
152
-
153
- // Validate project exists
154
- try {
155
- await getProject(ticket.projectName);
156
- } catch {
157
- showWarning(`Project '${ticket.projectName}' not found.`);
158
- log.dim('Skipping this ticket.');
159
- log.newline();
160
- skipped++;
161
- continue;
162
- }
163
-
164
- // Confirm before starting AI session
165
- const proceed = await confirm({
166
- message: `${emoji.donut} Start ${providerName} refinement session for this ticket?`,
167
- default: true,
168
- });
169
-
170
- if (!proceed) {
171
- log.dim('Skipped. You can refine this ticket later.');
172
- log.newline();
173
- skipped++;
174
- continue;
175
- }
176
-
177
- // Fetch live issue data if ticket has an issue link
178
- let issueContext = '';
179
- if (ticket.link) {
180
- const fetchSpinner = createSpinner('Fetching issue data...');
181
- fetchSpinner.start();
182
- try {
183
- const issueData = fetchIssueFromUrl(ticket.link);
184
- if (issueData) {
185
- issueContext = formatIssueContext(issueData);
186
- fetchSpinner.succeed(`Issue data fetched (${String(issueData.comments.length)} comment(s))`);
187
- } else {
188
- fetchSpinner.stop();
189
- }
190
- } catch (err) {
191
- fetchSpinner.fail('Could not fetch issue data');
192
- if (err instanceof IssueFetchError) {
193
- showWarning(`${err.message} — continuing without issue context`);
194
- } else if (err instanceof Error) {
195
- showWarning(`${err.message} — continuing without issue context`);
196
- }
197
- }
198
- }
199
-
200
- // Prepare AI session - use sprint's refinement directory
201
- const refineDir = getRefinementDir(id, ticket.id);
202
- await mkdir(refineDir, { recursive: true });
203
- const outputFile = join(refineDir, 'requirements.json');
204
- const ticketContent = formatTicketForPrompt(ticket);
205
- const prompt = buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext);
206
-
207
- log.dim(`Working directory: ${refineDir}`);
208
- log.dim(`Requirements output: ${outputFile}`);
209
- log.newline();
210
-
211
- const spinner = createSpinner(`Starting ${providerName} session...`);
212
- spinner.start();
213
-
214
- try {
215
- await runAiSession(refineDir, prompt, ticket.title);
216
- spinner.succeed(`${providerName} session completed`);
217
- } catch (err) {
218
- spinner.fail(`${providerName} session failed`);
219
- if (err instanceof Error) {
220
- showError(err.message);
221
- }
222
- log.newline();
223
- skipped++;
224
- continue;
225
- }
226
-
227
- log.newline();
228
-
229
- // Process the requirements file
230
- if (await fileExists(outputFile)) {
231
- let content: string;
232
- try {
233
- content = await readFile(outputFile, 'utf-8');
234
- } catch {
235
- showError(`Failed to read requirements file: ${outputFile}`);
236
- log.newline();
237
- skipped++;
238
- continue;
239
- }
240
-
241
- let refinedRequirements: RefinedRequirement[];
242
- try {
243
- refinedRequirements = parseRequirementsFile(content);
244
- } catch (err) {
245
- if (err instanceof Error) {
246
- showError(`Failed to parse requirements file: ${err.message}`);
247
- }
248
- log.newline();
249
- skipped++;
250
- continue;
251
- }
252
-
253
- if (refinedRequirements.length === 0) {
254
- showWarning('No requirements found in output file.');
255
- log.newline();
256
- skipped++;
257
- continue;
258
- }
259
-
260
- // Find all matching requirements (Claude may output multiple for one ticket)
261
- const matchingRequirements = refinedRequirements.filter((r) => r.ref === ticket.id || r.ref === ticket.title);
262
-
263
- if (matchingRequirements.length === 0) {
264
- showWarning('Requirement reference does not match this ticket.');
265
- log.newline();
266
- skipped++;
267
- continue;
268
- }
269
-
270
- // Combine multiple requirements into one (safety net for split outputs)
271
- const requirement: RefinedRequirement =
272
- matchingRequirements.length === 1
273
- ? {
274
- ref: matchingRequirements[0]?.ref ?? '',
275
- requirements: matchingRequirements[0]?.requirements ?? '',
276
- }
277
- : {
278
- ref: matchingRequirements[0]?.ref ?? '',
279
- requirements: matchingRequirements
280
- .map((r, idx) => {
281
- const text = r.requirements.trim();
282
- if (/^#\s/.test(text)) return text;
283
- return `# ${String(idx + 1)}. Section ${String(idx + 1)}\n\n${text}`;
284
- })
285
- .join('\n\n---\n\n'),
286
- };
287
-
288
- // Show requirement for review
289
- const reqLines = requirement.requirements.split('\n');
290
- console.log(renderCard(`${icons.ticket} Refined Requirements`, reqLines));
291
- log.newline();
292
-
293
- const approveRequirement = await confirm({
294
- message: `${emoji.donut} Approve these requirements?`,
295
- default: true,
296
- });
297
-
298
- if (approveRequirement) {
299
- // Save requirements to ticket
300
- const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
301
- const ticketToSave = sprint.tickets[ticketIdx];
302
- if (ticketIdx !== -1 && ticketToSave) {
303
- ticketToSave.requirements = requirement.requirements;
304
- ticketToSave.requirementStatus = 'approved';
305
- }
306
- await saveSprint(sprint);
307
- showSuccess('Requirements approved and saved!');
308
- approved++;
309
- } else {
310
- log.dim('Requirements not approved. You can refine this ticket later.');
311
- skipped++;
312
- }
313
- } else {
314
- showWarning('No requirements file found from AI session.');
315
- log.dim('You can refine this ticket later.');
316
- skipped++;
317
- }
318
-
319
- log.newline();
320
- }
321
-
322
- // Final summary
323
- printSeparator(60);
324
- log.newline();
325
- printHeader('Summary', icons.success);
326
- console.log(field('Approved', String(approved)));
327
- console.log(field('Skipped', String(skipped)));
328
- console.log(field('Total', String(pendingTickets.length)));
329
- log.newline();
330
-
331
- // Re-read sprint to get the latest state after all saves
332
- const updatedSprint = await getSprint(id);
333
- const remainingPending = getPendingRequirements(updatedSprint.tickets);
334
-
335
- if (remainingPending.length === 0) {
336
- showSuccess('All requirements approved!');
337
-
338
- // Auto-export requirements to sprint directory
339
- const sprintDir = getSprintDir(id);
340
- const outputPath = join(sprintDir, 'requirements.md');
341
-
342
- try {
343
- await exportRequirementsToMarkdown(updatedSprint, outputPath);
344
- log.dim(`Requirements saved to: ${outputPath}`);
345
- } catch (err) {
346
- if (err instanceof Error) {
347
- showError(`Failed to write requirements: ${err.message}`);
348
- } else {
349
- showError('Failed to write requirements: Unknown error');
350
- }
351
- }
352
-
353
- showTip('Run "ralphctl sprint plan" to generate tasks.');
354
- } else {
355
- log.info(`${String(remainingPending.length)} ticket(s) still pending.`);
356
- showTip('Continue refinement with: ralphctl sprint refine');
357
- }
358
- log.newline();
359
- }
@@ -1,58 +0,0 @@
1
- import { join } from 'node:path';
2
- import { getSprint, resolveSprintId } from '@src/store/sprint.ts';
3
- import { getSprintDir } from '@src/utils/paths.ts';
4
- import { exportRequirementsToMarkdown } from '@src/utils/requirements-export.ts';
5
- import { field, icons, log, printHeader, showEmpty, showError, showSuccess, showWarning } from '@src/theme/ui.ts';
6
- import { selectSprint } from '@src/interactive/selectors.ts';
7
-
8
- export async function sprintRequirementsCommand(args: string[] = []): Promise<void> {
9
- const sprintId = args.find((a) => !a.startsWith('-'));
10
-
11
- let id: string;
12
- try {
13
- id = await resolveSprintId(sprintId);
14
- } catch {
15
- const selected = await selectSprint('Select sprint to export requirements from:');
16
- if (!selected) return;
17
- id = selected;
18
- }
19
-
20
- const sprint = await getSprint(id);
21
-
22
- if (sprint.tickets.length === 0) {
23
- showEmpty('tickets in this sprint', 'Add tickets first: ralphctl ticket add --project <name>');
24
- return;
25
- }
26
-
27
- const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === 'approved');
28
- if (approvedTickets.length === 0) {
29
- showWarning('No approved requirements to export.');
30
- log.dim('Refine requirements first: ralphctl sprint refine');
31
- log.newline();
32
- return;
33
- }
34
-
35
- printHeader('Export Requirements', icons.sprint);
36
- console.log(field('Sprint', sprint.name));
37
- console.log(field('Tickets', `${String(sprint.tickets.length)} total, ${String(approvedTickets.length)} approved`));
38
- log.newline();
39
-
40
- // Export to sprint directory
41
- const sprintDir = getSprintDir(id);
42
- const outputPath = join(sprintDir, 'requirements.md');
43
-
44
- try {
45
- await exportRequirementsToMarkdown(sprint, outputPath);
46
- showSuccess('Requirements written to:');
47
- log.item(outputPath);
48
- } catch (err) {
49
- if (err instanceof Error) {
50
- showError(`Failed to write requirements: ${err.message}`);
51
- } else {
52
- showError('Failed to write requirements: Unknown error');
53
- }
54
- return;
55
- }
56
-
57
- log.newline();
58
- }
@@ -1,140 +0,0 @@
1
- import { colors, muted } from '@src/theme/index.ts';
2
- import { getSprint, resolveSprintId } from '@src/store/sprint.ts';
3
- import { listTasks } from '@src/store/task.ts';
4
- import { getCurrentSprint } from '@src/store/config.ts';
5
- import { formatTicketDisplay, getPendingRequirements, groupTicketsByProject } from '@src/store/ticket.ts';
6
- import {
7
- badge,
8
- formatSprintStatus,
9
- formatTaskStatus,
10
- horizontalLine,
11
- icons,
12
- labelValue,
13
- log,
14
- printCountSummary,
15
- renderCard,
16
- showNextStep,
17
- } from '@src/theme/ui.ts';
18
- import { selectSprint } from '@src/interactive/selectors.ts';
19
-
20
- export async function sprintShowCommand(args: string[]): Promise<void> {
21
- const sprintId = args[0];
22
-
23
- let id: string;
24
- try {
25
- id = await resolveSprintId(sprintId);
26
- } catch {
27
- const selected = await selectSprint('Select sprint to show:');
28
- if (!selected) return;
29
- id = selected;
30
- }
31
-
32
- const sprint = await getSprint(id);
33
- const tasks = await listTasks(id);
34
- const currentSprintId = await getCurrentSprint();
35
- const isCurrent = sprint.id === currentSprintId;
36
-
37
- // Sprint info card
38
- const infoLines: string[] = [
39
- labelValue('ID', sprint.id + (isCurrent ? ' ' + badge('current', 'success') : '')),
40
- labelValue('Status', formatSprintStatus(sprint.status)),
41
- labelValue('Created', new Date(sprint.createdAt).toLocaleString()),
42
- ];
43
- if (sprint.activatedAt) {
44
- infoLines.push(labelValue('Activated', new Date(sprint.activatedAt).toLocaleString()));
45
- }
46
- if (sprint.closedAt) {
47
- infoLines.push(labelValue('Closed', new Date(sprint.closedAt).toLocaleString()));
48
- }
49
- if (sprint.branch) {
50
- infoLines.push(labelValue('Branch', sprint.branch));
51
- }
52
-
53
- log.newline();
54
- console.log(renderCard(`${icons.sprint} ${sprint.name}`, infoLines));
55
-
56
- // Tickets card
57
- log.newline();
58
- const ticketLines: string[] = [];
59
-
60
- if (sprint.tickets.length === 0) {
61
- ticketLines.push(muted('No tickets yet'));
62
- ticketLines.push(muted(`${icons.tip} Add with: ralphctl ticket add`));
63
- } else {
64
- const ticketsByProject = groupTicketsByProject(sprint.tickets);
65
- let first = true;
66
- for (const [projectName, tickets] of ticketsByProject) {
67
- if (!first) ticketLines.push('');
68
- first = false;
69
- ticketLines.push(`${colors.info(icons.project)} ${colors.info(projectName)}`);
70
- for (const ticket of tickets) {
71
- const reqBadge =
72
- ticket.requirementStatus === 'approved' ? badge('approved', 'success') : badge('pending', 'warning');
73
- ticketLines.push(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
74
- }
75
- }
76
- }
77
-
78
- console.log(renderCard(`${icons.ticket} Tickets (${String(sprint.tickets.length)})`, ticketLines));
79
-
80
- // Tasks card
81
- log.newline();
82
- const taskLines: string[] = [];
83
-
84
- const tasksByStatus = {
85
- todo: tasks.filter((t) => t.status === 'todo').length,
86
- in_progress: tasks.filter((t) => t.status === 'in_progress').length,
87
- done: tasks.filter((t) => t.status === 'done').length,
88
- };
89
-
90
- if (tasks.length === 0) {
91
- taskLines.push(muted('No tasks yet'));
92
- taskLines.push(muted(`${icons.tip} Plan with: ralphctl sprint plan`));
93
- } else {
94
- // Status summary row
95
- taskLines.push(
96
- `${formatTaskStatus('todo')} ${String(tasksByStatus.todo)} ` +
97
- `${formatTaskStatus('in_progress')} ${String(tasksByStatus.in_progress)} ` +
98
- `${formatTaskStatus('done')} ${String(tasksByStatus.done)}`
99
- );
100
- taskLines.push(colors.muted(horizontalLine(40, 'rounded')));
101
-
102
- // Task list
103
- for (const task of tasks) {
104
- const statusIcon =
105
- task.status === 'done' ? icons.success : task.status === 'in_progress' ? icons.active : icons.inactive;
106
- const statusColor = task.status === 'done' ? 'success' : task.status === 'in_progress' ? 'warning' : 'muted';
107
- taskLines.push(`${muted(String(task.order) + '.')} ${badge(statusIcon, statusColor)} ${task.name}`);
108
- }
109
- }
110
-
111
- console.log(renderCard(`${icons.task} Tasks (${String(tasks.length)})`, taskLines));
112
-
113
- // Progress summary (outside card for consistent formatting)
114
- if (tasks.length > 0) {
115
- printCountSummary('Progress', tasksByStatus.done, tasks.length);
116
- }
117
-
118
- // State-aware next steps
119
- log.newline();
120
- if (sprint.status === 'draft') {
121
- const pendingCount = getPendingRequirements(sprint.tickets).length;
122
- if (sprint.tickets.length === 0) {
123
- showNextStep('ralphctl ticket add --project <name>', 'add tickets to this sprint');
124
- } else if (pendingCount > 0) {
125
- showNextStep('ralphctl sprint refine', 'refine ticket requirements');
126
- } else if (tasks.length === 0) {
127
- showNextStep('ralphctl sprint plan', 'generate tasks from tickets');
128
- } else {
129
- showNextStep('ralphctl sprint start', 'begin implementation');
130
- }
131
- } else if (sprint.status === 'active') {
132
- if (tasksByStatus.done === tasks.length && tasks.length > 0) {
133
- showNextStep('ralphctl sprint close', 'all tasks done — close the sprint');
134
- } else {
135
- showNextStep('ralphctl sprint start', 'continue implementation');
136
- }
137
- }
138
-
139
- log.newline();
140
- }
@@ -1,119 +0,0 @@
1
- import { type RunnerOptions, runSprint } from '@src/ai/runner.ts';
2
- import { SprintNotFoundError, SprintStatusError } from '@src/store/sprint.ts';
3
- import { EXIT_ERROR, EXIT_NO_TASKS, exitWithCode } from '@src/utils/exit-codes.ts';
4
- import { log, showError, showNextStep, showWarning } from '@src/theme/ui.ts';
5
-
6
- function parseArgs(args: string[]): { sprintId?: string; options: RunnerOptions } {
7
- const options: RunnerOptions = {
8
- step: false,
9
- count: null,
10
- session: false,
11
- noCommit: false,
12
- };
13
- let sprintId: string | undefined;
14
-
15
- for (let i = 0; i < args.length; i++) {
16
- const arg = args[i];
17
-
18
- if (arg === '-t' || arg === '--step') {
19
- options.step = true;
20
- } else if (arg === '-s' || arg === '--session') {
21
- options.session = true;
22
- } else if (arg === '--no-commit') {
23
- options.noCommit = true;
24
- } else if (arg === '-c' || arg === '--count') {
25
- const countStr = args[++i];
26
- if (!countStr) {
27
- throw new Error('--count requires a number');
28
- }
29
- const count = parseInt(countStr, 10);
30
- if (isNaN(count) || count < 1 || count > 10000) {
31
- throw new Error('--count must be an integer between 1 and 10000');
32
- }
33
- options.count = count;
34
- } else if (arg === '--concurrency') {
35
- const concStr = args[++i];
36
- if (!concStr) {
37
- throw new Error('--concurrency requires a number');
38
- }
39
- const conc = parseInt(concStr, 10);
40
- if (isNaN(conc) || conc < 1 || conc > 10) {
41
- throw new Error('--concurrency must be an integer between 1 and 10');
42
- }
43
- options.concurrency = conc;
44
- } else if (arg === '--max-retries') {
45
- const retryStr = args[++i];
46
- if (!retryStr) {
47
- throw new Error('--max-retries requires a number');
48
- }
49
- const retries = parseInt(retryStr, 10);
50
- if (isNaN(retries) || retries < 0 || retries > 20) {
51
- throw new Error('--max-retries must be an integer between 0 and 20');
52
- }
53
- options.maxRetries = retries;
54
- } else if (arg === '--fail-fast') {
55
- options.failFast = true;
56
- } else if (arg === '--no-fail-fast') {
57
- options.failFast = false;
58
- } else if (arg === '-f' || arg === '--force') {
59
- options.force = true;
60
- } else if (arg === '--refresh-check') {
61
- options.refreshCheck = true;
62
- } else if (arg === '-b' || arg === '--branch') {
63
- options.branch = true;
64
- } else if (arg === '--branch-name') {
65
- const nameStr = args[++i];
66
- if (!nameStr) {
67
- throw new Error('--branch-name requires a value');
68
- }
69
- options.branchName = nameStr;
70
- } else if (!arg?.startsWith('-')) {
71
- sprintId = arg;
72
- }
73
- }
74
-
75
- return { sprintId, options };
76
- }
77
-
78
- export async function sprintStartCommand(args: string[]): Promise<void> {
79
- let sprintId: string | undefined;
80
- let options: RunnerOptions;
81
-
82
- try {
83
- const parsed = parseArgs(args);
84
- sprintId = parsed.sprintId;
85
- options = parsed.options;
86
- } catch (err) {
87
- if (err instanceof Error) {
88
- showError(err.message);
89
- log.newline();
90
- }
91
- exitWithCode(EXIT_ERROR);
92
- }
93
-
94
- try {
95
- const summary = await runSprint(sprintId, options);
96
-
97
- // Exit with appropriate code based on execution summary
98
- if (summary) {
99
- exitWithCode(summary.exitCode);
100
- }
101
- } catch (err) {
102
- if (err instanceof SprintNotFoundError) {
103
- showError(`Sprint not found: ${sprintId ?? 'unknown'}`);
104
- log.newline();
105
- exitWithCode(EXIT_ERROR);
106
- } else if (err instanceof SprintStatusError) {
107
- showError(err.message);
108
- log.newline();
109
- exitWithCode(EXIT_ERROR);
110
- } else if (err instanceof Error && err.message.includes('No sprint specified')) {
111
- showWarning('No sprint specified and no active sprint set.');
112
- showNextStep('ralphctl sprint start <id>', 'specify a sprint ID');
113
- log.newline();
114
- exitWithCode(EXIT_NO_TASKS);
115
- } else {
116
- throw err;
117
- }
118
- }
119
- }
@@ -1,20 +0,0 @@
1
- import { setCurrentSprint } from '@src/store/config.ts';
2
- import { getSprint } from '@src/store/sprint.ts';
3
- import { selectSprint } from '@src/interactive/selectors.ts';
4
- import { log, showSuccess } from '@src/theme/ui.ts';
5
-
6
- /**
7
- * Quick sprint switcher for interactive mode
8
- */
9
- export async function sprintSwitchCommand(): Promise<void> {
10
- const selectedId = await selectSprint('Select sprint to switch to:');
11
- if (!selectedId) return;
12
-
13
- await setCurrentSprint(selectedId);
14
- const sprint = await getSprint(selectedId);
15
- showSuccess('Switched to sprint!', [
16
- ['ID', sprint.id],
17
- ['Name', sprint.name],
18
- ]);
19
- log.newline();
20
- }