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
package/dist/cli.mjs ADDED
@@ -0,0 +1,4245 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addCheckScriptToRepository,
4
+ projectAddCommand
5
+ } from "./chunk-MRKOFVTM.mjs";
6
+ import {
7
+ TaskNotFoundError,
8
+ addTask,
9
+ areAllTasksDone,
10
+ branchExists,
11
+ buildIdeateAutoPrompt,
12
+ buildIdeatePrompt,
13
+ buildTicketRefinePrompt,
14
+ exportRequirementsToMarkdown,
15
+ extractJsonObject,
16
+ formatTicketForPrompt,
17
+ getActiveProvider,
18
+ getCurrentBranch,
19
+ getDefaultBranch,
20
+ getNextTask,
21
+ getTask,
22
+ getTaskImportSchema,
23
+ getTasks,
24
+ importTasks,
25
+ inputPositiveInt,
26
+ isGhAvailable,
27
+ isGlabAvailable,
28
+ listTasks,
29
+ parsePlanningBlocked,
30
+ parseRequirementsFile,
31
+ parseTasksJson,
32
+ providerDisplayName,
33
+ removeTask,
34
+ renderParsedTasksTable,
35
+ reorderTask,
36
+ resolveProvider,
37
+ runAiSession,
38
+ saveTasks,
39
+ selectProject,
40
+ selectProjectPaths,
41
+ selectProjectRepository,
42
+ selectSprint,
43
+ selectTask,
44
+ selectTaskStatus,
45
+ selectTicket,
46
+ spawnHeadless,
47
+ spawnInteractive,
48
+ sprintPlanCommand,
49
+ sprintRefineCommand,
50
+ sprintStartCommand,
51
+ updateTaskStatus,
52
+ validateImportTasks
53
+ } from "./chunk-LOR7QBXX.mjs";
54
+ import {
55
+ escapableSelect
56
+ } from "./chunk-NTWO2LXB.mjs";
57
+ import {
58
+ sprintCreateCommand
59
+ } from "./chunk-JON4GCLR.mjs";
60
+ import {
61
+ IssueFetchError,
62
+ TicketNotFoundError,
63
+ addTicket,
64
+ allRequirementsApproved,
65
+ editorInput,
66
+ fetchIssueFromUrl,
67
+ formatIssueContext,
68
+ formatTicketDisplay,
69
+ getPendingRequirements,
70
+ getTicket,
71
+ groupTicketsByProject,
72
+ listTickets,
73
+ removeTicket,
74
+ ticketAddCommand,
75
+ updateTicket
76
+ } from "./chunk-MNMQC36F.mjs";
77
+ import {
78
+ EXIT_ERROR,
79
+ exitWithCode
80
+ } from "./chunk-7TG3EAQ2.mjs";
81
+ import {
82
+ ProjectNotFoundError,
83
+ addProjectRepo,
84
+ getProject,
85
+ listProjects,
86
+ removeProject,
87
+ removeProjectRepo
88
+ } from "./chunk-WGHJI3OI.mjs";
89
+ import {
90
+ NoCurrentSprintError,
91
+ SprintNotFoundError,
92
+ SprintStatusError,
93
+ assertSprintStatus,
94
+ closeSprint,
95
+ deleteSprint,
96
+ getAiProvider,
97
+ getConfig,
98
+ getCurrentSprint,
99
+ getCurrentSprintOrThrow,
100
+ getEditor,
101
+ getProgress,
102
+ getSprint,
103
+ listSprints,
104
+ logProgress,
105
+ resolveSprintId,
106
+ saveSprint,
107
+ setAiProvider,
108
+ setCurrentSprint,
109
+ setEditor,
110
+ withFileLock
111
+ } from "./chunk-EKMZZRWI.mjs";
112
+ import {
113
+ AiProviderSchema,
114
+ IdeateOutputSchema,
115
+ ImportTasksSchema,
116
+ RequirementStatusSchema,
117
+ SprintSchema,
118
+ SprintStatusSchema,
119
+ TaskStatusSchema,
120
+ TasksSchema,
121
+ assertSafeCwd,
122
+ expandTilde,
123
+ fileExists,
124
+ getDataDir,
125
+ getIdeateDir,
126
+ getRefinementDir,
127
+ getSchemaPath,
128
+ getSprintDir,
129
+ getSprintFilePath,
130
+ getTasksFilePath,
131
+ readValidatedJson,
132
+ validateProjectPath
133
+ } from "./chunk-6PYTKGB5.mjs";
134
+ import {
135
+ DETAIL_LABEL_WIDTH,
136
+ badge,
137
+ boxChars,
138
+ clearScreen,
139
+ colors,
140
+ createSpinner,
141
+ emoji,
142
+ error,
143
+ field,
144
+ fieldMultiline,
145
+ formatSprintStatus,
146
+ formatTaskStatus,
147
+ getQuoteForContext,
148
+ horizontalLine,
149
+ icons,
150
+ labelValue,
151
+ log,
152
+ muted,
153
+ printCountSummary,
154
+ printHeader,
155
+ printSeparator,
156
+ progressBar,
157
+ renderCard,
158
+ renderTable,
159
+ showBanner,
160
+ showEmpty,
161
+ showError,
162
+ showInfo,
163
+ showNextStep,
164
+ showNextSteps,
165
+ showRandomQuote,
166
+ showSuccess,
167
+ showTip,
168
+ showWarning,
169
+ success,
170
+ terminalBell
171
+ } from "./chunk-QBXHAXHI.mjs";
172
+
173
+ // src/cli.ts
174
+ import { Command } from "commander";
175
+
176
+ // src/interactive/menu.ts
177
+ import { Separator } from "@inquirer/prompts";
178
+ var SEPARATOR_WIDTH = 48;
179
+ function titled(label) {
180
+ const lineLen = Math.max(2, SEPARATOR_WIDTH - label.length - 4);
181
+ return new Separator(colors.muted(`
182
+ \u2500\u2500 ${label} ${"\u2500".repeat(lineLen)}`));
183
+ }
184
+ function line() {
185
+ return new Separator(colors.muted("\u2500".repeat(SEPARATOR_WIDTH)));
186
+ }
187
+ var WORKFLOW_ACTIONS = {
188
+ sprint: /* @__PURE__ */ new Set(["create", "refine", "ideate", "plan", "start", "close"]),
189
+ ticket: /* @__PURE__ */ new Set(["add", "refine"]),
190
+ task: /* @__PURE__ */ new Set(["add", "import"]),
191
+ progress: /* @__PURE__ */ new Set(["log"])
192
+ };
193
+ function isWorkflowAction(group, subCommand) {
194
+ return WORKFLOW_ACTIONS[group]?.has(subCommand) ?? false;
195
+ }
196
+ function buildPlanActions(ctx) {
197
+ const items = [];
198
+ const isDraft = ctx.currentSprintStatus === "draft";
199
+ const hasSprint = ctx.currentSprintId !== null;
200
+ items.push({ name: "Create Sprint", value: "action:sprint:create", description: "Start a new sprint" });
201
+ const addTicketDisabled = !hasSprint ? "create a sprint first" : !isDraft ? "need draft sprint" : !ctx.hasProjects ? "add a project first" : false;
202
+ items.push({
203
+ name: "Add Ticket",
204
+ value: "action:ticket:add",
205
+ description: "Add work to current sprint",
206
+ disabled: addTicketDisabled
207
+ });
208
+ let refineDisabled = false;
209
+ let refineDesc = "Clarify ticket requirements";
210
+ if (!hasSprint) {
211
+ refineDisabled = "create a sprint first";
212
+ } else if (!isDraft) {
213
+ refineDisabled = "need draft sprint";
214
+ } else if (ctx.ticketCount === 0) {
215
+ refineDisabled = "add tickets first";
216
+ } else if (ctx.pendingRequirements === 0) {
217
+ refineDisabled = "all tickets refined";
218
+ } else {
219
+ refineDesc = `${String(ctx.pendingRequirements)} ticket${ctx.pendingRequirements !== 1 ? "s" : ""} pending`;
220
+ }
221
+ items.push({
222
+ name: "Refine Requirements",
223
+ value: "action:sprint:refine",
224
+ description: refineDesc,
225
+ disabled: refineDisabled
226
+ });
227
+ let planDisabled = false;
228
+ const planDesc = "Generate tasks from requirements";
229
+ if (!hasSprint) {
230
+ planDisabled = "create a sprint first";
231
+ } else if (!isDraft) {
232
+ planDisabled = "need draft sprint";
233
+ } else if (ctx.ticketCount === 0) {
234
+ planDisabled = "add tickets first";
235
+ } else if (!ctx.allRequirementsApproved) {
236
+ planDisabled = "refine all tickets first";
237
+ }
238
+ items.push({
239
+ name: ctx.taskCount > 0 ? "Re-Plan Tasks" : "Plan Tasks",
240
+ value: "action:sprint:plan",
241
+ description: planDesc,
242
+ disabled: planDisabled
243
+ });
244
+ const ideateDisabled = !hasSprint ? "create a sprint first" : !isDraft ? "need draft sprint" : !ctx.hasProjects ? "add a project first" : false;
245
+ items.push({
246
+ name: "Ideate",
247
+ value: "action:sprint:ideate",
248
+ description: "Quick idea to tasks",
249
+ disabled: ideateDisabled
250
+ });
251
+ return items;
252
+ }
253
+ function buildExecuteActions(ctx) {
254
+ const items = [];
255
+ const isDraft = ctx.currentSprintStatus === "draft";
256
+ const isActive = ctx.currentSprintStatus === "active";
257
+ const hasSprint = ctx.currentSprintId !== null;
258
+ let startDisabled = false;
259
+ if (!hasSprint) {
260
+ startDisabled = "create a sprint first";
261
+ } else if (!isDraft && !isActive) {
262
+ startDisabled = "need draft or active sprint";
263
+ } else if (ctx.taskCount === 0) {
264
+ startDisabled = "plan tasks first";
265
+ }
266
+ items.push({
267
+ name: "Start Sprint",
268
+ value: "action:sprint:start",
269
+ description: "Begin implementation",
270
+ disabled: startDisabled
271
+ });
272
+ items.push({
273
+ name: "Health Check",
274
+ value: "action:sprint:health",
275
+ description: "Diagnose blockers and stale tasks",
276
+ disabled: !hasSprint ? "no sprint" : false
277
+ });
278
+ items.push({
279
+ name: "Close Sprint",
280
+ value: "action:sprint:close",
281
+ description: "Close the current sprint",
282
+ disabled: !isActive ? "need active sprint" : false
283
+ });
284
+ return items;
285
+ }
286
+ function buildMainMenu(ctx) {
287
+ const items = [];
288
+ let defaultValue;
289
+ if (ctx.nextAction) {
290
+ const actionValue = `action:${ctx.nextAction.group}:${ctx.nextAction.subCommand}`;
291
+ items.push({
292
+ name: `\u2192 ${ctx.nextAction.label}`,
293
+ value: actionValue,
294
+ description: ctx.nextAction.description
295
+ });
296
+ defaultValue = actionValue;
297
+ }
298
+ items.push(titled("PLAN"));
299
+ for (const action of buildPlanActions(ctx)) {
300
+ items.push(action);
301
+ }
302
+ items.push(titled("EXECUTE"));
303
+ for (const action of buildExecuteActions(ctx)) {
304
+ items.push(action);
305
+ }
306
+ items.push(titled("BROWSE"));
307
+ items.push({ name: "Sprints", value: "sprint", description: "List, show, switch" });
308
+ items.push({ name: "Tickets", value: "ticket", description: "List, show, edit" });
309
+ items.push({ name: "Tasks", value: "task", description: "List, show, manage" });
310
+ items.push(titled("SETUP"));
311
+ items.push({ name: "Projects", value: "project", description: "Manage projects & repositories" });
312
+ items.push({ name: "Configuration", value: "config", description: "AI provider, settings" });
313
+ items.push({ name: "Doctor", value: "action:doctor:run", description: "Check environment health" });
314
+ items.push(titled("SESSION"));
315
+ if (!ctx.currentSprintId) {
316
+ items.push({ name: "Quick Start Wizard", value: "wizard", description: "Guided sprint setup" });
317
+ }
318
+ items.push({ name: "Exit", value: "exit" });
319
+ return { items, defaultValue };
320
+ }
321
+ function buildSprintSubMenu(ctx) {
322
+ const items = [];
323
+ items.push(titled("BROWSE"));
324
+ items.push({ name: "List", value: "list", description: "List all sprints" });
325
+ items.push({ name: "Show", value: "show", description: "Show sprint details" });
326
+ items.push({ name: "Set Current", value: "current", description: "Set current sprint" });
327
+ items.push(titled("EXPORT"));
328
+ items.push({
329
+ name: "Requirements",
330
+ value: "requirements",
331
+ description: "Export refined requirements"
332
+ });
333
+ items.push({ name: "Context", value: "context", description: "Output full sprint context" });
334
+ items.push({ name: "Progress", value: "progress show", description: "View progress log" });
335
+ items.push(titled("MANAGE"));
336
+ items.push({ name: "Log Progress", value: "progress log", description: "Add progress entry" });
337
+ items.push({ name: "Delete", value: "delete", description: "Delete a sprint permanently" });
338
+ items.push(line());
339
+ items.push({ name: "Back", value: "back", description: "Return to main menu" });
340
+ const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName} (${ctx.currentSprintStatus ?? "unknown"})` : "";
341
+ return { title: `Sprint${titleSuffix}`, items };
342
+ }
343
+ function buildTicketSubMenu(ctx) {
344
+ const items = [];
345
+ items.push({
346
+ name: "Add",
347
+ value: "add",
348
+ description: ctx.hasProjects ? "Add a ticket" : "Add a ticket (add a project first)",
349
+ disabled: !ctx.hasProjects ? "add a project first" : false
350
+ });
351
+ items.push({ name: "Edit", value: "edit", description: "Edit a ticket" });
352
+ items.push({ name: "List", value: "list", description: "List all tickets" });
353
+ items.push({ name: "Show", value: "show", description: "Show ticket details" });
354
+ const approvedCount = ctx.ticketCount - ctx.pendingRequirements;
355
+ let refineDisabled = false;
356
+ if (ctx.currentSprintStatus !== "draft") {
357
+ refineDisabled = "need draft sprint";
358
+ } else if (approvedCount === 0) {
359
+ refineDisabled = "no approved tickets";
360
+ }
361
+ items.push({
362
+ name: "Refine",
363
+ value: "refine",
364
+ description: "Re-refine approved requirements",
365
+ disabled: refineDisabled
366
+ });
367
+ items.push(line());
368
+ items.push({ name: "Remove", value: "remove", description: "Remove a ticket" });
369
+ items.push({ name: "Back", value: "back", description: "Return to main menu" });
370
+ const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName}` : "";
371
+ return { title: `Ticket${titleSuffix}`, items };
372
+ }
373
+ function buildTaskSubMenu(ctx) {
374
+ const items = [];
375
+ items.push(titled("VIEW"));
376
+ items.push({ name: "List", value: "list", description: "List all tasks" });
377
+ items.push({ name: "Show", value: "show", description: "Show task details" });
378
+ items.push({ name: "Next", value: "next", description: "Get next task" });
379
+ items.push(titled("MANAGE"));
380
+ items.push({ name: "Add", value: "add", description: "Add a new task" });
381
+ items.push({ name: "Import", value: "import", description: "Import from JSON" });
382
+ items.push({ name: "Status", value: "status", description: "Update status" });
383
+ items.push({ name: "Reorder", value: "reorder", description: "Change priority" });
384
+ items.push(line());
385
+ items.push({ name: "Remove", value: "remove", description: "Remove a task" });
386
+ items.push({ name: "Back", value: "back", description: "Return to main menu" });
387
+ const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName}` : "";
388
+ return { title: `Task${titleSuffix}`, items };
389
+ }
390
+ function buildProjectSubMenu() {
391
+ const items = [];
392
+ items.push({ name: "Add", value: "add", description: "Add a new project" });
393
+ items.push({ name: "List", value: "list", description: "List all projects" });
394
+ items.push({ name: "Show", value: "show", description: "Show project details" });
395
+ items.push(titled("REPOSITORIES"));
396
+ items.push({
397
+ name: "Add Repository",
398
+ value: "repo add",
399
+ description: "Add repository to project"
400
+ });
401
+ items.push({ name: "Remove Repository", value: "repo remove", description: "Remove repository" });
402
+ items.push(line());
403
+ items.push({ name: "Remove", value: "remove", description: "Remove a project" });
404
+ items.push({ name: "Back", value: "back", description: "Return to main menu" });
405
+ return { title: "Project", items };
406
+ }
407
+ function buildConfigSubMenu() {
408
+ const items = [];
409
+ items.push({ name: "Show Settings", value: "show", description: "View current configuration" });
410
+ items.push({ name: "Set AI Provider", value: "set provider", description: "Choose Claude Code or GitHub Copilot" });
411
+ items.push(line());
412
+ items.push({ name: "Back", value: "back", description: "Return to main menu" });
413
+ return { title: "Configuration", items };
414
+ }
415
+ function buildSubMenu(group, ctx) {
416
+ switch (group) {
417
+ case "sprint":
418
+ return buildSprintSubMenu(ctx);
419
+ case "ticket":
420
+ return buildTicketSubMenu(ctx);
421
+ case "task":
422
+ return buildTaskSubMenu(ctx);
423
+ case "project":
424
+ return buildProjectSubMenu();
425
+ case "config":
426
+ return buildConfigSubMenu();
427
+ default:
428
+ return null;
429
+ }
430
+ }
431
+
432
+ // src/interactive/dashboard.ts
433
+ async function loadDashboardData() {
434
+ const sprintId = await getCurrentSprint();
435
+ if (!sprintId) return null;
436
+ try {
437
+ const sprint = await getSprint(sprintId);
438
+ const tasks = await getTasks(sprintId);
439
+ const pendingTickets = getPendingRequirements(sprint.tickets);
440
+ const pendingCount = pendingTickets.length;
441
+ const approvedCount = sprint.tickets.length - pendingCount;
442
+ const doneIds = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
443
+ const blockedCount = tasks.filter(
444
+ (t) => t.status !== "done" && t.blockedBy.length > 0 && !t.blockedBy.every((id) => doneIds.has(id))
445
+ ).length;
446
+ const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
447
+ const plannedTicketCount = sprint.tickets.filter((t) => ticketIdsWithTasks.has(t.id)).length;
448
+ const aiProvider = await getAiProvider();
449
+ return { sprint, tasks, approvedCount, pendingCount, blockedCount, plannedTicketCount, aiProvider };
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+ function getNextAction(data) {
455
+ const { sprint, tasks, pendingCount, approvedCount } = data;
456
+ const ticketCount = sprint.tickets.length;
457
+ const totalTasks = tasks.length;
458
+ const allDone = totalTasks > 0 && tasks.every((t) => t.status === "done");
459
+ if (sprint.status === "draft") {
460
+ if (ticketCount === 0) {
461
+ return { label: "Add Ticket", description: "No tickets yet", group: "ticket", subCommand: "add" };
462
+ }
463
+ if (pendingCount > 0) {
464
+ return {
465
+ label: "Refine Requirements",
466
+ description: `${String(pendingCount)} ticket${pendingCount !== 1 ? "s" : ""} pending`,
467
+ group: "sprint",
468
+ subCommand: "refine"
469
+ };
470
+ }
471
+ if (approvedCount > 0 && totalTasks === 0) {
472
+ return { label: "Plan Tasks", description: "Requirements approved", group: "sprint", subCommand: "plan" };
473
+ }
474
+ if (totalTasks > 0 && data.plannedTicketCount < ticketCount) {
475
+ const unplanned = ticketCount - data.plannedTicketCount;
476
+ return {
477
+ label: "Re-Plan Tasks",
478
+ description: `${String(unplanned)} unplanned ticket${unplanned !== 1 ? "s" : ""}`,
479
+ group: "sprint",
480
+ subCommand: "plan"
481
+ };
482
+ }
483
+ if (totalTasks > 0) {
484
+ return {
485
+ label: "Start Sprint",
486
+ description: `${String(totalTasks)} task${totalTasks !== 1 ? "s" : ""} ready`,
487
+ group: "sprint",
488
+ subCommand: "start"
489
+ };
490
+ }
491
+ }
492
+ if (sprint.status === "active") {
493
+ if (allDone) {
494
+ return { label: "Close Sprint", description: "All tasks done", group: "sprint", subCommand: "close" };
495
+ }
496
+ return {
497
+ label: "Continue Work",
498
+ description: `${String(totalTasks - tasks.filter((t) => t.status === "done").length)} task${totalTasks - tasks.filter((t) => t.status === "done").length !== 1 ? "s" : ""} remaining`,
499
+ group: "sprint",
500
+ subCommand: "start"
501
+ };
502
+ }
503
+ return null;
504
+ }
505
+ function renderStatusHeader(data) {
506
+ if (!data) return [];
507
+ const { sprint, tasks, approvedCount, aiProvider } = data;
508
+ const totalTasks = tasks.length;
509
+ const ticketCount = sprint.tickets.length;
510
+ const lines = [];
511
+ const sprintLabel = colors.highlight(sprint.name);
512
+ const statusBadge = formatSprintStatus(sprint.status);
513
+ const ticketPart = `${String(ticketCount)} ticket${ticketCount !== 1 ? "s" : ""}`;
514
+ const taskPart = `${String(totalTasks)} task${totalTasks !== 1 ? "s" : ""}`;
515
+ const providerPart = aiProvider === "claude" ? "Claude" : aiProvider === "copilot" ? "Copilot" : null;
516
+ const providerSuffix = providerPart ? ` | ${providerPart}` : "";
517
+ lines.push(
518
+ ` ${icons.sprint} ${sprintLabel} ${statusBadge} ${colors.muted(`| ${ticketPart} | ${taskPart}${providerSuffix}`)}`
519
+ );
520
+ if ((sprint.status === "active" || sprint.status === "closed") && totalTasks > 0) {
521
+ const doneCount = tasks.filter((t) => t.status === "done").length;
522
+ const bar = progressBar(doneCount, totalTasks, { width: 15 });
523
+ const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
524
+ const todoCount = tasks.filter((t) => t.status === "todo").length;
525
+ lines.push(
526
+ ` ${bar} ${colors.muted(`${String(doneCount)} done, ${String(inProgressCount)} active, ${String(todoCount)} todo`)}`
527
+ );
528
+ } else if (sprint.status === "draft" && ticketCount > 0) {
529
+ const refinedColor = approvedCount === ticketCount ? colors.success : colors.warning;
530
+ const refinedPart = refinedColor(`Refined: ${String(approvedCount)}/${String(ticketCount)}`);
531
+ const plannedColor = data.plannedTicketCount === ticketCount ? colors.success : colors.muted;
532
+ const plannedPart = plannedColor(`Planned: ${String(data.plannedTicketCount)}/${String(ticketCount)}`);
533
+ lines.push(` ${refinedPart} ${colors.muted("|")} ${plannedPart}`);
534
+ }
535
+ return lines;
536
+ }
537
+ function renderDashboard(data) {
538
+ const { sprint, tasks, approvedCount, blockedCount } = data;
539
+ const chars = boxChars.rounded;
540
+ const todoCount = tasks.filter((t) => t.status === "todo").length;
541
+ const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
542
+ const doneCount = tasks.filter((t) => t.status === "done").length;
543
+ const totalTasks = tasks.length;
544
+ const ticketCount = sprint.tickets.length;
545
+ const lines = [];
546
+ const sprintLabel = colors.highlight(sprint.name);
547
+ const statusBadge = formatSprintStatus(sprint.status);
548
+ lines.push(` ${icons.sprint} ${sprintLabel} ${statusBadge}`);
549
+ const ticketSummary = `${String(ticketCount)} ticket${ticketCount !== 1 ? "s" : ""}`;
550
+ const taskSummary = `${String(totalTasks)} task${totalTasks !== 1 ? "s" : ""}`;
551
+ lines.push(` ${colors.muted(`${ticketSummary} ${chars.vertical} ${taskSummary}`)}`);
552
+ if (totalTasks > 0) {
553
+ const bar = progressBar(doneCount, totalTasks);
554
+ const detail = colors.muted(
555
+ `${String(doneCount)} done, ${String(inProgressCount)} active, ${String(todoCount)} todo`
556
+ );
557
+ lines.push(` ${bar} ${detail}`);
558
+ }
559
+ if (sprint.status === "draft" && ticketCount > 0) {
560
+ const refinedColor = approvedCount === ticketCount ? colors.success : colors.warning;
561
+ const refinedPart = refinedColor(`Refined: ${String(approvedCount)}/${String(ticketCount)}`);
562
+ const plannedColor = data.plannedTicketCount === ticketCount ? colors.success : colors.muted;
563
+ const plannedPart = plannedColor(`Planned: ${String(data.plannedTicketCount)}/${String(ticketCount)}`);
564
+ lines.push(` ${refinedPart} ${colors.muted("|")} ${plannedPart}`);
565
+ }
566
+ if (blockedCount > 0) {
567
+ lines.push(
568
+ ` ${colors.warning(icons.warning)} ${colors.warning(`${String(blockedCount)} blocked task${blockedCount !== 1 ? "s" : ""}`)}`
569
+ );
570
+ }
571
+ const nextAction = getNextAction(data);
572
+ if (nextAction) {
573
+ lines.push(
574
+ ` ${colors.muted(icons.tip)} ${colors.muted(nextAction.label + ":")} ${colors.highlight(nextAction.description)}`
575
+ );
576
+ }
577
+ return lines;
578
+ }
579
+ function renderEmptyDashboard() {
580
+ const quote = getQuoteForContext("idle");
581
+ return [
582
+ ` ${emoji.donut} ${colors.muted("No current sprint")}`,
583
+ ` ${colors.muted(`"${quote}"`)}`,
584
+ "",
585
+ ` ${colors.muted(icons.tip)} ${colors.muted("Get started:")}`,
586
+ ` ${colors.muted("1.")} ${colors.muted("Add a project:")} ${colors.highlight("ralphctl project add")}`,
587
+ ` ${colors.muted("2.")} ${colors.muted("Create a sprint:")} ${colors.highlight("ralphctl sprint create")}`
588
+ ];
589
+ }
590
+ async function showDashboard() {
591
+ const data = await loadDashboardData();
592
+ console.log("");
593
+ if (data) {
594
+ const lines = renderDashboard(data);
595
+ for (const line2 of lines) {
596
+ console.log(line2);
597
+ }
598
+ } else {
599
+ const lines = renderEmptyDashboard();
600
+ for (const line2 of lines) {
601
+ console.log(line2);
602
+ }
603
+ }
604
+ console.log("");
605
+ }
606
+
607
+ // src/interactive/index.ts
608
+ import { select as select3 } from "@inquirer/prompts";
609
+
610
+ // src/commands/project/list.ts
611
+ async function projectListCommand() {
612
+ const projects = await listProjects();
613
+ if (projects.length === 0) {
614
+ showEmpty("projects", "Add one with: ralphctl project add");
615
+ return;
616
+ }
617
+ printHeader("Projects", icons.project);
618
+ for (const project of projects) {
619
+ const repoCount = muted(
620
+ `(${String(project.repositories.length)} repo${project.repositories.length !== 1 ? "s" : ""})`
621
+ );
622
+ log.raw(`${colors.highlight(project.name)} ${project.displayName} ${repoCount}`);
623
+ for (const repo of project.repositories) {
624
+ log.item(`${repo.name} ${muted("\u2192")} ${muted(repo.path)}`);
625
+ }
626
+ if (project.description) {
627
+ log.dim(` ${project.description}`);
628
+ }
629
+ log.newline();
630
+ }
631
+ log.dim(`Showing ${String(projects.length)} project(s)`);
632
+ log.newline();
633
+ }
634
+
635
+ // src/commands/project/show.ts
636
+ async function projectShowCommand(args) {
637
+ let projectName = args[0];
638
+ if (!projectName) {
639
+ const selected = await selectProject("Select project to show:");
640
+ if (!selected) return;
641
+ projectName = selected;
642
+ }
643
+ try {
644
+ const project = await getProject(projectName);
645
+ const infoLines = [labelValue("Name", project.name), labelValue("Display Name", project.displayName)];
646
+ if (project.description) {
647
+ infoLines.push(labelValue("Description", project.description));
648
+ }
649
+ infoLines.push(labelValue("Repositories", String(project.repositories.length)));
650
+ log.newline();
651
+ console.log(renderCard(`${icons.project} ${project.displayName}`, infoLines));
652
+ for (const repo of project.repositories) {
653
+ log.newline();
654
+ const repoLines = [labelValue("Path", repo.path)];
655
+ if (repo.checkScript) {
656
+ repoLines.push(labelValue("Check", colors.info(repo.checkScript)));
657
+ } else {
658
+ repoLines.push(muted("No check script configured"));
659
+ }
660
+ console.log(renderCard(` ${repo.name}`, repoLines));
661
+ }
662
+ log.newline();
663
+ } catch (err) {
664
+ if (err instanceof ProjectNotFoundError) {
665
+ showError(`Project not found: ${projectName}`);
666
+ log.newline();
667
+ } else {
668
+ throw err;
669
+ }
670
+ }
671
+ }
672
+
673
+ // src/commands/project/remove.ts
674
+ import { confirm } from "@inquirer/prompts";
675
+ async function projectRemoveCommand(args) {
676
+ const skipConfirm = args.includes("-y") || args.includes("--yes");
677
+ let projectName = args.find((a) => !a.startsWith("-"));
678
+ if (!projectName) {
679
+ const selected = await selectProject("Select project to remove:");
680
+ if (!selected) return;
681
+ projectName = selected;
682
+ }
683
+ try {
684
+ const project = await getProject(projectName);
685
+ if (!skipConfirm) {
686
+ const confirmed = await confirm({
687
+ message: `Remove project "${project.displayName}" (${project.name})?`,
688
+ default: false
689
+ });
690
+ if (!confirmed) {
691
+ console.log(muted("\nProject removal cancelled.\n"));
692
+ return;
693
+ }
694
+ }
695
+ await removeProject(projectName);
696
+ showSuccess("Project removed", [["Name", projectName]]);
697
+ console.log("");
698
+ } catch (err) {
699
+ if (err instanceof ProjectNotFoundError) {
700
+ showError(`Project not found: ${projectName}`);
701
+ console.log("");
702
+ } else {
703
+ throw err;
704
+ }
705
+ }
706
+ }
707
+
708
+ // src/commands/project/repo.ts
709
+ import { basename, resolve } from "path";
710
+ import { confirm as confirm2, input, select } from "@inquirer/prompts";
711
+ async function projectRepoAddCommand(args) {
712
+ let projectName = args[0];
713
+ let path = args[1];
714
+ if (!projectName) {
715
+ const selected = await selectProject("Select project to add repository to:");
716
+ if (!selected) return;
717
+ projectName = selected;
718
+ }
719
+ path ??= await input({
720
+ message: `${emoji.donut} Repository path to add:`,
721
+ validate: (v) => v.trim().length > 0 ? true : "Path is required"
722
+ });
723
+ try {
724
+ const resolvedPath = resolve(expandTilde(path));
725
+ const bareRepo = { name: basename(resolvedPath), path: resolvedPath };
726
+ log.info(`
727
+ Configuring: ${bareRepo.name}`);
728
+ const repoWithScripts = await addCheckScriptToRepository(bareRepo);
729
+ const project = await addProjectRepo(projectName, repoWithScripts);
730
+ showSuccess("Repository added", [["Project", projectName]]);
731
+ log.newline();
732
+ log.info("Current repositories:");
733
+ for (const repo of project.repositories) {
734
+ log.item(`${repo.name} \u2192 ${repo.path}`);
735
+ }
736
+ log.newline();
737
+ } catch (err) {
738
+ if (err instanceof ProjectNotFoundError) {
739
+ showError(`Project not found: ${projectName}`);
740
+ log.newline();
741
+ } else if (err instanceof Error) {
742
+ showError(err.message);
743
+ log.newline();
744
+ } else {
745
+ throw err;
746
+ }
747
+ }
748
+ }
749
+ async function projectRepoRemoveCommand(args) {
750
+ const skipConfirm = args.includes("-y") || args.includes("--yes");
751
+ const filteredArgs = args.filter((a) => !a.startsWith("-"));
752
+ let projectName = filteredArgs[0];
753
+ let path = filteredArgs[1];
754
+ if (!projectName) {
755
+ const selected = await selectProject("Select project to remove repository from:");
756
+ if (!selected) return;
757
+ projectName = selected;
758
+ }
759
+ try {
760
+ const project = await getProject(projectName);
761
+ if (!path) {
762
+ if (project.repositories.length === 0) {
763
+ console.log(muted("\nNo repositories to remove.\n"));
764
+ return;
765
+ }
766
+ path = await select({
767
+ message: `${emoji.donut} Select repository to remove:`,
768
+ choices: project.repositories.map((r) => ({
769
+ name: `${r.name} (${r.path})`,
770
+ value: r.path
771
+ }))
772
+ });
773
+ }
774
+ if (!skipConfirm) {
775
+ const confirmed = await confirm2({
776
+ message: `Remove repository "${path}" from project "${project.displayName}"?`,
777
+ default: false
778
+ });
779
+ if (!confirmed) {
780
+ console.log(muted("\nRepository removal cancelled.\n"));
781
+ return;
782
+ }
783
+ }
784
+ const updatedProject = await removeProjectRepo(projectName, path);
785
+ showSuccess("Repository removed", [["Project", projectName]]);
786
+ log.newline();
787
+ log.info("Remaining repositories:");
788
+ for (const repo of updatedProject.repositories) {
789
+ log.item(`${repo.name} \u2192 ${repo.path}`);
790
+ }
791
+ log.newline();
792
+ } catch (err) {
793
+ if (err instanceof ProjectNotFoundError) {
794
+ showError(`Project not found: ${projectName}`);
795
+ log.newline();
796
+ } else if (err instanceof Error) {
797
+ showError(err.message);
798
+ log.newline();
799
+ } else {
800
+ throw err;
801
+ }
802
+ }
803
+ }
804
+
805
+ // src/commands/sprint/list.ts
806
+ async function sprintListCommand(args = []) {
807
+ let statusFilter;
808
+ for (let i = 0; i < args.length; i++) {
809
+ if (args[i] === "--status" && args[i + 1]) {
810
+ statusFilter = args[i + 1];
811
+ i++;
812
+ }
813
+ }
814
+ if (statusFilter) {
815
+ const result = SprintStatusSchema.safeParse(statusFilter);
816
+ if (!result.success) {
817
+ showError(`Invalid status: "${statusFilter}". Valid values: draft, active, closed`);
818
+ return;
819
+ }
820
+ }
821
+ const sprints = await listSprints();
822
+ if (sprints.length === 0) {
823
+ showEmpty("sprints", "Create one with: ralphctl sprint create");
824
+ return;
825
+ }
826
+ const filtered = statusFilter ? sprints.filter((s) => s.status === statusFilter) : sprints;
827
+ const isFiltered = filtered.length !== sprints.length;
828
+ const filterStr = statusFilter ? ` (filtered: status=${statusFilter})` : "";
829
+ if (filtered.length === 0) {
830
+ showEmpty("matching sprints", "Try adjusting your filters");
831
+ return;
832
+ }
833
+ printHeader("Sprints", icons.sprint);
834
+ const currentSprintId = await getCurrentSprint();
835
+ const rows = filtered.map((sprint) => {
836
+ const isCurrent = sprint.id === currentSprintId;
837
+ const marker = isCurrent ? badge("current", "success") : "";
838
+ return [marker, sprint.id, formatSprintStatus(sprint.status), sprint.name, String(sprint.tickets.length)];
839
+ });
840
+ console.log(
841
+ renderTable(
842
+ [
843
+ { header: "", minWidth: 0 },
844
+ { header: "ID" },
845
+ { header: "Status" },
846
+ { header: "Name" },
847
+ { header: "Tickets", align: "right" }
848
+ ],
849
+ rows
850
+ )
851
+ );
852
+ log.newline();
853
+ const showingLabel = isFiltered ? `Showing ${String(filtered.length)} of ${String(sprints.length)} sprint(s)${filterStr}` : `Showing ${String(sprints.length)} sprint(s)`;
854
+ log.dim(showingLabel);
855
+ const hasActive = sprints.some((s) => s.status === "active");
856
+ if (!hasActive) {
857
+ log.newline();
858
+ showNextStep("ralphctl sprint start", "start a sprint");
859
+ }
860
+ log.newline();
861
+ }
862
+
863
+ // src/commands/sprint/show.ts
864
+ async function sprintShowCommand(args) {
865
+ const sprintId = args[0];
866
+ let id;
867
+ try {
868
+ id = await resolveSprintId(sprintId);
869
+ } catch {
870
+ const selected = await selectSprint("Select sprint to show:");
871
+ if (!selected) return;
872
+ id = selected;
873
+ }
874
+ const sprint = await getSprint(id);
875
+ const tasks = await listTasks(id);
876
+ const currentSprintId = await getCurrentSprint();
877
+ const isCurrent = sprint.id === currentSprintId;
878
+ const infoLines = [
879
+ labelValue("ID", sprint.id + (isCurrent ? " " + badge("current", "success") : "")),
880
+ labelValue("Status", formatSprintStatus(sprint.status)),
881
+ labelValue("Created", new Date(sprint.createdAt).toLocaleString())
882
+ ];
883
+ if (sprint.activatedAt) {
884
+ infoLines.push(labelValue("Activated", new Date(sprint.activatedAt).toLocaleString()));
885
+ }
886
+ if (sprint.closedAt) {
887
+ infoLines.push(labelValue("Closed", new Date(sprint.closedAt).toLocaleString()));
888
+ }
889
+ if (sprint.branch) {
890
+ infoLines.push(labelValue("Branch", sprint.branch));
891
+ }
892
+ log.newline();
893
+ console.log(renderCard(`${icons.sprint} ${sprint.name}`, infoLines));
894
+ log.newline();
895
+ const ticketLines = [];
896
+ if (sprint.tickets.length === 0) {
897
+ ticketLines.push(muted("No tickets yet"));
898
+ ticketLines.push(muted(`${icons.tip} Add with: ralphctl ticket add`));
899
+ } else {
900
+ const ticketsByProject = groupTicketsByProject(sprint.tickets);
901
+ let first = true;
902
+ for (const [projectName, tickets] of ticketsByProject) {
903
+ if (!first) ticketLines.push("");
904
+ first = false;
905
+ ticketLines.push(`${colors.info(icons.project)} ${colors.info(projectName)}`);
906
+ for (const ticket of tickets) {
907
+ const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "warning");
908
+ ticketLines.push(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
909
+ }
910
+ }
911
+ }
912
+ console.log(renderCard(`${icons.ticket} Tickets (${String(sprint.tickets.length)})`, ticketLines));
913
+ log.newline();
914
+ const taskLines = [];
915
+ const tasksByStatus = {
916
+ todo: tasks.filter((t) => t.status === "todo").length,
917
+ in_progress: tasks.filter((t) => t.status === "in_progress").length,
918
+ done: tasks.filter((t) => t.status === "done").length
919
+ };
920
+ if (tasks.length === 0) {
921
+ taskLines.push(muted("No tasks yet"));
922
+ taskLines.push(muted(`${icons.tip} Plan with: ralphctl sprint plan`));
923
+ } else {
924
+ taskLines.push(
925
+ `${formatTaskStatus("todo")} ${String(tasksByStatus.todo)} ${formatTaskStatus("in_progress")} ${String(tasksByStatus.in_progress)} ${formatTaskStatus("done")} ${String(tasksByStatus.done)}`
926
+ );
927
+ taskLines.push(colors.muted(horizontalLine(40, "rounded")));
928
+ for (const task of tasks) {
929
+ const statusIcon = task.status === "done" ? icons.success : task.status === "in_progress" ? icons.active : icons.inactive;
930
+ const statusColor = task.status === "done" ? "success" : task.status === "in_progress" ? "warning" : "muted";
931
+ taskLines.push(`${muted(String(task.order) + ".")} ${badge(statusIcon, statusColor)} ${task.name}`);
932
+ }
933
+ }
934
+ console.log(renderCard(`${icons.task} Tasks (${String(tasks.length)})`, taskLines));
935
+ if (tasks.length > 0) {
936
+ printCountSummary("Progress", tasksByStatus.done, tasks.length);
937
+ }
938
+ log.newline();
939
+ if (sprint.status === "draft") {
940
+ const pendingCount = getPendingRequirements(sprint.tickets).length;
941
+ if (sprint.tickets.length === 0) {
942
+ showNextStep("ralphctl ticket add --project <name>", "add tickets to this sprint");
943
+ } else if (pendingCount > 0) {
944
+ showNextStep("ralphctl sprint refine", "refine ticket requirements");
945
+ } else if (tasks.length === 0) {
946
+ showNextStep("ralphctl sprint plan", "generate tasks from tickets");
947
+ } else {
948
+ showNextStep("ralphctl sprint start", "begin implementation");
949
+ }
950
+ } else if (sprint.status === "active") {
951
+ if (tasksByStatus.done === tasks.length && tasks.length > 0) {
952
+ showNextStep("ralphctl sprint close", "all tasks done \u2014 close the sprint");
953
+ } else {
954
+ showNextStep("ralphctl sprint start", "continue implementation");
955
+ }
956
+ }
957
+ log.newline();
958
+ }
959
+
960
+ // src/commands/sprint/context.ts
961
+ async function sprintContextCommand(args) {
962
+ const sprintId = args[0];
963
+ let id;
964
+ try {
965
+ id = await resolveSprintId(sprintId);
966
+ } catch {
967
+ const selected = await selectSprint("Select sprint to show context for:");
968
+ if (!selected) {
969
+ showWarning("No sprints available.");
970
+ showNextStep("ralphctl sprint create", "create a sprint first");
971
+ log.newline();
972
+ return;
973
+ }
974
+ id = selected;
975
+ }
976
+ const sprint = await getSprint(id);
977
+ const tasks = await listTasks(id);
978
+ console.log(`# Sprint: ${sprint.name}`);
979
+ console.log(`ID: ${sprint.id}`);
980
+ console.log(`Status: ${sprint.status}`);
981
+ console.log("");
982
+ console.log("## Tickets");
983
+ console.log("");
984
+ if (sprint.tickets.length === 0) {
985
+ console.log("_No tickets defined_");
986
+ } else {
987
+ const ticketsByProject = groupTicketsByProject(sprint.tickets);
988
+ for (const [projectName, tickets] of ticketsByProject) {
989
+ console.log(`### Project: ${projectName}`);
990
+ try {
991
+ const project = await getProject(projectName);
992
+ const repoPaths = project.repositories.map((r) => `${r.name} (${r.path})`);
993
+ console.log(`Repositories: ${repoPaths.join(", ")}`);
994
+ } catch {
995
+ console.log("Repositories: (project not found)");
996
+ }
997
+ console.log("");
998
+ for (const ticket of tickets) {
999
+ const reqBadge = ticket.requirementStatus === "approved" ? " [approved]" : " [pending]";
1000
+ console.log(`#### ${formatTicketDisplay(ticket)}${reqBadge}`);
1001
+ if (ticket.description) {
1002
+ console.log("");
1003
+ console.log(ticket.description);
1004
+ }
1005
+ if (ticket.link) {
1006
+ console.log("");
1007
+ console.log(`Link: ${ticket.link}`);
1008
+ }
1009
+ if (ticket.requirements) {
1010
+ console.log("");
1011
+ console.log("**Refined Requirements:**");
1012
+ console.log("");
1013
+ console.log(ticket.requirements);
1014
+ }
1015
+ console.log("");
1016
+ }
1017
+ }
1018
+ }
1019
+ console.log("## Tasks");
1020
+ console.log("");
1021
+ if (tasks.length === 0) {
1022
+ console.log("_No tasks defined yet_");
1023
+ } else {
1024
+ for (const task of tasks) {
1025
+ const ticketRef = task.ticketId ? ` [${task.ticketId}]` : "";
1026
+ console.log(`### ${task.id}: ${task.name}${ticketRef}`);
1027
+ console.log(`Status: ${task.status} | Order: ${String(task.order)} | Project: ${task.projectPath}`);
1028
+ if (task.blockedBy.length > 0) {
1029
+ console.log(`Blocked By: ${task.blockedBy.join(", ")}`);
1030
+ }
1031
+ if (task.description) {
1032
+ console.log("");
1033
+ console.log(task.description);
1034
+ }
1035
+ if (task.steps.length > 0) {
1036
+ console.log("");
1037
+ console.log("Steps:");
1038
+ task.steps.forEach((step, i) => {
1039
+ console.log(`${String(i + 1)}. ${step}`);
1040
+ });
1041
+ }
1042
+ console.log("");
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ // src/commands/sprint/current.ts
1048
+ async function sprintCurrentCommand(args) {
1049
+ const sprintId = args[0];
1050
+ if (!sprintId) {
1051
+ const currentSprintId = await getCurrentSprint();
1052
+ if (!currentSprintId) {
1053
+ showWarning("No current sprint set.");
1054
+ showNextStep("ralphctl sprint create", "create a new sprint");
1055
+ log.newline();
1056
+ return;
1057
+ }
1058
+ try {
1059
+ const sprint = await getSprint(currentSprintId);
1060
+ printHeader("Current Sprint");
1061
+ console.log(field("ID", sprint.id));
1062
+ console.log(field("Name", sprint.name));
1063
+ console.log(field("Status", formatSprintStatus(sprint.status)));
1064
+ log.newline();
1065
+ } catch {
1066
+ showWarning(`Current sprint "${currentSprintId}" no longer exists.`);
1067
+ showNextStep("ralphctl sprint current -", "select a different sprint");
1068
+ log.newline();
1069
+ }
1070
+ return;
1071
+ }
1072
+ if (sprintId === "-" || sprintId === "--select") {
1073
+ const selectedId = await selectSprint("Select current sprint:", ["draft", "active"]);
1074
+ if (!selectedId) return;
1075
+ await setCurrentSprint(selectedId);
1076
+ const sprint = await getSprint(selectedId);
1077
+ showSuccess("Current sprint set!", [
1078
+ ["ID", sprint.id],
1079
+ ["Name", sprint.name]
1080
+ ]);
1081
+ log.newline();
1082
+ } else {
1083
+ try {
1084
+ const sprint = await getSprint(sprintId);
1085
+ await setCurrentSprint(sprintId);
1086
+ showSuccess("Current sprint set!", [
1087
+ ["ID", sprint.id],
1088
+ ["Name", sprint.name]
1089
+ ]);
1090
+ log.newline();
1091
+ } catch (err) {
1092
+ if (err instanceof SprintNotFoundError) {
1093
+ showError(`Sprint not found: ${sprintId}`);
1094
+ showNextStep("ralphctl sprint list", "see available sprints");
1095
+ log.newline();
1096
+ } else {
1097
+ throw err;
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ // src/commands/sprint/ideate.ts
1104
+ import { mkdir, readFile, writeFile } from "fs/promises";
1105
+ import { join } from "path";
1106
+ import { input as input2, select as select2 } from "@inquirer/prompts";
1107
+ function parseArgs(args) {
1108
+ const options = {
1109
+ auto: false,
1110
+ allPaths: false
1111
+ };
1112
+ let sprintId;
1113
+ for (let i = 0; i < args.length; i++) {
1114
+ const arg = args[i];
1115
+ const nextArg = args[i + 1];
1116
+ if (arg === "--auto") {
1117
+ options.auto = true;
1118
+ } else if (arg === "--all-paths") {
1119
+ options.allPaths = true;
1120
+ } else if (arg === "--project") {
1121
+ options.project = nextArg;
1122
+ i++;
1123
+ } else if (!arg?.startsWith("-")) {
1124
+ sprintId = arg;
1125
+ }
1126
+ }
1127
+ return { sprintId, options };
1128
+ }
1129
+ async function invokeAiInteractive(prompt, repoPaths, ideateDir) {
1130
+ const contextFile = join(ideateDir, "ideate-context.md");
1131
+ await writeFile(contextFile, prompt, "utf-8");
1132
+ const provider = await getActiveProvider();
1133
+ 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.`;
1134
+ const args = ["--add-dir", ...repoPaths];
1135
+ const result = spawnInteractive(
1136
+ startPrompt,
1137
+ {
1138
+ cwd: ideateDir,
1139
+ args,
1140
+ env: provider.getSpawnEnv()
1141
+ },
1142
+ provider
1143
+ );
1144
+ if (result.error) {
1145
+ throw new Error(result.error);
1146
+ }
1147
+ }
1148
+ async function invokeAiAuto(prompt, repoPaths, ideateDir) {
1149
+ const provider = await getActiveProvider();
1150
+ const args = ["--permission-mode", "plan", "--print"];
1151
+ for (const path of repoPaths) {
1152
+ args.push("--add-dir", path);
1153
+ }
1154
+ args.push("-p", prompt);
1155
+ return spawnHeadless(
1156
+ {
1157
+ cwd: ideateDir,
1158
+ args,
1159
+ env: provider.getSpawnEnv()
1160
+ },
1161
+ provider
1162
+ );
1163
+ }
1164
+ function parseIdeateOutput(output) {
1165
+ const jsonStr = extractJsonObject(output);
1166
+ let parsed;
1167
+ try {
1168
+ parsed = JSON.parse(jsonStr);
1169
+ } catch (err) {
1170
+ throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : "parse error"}`, { cause: err });
1171
+ }
1172
+ const result = IdeateOutputSchema.safeParse(parsed);
1173
+ if (!result.success) {
1174
+ const issues = result.error.issues.map((issue) => {
1175
+ const path = issue.path.length > 0 ? `[${issue.path.join(".")}]` : "";
1176
+ return ` ${path}: ${issue.message}`;
1177
+ }).join("\n");
1178
+ throw new Error(`Invalid ideate output format:
1179
+ ${issues}`);
1180
+ }
1181
+ return result.data;
1182
+ }
1183
+ async function sprintIdeateCommand(args) {
1184
+ const { sprintId, options } = parseArgs(args);
1185
+ let id;
1186
+ try {
1187
+ id = await resolveSprintId(sprintId);
1188
+ } catch {
1189
+ showWarning("No sprint specified and no current sprint set.");
1190
+ showNextStep("ralphctl sprint create", "create a new sprint");
1191
+ log.newline();
1192
+ return;
1193
+ }
1194
+ const sprint = await getSprint(id);
1195
+ try {
1196
+ assertSprintStatus(sprint, ["draft"], "ideate");
1197
+ } catch (err) {
1198
+ if (err instanceof Error) {
1199
+ showError(err.message);
1200
+ log.newline();
1201
+ }
1202
+ return;
1203
+ }
1204
+ const projects = await listProjects();
1205
+ if (projects.length === 0) {
1206
+ showWarning("No projects configured.");
1207
+ showNextStep("ralphctl project add", "add a project first");
1208
+ log.newline();
1209
+ return;
1210
+ }
1211
+ printHeader("Quick Ideation", icons.ticket);
1212
+ console.log(field("Sprint", sprint.name));
1213
+ console.log(field("ID", sprint.id));
1214
+ console.log(field("Mode", options.auto ? "Auto (headless)" : "Interactive"));
1215
+ log.newline();
1216
+ let projectName = options.project;
1217
+ if (!projectName) {
1218
+ if (projects.length === 1) {
1219
+ projectName = projects[0]?.name;
1220
+ console.log(field("Project", projectName ?? "(unknown)"));
1221
+ } else {
1222
+ projectName = await select2({
1223
+ message: "Select project:",
1224
+ choices: projects.map((p) => ({ name: p.displayName, value: p.name }))
1225
+ });
1226
+ }
1227
+ }
1228
+ if (!projectName) {
1229
+ showError("No project selected.");
1230
+ log.newline();
1231
+ return;
1232
+ }
1233
+ let project;
1234
+ try {
1235
+ project = await getProject(projectName);
1236
+ } catch {
1237
+ showError(`Project '${projectName}' not found.`);
1238
+ log.newline();
1239
+ return;
1240
+ }
1241
+ const ideaTitle = await input2({
1242
+ message: "Idea title (short summary):",
1243
+ validate: (value) => value.trim().length > 0 ? true : "Title is required"
1244
+ });
1245
+ const ideaDescription = await editorInput({
1246
+ message: "Idea description (what you want to build):"
1247
+ });
1248
+ if (!ideaDescription.trim()) {
1249
+ showError("Description is required.");
1250
+ log.newline();
1251
+ return;
1252
+ }
1253
+ log.newline();
1254
+ showInfo("Creating ticket...");
1255
+ const ticket = await addTicket(
1256
+ {
1257
+ title: ideaTitle,
1258
+ description: ideaDescription,
1259
+ projectName
1260
+ },
1261
+ id
1262
+ );
1263
+ console.log(field("Ticket ID", ticket.id));
1264
+ log.newline();
1265
+ const providerName = providerDisplayName(await resolveProvider());
1266
+ let selectedPaths;
1267
+ const totalRepos = project.repositories.length;
1268
+ if (options.allPaths) {
1269
+ selectedPaths = project.repositories.map((r) => r.path);
1270
+ } else if (options.auto) {
1271
+ selectedPaths = project.repositories.slice(0, 1).map((r) => r.path);
1272
+ } else if (totalRepos === 1) {
1273
+ selectedPaths = [project.repositories[0]?.path ?? ""];
1274
+ } else {
1275
+ const reposByProject = /* @__PURE__ */ new Map();
1276
+ reposByProject.set(projectName, project.repositories);
1277
+ selectedPaths = await selectProjectPaths(reposByProject, "Select paths to explore:");
1278
+ }
1279
+ ticket.affectedRepositories = selectedPaths;
1280
+ await saveSprint(sprint);
1281
+ if (selectedPaths.length > 1) {
1282
+ console.log(muted(`Paths: ${selectedPaths.join(", ")}`));
1283
+ } else {
1284
+ console.log(muted(`Path: ${selectedPaths[0] ?? process.cwd()}`));
1285
+ }
1286
+ const repositoriesText = selectedPaths.map((path) => `- ${path}`).join("\n");
1287
+ const schema = await getTaskImportSchema();
1288
+ const ideateDir = getIdeateDir(id, ticket.id);
1289
+ await mkdir(ideateDir, { recursive: true });
1290
+ if (options.auto) {
1291
+ const prompt = buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositoriesText, schema);
1292
+ const spinner = createSpinner(`${providerName} is refining idea and planning tasks...`);
1293
+ spinner.start();
1294
+ let output;
1295
+ try {
1296
+ output = await invokeAiAuto(prompt, selectedPaths, ideateDir);
1297
+ spinner.succeed(`${providerName} finished`);
1298
+ } catch (err) {
1299
+ spinner.fail(`${providerName} session failed`);
1300
+ if (err instanceof Error) {
1301
+ showError(`Failed to invoke ${providerName}: ${err.message}`);
1302
+ showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
1303
+ log.newline();
1304
+ }
1305
+ return;
1306
+ }
1307
+ const blockedReason = parsePlanningBlocked(output);
1308
+ if (blockedReason) {
1309
+ showWarning(`Planning blocked: ${blockedReason}`);
1310
+ log.newline();
1311
+ return;
1312
+ }
1313
+ log.dim("Parsing response...");
1314
+ let ideateOutput;
1315
+ try {
1316
+ ideateOutput = parseIdeateOutput(output);
1317
+ } catch (err) {
1318
+ if (err instanceof Error) {
1319
+ showError(`Failed to parse ${providerName} output: ${err.message}`);
1320
+ log.dim("Raw output:");
1321
+ console.log(output);
1322
+ log.newline();
1323
+ }
1324
+ return;
1325
+ }
1326
+ const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
1327
+ const ticketToUpdate = sprint.tickets[ticketIdx];
1328
+ if (ticketIdx !== -1 && ticketToUpdate) {
1329
+ ticketToUpdate.requirements = ideateOutput.requirements;
1330
+ ticketToUpdate.requirementStatus = "approved";
1331
+ }
1332
+ await saveSprint(sprint);
1333
+ showSuccess("Requirements approved and saved!");
1334
+ log.newline();
1335
+ let parsedTasks;
1336
+ try {
1337
+ parsedTasks = parseTasksJson(JSON.stringify(ideateOutput.tasks));
1338
+ } catch (err) {
1339
+ if (err instanceof Error) {
1340
+ showError(`Failed to parse tasks: ${err.message}`);
1341
+ log.newline();
1342
+ }
1343
+ return;
1344
+ }
1345
+ if (parsedTasks.length === 0) {
1346
+ showWarning("No tasks generated.");
1347
+ log.newline();
1348
+ return;
1349
+ }
1350
+ showSuccess(`Generated ${String(parsedTasks.length)} task(s):`);
1351
+ log.newline();
1352
+ console.log(renderParsedTasksTable(parsedTasks));
1353
+ console.log("");
1354
+ const existingTasks = await getTasks(id);
1355
+ const ticketIds = new Set(sprint.tickets.map((t) => t.id));
1356
+ const validationErrors = validateImportTasks(parsedTasks, existingTasks, ticketIds);
1357
+ if (validationErrors.length > 0) {
1358
+ showError("Validation failed");
1359
+ for (const err of validationErrors) {
1360
+ log.item(error(err));
1361
+ }
1362
+ log.newline();
1363
+ return;
1364
+ }
1365
+ showInfo("Importing tasks...");
1366
+ const imported = await importTasks(parsedTasks, id);
1367
+ terminalBell();
1368
+ showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
1369
+ log.newline();
1370
+ } else {
1371
+ const outputFile = join(ideateDir, "output.json");
1372
+ const prompt = buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositoriesText, outputFile, schema);
1373
+ showInfo(`Starting interactive ${providerName} session...`);
1374
+ console.log(muted(` Exploring: ${selectedPaths.join(", ")}`));
1375
+ console.log(muted(`
1376
+ ${providerName} will guide you through requirements refinement and task planning.`));
1377
+ console.log(muted(` When done, ask ${providerName} to write the output to: ${outputFile}
1378
+ `));
1379
+ try {
1380
+ await invokeAiInteractive(prompt, selectedPaths, ideateDir);
1381
+ } catch (err) {
1382
+ if (err instanceof Error) {
1383
+ showError(`Failed to invoke ${providerName}: ${err.message}`);
1384
+ showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
1385
+ log.newline();
1386
+ }
1387
+ return;
1388
+ }
1389
+ console.log("");
1390
+ if (await fileExists(outputFile)) {
1391
+ showInfo("Output file found. Processing...");
1392
+ let content;
1393
+ try {
1394
+ content = await readFile(outputFile, "utf-8");
1395
+ } catch {
1396
+ showError(`Failed to read output file: ${outputFile}`);
1397
+ log.newline();
1398
+ return;
1399
+ }
1400
+ let ideateOutput;
1401
+ try {
1402
+ ideateOutput = parseIdeateOutput(content);
1403
+ } catch (err) {
1404
+ if (err instanceof Error) {
1405
+ showError(`Failed to parse output file: ${err.message}`);
1406
+ log.newline();
1407
+ }
1408
+ return;
1409
+ }
1410
+ const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
1411
+ const ticketToUpdate = sprint.tickets[ticketIdx];
1412
+ if (ticketIdx !== -1 && ticketToUpdate) {
1413
+ ticketToUpdate.requirements = ideateOutput.requirements;
1414
+ ticketToUpdate.requirementStatus = "approved";
1415
+ }
1416
+ await saveSprint(sprint);
1417
+ showSuccess("Requirements approved and saved!");
1418
+ log.newline();
1419
+ let parsedTasks;
1420
+ try {
1421
+ parsedTasks = parseTasksJson(JSON.stringify(ideateOutput.tasks));
1422
+ } catch (err) {
1423
+ if (err instanceof Error) {
1424
+ showError(`Failed to parse tasks: ${err.message}`);
1425
+ log.newline();
1426
+ }
1427
+ return;
1428
+ }
1429
+ if (parsedTasks.length === 0) {
1430
+ showWarning("No tasks in file.");
1431
+ log.newline();
1432
+ return;
1433
+ }
1434
+ showSuccess(`Found ${String(parsedTasks.length)} task(s):`);
1435
+ log.newline();
1436
+ console.log(renderParsedTasksTable(parsedTasks));
1437
+ console.log("");
1438
+ const existingTasks = await getTasks(id);
1439
+ const ticketIds = new Set(sprint.tickets.map((t) => t.id));
1440
+ const validationErrors = validateImportTasks(parsedTasks, existingTasks, ticketIds);
1441
+ if (validationErrors.length > 0) {
1442
+ showError("Validation failed");
1443
+ for (const err of validationErrors) {
1444
+ log.item(error(err));
1445
+ }
1446
+ log.newline();
1447
+ return;
1448
+ }
1449
+ showInfo("Importing tasks...");
1450
+ const imported = await importTasks(parsedTasks, id);
1451
+ terminalBell();
1452
+ showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
1453
+ log.newline();
1454
+ } else {
1455
+ showWarning("No output file found.");
1456
+ showTip(`Expected: ${outputFile}`);
1457
+ showNextStep("ralphctl sprint ideate", "run ideation again");
1458
+ log.newline();
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ // src/commands/sprint/close.ts
1464
+ import { spawnSync } from "child_process";
1465
+ import { confirm as confirm3 } from "@inquirer/prompts";
1466
+ async function sprintCloseCommand(args) {
1467
+ let sprintId;
1468
+ let createPr = false;
1469
+ const positionalArgs = [];
1470
+ for (const arg of args) {
1471
+ if (arg === "--create-pr") {
1472
+ createPr = true;
1473
+ } else {
1474
+ positionalArgs.push(arg);
1475
+ }
1476
+ }
1477
+ if (positionalArgs[0]) {
1478
+ sprintId = positionalArgs[0];
1479
+ } else {
1480
+ const sprints = await listSprints();
1481
+ const activeSprints = sprints.filter((s) => s.status === "active");
1482
+ if (activeSprints.length === 0) {
1483
+ showError("No active sprints to close.");
1484
+ log.newline();
1485
+ return;
1486
+ } else if (activeSprints.length === 1 && activeSprints[0]) {
1487
+ sprintId = activeSprints[0].id;
1488
+ } else {
1489
+ const selected = await selectSprint("Select sprint to close:", ["active"]);
1490
+ if (!selected) return;
1491
+ sprintId = selected;
1492
+ }
1493
+ }
1494
+ const allDone = await areAllTasksDone(sprintId);
1495
+ if (!allDone) {
1496
+ const tasks = await listTasks(sprintId);
1497
+ const remaining = tasks.filter((t) => t.status !== "done");
1498
+ log.newline();
1499
+ showWarning(`${String(remaining.length)} task(s) are not done:`);
1500
+ for (const task of remaining) {
1501
+ log.item(`${task.id}: ${task.name} (${task.status})`);
1502
+ }
1503
+ log.newline();
1504
+ const proceed = await confirm3({
1505
+ message: "Close sprint anyway?",
1506
+ default: false
1507
+ });
1508
+ if (!proceed) {
1509
+ console.log(muted("\nSprint close cancelled.\n"));
1510
+ return;
1511
+ }
1512
+ }
1513
+ try {
1514
+ const sprintBeforeClose = await getSprint(sprintId);
1515
+ const sprint = await closeSprint(sprintId);
1516
+ showSuccess("Sprint closed!", [
1517
+ ["ID", sprint.id],
1518
+ ["Name", sprint.name],
1519
+ ["Status", formatSprintStatus(sprint.status)]
1520
+ ]);
1521
+ showRandomQuote();
1522
+ log.newline();
1523
+ if (createPr && sprintBeforeClose.branch) {
1524
+ await createPullRequests(sprintId, sprintBeforeClose.branch, sprint.name);
1525
+ } else if (createPr && !sprintBeforeClose.branch) {
1526
+ log.dim("No sprint branch set \u2014 skipping PR creation.");
1527
+ log.newline();
1528
+ }
1529
+ } catch (err) {
1530
+ if (err instanceof SprintNotFoundError) {
1531
+ showError(`Sprint not found: ${sprintId}`);
1532
+ log.newline();
1533
+ } else if (err instanceof SprintStatusError) {
1534
+ showError(err.message);
1535
+ log.newline();
1536
+ } else {
1537
+ throw err;
1538
+ }
1539
+ }
1540
+ }
1541
+ async function createPullRequests(sprintId, branchName, sprintName) {
1542
+ if (!isGhAvailable()) {
1543
+ showWarning("GitHub CLI (gh) not found. Install it to create PRs automatically.");
1544
+ log.dim(` Manual: gh pr create --head ${branchName} --title "Sprint: ${sprintName}"`);
1545
+ log.newline();
1546
+ return;
1547
+ }
1548
+ const tasks = await listTasks(sprintId);
1549
+ const uniquePaths = [...new Set(tasks.map((t) => t.projectPath))];
1550
+ for (const projectPath of uniquePaths) {
1551
+ try {
1552
+ assertSafeCwd(projectPath);
1553
+ if (!branchExists(projectPath, branchName)) {
1554
+ log.dim(`Branch '${branchName}' not found in ${projectPath} \u2014 skipping`);
1555
+ continue;
1556
+ }
1557
+ const baseBranch = getDefaultBranch(projectPath);
1558
+ const title = `Sprint: ${sprintName}`;
1559
+ log.info(`Creating PR in ${projectPath}...`);
1560
+ const pushResult = spawnSync("git", ["push", "-u", "origin", branchName], {
1561
+ cwd: projectPath,
1562
+ encoding: "utf-8",
1563
+ stdio: ["pipe", "pipe", "pipe"]
1564
+ });
1565
+ if (pushResult.status !== 0) {
1566
+ showWarning(`Failed to push branch in ${projectPath}: ${pushResult.stderr.trim()}`);
1567
+ log.dim(
1568
+ ` Manual: cd ${projectPath} && git push -u origin ${branchName} && gh pr create --base ${baseBranch} --head ${branchName} --title "${title}"`
1569
+ );
1570
+ continue;
1571
+ }
1572
+ const result = spawnSync(
1573
+ "gh",
1574
+ [
1575
+ "pr",
1576
+ "create",
1577
+ "--base",
1578
+ baseBranch,
1579
+ "--head",
1580
+ branchName,
1581
+ "--title",
1582
+ title,
1583
+ "--body",
1584
+ `Sprint: ${sprintName}
1585
+ ID: ${sprintId}`
1586
+ ],
1587
+ {
1588
+ cwd: projectPath,
1589
+ encoding: "utf-8",
1590
+ stdio: ["pipe", "pipe", "pipe"]
1591
+ }
1592
+ );
1593
+ if (result.status === 0) {
1594
+ const prUrl = result.stdout.trim();
1595
+ showSuccess(`PR created: ${prUrl}`);
1596
+ } else {
1597
+ showWarning(`Failed to create PR in ${projectPath}: ${result.stderr.trim()}`);
1598
+ log.dim(
1599
+ ` Manual: cd ${projectPath} && gh pr create --base ${baseBranch} --head ${branchName} --title "${title}"`
1600
+ );
1601
+ }
1602
+ } catch (err) {
1603
+ showWarning(`Error creating PR for ${projectPath}: ${err instanceof Error ? err.message : String(err)}`);
1604
+ }
1605
+ }
1606
+ log.newline();
1607
+ }
1608
+
1609
+ // src/commands/sprint/delete.ts
1610
+ import { confirm as confirm4 } from "@inquirer/prompts";
1611
+ async function sprintDeleteCommand(args) {
1612
+ const skipConfirm = args.includes("-y") || args.includes("--yes");
1613
+ let sprintId = args.find((a) => !a.startsWith("-"));
1614
+ if (!sprintId) {
1615
+ const selected = await selectSprint("Select sprint to delete:");
1616
+ if (!selected) return;
1617
+ sprintId = selected;
1618
+ }
1619
+ try {
1620
+ const sprint = await getSprint(sprintId);
1621
+ let taskCount = 0;
1622
+ try {
1623
+ const tasks = await listTasks(sprintId);
1624
+ taskCount = tasks.length;
1625
+ } catch {
1626
+ }
1627
+ if (!skipConfirm) {
1628
+ log.newline();
1629
+ log.warn("This will permanently delete the sprint and all its data.");
1630
+ log.item(`Name: ${sprint.name}`);
1631
+ log.item(`Status: ${formatSprintStatus(sprint.status)}`);
1632
+ log.item(`Tickets: ${String(sprint.tickets.length)}`);
1633
+ log.item(`Tasks: ${String(taskCount)}`);
1634
+ log.newline();
1635
+ const confirmed = await confirm4({
1636
+ message: `Delete sprint "${sprint.name}"?`,
1637
+ default: false
1638
+ });
1639
+ if (!confirmed) {
1640
+ console.log(muted("\nSprint deletion cancelled.\n"));
1641
+ return;
1642
+ }
1643
+ }
1644
+ const currentSprintId = await getCurrentSprint();
1645
+ await deleteSprint(sprintId);
1646
+ if (currentSprintId === sprintId) {
1647
+ await setCurrentSprint(null);
1648
+ showTip('Current sprint was cleared. Use "ralphctl sprint current" to set a new one.');
1649
+ }
1650
+ showSuccess("Sprint deleted", [
1651
+ ["Name", sprint.name],
1652
+ ["ID", sprint.id]
1653
+ ]);
1654
+ showRandomQuote();
1655
+ log.newline();
1656
+ } catch (err) {
1657
+ if (err instanceof SprintNotFoundError) {
1658
+ showError(`Sprint not found: ${sprintId}`);
1659
+ log.newline();
1660
+ } else {
1661
+ throw err;
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ // src/commands/sprint/requirements.ts
1667
+ import { join as join2 } from "path";
1668
+ async function sprintRequirementsCommand(args = []) {
1669
+ const sprintId = args.find((a) => !a.startsWith("-"));
1670
+ let id;
1671
+ try {
1672
+ id = await resolveSprintId(sprintId);
1673
+ } catch {
1674
+ const selected = await selectSprint("Select sprint to export requirements from:");
1675
+ if (!selected) return;
1676
+ id = selected;
1677
+ }
1678
+ const sprint = await getSprint(id);
1679
+ if (sprint.tickets.length === 0) {
1680
+ showEmpty("tickets in this sprint", "Add tickets first: ralphctl ticket add --project <name>");
1681
+ return;
1682
+ }
1683
+ const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === "approved");
1684
+ if (approvedTickets.length === 0) {
1685
+ showWarning("No approved requirements to export.");
1686
+ log.dim("Refine requirements first: ralphctl sprint refine");
1687
+ log.newline();
1688
+ return;
1689
+ }
1690
+ printHeader("Export Requirements", icons.sprint);
1691
+ console.log(field("Sprint", sprint.name));
1692
+ console.log(field("Tickets", `${String(sprint.tickets.length)} total, ${String(approvedTickets.length)} approved`));
1693
+ log.newline();
1694
+ const sprintDir = getSprintDir(id);
1695
+ const outputPath = join2(sprintDir, "requirements.md");
1696
+ try {
1697
+ await exportRequirementsToMarkdown(sprint, outputPath);
1698
+ showSuccess("Requirements written to:");
1699
+ log.item(outputPath);
1700
+ } catch (err) {
1701
+ if (err instanceof Error) {
1702
+ showError(`Failed to write requirements: ${err.message}`);
1703
+ } else {
1704
+ showError("Failed to write requirements: Unknown error");
1705
+ }
1706
+ return;
1707
+ }
1708
+ log.newline();
1709
+ }
1710
+
1711
+ // src/commands/sprint/health.ts
1712
+ function checkBlockers(tasks) {
1713
+ const doneTasks = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
1714
+ const allTaskIds = new Set(tasks.map((t) => t.id));
1715
+ const blocked = [];
1716
+ for (const task of tasks) {
1717
+ if (task.status === "done") continue;
1718
+ const unresolvedDeps = task.blockedBy.filter((depId) => allTaskIds.has(depId) && !doneTasks.has(depId));
1719
+ if (unresolvedDeps.length > 0) {
1720
+ blocked.push(`${task.name} ${colors.muted(`(${task.id})`)} blocked by ${unresolvedDeps.join(", ")}`);
1721
+ }
1722
+ }
1723
+ return {
1724
+ name: "Blockers",
1725
+ status: blocked.length > 0 ? "fail" : "pass",
1726
+ items: blocked
1727
+ };
1728
+ }
1729
+ function checkStaleTasks(tasks) {
1730
+ const stale = tasks.filter((t) => t.status === "in_progress");
1731
+ const items = stale.map((t) => `${t.name} ${colors.muted(`(${t.id})`)}`);
1732
+ return {
1733
+ name: "Stale Tasks",
1734
+ status: items.length > 0 ? "warn" : "pass",
1735
+ items
1736
+ };
1737
+ }
1738
+ function checkOrphanedDeps(tasks) {
1739
+ const allTaskIds = new Set(tasks.map((t) => t.id));
1740
+ const orphaned = [];
1741
+ for (const task of tasks) {
1742
+ const missingDeps = task.blockedBy.filter((depId) => !allTaskIds.has(depId));
1743
+ if (missingDeps.length > 0) {
1744
+ orphaned.push(`${task.name} ${colors.muted(`(${task.id})`)} references missing: ${missingDeps.join(", ")}`);
1745
+ }
1746
+ }
1747
+ return {
1748
+ name: "Orphaned Dependencies",
1749
+ status: orphaned.length > 0 ? "fail" : "pass",
1750
+ items: orphaned
1751
+ };
1752
+ }
1753
+ function checkTicketsWithoutTasks(sprint, tasks) {
1754
+ const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
1755
+ const orphanedTickets = sprint.tickets.filter((t) => !ticketIdsWithTasks.has(t.id));
1756
+ const items = orphanedTickets.map((t) => `${t.title} ${colors.muted(`(${t.id})`)}`);
1757
+ return {
1758
+ name: "Tickets Without Tasks",
1759
+ status: items.length > 0 ? "warn" : "pass",
1760
+ items
1761
+ };
1762
+ }
1763
+ function checkDuplicateOrders(tasks) {
1764
+ const orderCounts = /* @__PURE__ */ new Map();
1765
+ for (const task of tasks) {
1766
+ const existing = orderCounts.get(task.order) ?? [];
1767
+ existing.push(`${task.name} ${colors.muted(`(${task.id})`)}`);
1768
+ orderCounts.set(task.order, existing);
1769
+ }
1770
+ const items = [];
1771
+ for (const [order, taskNames] of orderCounts) {
1772
+ if (taskNames.length > 1) {
1773
+ items.push(`Order ${String(order)}: ${taskNames.join(", ")}`);
1774
+ }
1775
+ }
1776
+ return {
1777
+ name: "Duplicate Task Orders",
1778
+ status: items.length > 0 ? "warn" : "pass",
1779
+ items
1780
+ };
1781
+ }
1782
+ function checkPendingRequirementsOnActive(sprint) {
1783
+ if (sprint.status !== "active") {
1784
+ return { name: "Pending Requirements", status: "pass", items: [] };
1785
+ }
1786
+ const pending = sprint.tickets.filter((t) => t.requirementStatus === "pending");
1787
+ const items = pending.map((t) => `${t.title} ${colors.muted(`(${t.id})`)} \u2014 refine before planning`);
1788
+ return {
1789
+ name: "Pending Requirements",
1790
+ status: items.length > 0 ? "warn" : "pass",
1791
+ items
1792
+ };
1793
+ }
1794
+ function checkBranchConsistency(sprint, tasks) {
1795
+ if (!sprint.branch) {
1796
+ return { name: "Branch Consistency", status: "pass", items: [] };
1797
+ }
1798
+ const remainingTasks = tasks.filter((t) => t.status !== "done");
1799
+ const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
1800
+ const items = [];
1801
+ for (const projectPath of uniquePaths) {
1802
+ try {
1803
+ const current = getCurrentBranch(projectPath);
1804
+ if (current !== sprint.branch) {
1805
+ items.push(`${projectPath} \u2014 on '${current}', expected '${sprint.branch}'`);
1806
+ }
1807
+ } catch {
1808
+ items.push(`${projectPath} \u2014 unable to determine branch`);
1809
+ }
1810
+ }
1811
+ return {
1812
+ name: "Branch Consistency",
1813
+ status: items.length > 0 ? "warn" : "pass",
1814
+ items
1815
+ };
1816
+ }
1817
+ function checkTasksWithoutSteps(tasks) {
1818
+ const empty = tasks.filter((t) => t.steps.length === 0);
1819
+ const items = empty.map((t) => `${t.name} ${colors.muted(`(${t.id})`)}`);
1820
+ return {
1821
+ name: "Tasks Without Steps",
1822
+ status: items.length > 0 ? "warn" : "pass",
1823
+ items
1824
+ };
1825
+ }
1826
+ function renderCheckCard(check) {
1827
+ const colorFn = check.status === "pass" ? colors.success : check.status === "warn" ? colors.warning : colors.error;
1828
+ const statusIcon = check.status === "pass" ? icons.success : check.status === "warn" ? icons.warning : icons.error;
1829
+ const lines = [];
1830
+ if (check.items.length === 0) {
1831
+ lines.push(colors.success(`${icons.success} No issues found`));
1832
+ } else {
1833
+ for (const item of check.items) {
1834
+ lines.push(`${colorFn(statusIcon)} ${item}`);
1835
+ }
1836
+ }
1837
+ return renderCard(check.name, lines, { colorFn });
1838
+ }
1839
+ async function sprintHealthCommand() {
1840
+ let sprint;
1841
+ try {
1842
+ sprint = await getCurrentSprintOrThrow();
1843
+ } catch (err) {
1844
+ if (err instanceof Error) {
1845
+ showError(err.message);
1846
+ } else {
1847
+ showError("Unknown error");
1848
+ }
1849
+ return;
1850
+ }
1851
+ const tasks = await getTasks(sprint.id);
1852
+ printHeader(`Sprint Health: ${sprint.name}`, icons.sprint);
1853
+ const checks = [
1854
+ checkBlockers(tasks),
1855
+ checkStaleTasks(tasks),
1856
+ checkOrphanedDeps(tasks),
1857
+ checkTicketsWithoutTasks(sprint, tasks),
1858
+ checkTasksWithoutSteps(tasks),
1859
+ checkDuplicateOrders(tasks),
1860
+ checkPendingRequirementsOnActive(sprint),
1861
+ checkBranchConsistency(sprint, tasks)
1862
+ ];
1863
+ for (const check of checks) {
1864
+ console.log(renderCheckCard(check));
1865
+ log.newline();
1866
+ }
1867
+ const passing = checks.filter((c) => c.status === "pass").length;
1868
+ const total = checks.length;
1869
+ const bar = progressBar(passing, total);
1870
+ log.info(`Health Score: ${bar} ${colors.muted(`${String(passing)}/${String(total)} checks passing`)}`);
1871
+ log.newline();
1872
+ const category = passing === total ? "success" : "error";
1873
+ const quote = getQuoteForContext(category);
1874
+ console.log(colors.muted(` "${quote}"`));
1875
+ log.newline();
1876
+ }
1877
+
1878
+ // src/commands/ticket/edit.ts
1879
+ import { input as input3 } from "@inquirer/prompts";
1880
+ function validateUrl(url) {
1881
+ try {
1882
+ new URL(url);
1883
+ return true;
1884
+ } catch {
1885
+ return false;
1886
+ }
1887
+ }
1888
+ async function ticketEditCommand(ticketId, options = {}) {
1889
+ const isInteractive = options.interactive !== false;
1890
+ let resolvedId = ticketId;
1891
+ if (!resolvedId) {
1892
+ if (!isInteractive) {
1893
+ showError("Ticket ID is required in non-interactive mode");
1894
+ exitWithCode(EXIT_ERROR);
1895
+ }
1896
+ const selected = await selectTicket("Select ticket to edit:");
1897
+ if (!selected) {
1898
+ return;
1899
+ }
1900
+ resolvedId = selected;
1901
+ }
1902
+ let ticket;
1903
+ try {
1904
+ ticket = await getTicket(resolvedId);
1905
+ } catch (err) {
1906
+ if (err instanceof TicketNotFoundError) {
1907
+ showError(`Ticket not found: ${resolvedId}`);
1908
+ showNextStep("ralphctl ticket list", "see available tickets");
1909
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
1910
+ return;
1911
+ }
1912
+ throw err;
1913
+ }
1914
+ let newTitle;
1915
+ let newDescription;
1916
+ let newLink;
1917
+ if (isInteractive) {
1918
+ console.log(`
1919
+ Editing: ${formatTicketDisplay(ticket)}`);
1920
+ console.log(muted(` Project: ${ticket.projectName} (read-only)
1921
+ `));
1922
+ newTitle = await input3({
1923
+ message: `${icons.ticket} Title:`,
1924
+ default: ticket.title,
1925
+ validate: (v) => v.trim().length > 0 ? true : "Title is required"
1926
+ });
1927
+ newDescription = await editorInput({
1928
+ message: "Description:",
1929
+ default: ticket.description
1930
+ });
1931
+ newLink = await input3({
1932
+ message: `${icons.info} Link:`,
1933
+ default: ticket.link ?? "",
1934
+ validate: (v) => {
1935
+ if (!v) return true;
1936
+ return validateUrl(v) ? true : "Invalid URL format";
1937
+ }
1938
+ });
1939
+ newTitle = newTitle.trim();
1940
+ newDescription = newDescription.trim() || void 0;
1941
+ newLink = newLink.trim() || void 0;
1942
+ } else {
1943
+ if (options.title !== void 0) {
1944
+ const trimmed = options.title.trim();
1945
+ if (trimmed.length === 0) {
1946
+ showError("--title cannot be empty");
1947
+ exitWithCode(EXIT_ERROR);
1948
+ }
1949
+ newTitle = trimmed;
1950
+ }
1951
+ if (options.description !== void 0) {
1952
+ newDescription = options.description.trim() || void 0;
1953
+ }
1954
+ if (options.link !== void 0) {
1955
+ const trimmed = options.link.trim();
1956
+ if (trimmed && !validateUrl(trimmed)) {
1957
+ showError("--link must be a valid URL");
1958
+ exitWithCode(EXIT_ERROR);
1959
+ }
1960
+ newLink = trimmed || void 0;
1961
+ }
1962
+ if (newTitle === void 0 && newDescription === void 0 && newLink === void 0) {
1963
+ showError("No updates provided. Use --title, --description, or --link.");
1964
+ exitWithCode(EXIT_ERROR);
1965
+ }
1966
+ }
1967
+ const updates = {};
1968
+ if (newTitle !== void 0 && newTitle !== ticket.title) {
1969
+ updates.title = newTitle;
1970
+ }
1971
+ if (newDescription !== void 0 && newDescription !== ticket.description) {
1972
+ updates.description = newDescription;
1973
+ }
1974
+ if (newLink !== void 0 && newLink !== ticket.link) {
1975
+ updates.link = newLink;
1976
+ }
1977
+ if (Object.keys(updates).length === 0) {
1978
+ console.log(muted("\n No changes made.\n"));
1979
+ return;
1980
+ }
1981
+ try {
1982
+ const updated = await updateTicket(ticket.id, updates);
1983
+ showSuccess("Ticket updated!", [
1984
+ ["ID", updated.id],
1985
+ ["Title", updated.title],
1986
+ ["Project", updated.projectName]
1987
+ ]);
1988
+ if (updated.description) {
1989
+ console.log(fieldMultiline("Description", updated.description));
1990
+ }
1991
+ if (updated.link) {
1992
+ console.log(field("Link", updated.link));
1993
+ }
1994
+ console.log("");
1995
+ } catch (err) {
1996
+ if (err instanceof SprintStatusError) {
1997
+ showError(err.message);
1998
+ } else {
1999
+ throw err;
2000
+ }
2001
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
2002
+ }
2003
+ }
2004
+
2005
+ // src/commands/ticket/list.ts
2006
+ function parseListArgs(args) {
2007
+ const result = {
2008
+ brief: false
2009
+ };
2010
+ for (let i = 0; i < args.length; i++) {
2011
+ const arg = args[i];
2012
+ const next = args[i + 1];
2013
+ if (arg === "-b" || arg === "--brief") result.brief = true;
2014
+ else if (arg === "--project" && next) {
2015
+ result.projectFilter = next;
2016
+ i++;
2017
+ } else if (arg === "--status" && next) {
2018
+ result.statusFilter = next;
2019
+ i++;
2020
+ }
2021
+ }
2022
+ return result;
2023
+ }
2024
+ function buildFilterSummary(filters) {
2025
+ const parts = [];
2026
+ if (filters.projectFilter) parts.push(`project=${filters.projectFilter}`);
2027
+ if (filters.statusFilter) parts.push(`status=${filters.statusFilter}`);
2028
+ return parts.length > 0 ? ` (filtered: ${parts.join(", ")})` : "";
2029
+ }
2030
+ async function ticketListCommand(args) {
2031
+ const { brief, projectFilter, statusFilter } = parseListArgs(args);
2032
+ if (statusFilter) {
2033
+ const result = RequirementStatusSchema.safeParse(statusFilter);
2034
+ if (!result.success) {
2035
+ showError(`Invalid status: "${statusFilter}". Valid values: pending, approved`);
2036
+ return;
2037
+ }
2038
+ }
2039
+ const tickets = await listTickets();
2040
+ if (tickets.length === 0) {
2041
+ showEmpty("tickets", "Add one with: ralphctl ticket add --project <project-name>");
2042
+ return;
2043
+ }
2044
+ let filtered = tickets;
2045
+ if (projectFilter) filtered = filtered.filter((t) => t.projectName === projectFilter);
2046
+ if (statusFilter) filtered = filtered.filter((t) => t.requirementStatus === statusFilter);
2047
+ const filterStr = buildFilterSummary({ brief, projectFilter, statusFilter });
2048
+ const isFiltered = filtered.length !== tickets.length;
2049
+ if (filtered.length === 0) {
2050
+ showEmpty("matching tickets", "Try adjusting your filters");
2051
+ return;
2052
+ }
2053
+ if (brief) {
2054
+ const countLabel = isFiltered ? `${String(filtered.length)} of ${String(tickets.length)}` : String(tickets.length);
2055
+ console.log(`
2056
+ # Tickets (${countLabel})${filterStr}
2057
+ `);
2058
+ for (const ticket of filtered) {
2059
+ const display = `[${ticket.id}] ${ticket.title}`;
2060
+ const reqBadge = ticket.requirementStatus === "approved" ? " [approved]" : " [pending]";
2061
+ console.log(`- ${display}${reqBadge} (${ticket.projectName})`);
2062
+ }
2063
+ console.log("");
2064
+ return;
2065
+ }
2066
+ const ticketsByProject = groupTicketsByProject(filtered);
2067
+ printHeader(`Tickets (${String(filtered.length)})`, icons.ticket);
2068
+ for (const [projectName, projectTickets] of ticketsByProject) {
2069
+ log.raw(`${colors.info(icons.project)} ${colors.info(projectName)}`);
2070
+ try {
2071
+ const project = await getProject(projectName);
2072
+ for (const repo of project.repositories) {
2073
+ log.raw(` ${muted(repo.name)} ${muted("\u2192")} ${muted(repo.path)}`, 1);
2074
+ }
2075
+ } catch {
2076
+ log.raw(` ${muted("(project not found)")}`, 1);
2077
+ }
2078
+ log.newline();
2079
+ for (const ticket of projectTickets) {
2080
+ const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "muted");
2081
+ log.raw(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
2082
+ if (ticket.description) {
2083
+ const preview = ticket.description.split("\n")[0] ?? "";
2084
+ const truncated = preview.length > 60 ? preview.slice(0, 57) + "..." : preview;
2085
+ log.raw(` ${muted(truncated)}`, 1);
2086
+ }
2087
+ }
2088
+ log.newline();
2089
+ }
2090
+ const approved = filtered.filter((t) => t.requirementStatus === "approved").length;
2091
+ log.dim(
2092
+ `Requirements: ${success(`${String(approved)} approved`)} / ${muted(`${String(filtered.length - approved)} pending`)}`
2093
+ );
2094
+ const showingLabel = isFiltered ? `Showing ${String(filtered.length)} of ${String(tickets.length)} ticket(s)${filterStr}` : `Showing ${String(tickets.length)} ticket(s)`;
2095
+ log.dim(showingLabel);
2096
+ log.newline();
2097
+ }
2098
+
2099
+ // src/commands/ticket/show.ts
2100
+ async function ticketShowCommand(args) {
2101
+ let ticketId = args[0];
2102
+ if (!ticketId) {
2103
+ const selected = await selectTicket("Select ticket to show:");
2104
+ if (!selected) return;
2105
+ ticketId = selected;
2106
+ }
2107
+ try {
2108
+ const ticket = await getTicket(ticketId);
2109
+ const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "muted");
2110
+ const infoLines = [labelValue("ID", ticket.id)];
2111
+ infoLines.push(labelValue("Project", ticket.projectName));
2112
+ infoLines.push(labelValue("Requirements", reqBadge));
2113
+ if (ticket.link) {
2114
+ infoLines.push(labelValue("Link", ticket.link));
2115
+ }
2116
+ try {
2117
+ const project = await getProject(ticket.projectName);
2118
+ infoLines.push("");
2119
+ for (const repo of project.repositories) {
2120
+ infoLines.push(` ${icons.bullet} ${repo.name} ${muted("\u2192")} ${muted(repo.path)}`);
2121
+ }
2122
+ } catch {
2123
+ infoLines.push(labelValue("Repositories", muted("(project not found)")));
2124
+ }
2125
+ log.newline();
2126
+ console.log(renderCard(`${icons.ticket} ${ticket.title}`, infoLines));
2127
+ if (ticket.description) {
2128
+ log.newline();
2129
+ const descLines = [];
2130
+ for (const line2 of ticket.description.split("\n")) {
2131
+ descLines.push(line2);
2132
+ }
2133
+ console.log(renderCard(`${icons.edit} Description`, descLines));
2134
+ }
2135
+ if (ticket.affectedRepositories && ticket.affectedRepositories.length > 0) {
2136
+ log.newline();
2137
+ const affectedLines = [];
2138
+ for (const repoPath of ticket.affectedRepositories) {
2139
+ affectedLines.push(`${icons.bullet} ${repoPath}`);
2140
+ }
2141
+ console.log(renderCard(`${icons.project} Affected Repositories`, affectedLines));
2142
+ }
2143
+ log.newline();
2144
+ } catch (err) {
2145
+ if (err instanceof TicketNotFoundError) {
2146
+ showError(`Ticket not found: ${ticketId}`);
2147
+ showNextStep("ralphctl ticket list", "see available tickets");
2148
+ log.newline();
2149
+ } else {
2150
+ throw err;
2151
+ }
2152
+ }
2153
+ }
2154
+
2155
+ // src/commands/ticket/remove.ts
2156
+ import { confirm as confirm5 } from "@inquirer/prompts";
2157
+ async function ticketRemoveCommand(args) {
2158
+ const skipConfirm = args.includes("-y") || args.includes("--yes");
2159
+ let ticketId = args.find((a) => !a.startsWith("-"));
2160
+ if (!ticketId) {
2161
+ const selected = await selectTicket("Select ticket to remove:");
2162
+ if (!selected) return;
2163
+ ticketId = selected;
2164
+ }
2165
+ try {
2166
+ const ticket = await getTicket(ticketId);
2167
+ if (!skipConfirm) {
2168
+ const confirmed = await confirm5({
2169
+ message: `Remove ticket ${formatTicketDisplay(ticket)}?`,
2170
+ default: false
2171
+ });
2172
+ if (!confirmed) {
2173
+ console.log(muted("\nTicket removal cancelled.\n"));
2174
+ return;
2175
+ }
2176
+ }
2177
+ await removeTicket(ticketId);
2178
+ showSuccess("Ticket removed", [["ID", ticketId]]);
2179
+ log.newline();
2180
+ } catch (err) {
2181
+ if (err instanceof TicketNotFoundError) {
2182
+ showError(`Ticket not found: ${ticketId}`);
2183
+ showNextStep("ralphctl ticket list", "see available tickets");
2184
+ log.newline();
2185
+ } else if (err instanceof SprintStatusError) {
2186
+ showError(err.message);
2187
+ log.newline();
2188
+ } else {
2189
+ throw err;
2190
+ }
2191
+ }
2192
+ }
2193
+
2194
+ // src/commands/ticket/refine.ts
2195
+ import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2196
+ import { join as join3 } from "path";
2197
+ import { confirm as confirm6 } from "@inquirer/prompts";
2198
+ async function ticketRefineCommand(ticketId, options = {}) {
2199
+ const isInteractive = options.interactive !== false;
2200
+ let sprintId;
2201
+ try {
2202
+ sprintId = await resolveSprintId();
2203
+ } catch {
2204
+ showWarning("No current sprint set.");
2205
+ showTip("Create a sprint first or set one with: ralphctl sprint current");
2206
+ log.newline();
2207
+ return;
2208
+ }
2209
+ const sprint = await getSprint(sprintId);
2210
+ try {
2211
+ assertSprintStatus(sprint, ["draft"], "refine ticket");
2212
+ } catch (err) {
2213
+ if (err instanceof Error) {
2214
+ showError(err.message);
2215
+ log.newline();
2216
+ }
2217
+ return;
2218
+ }
2219
+ const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === "approved");
2220
+ if (approvedTickets.length === 0) {
2221
+ showWarning("No approved tickets to re-refine.");
2222
+ showTip('Run "ralphctl sprint refine" to refine pending tickets first.');
2223
+ log.newline();
2224
+ return;
2225
+ }
2226
+ let resolvedId = ticketId;
2227
+ if (!resolvedId) {
2228
+ if (!isInteractive) {
2229
+ showError("Ticket ID is required in non-interactive mode");
2230
+ exitWithCode(EXIT_ERROR);
2231
+ }
2232
+ const selected = await selectTicket("Select ticket to re-refine:", (t) => t.requirementStatus === "approved");
2233
+ if (!selected) return;
2234
+ resolvedId = selected;
2235
+ }
2236
+ const ticket = sprint.tickets.find((t) => t.id === resolvedId);
2237
+ if (!ticket) {
2238
+ showError(`Ticket not found: ${resolvedId}`);
2239
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
2240
+ return;
2241
+ }
2242
+ if (ticket.requirementStatus !== "approved") {
2243
+ showError('Only approved tickets can be re-refined. Run "ralphctl sprint refine" for pending tickets.');
2244
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
2245
+ return;
2246
+ }
2247
+ printHeader("Re-Refine Ticket", icons.ticket);
2248
+ console.log(field("Sprint", sprint.name));
2249
+ console.log(field("Ticket", formatTicketDisplay(ticket)));
2250
+ console.log(field("Project", ticket.projectName));
2251
+ if (ticket.link) {
2252
+ console.log(field("Link", ticket.link));
2253
+ }
2254
+ if (ticket.description) {
2255
+ console.log(fieldMultiline("Description", ticket.description));
2256
+ }
2257
+ log.newline();
2258
+ const schemaPath = getSchemaPath("requirements-output.schema.json");
2259
+ const schema = await readFile2(schemaPath, "utf-8");
2260
+ const providerName = providerDisplayName(await resolveProvider());
2261
+ const proceed = await confirm6({
2262
+ message: `${emoji.donut} Start ${providerName} re-refinement session?`,
2263
+ default: true
2264
+ });
2265
+ if (!proceed) {
2266
+ log.dim("Cancelled.");
2267
+ log.newline();
2268
+ return;
2269
+ }
2270
+ let issueContext = "";
2271
+ if (ticket.link) {
2272
+ const fetchSpinner = createSpinner("Fetching issue data...");
2273
+ fetchSpinner.start();
2274
+ try {
2275
+ const issueData = fetchIssueFromUrl(ticket.link);
2276
+ if (issueData) {
2277
+ issueContext = formatIssueContext(issueData);
2278
+ fetchSpinner.succeed(`Issue data fetched (${String(issueData.comments.length)} comment(s))`);
2279
+ } else {
2280
+ fetchSpinner.stop();
2281
+ }
2282
+ } catch (err) {
2283
+ fetchSpinner.fail("Could not fetch issue data");
2284
+ if (err instanceof IssueFetchError || err instanceof Error) {
2285
+ showWarning(`${err.message} \u2014 continuing without issue context`);
2286
+ }
2287
+ }
2288
+ }
2289
+ const refineDir = getRefinementDir(sprintId, ticket.id);
2290
+ await mkdir2(refineDir, { recursive: true });
2291
+ const outputFile = join3(refineDir, "requirements.json");
2292
+ let ticketContent = formatTicketForPrompt(ticket);
2293
+ if (ticket.requirements) {
2294
+ ticketContent += "\n### Previously Approved Requirements\n\n";
2295
+ ticketContent += ticket.requirements;
2296
+ ticketContent += "\n";
2297
+ }
2298
+ const prompt = buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext);
2299
+ log.dim(`Working directory: ${refineDir}`);
2300
+ log.dim(`Requirements output: ${outputFile}`);
2301
+ log.newline();
2302
+ const spinner = createSpinner(`Starting ${providerName} session...`);
2303
+ spinner.start();
2304
+ try {
2305
+ await runAiSession(refineDir, prompt, ticket.title);
2306
+ spinner.succeed(`${providerName} session completed`);
2307
+ } catch (err) {
2308
+ spinner.fail(`${providerName} session failed`);
2309
+ if (err instanceof Error) {
2310
+ showError(err.message);
2311
+ }
2312
+ log.newline();
2313
+ return;
2314
+ }
2315
+ log.newline();
2316
+ if (!await fileExists(outputFile)) {
2317
+ showWarning("No requirements file found from AI session.");
2318
+ log.newline();
2319
+ return;
2320
+ }
2321
+ let content;
2322
+ try {
2323
+ content = await readFile2(outputFile, "utf-8");
2324
+ } catch {
2325
+ showError(`Failed to read requirements file: ${outputFile}`);
2326
+ log.newline();
2327
+ return;
2328
+ }
2329
+ let refinedRequirements;
2330
+ try {
2331
+ refinedRequirements = parseRequirementsFile(content);
2332
+ } catch (err) {
2333
+ if (err instanceof Error) {
2334
+ showError(`Failed to parse requirements file: ${err.message}`);
2335
+ }
2336
+ log.newline();
2337
+ return;
2338
+ }
2339
+ if (refinedRequirements.length === 0) {
2340
+ showWarning("No requirements found in output file.");
2341
+ log.newline();
2342
+ return;
2343
+ }
2344
+ const matchingRequirements = refinedRequirements.filter((r) => r.ref === ticket.id || r.ref === ticket.title);
2345
+ if (matchingRequirements.length === 0) {
2346
+ showWarning("Requirement reference does not match this ticket.");
2347
+ log.newline();
2348
+ return;
2349
+ }
2350
+ const requirement = matchingRequirements.length === 1 ? {
2351
+ ref: matchingRequirements[0]?.ref ?? "",
2352
+ requirements: matchingRequirements[0]?.requirements ?? ""
2353
+ } : {
2354
+ ref: matchingRequirements[0]?.ref ?? "",
2355
+ requirements: matchingRequirements.map((r, idx) => {
2356
+ const text = r.requirements.trim();
2357
+ if (/^#\s/.test(text)) return text;
2358
+ return `# ${String(idx + 1)}. Section ${String(idx + 1)}
2359
+
2360
+ ${text}`;
2361
+ }).join("\n\n---\n\n")
2362
+ };
2363
+ const reqLines = requirement.requirements.split("\n");
2364
+ console.log(renderCard(`${icons.ticket} Re-Refined Requirements`, reqLines));
2365
+ log.newline();
2366
+ const approveRequirement = await confirm6({
2367
+ message: `${emoji.donut} Approve these requirements?`,
2368
+ default: true
2369
+ });
2370
+ if (approveRequirement) {
2371
+ const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
2372
+ const ticketToSave = sprint.tickets[ticketIdx];
2373
+ if (ticketIdx !== -1 && ticketToSave) {
2374
+ ticketToSave.requirements = requirement.requirements;
2375
+ }
2376
+ await saveSprint(sprint);
2377
+ showSuccess("Requirements updated and saved!");
2378
+ } else {
2379
+ log.dim("Requirements not approved. Previous requirements unchanged.");
2380
+ }
2381
+ log.newline();
2382
+ }
2383
+
2384
+ // src/commands/task/add.ts
2385
+ import { resolve as resolve2 } from "path";
2386
+ import { confirm as confirm7, input as input4 } from "@inquirer/prompts";
2387
+ async function taskAddCommand(options = {}) {
2388
+ const isInteractive = options.interactive !== false;
2389
+ try {
2390
+ const sprintId = await resolveSprintId();
2391
+ const sprint = await getSprint(sprintId);
2392
+ assertSprintStatus(sprint, ["draft"], "add tasks");
2393
+ } catch (err) {
2394
+ if (err instanceof SprintStatusError) {
2395
+ const mainError = err.message.split("\n")[0] ?? err.message;
2396
+ showError(mainError);
2397
+ showNextSteps([
2398
+ ["ralphctl sprint close", "close current sprint"],
2399
+ ["ralphctl sprint create", "start a new draft sprint"]
2400
+ ]);
2401
+ log.newline();
2402
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
2403
+ return;
2404
+ }
2405
+ if (err instanceof NoCurrentSprintError) {
2406
+ showError("No current sprint set.");
2407
+ showNextSteps([["ralphctl sprint create", "create a new sprint"]]);
2408
+ log.newline();
2409
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
2410
+ return;
2411
+ }
2412
+ throw err;
2413
+ }
2414
+ let name;
2415
+ let description;
2416
+ let steps;
2417
+ let ticketId;
2418
+ let projectPath;
2419
+ if (options.interactive === false) {
2420
+ const errors = [];
2421
+ const trimmedName = options.name?.trim();
2422
+ const trimmedProject = options.project?.trim();
2423
+ if (!trimmedName) {
2424
+ errors.push("--name is required");
2425
+ }
2426
+ if (!trimmedProject && !options.ticket) {
2427
+ errors.push("--project is required (or --ticket to inherit from ticket)");
2428
+ }
2429
+ if (errors.length > 0 || !trimmedName) {
2430
+ showError("Validation failed");
2431
+ for (const e of errors) {
2432
+ log.item(error(e));
2433
+ }
2434
+ log.newline();
2435
+ exitWithCode(EXIT_ERROR);
2436
+ }
2437
+ name = trimmedName;
2438
+ const trimmedDesc = options.description?.trim();
2439
+ description = trimmedDesc === "" ? void 0 : trimmedDesc;
2440
+ steps = options.steps ?? [];
2441
+ const trimmedTicket = options.ticket?.trim();
2442
+ ticketId = trimmedTicket === "" ? void 0 : trimmedTicket;
2443
+ if (ticketId) {
2444
+ try {
2445
+ const ticket = await getTicket(ticketId);
2446
+ const project = await getProject(ticket.projectName);
2447
+ projectPath = project.repositories[0]?.path;
2448
+ } catch {
2449
+ if (!trimmedProject) {
2450
+ showError(`Ticket not found: ${ticketId}`);
2451
+ console.log(muted(" Provide --project or a valid --ticket\n"));
2452
+ exitWithCode(EXIT_ERROR);
2453
+ }
2454
+ const validation = await validateProjectPath(trimmedProject);
2455
+ if (validation !== true) {
2456
+ showError(`Invalid project path: ${validation}`);
2457
+ exitWithCode(EXIT_ERROR);
2458
+ }
2459
+ projectPath = resolve2(trimmedProject);
2460
+ }
2461
+ } else if (trimmedProject) {
2462
+ const validation = await validateProjectPath(trimmedProject);
2463
+ if (validation !== true) {
2464
+ showError(`Invalid project path: ${validation}`);
2465
+ exitWithCode(EXIT_ERROR);
2466
+ }
2467
+ projectPath = resolve2(trimmedProject);
2468
+ } else {
2469
+ showError("--project is required");
2470
+ exitWithCode(EXIT_ERROR);
2471
+ }
2472
+ } else {
2473
+ name = await input4({
2474
+ message: `${icons.task} Task name:`,
2475
+ default: options.name?.trim(),
2476
+ validate: (v) => v.trim().length > 0 ? true : "Name is required"
2477
+ });
2478
+ description = await editorInput({
2479
+ message: "Description (optional):",
2480
+ default: options.description?.trim()
2481
+ });
2482
+ steps = options.steps ? [...options.steps] : [];
2483
+ const addSteps = await confirm7({
2484
+ message: `${emoji.donut} ${steps.length > 0 ? `Add more steps? (${String(steps.length)} pre-filled)` : "Add implementation steps?"}`,
2485
+ default: steps.length === 0
2486
+ });
2487
+ if (addSteps) {
2488
+ let stepNum = steps.length + 1;
2489
+ let adding = true;
2490
+ while (adding) {
2491
+ const step = await input4({
2492
+ message: ` Step ${String(stepNum)} (empty to finish):`
2493
+ });
2494
+ if (step.trim()) {
2495
+ steps.push(step.trim());
2496
+ stepNum++;
2497
+ } else {
2498
+ adding = false;
2499
+ }
2500
+ }
2501
+ }
2502
+ const tickets = await listTickets();
2503
+ if (tickets.length > 0) {
2504
+ const { select: select4 } = await import("@inquirer/prompts");
2505
+ const defaultTicketValue = options.ticket ? tickets.find((t) => t.id === options.ticket)?.id ?? "" : "";
2506
+ const ticketChoice = await select4({
2507
+ message: `${icons.ticket} Link to ticket:`,
2508
+ default: defaultTicketValue,
2509
+ choices: [
2510
+ { name: `${emoji.donut} None (select project/repo manually)`, value: "" },
2511
+ ...tickets.map((t) => ({
2512
+ name: `${icons.ticket} ${formatTicketDisplay(t)} ${muted(`(${t.projectName})`)}`,
2513
+ value: t.id
2514
+ }))
2515
+ ]
2516
+ });
2517
+ if (ticketChoice) {
2518
+ ticketId = ticketChoice;
2519
+ const ticket = tickets.find((t) => t.id === ticketChoice);
2520
+ if (ticket) {
2521
+ try {
2522
+ const project = await getProject(ticket.projectName);
2523
+ if (project.repositories.length === 1) {
2524
+ projectPath = project.repositories[0]?.path;
2525
+ } else {
2526
+ const { select: selectRepo } = await import("@inquirer/prompts");
2527
+ projectPath = await selectRepo({
2528
+ message: `${emoji.donut} Select repository for this task:`,
2529
+ choices: project.repositories.map((r) => ({
2530
+ name: `${r.name} (${r.path})`,
2531
+ value: r.path
2532
+ }))
2533
+ });
2534
+ }
2535
+ } catch {
2536
+ log.warn(`Project '${ticket.projectName}' not found, will prompt for path.`);
2537
+ }
2538
+ }
2539
+ }
2540
+ } else if (options.ticket) {
2541
+ ticketId = options.ticket;
2542
+ try {
2543
+ const ticket = await getTicket(ticketId);
2544
+ const project = await getProject(ticket.projectName);
2545
+ projectPath = project.repositories[0]?.path;
2546
+ } catch {
2547
+ }
2548
+ }
2549
+ if (projectPath === void 0) {
2550
+ const projects = await listProjects();
2551
+ if (projects.length > 0) {
2552
+ const { select: select4 } = await import("@inquirer/prompts");
2553
+ const choice = await select4({
2554
+ message: `${icons.project} Select project:`,
2555
+ choices: [
2556
+ { name: `${icons.edit} Enter path manually`, value: "__manual__" },
2557
+ { name: `${emoji.donut} Select project/repository`, value: "__select__" }
2558
+ ]
2559
+ });
2560
+ if (choice === "__manual__") {
2561
+ projectPath = await input4({
2562
+ message: `${icons.project} Project path:`,
2563
+ default: options.project?.trim() ?? process.cwd(),
2564
+ validate: async (v) => {
2565
+ const result = await validateProjectPath(v.trim());
2566
+ return result;
2567
+ }
2568
+ });
2569
+ projectPath = resolve2(expandTilde(projectPath.trim()));
2570
+ } else {
2571
+ const selectedPath = await selectProjectRepository("Select repository:");
2572
+ if (!selectedPath) {
2573
+ showError("No repository selected");
2574
+ exitWithCode(EXIT_ERROR);
2575
+ }
2576
+ projectPath = selectedPath;
2577
+ }
2578
+ } else {
2579
+ projectPath = await input4({
2580
+ message: `${icons.project} Project path:`,
2581
+ default: options.project?.trim() ?? process.cwd(),
2582
+ validate: async (v) => {
2583
+ const result = await validateProjectPath(v.trim());
2584
+ return result;
2585
+ }
2586
+ });
2587
+ projectPath = resolve2(expandTilde(projectPath.trim()));
2588
+ }
2589
+ }
2590
+ name = name.trim();
2591
+ const trimmedDescription = description.trim();
2592
+ description = trimmedDescription === "" ? void 0 : trimmedDescription;
2593
+ }
2594
+ if (!projectPath) {
2595
+ showError("Project path is required");
2596
+ exitWithCode(EXIT_ERROR);
2597
+ }
2598
+ try {
2599
+ const task = await addTask({
2600
+ name,
2601
+ description,
2602
+ steps,
2603
+ ticketId,
2604
+ projectPath
2605
+ });
2606
+ showSuccess("Task added!", [
2607
+ ["ID", task.id],
2608
+ ["Name", task.name],
2609
+ ["Project", task.projectPath],
2610
+ ["Order", String(task.order)]
2611
+ ]);
2612
+ if (task.ticketId) {
2613
+ console.log(field("Ticket", task.ticketId));
2614
+ }
2615
+ if (task.steps.length > 0) {
2616
+ console.log(field("Steps", ""));
2617
+ task.steps.forEach((step, i) => {
2618
+ console.log(muted(` ${String(i + 1)}. ${step}`));
2619
+ });
2620
+ }
2621
+ console.log("");
2622
+ } catch (err) {
2623
+ if (err instanceof SprintStatusError) {
2624
+ const mainError = err.message.split("\n")[0] ?? err.message;
2625
+ showError(mainError);
2626
+ showNextSteps([
2627
+ ["ralphctl sprint close", "close current sprint"],
2628
+ ["ralphctl sprint create", "start a new draft sprint"]
2629
+ ]);
2630
+ log.newline();
2631
+ if (!isInteractive) exitWithCode(EXIT_ERROR);
2632
+ return;
2633
+ }
2634
+ throw err;
2635
+ }
2636
+ }
2637
+
2638
+ // src/commands/task/import.ts
2639
+ import { readFile as readFile3 } from "fs/promises";
2640
+ async function taskImportCommand(args) {
2641
+ const filePath = args[0];
2642
+ if (!filePath) {
2643
+ showError("File path required.");
2644
+ showNextStep("ralphctl task import <file.json>", "provide a task file");
2645
+ log.dim("Expected JSON format:");
2646
+ console.log(
2647
+ muted(`[
2648
+ {
2649
+ "id": "1",
2650
+ "name": "Task name",
2651
+ "projectPath": "/path/to/repo",
2652
+ "description": "Optional description",
2653
+ "steps": ["Step 1", "Step 2"],
2654
+ "ticketId": "abc12345",
2655
+ "blockedBy": ["task-001"]
2656
+ }
2657
+ ]`)
2658
+ );
2659
+ log.dim("Note: projectPath is required for each task.");
2660
+ log.newline();
2661
+ return;
2662
+ }
2663
+ let content;
2664
+ try {
2665
+ content = await readFile3(filePath, "utf-8");
2666
+ } catch {
2667
+ showError(`Failed to read file: ${filePath}`);
2668
+ log.newline();
2669
+ return;
2670
+ }
2671
+ let data;
2672
+ try {
2673
+ data = JSON.parse(content);
2674
+ } catch {
2675
+ showError("Invalid JSON format.");
2676
+ log.newline();
2677
+ return;
2678
+ }
2679
+ const result = ImportTasksSchema.safeParse(data);
2680
+ if (!result.success) {
2681
+ showError("Invalid task format");
2682
+ for (const issue of result.error.issues) {
2683
+ log.item(error(`${issue.path.join(".")}: ${issue.message}`));
2684
+ }
2685
+ log.newline();
2686
+ return;
2687
+ }
2688
+ const tasks = result.data;
2689
+ if (tasks.length === 0) {
2690
+ showError("No tasks to import.");
2691
+ log.newline();
2692
+ return;
2693
+ }
2694
+ const existingTasks = await getTasks();
2695
+ const sprintId = await resolveSprintId();
2696
+ const sprint = await getSprint(sprintId);
2697
+ const ticketIds = new Set(sprint.tickets.map((t) => t.id));
2698
+ const validationErrors = validateImportTasks(tasks, existingTasks, ticketIds);
2699
+ if (validationErrors.length > 0) {
2700
+ showError("Dependency validation failed");
2701
+ for (const err of validationErrors) {
2702
+ log.item(error(err));
2703
+ }
2704
+ log.newline();
2705
+ return;
2706
+ }
2707
+ const localToRealId = /* @__PURE__ */ new Map();
2708
+ const createdTasks = [];
2709
+ const spinner = createSpinner(`Importing ${String(tasks.length)} task(s)...`).start();
2710
+ let imported = 0;
2711
+ for (const taskInput of tasks) {
2712
+ try {
2713
+ const task = await addTask({
2714
+ name: taskInput.name,
2715
+ description: taskInput.description,
2716
+ steps: taskInput.steps ?? [],
2717
+ ticketId: taskInput.ticketId,
2718
+ blockedBy: [],
2719
+ // Set later
2720
+ projectPath: taskInput.projectPath
2721
+ });
2722
+ if (taskInput.id) {
2723
+ localToRealId.set(taskInput.id, task.id);
2724
+ }
2725
+ createdTasks.push({ task: taskInput, realId: task.id });
2726
+ imported++;
2727
+ spinner.text = `Importing tasks... (${String(imported)}/${String(tasks.length)})`;
2728
+ } catch (err) {
2729
+ if (err instanceof SprintStatusError) {
2730
+ spinner.fail("Import failed");
2731
+ showError(err.message);
2732
+ log.newline();
2733
+ return;
2734
+ }
2735
+ log.itemError(`Failed to add: ${taskInput.name}`);
2736
+ if (err instanceof Error) {
2737
+ console.log(muted(` ${err.message}`));
2738
+ }
2739
+ }
2740
+ }
2741
+ spinner.text = "Resolving task dependencies...";
2742
+ const tasksFilePath = getTasksFilePath(sprintId);
2743
+ await withFileLock(tasksFilePath, async () => {
2744
+ const allTasks = await getTasks();
2745
+ for (const { task: taskInput, realId } of createdTasks) {
2746
+ const blockedBy = (taskInput.blockedBy ?? []).map((localId) => localToRealId.get(localId) ?? "").filter((id) => id !== "");
2747
+ if (blockedBy.length > 0) {
2748
+ const taskToUpdate = allTasks.find((t) => t.id === realId);
2749
+ if (taskToUpdate) {
2750
+ taskToUpdate.blockedBy = blockedBy;
2751
+ }
2752
+ }
2753
+ }
2754
+ await saveTasks(allTasks);
2755
+ });
2756
+ spinner.succeed(`Imported ${String(imported)}/${String(tasks.length)} tasks`);
2757
+ for (const { task: taskInput, realId } of createdTasks) {
2758
+ log.itemSuccess(`${realId}: ${taskInput.name}`);
2759
+ }
2760
+ }
2761
+
2762
+ // src/commands/task/list.ts
2763
+ function parseListArgs2(args) {
2764
+ const result = {
2765
+ brief: false,
2766
+ blockedOnly: false
2767
+ };
2768
+ for (let i = 0; i < args.length; i++) {
2769
+ const arg = args[i];
2770
+ const next = args[i + 1];
2771
+ if (arg === "-b" || arg === "--brief") result.brief = true;
2772
+ else if (arg === "--status" && next) {
2773
+ result.statusFilter = next;
2774
+ i++;
2775
+ } else if (arg === "--project" && next) {
2776
+ result.projectFilter = next;
2777
+ i++;
2778
+ } else if (arg === "--ticket" && next) {
2779
+ result.ticketFilter = next;
2780
+ i++;
2781
+ } else if (arg === "--blocked") result.blockedOnly = true;
2782
+ }
2783
+ return result;
2784
+ }
2785
+ function buildFilterSummary2(filters) {
2786
+ const parts = [];
2787
+ if (filters.statusFilter) parts.push(`status=${filters.statusFilter}`);
2788
+ if (filters.projectFilter) parts.push(`project=${filters.projectFilter}`);
2789
+ if (filters.ticketFilter) parts.push(`ticket=${filters.ticketFilter}`);
2790
+ if (filters.blockedOnly) parts.push("blocked");
2791
+ return parts.length > 0 ? ` (filtered: ${parts.join(", ")})` : "";
2792
+ }
2793
+ async function taskListCommand(args = []) {
2794
+ const { brief, statusFilter, projectFilter, ticketFilter, blockedOnly } = parseListArgs2(args);
2795
+ if (statusFilter) {
2796
+ const result = TaskStatusSchema.safeParse(statusFilter);
2797
+ if (!result.success) {
2798
+ showError(`Invalid status: "${statusFilter}". Valid values: todo, in_progress, done`);
2799
+ return;
2800
+ }
2801
+ }
2802
+ const tasks = await listTasks();
2803
+ if (tasks.length === 0) {
2804
+ showEmpty("tasks", "Add one with: ralphctl task add");
2805
+ return;
2806
+ }
2807
+ let filtered = tasks;
2808
+ if (statusFilter) filtered = filtered.filter((t) => t.status === statusFilter);
2809
+ if (projectFilter) filtered = filtered.filter((t) => t.projectPath.includes(projectFilter));
2810
+ if (ticketFilter) filtered = filtered.filter((t) => t.ticketId === ticketFilter);
2811
+ if (blockedOnly) filtered = filtered.filter((t) => t.blockedBy.length > 0);
2812
+ const filterStr = buildFilterSummary2({ brief, statusFilter, projectFilter, ticketFilter, blockedOnly });
2813
+ const isFiltered = filtered.length !== tasks.length;
2814
+ if (filtered.length === 0) {
2815
+ showEmpty("matching tasks", "Try adjusting your filters");
2816
+ return;
2817
+ }
2818
+ if (brief) {
2819
+ const countLabel = isFiltered ? `${String(filtered.length)} of ${String(tasks.length)}` : String(tasks.length);
2820
+ console.log(`
2821
+ # Tasks (${countLabel})${filterStr}
2822
+ `);
2823
+ for (const task of filtered) {
2824
+ const ticketRef = task.ticketId ? ` [${task.ticketId}]` : "";
2825
+ const blockedRef = task.blockedBy.length > 0 ? ` (blocked by: ${task.blockedBy.join(", ")})` : "";
2826
+ console.log(
2827
+ `- ${String(task.order)}. **[${task.status}]** ${task.id}: ${task.name} (${task.projectPath})${ticketRef}${blockedRef}`
2828
+ );
2829
+ }
2830
+ console.log("");
2831
+ return;
2832
+ }
2833
+ const tasksByStatus = {
2834
+ todo: filtered.filter((t) => t.status === "todo").length,
2835
+ in_progress: filtered.filter((t) => t.status === "in_progress").length,
2836
+ done: filtered.filter((t) => t.status === "done").length
2837
+ };
2838
+ printHeader(`Tasks (${String(filtered.length)})`, icons.task);
2839
+ log.raw(
2840
+ `${formatTaskStatus("todo")} ${String(tasksByStatus.todo)} ${formatTaskStatus("in_progress")} ${String(tasksByStatus.in_progress)} ${formatTaskStatus("done")} ${String(tasksByStatus.done)}`
2841
+ );
2842
+ log.newline();
2843
+ const rows = filtered.map((task) => {
2844
+ const statusIcon = task.status === "done" ? icons.success : task.status === "in_progress" ? icons.active : icons.inactive;
2845
+ const statusColor = task.status === "done" ? "success" : task.status === "in_progress" ? "warning" : "muted";
2846
+ const blocked = task.blockedBy.length > 0 ? colors.warning("(blocked)") : "";
2847
+ return [badge(statusIcon, statusColor), String(task.order), task.name, task.id, blocked];
2848
+ });
2849
+ console.log(
2850
+ renderTable(
2851
+ [
2852
+ { header: "", minWidth: 0 },
2853
+ { header: "#", align: "right" },
2854
+ { header: "Name" },
2855
+ { header: "ID" },
2856
+ { header: "" }
2857
+ ],
2858
+ rows
2859
+ )
2860
+ );
2861
+ const percent = filtered.length > 0 ? Math.round(tasksByStatus.done / filtered.length * 100) : 0;
2862
+ const progressColor = percent === 100 ? colors.success : percent > 50 ? colors.warning : colors.muted;
2863
+ const showingLabel = isFiltered ? `Showing ${String(filtered.length)} of ${String(tasks.length)} task(s)${filterStr}` : `Showing ${String(tasks.length)} task(s)`;
2864
+ log.newline();
2865
+ log.dim(
2866
+ `Progress: ${progressColor(`${String(tasksByStatus.done)}/${String(filtered.length)} (${String(percent)}%)`)} | ${showingLabel}`
2867
+ );
2868
+ log.newline();
2869
+ }
2870
+
2871
+ // src/commands/task/show.ts
2872
+ async function taskShowCommand(args) {
2873
+ let taskId = args[0];
2874
+ if (!taskId) {
2875
+ const selected = await selectTask("Select task to show:");
2876
+ if (!selected) return;
2877
+ taskId = selected;
2878
+ }
2879
+ try {
2880
+ const task = await getTask(taskId);
2881
+ const infoLines = [
2882
+ labelValue("ID", task.id),
2883
+ labelValue("Status", formatTaskStatus(task.status)),
2884
+ labelValue("Order", String(task.order)),
2885
+ labelValue("Project", task.projectPath)
2886
+ ];
2887
+ if (task.ticketId) {
2888
+ infoLines.push(labelValue("Ticket", task.ticketId));
2889
+ }
2890
+ if (task.description) {
2891
+ infoLines.push("");
2892
+ infoLines.push(labelValue("Description", ""));
2893
+ for (const line2 of task.description.split("\n")) {
2894
+ infoLines.push(`${" ".repeat(DETAIL_LABEL_WIDTH + 1)}${line2}`);
2895
+ }
2896
+ }
2897
+ log.newline();
2898
+ console.log(renderCard(`${icons.task} ${task.name}`, infoLines));
2899
+ if (task.steps.length > 0) {
2900
+ log.newline();
2901
+ const stepLines = [];
2902
+ for (let i = 0; i < task.steps.length; i++) {
2903
+ const step = task.steps[i] ?? "";
2904
+ const checkbox = task.status === "done" ? colors.success("[x]") : muted("[ ]");
2905
+ stepLines.push(`${checkbox} ${muted(String(i + 1) + ".")} ${step}`);
2906
+ }
2907
+ console.log(renderCard(`${icons.bullet} Steps (${String(task.steps.length)})`, stepLines));
2908
+ }
2909
+ if (task.blockedBy.length > 0) {
2910
+ log.newline();
2911
+ const depLines = [];
2912
+ for (const dep of task.blockedBy) {
2913
+ depLines.push(`${icons.bullet} ${dep}`);
2914
+ }
2915
+ console.log(renderCard(`${icons.warning} Blocked By`, depLines));
2916
+ }
2917
+ if (task.ticketId) {
2918
+ try {
2919
+ const ticket = await getTicket(task.ticketId);
2920
+ if (ticket.requirements) {
2921
+ log.newline();
2922
+ const reqLines = ticket.requirements.split("\n");
2923
+ console.log(renderCard(`${icons.ticket} Requirements`, reqLines));
2924
+ }
2925
+ } catch {
2926
+ }
2927
+ }
2928
+ if (task.verified) {
2929
+ log.newline();
2930
+ const verifyLines = [`${colors.success(icons.success)} Verified`];
2931
+ if (task.verificationOutput) {
2932
+ verifyLines.push(colors.muted(horizontalLine(30, "rounded")));
2933
+ for (const line2 of task.verificationOutput.split("\n").slice(0, 10)) {
2934
+ verifyLines.push(muted(line2));
2935
+ }
2936
+ }
2937
+ console.log(renderCard(`${icons.success} Verification`, verifyLines));
2938
+ }
2939
+ log.newline();
2940
+ } catch (err) {
2941
+ if (err instanceof TaskNotFoundError) {
2942
+ showError(`Task not found: ${taskId}`);
2943
+ showNextStep("ralphctl task list", "see available tasks");
2944
+ log.newline();
2945
+ } else {
2946
+ throw err;
2947
+ }
2948
+ }
2949
+ }
2950
+
2951
+ // src/commands/task/status.ts
2952
+ var VALID_STATUSES = ["todo", "in_progress", "done"];
2953
+ async function taskStatusCommand(args, options = {}) {
2954
+ let taskId = args[0] ?? options.taskId;
2955
+ let newStatus = args[1] ?? options.status;
2956
+ if (options.noInteractive) {
2957
+ const errors = [];
2958
+ if (!taskId?.trim()) {
2959
+ errors.push("Task ID is required");
2960
+ }
2961
+ if (!newStatus?.trim()) {
2962
+ errors.push("Status is required");
2963
+ } else {
2964
+ const result2 = TaskStatusSchema.safeParse(newStatus);
2965
+ if (!result2.success) {
2966
+ errors.push(`Invalid status: ${newStatus} (valid: ${VALID_STATUSES.join(", ")})`);
2967
+ }
2968
+ }
2969
+ if (errors.length > 0) {
2970
+ showError("Validation failed");
2971
+ for (const e of errors) {
2972
+ log.error(e);
2973
+ }
2974
+ log.newline();
2975
+ exitWithCode(EXIT_ERROR);
2976
+ }
2977
+ }
2978
+ if (!taskId) {
2979
+ const selected = await selectTask("Select task to update:");
2980
+ if (!selected) return;
2981
+ taskId = selected;
2982
+ }
2983
+ if (!newStatus) {
2984
+ const selected = await selectTaskStatus("Select new status:");
2985
+ if (!selected) return;
2986
+ newStatus = selected;
2987
+ }
2988
+ const result = TaskStatusSchema.safeParse(newStatus);
2989
+ if (!result.success) {
2990
+ showError(`Invalid status: ${newStatus}`);
2991
+ log.dim(`Valid statuses: ${VALID_STATUSES.join(", ")}`);
2992
+ log.newline();
2993
+ if (options.noInteractive) {
2994
+ exitWithCode(EXIT_ERROR);
2995
+ }
2996
+ return;
2997
+ }
2998
+ try {
2999
+ const task = await updateTaskStatus(taskId, result.data);
3000
+ showSuccess("Task status updated!", [
3001
+ ["ID", task.id],
3002
+ ["Name", task.name],
3003
+ ["Status", formatTaskStatus(task.status)]
3004
+ ]);
3005
+ log.newline();
3006
+ } catch (err) {
3007
+ if (err instanceof TaskNotFoundError) {
3008
+ showError(`Task not found: ${taskId}`);
3009
+ showNextStep("ralphctl task list", "see available tasks");
3010
+ log.newline();
3011
+ if (options.noInteractive) {
3012
+ exitWithCode(EXIT_ERROR);
3013
+ }
3014
+ } else if (err instanceof SprintStatusError) {
3015
+ showError(err.message);
3016
+ log.newline();
3017
+ if (options.noInteractive) {
3018
+ exitWithCode(EXIT_ERROR);
3019
+ }
3020
+ } else {
3021
+ throw err;
3022
+ }
3023
+ }
3024
+ }
3025
+
3026
+ // src/commands/task/next.ts
3027
+ async function taskNextCommand() {
3028
+ const task = await getNextTask();
3029
+ if (!task) {
3030
+ showEmpty("pending tasks", "All tasks are done, or add more with: ralphctl task add");
3031
+ return;
3032
+ }
3033
+ printHeader("Next Task");
3034
+ console.log(field("ID", task.id));
3035
+ console.log(field("Name", task.name));
3036
+ console.log(field("Status", formatTaskStatus(task.status)));
3037
+ console.log(field("Order", String(task.order)));
3038
+ if (task.ticketId) {
3039
+ console.log(field("Ticket", task.ticketId));
3040
+ }
3041
+ if (task.description) {
3042
+ log.newline();
3043
+ console.log(field("Description", ""));
3044
+ log.raw(task.description, 2);
3045
+ }
3046
+ if (task.steps.length > 0) {
3047
+ log.newline();
3048
+ console.log(field("Steps", ""));
3049
+ task.steps.forEach((step, i) => {
3050
+ log.raw(`${String(i + 1)}. ${step}`, 2);
3051
+ });
3052
+ }
3053
+ if (task.blockedBy.length > 0) {
3054
+ log.newline();
3055
+ console.log(field("Blocked By", ""));
3056
+ task.blockedBy.forEach((dep) => {
3057
+ log.item(dep);
3058
+ });
3059
+ }
3060
+ showNextStep(`ralphctl task status ${task.id} in_progress`, "Start working on this task");
3061
+ }
3062
+
3063
+ // src/commands/task/reorder.ts
3064
+ async function taskReorderCommand(args) {
3065
+ let taskId = args[0];
3066
+ let newOrder;
3067
+ if (args[1]) {
3068
+ newOrder = parseInt(args[1], 10);
3069
+ }
3070
+ if (!taskId) {
3071
+ const selected = await selectTask("Select task to reorder:");
3072
+ if (!selected) return;
3073
+ taskId = selected;
3074
+ }
3075
+ if (newOrder === void 0 || isNaN(newOrder) || newOrder < 1) {
3076
+ newOrder = await inputPositiveInt("New position (1 = highest priority):");
3077
+ }
3078
+ try {
3079
+ const task = await reorderTask(taskId, newOrder);
3080
+ showSuccess("Task reordered!", [
3081
+ ["ID", task.id],
3082
+ ["Name", task.name],
3083
+ ["New Order", String(task.order)]
3084
+ ]);
3085
+ log.newline();
3086
+ } catch (err) {
3087
+ if (err instanceof TaskNotFoundError) {
3088
+ showError(`Task not found: ${taskId}`);
3089
+ log.newline();
3090
+ } else if (err instanceof SprintStatusError) {
3091
+ showError(err.message);
3092
+ log.newline();
3093
+ } else {
3094
+ throw err;
3095
+ }
3096
+ }
3097
+ }
3098
+
3099
+ // src/commands/task/remove.ts
3100
+ import { confirm as confirm8 } from "@inquirer/prompts";
3101
+ async function taskRemoveCommand(args) {
3102
+ const skipConfirm = args.includes("-y") || args.includes("--yes");
3103
+ let taskId = args.find((a) => !a.startsWith("-"));
3104
+ if (!taskId) {
3105
+ const selected = await selectTask("Select task to remove:");
3106
+ if (!selected) return;
3107
+ taskId = selected;
3108
+ }
3109
+ try {
3110
+ const task = await getTask(taskId);
3111
+ if (!skipConfirm) {
3112
+ const confirmed = await confirm8({
3113
+ message: `Remove task "${task.name}" (${task.id})?`,
3114
+ default: false
3115
+ });
3116
+ if (!confirmed) {
3117
+ console.log(muted("\nTask removal cancelled.\n"));
3118
+ return;
3119
+ }
3120
+ }
3121
+ await removeTask(taskId);
3122
+ showSuccess("Task removed", [["ID", taskId]]);
3123
+ log.newline();
3124
+ } catch (err) {
3125
+ if (err instanceof TaskNotFoundError) {
3126
+ showError(`Task not found: ${taskId}`);
3127
+ log.newline();
3128
+ } else if (err instanceof SprintStatusError) {
3129
+ showError(err.message);
3130
+ log.newline();
3131
+ } else {
3132
+ throw err;
3133
+ }
3134
+ }
3135
+ }
3136
+
3137
+ // src/commands/progress/log.ts
3138
+ async function progressLogCommand(args) {
3139
+ try {
3140
+ const sprintId = await resolveSprintId();
3141
+ const sprint = await getSprint(sprintId);
3142
+ assertSprintStatus(sprint, ["active"], "log progress");
3143
+ } catch (err) {
3144
+ if (err instanceof SprintStatusError) {
3145
+ const mainError = err.message.split("\n")[0] ?? err.message;
3146
+ showError(mainError);
3147
+ showNextStep("ralphctl sprint start", "activate the sprint");
3148
+ log.newline();
3149
+ return;
3150
+ }
3151
+ if (err instanceof NoCurrentSprintError) {
3152
+ showError("No current sprint set.");
3153
+ showNextStep("ralphctl sprint create", "create a new sprint");
3154
+ log.newline();
3155
+ return;
3156
+ }
3157
+ throw err;
3158
+ }
3159
+ let message = args.join(" ").trim();
3160
+ if (!message) {
3161
+ message = await editorInput({
3162
+ message: "Progress message:"
3163
+ });
3164
+ message = message.trim();
3165
+ }
3166
+ if (!message) {
3167
+ showError("No message provided.");
3168
+ log.newline();
3169
+ return;
3170
+ }
3171
+ try {
3172
+ await logProgress(message);
3173
+ showSuccess("Progress logged.");
3174
+ log.newline();
3175
+ } catch (err) {
3176
+ if (err instanceof SprintStatusError) {
3177
+ showError(err.message);
3178
+ log.newline();
3179
+ } else {
3180
+ throw err;
3181
+ }
3182
+ }
3183
+ }
3184
+
3185
+ // src/commands/progress/show.ts
3186
+ async function progressShowCommand() {
3187
+ const content = await getProgress();
3188
+ if (!content.trim()) {
3189
+ showEmpty("progress entries", "Log with: ralphctl progress log");
3190
+ return;
3191
+ }
3192
+ printHeader("Progress Log");
3193
+ console.log(content);
3194
+ }
3195
+
3196
+ // src/commands/config/config.ts
3197
+ async function configSetCommand(args) {
3198
+ if (args.length < 2) {
3199
+ showError("Usage: ralphctl config set <key> <value>");
3200
+ log.dim("Available keys: provider, editor");
3201
+ log.newline();
3202
+ return;
3203
+ }
3204
+ const [key, value] = args;
3205
+ if (key === "provider") {
3206
+ const parsed = AiProviderSchema.safeParse(value);
3207
+ if (!parsed.success) {
3208
+ showError(`Invalid provider: ${value ?? "(empty)"}`);
3209
+ log.dim("Valid providers: claude, copilot");
3210
+ log.newline();
3211
+ return;
3212
+ }
3213
+ await setAiProvider(parsed.data);
3214
+ showSuccess(`AI provider set to: ${parsed.data}`);
3215
+ log.newline();
3216
+ return;
3217
+ }
3218
+ if (key === "editor") {
3219
+ const trimmed = value?.trim();
3220
+ if (!trimmed) {
3221
+ showError("Editor command cannot be empty");
3222
+ log.dim('Examples: "subl -w", "code --wait", "vim", "nano"');
3223
+ log.newline();
3224
+ return;
3225
+ }
3226
+ await setEditor(trimmed);
3227
+ showSuccess(`Editor set to: ${trimmed}`);
3228
+ log.newline();
3229
+ return;
3230
+ }
3231
+ showError(`Unknown config key: ${key ?? "(empty)"}`);
3232
+ log.dim("Available keys: provider, editor");
3233
+ log.newline();
3234
+ }
3235
+ async function configShowCommand() {
3236
+ const provider = await getAiProvider();
3237
+ const editorCmd = await getEditor();
3238
+ printHeader("Configuration", icons.info);
3239
+ console.log(field("AI Provider", provider ?? "(not set \u2014 will prompt on first use)"));
3240
+ console.log(field("Editor", editorCmd ?? "(not set \u2014 will prompt on first use)"));
3241
+ log.newline();
3242
+ }
3243
+
3244
+ // src/commands/doctor/doctor.ts
3245
+ import { access, constants } from "fs/promises";
3246
+ import { join as join4 } from "path";
3247
+ import { spawnSync as spawnSync2 } from "child_process";
3248
+ var REQUIRED_NODE_MAJOR = 24;
3249
+ function checkNodeVersion() {
3250
+ const version = process.version;
3251
+ const match = /^v(\d+)/.exec(version);
3252
+ const major = match ? Number(match[1]) : 0;
3253
+ if (major >= REQUIRED_NODE_MAJOR) {
3254
+ return { name: "Node.js version", status: "pass", detail: version };
3255
+ }
3256
+ return {
3257
+ name: "Node.js version",
3258
+ status: "fail",
3259
+ detail: `${version} (requires >= ${String(REQUIRED_NODE_MAJOR)}.0.0)`
3260
+ };
3261
+ }
3262
+ function checkGitInstalled() {
3263
+ const result = spawnSync2("git", ["--version"], {
3264
+ encoding: "utf-8",
3265
+ stdio: ["ignore", "pipe", "pipe"]
3266
+ });
3267
+ if (result.status === 0) {
3268
+ const version = result.stdout.trim();
3269
+ return { name: "Git installed", status: "pass", detail: version };
3270
+ }
3271
+ return { name: "Git installed", status: "fail", detail: "git not found in PATH" };
3272
+ }
3273
+ function checkGitIdentity() {
3274
+ const nameResult = spawnSync2("git", ["config", "user.name"], {
3275
+ encoding: "utf-8",
3276
+ stdio: ["ignore", "pipe", "pipe"]
3277
+ });
3278
+ const emailResult = spawnSync2("git", ["config", "user.email"], {
3279
+ encoding: "utf-8",
3280
+ stdio: ["ignore", "pipe", "pipe"]
3281
+ });
3282
+ const name = nameResult.status === 0 ? nameResult.stdout.trim() : "";
3283
+ const email = emailResult.status === 0 ? emailResult.stdout.trim() : "";
3284
+ if (name && email) {
3285
+ return { name: "Git identity", status: "pass", detail: `${name} <${email}>` };
3286
+ }
3287
+ const missing = [];
3288
+ if (!name) missing.push("user.name");
3289
+ if (!email) missing.push("user.email");
3290
+ return { name: "Git identity", status: "warn", detail: `missing: ${missing.join(", ")}` };
3291
+ }
3292
+ async function checkAiProvider() {
3293
+ const config = await getConfig();
3294
+ const provider = config.aiProvider;
3295
+ if (!provider) {
3296
+ return { name: "AI provider binary", status: "skip", detail: "not configured" };
3297
+ }
3298
+ const binary = provider === "claude" ? "claude" : "copilot";
3299
+ const result = spawnSync2("which", [binary], {
3300
+ encoding: "utf-8",
3301
+ stdio: ["ignore", "pipe", "pipe"]
3302
+ });
3303
+ if (result.status === 0) {
3304
+ return { name: "AI provider binary", status: "pass", detail: `${binary} found` };
3305
+ }
3306
+ return {
3307
+ name: "AI provider binary",
3308
+ status: "fail",
3309
+ detail: `${binary} not found in PATH (provider: ${provider})`
3310
+ };
3311
+ }
3312
+ function checkGlabInstalled() {
3313
+ if (isGlabAvailable()) {
3314
+ return { name: "GitLab CLI (glab)", status: "pass", detail: "installed" };
3315
+ }
3316
+ return {
3317
+ name: "GitLab CLI (glab)",
3318
+ status: "skip",
3319
+ detail: "not installed (optional \u2014 needed for GitLab issue enrichment)"
3320
+ };
3321
+ }
3322
+ async function checkDataDirectory() {
3323
+ const dataDir = getDataDir();
3324
+ try {
3325
+ await access(dataDir, constants.R_OK | constants.W_OK);
3326
+ return { name: "Data directory", status: "pass", detail: dataDir };
3327
+ } catch {
3328
+ return { name: "Data directory", status: "fail", detail: `${dataDir} not accessible or writable` };
3329
+ }
3330
+ }
3331
+ async function checkProjectPaths() {
3332
+ const projects = await listProjects();
3333
+ if (projects.length === 0) {
3334
+ return { name: "Project paths", status: "skip", detail: "no projects registered" };
3335
+ }
3336
+ const issues = [];
3337
+ for (const project of projects) {
3338
+ for (const repo of project.repositories) {
3339
+ const validation = await validateProjectPath(repo.path);
3340
+ if (validation !== true) {
3341
+ issues.push(`${project.name}/${repo.name}: ${validation}`);
3342
+ continue;
3343
+ }
3344
+ const gitDir = join4(repo.path, ".git");
3345
+ if (!await fileExists(gitDir)) {
3346
+ issues.push(`${project.name}/${repo.name}: not a git repository`);
3347
+ }
3348
+ }
3349
+ }
3350
+ if (issues.length === 0) {
3351
+ const repoCount = projects.reduce((sum, p) => sum + p.repositories.length, 0);
3352
+ return {
3353
+ name: "Project paths",
3354
+ status: "pass",
3355
+ detail: `${String(repoCount)} repo${repoCount !== 1 ? "s" : ""} verified`
3356
+ };
3357
+ }
3358
+ return { name: "Project paths", status: "fail", detail: issues.join("; ") };
3359
+ }
3360
+ async function checkCurrentSprint() {
3361
+ const config = await getConfig();
3362
+ const sprintId = config.currentSprint;
3363
+ if (!sprintId) {
3364
+ return { name: "Current sprint", status: "skip", detail: "no current sprint set" };
3365
+ }
3366
+ const sprintPath = getSprintFilePath(sprintId);
3367
+ if (!await fileExists(sprintPath)) {
3368
+ return { name: "Current sprint", status: "fail", detail: `sprint file missing: ${sprintId}` };
3369
+ }
3370
+ try {
3371
+ const sprint = await readValidatedJson(sprintPath, SprintSchema);
3372
+ return { name: "Current sprint", status: "pass", detail: `${sprint.name} (${sprint.status})` };
3373
+ } catch (err) {
3374
+ const message = err instanceof Error ? err.message : String(err);
3375
+ return { name: "Current sprint", status: "fail", detail: `invalid sprint data: ${message}` };
3376
+ }
3377
+ }
3378
+ async function doctorCommand() {
3379
+ printHeader("System Health Check", icons.info);
3380
+ const results = [];
3381
+ results.push(checkNodeVersion());
3382
+ results.push(checkGitInstalled());
3383
+ results.push(checkGitIdentity());
3384
+ results.push(checkGlabInstalled());
3385
+ const asyncResults = await Promise.all([
3386
+ checkAiProvider(),
3387
+ checkDataDirectory(),
3388
+ checkProjectPaths(),
3389
+ checkCurrentSprint()
3390
+ ]);
3391
+ results.push(...asyncResults);
3392
+ for (const result of results) {
3393
+ if (result.status === "pass") {
3394
+ log.success(`${result.name}${result.detail ? colors.muted(` \u2014 ${result.detail}`) : ""}`);
3395
+ } else if (result.status === "warn") {
3396
+ log.warn(`${result.name}${result.detail ? colors.muted(` \u2014 ${result.detail}`) : ""}`);
3397
+ } else if (result.status === "fail") {
3398
+ log.error(result.name);
3399
+ if (result.detail) {
3400
+ log.dim(` ${result.detail}`);
3401
+ }
3402
+ } else {
3403
+ log.raw(
3404
+ `${icons.bullet} ${colors.muted(result.name)} ${colors.muted("\u2014")} ${colors.muted(result.detail ?? "skipped")}`
3405
+ );
3406
+ }
3407
+ }
3408
+ log.newline();
3409
+ const passed = results.filter((r) => r.status === "pass").length;
3410
+ const warned = results.filter((r) => r.status === "warn").length;
3411
+ const failed = results.filter((r) => r.status === "fail").length;
3412
+ const total = results.filter((r) => r.status !== "skip").length;
3413
+ if (failed === 0 && warned === 0) {
3414
+ log.success(`All checks passed (${String(passed)}/${String(total)})`);
3415
+ log.newline();
3416
+ const quote = getQuoteForContext("success");
3417
+ log.dim(`"${quote}"`);
3418
+ } else if (failed === 0) {
3419
+ log.success(
3420
+ `${String(passed)}/${String(total)} checks passed, ${String(warned)} warning${warned !== 1 ? "s" : ""}`
3421
+ );
3422
+ log.newline();
3423
+ const quote = getQuoteForContext("success");
3424
+ log.dim(`"${quote}"`);
3425
+ } else {
3426
+ log.error(`${String(passed)}/${String(total)} checks passed, ${String(failed)} failed`);
3427
+ log.newline();
3428
+ const quote = getQuoteForContext("error");
3429
+ log.dim(`"${quote}"`);
3430
+ process.exitCode = EXIT_ERROR;
3431
+ }
3432
+ log.newline();
3433
+ }
3434
+
3435
+ // src/interactive/index.ts
3436
+ var selectTheme = {
3437
+ icon: { cursor: emoji.donut },
3438
+ style: {
3439
+ highlight: (text) => colors.highlight(text),
3440
+ description: (text) => colors.muted(text)
3441
+ }
3442
+ };
3443
+ var commandMap = {
3444
+ project: {
3445
+ add: () => projectAddCommand({ interactive: true }),
3446
+ list: () => projectListCommand(),
3447
+ show: () => projectShowCommand([]),
3448
+ remove: () => projectRemoveCommand([]),
3449
+ "repo add": () => projectRepoAddCommand([]),
3450
+ "repo remove": () => projectRepoRemoveCommand([])
3451
+ },
3452
+ sprint: {
3453
+ create: () => sprintCreateCommand({ interactive: true }),
3454
+ list: () => sprintListCommand(),
3455
+ show: () => sprintShowCommand([]),
3456
+ context: () => sprintContextCommand([]),
3457
+ current: () => sprintCurrentCommand(["-"]),
3458
+ refine: () => sprintRefineCommand([]),
3459
+ ideate: () => sprintIdeateCommand([]),
3460
+ plan: () => sprintPlanCommand([]),
3461
+ start: () => sprintStartCommand([]),
3462
+ requirements: () => sprintRequirementsCommand([]),
3463
+ health: () => sprintHealthCommand(),
3464
+ close: () => sprintCloseCommand([]),
3465
+ delete: () => sprintDeleteCommand([]),
3466
+ "progress show": () => progressShowCommand(),
3467
+ "progress log": () => progressLogCommand([])
3468
+ },
3469
+ ticket: {
3470
+ add: () => ticketAddCommand({ interactive: true }),
3471
+ edit: () => ticketEditCommand(void 0, { interactive: true }),
3472
+ list: () => ticketListCommand([]),
3473
+ show: () => ticketShowCommand([]),
3474
+ refine: () => ticketRefineCommand(void 0, { interactive: true }),
3475
+ remove: () => ticketRemoveCommand([])
3476
+ },
3477
+ task: {
3478
+ add: () => taskAddCommand({ interactive: true }),
3479
+ import: () => taskImportCommand([]),
3480
+ list: () => taskListCommand([]),
3481
+ show: () => taskShowCommand([]),
3482
+ status: () => taskStatusCommand([]),
3483
+ next: () => taskNextCommand(),
3484
+ reorder: () => taskReorderCommand([]),
3485
+ remove: () => taskRemoveCommand([])
3486
+ },
3487
+ progress: {
3488
+ log: () => progressLogCommand([]),
3489
+ show: () => progressShowCommand()
3490
+ },
3491
+ doctor: {
3492
+ run: () => doctorCommand()
3493
+ },
3494
+ config: {
3495
+ show: () => configShowCommand(),
3496
+ "set provider": async () => {
3497
+ const choice = await select3({
3498
+ message: `${emoji.donut} Which AI buddy should help with my homework?`,
3499
+ choices: [
3500
+ { name: "Claude Code", value: "claude" },
3501
+ { name: "GitHub Copilot", value: "copilot" }
3502
+ ],
3503
+ default: await getAiProvider() ?? void 0,
3504
+ theme: selectTheme
3505
+ });
3506
+ await configSetCommand(["provider", choice]);
3507
+ }
3508
+ }
3509
+ };
3510
+ function showFarewell() {
3511
+ const quote = getQuoteForContext("farewell");
3512
+ console.log("");
3513
+ printSeparator();
3514
+ console.log(` ${emoji.donut} ${colors.muted(quote)}`);
3515
+ console.log("");
3516
+ }
3517
+ async function pressEnterToContinue() {
3518
+ const { createInterface } = await import("readline");
3519
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3520
+ await new Promise((resolve3) => {
3521
+ rl.question(colors.muted(" Press Enter to continue..."), () => {
3522
+ rl.close();
3523
+ resolve3();
3524
+ });
3525
+ });
3526
+ }
3527
+ function showWelcomeBanner() {
3528
+ showBanner();
3529
+ }
3530
+ async function readTasksSafe(sprintId) {
3531
+ try {
3532
+ return await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
3533
+ } catch {
3534
+ return [];
3535
+ }
3536
+ }
3537
+ async function getMenuContext() {
3538
+ let dashboardData = null;
3539
+ const ctx = {
3540
+ hasProjects: false,
3541
+ projectCount: 0,
3542
+ currentSprintId: null,
3543
+ currentSprintName: null,
3544
+ currentSprintStatus: null,
3545
+ ticketCount: 0,
3546
+ taskCount: 0,
3547
+ tasksDone: 0,
3548
+ tasksInProgress: 0,
3549
+ pendingRequirements: 0,
3550
+ allRequirementsApproved: false,
3551
+ plannedTicketCount: 0,
3552
+ nextAction: null,
3553
+ aiProvider: null
3554
+ };
3555
+ const [config, projects] = await Promise.all([getConfig().catch(() => null), listProjects().catch(() => [])]);
3556
+ ctx.hasProjects = projects.length > 0;
3557
+ ctx.projectCount = projects.length;
3558
+ ctx.aiProvider = config?.aiProvider ?? null;
3559
+ const sprintId = config?.currentSprint ?? null;
3560
+ if (!sprintId) return { ctx, dashboardData };
3561
+ ctx.currentSprintId = sprintId;
3562
+ const [sprint, tasks] = await Promise.all([getSprint(sprintId).catch(() => null), readTasksSafe(sprintId)]);
3563
+ if (!sprint) return { ctx, dashboardData };
3564
+ ctx.currentSprintName = sprint.name;
3565
+ ctx.currentSprintStatus = sprint.status;
3566
+ ctx.ticketCount = sprint.tickets.length;
3567
+ const pendingTickets = getPendingRequirements(sprint.tickets);
3568
+ ctx.pendingRequirements = pendingTickets.length;
3569
+ ctx.allRequirementsApproved = allRequirementsApproved(sprint.tickets);
3570
+ ctx.taskCount = tasks.length;
3571
+ ctx.tasksDone = tasks.filter((t) => t.status === "done").length;
3572
+ ctx.tasksInProgress = tasks.filter((t) => t.status === "in_progress").length;
3573
+ const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
3574
+ ctx.plannedTicketCount = sprint.tickets.filter((t) => ticketIdsWithTasks.has(t.id)).length;
3575
+ const doneIds = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
3576
+ const blockedCount = tasks.filter(
3577
+ (t) => t.status !== "done" && t.blockedBy.length > 0 && !t.blockedBy.every((id) => doneIds.has(id))
3578
+ ).length;
3579
+ dashboardData = {
3580
+ sprint,
3581
+ tasks,
3582
+ approvedCount: sprint.tickets.length - pendingTickets.length,
3583
+ pendingCount: pendingTickets.length,
3584
+ blockedCount,
3585
+ plannedTicketCount: ctx.plannedTicketCount,
3586
+ aiProvider: ctx.aiProvider
3587
+ };
3588
+ ctx.nextAction = getNextAction(dashboardData);
3589
+ return { ctx, dashboardData };
3590
+ }
3591
+ async function interactiveMode() {
3592
+ let escPressed = false;
3593
+ while (true) {
3594
+ try {
3595
+ const { ctx, dashboardData } = await getMenuContext();
3596
+ clearScreen();
3597
+ showWelcomeBanner();
3598
+ const statusLines = renderStatusHeader(dashboardData);
3599
+ if (statusLines.length > 0) {
3600
+ for (const line2 of statusLines) {
3601
+ console.log(line2);
3602
+ }
3603
+ log.newline();
3604
+ }
3605
+ const { items: mainMenu, defaultValue } = buildMainMenu(ctx);
3606
+ const effectiveDefault = escPressed ? "exit" : defaultValue;
3607
+ escPressed = false;
3608
+ const command = await escapableSelect(
3609
+ {
3610
+ message: `${emoji.donut} What would you like to do?`,
3611
+ choices: mainMenu,
3612
+ default: effectiveDefault,
3613
+ pageSize: 30,
3614
+ loop: true,
3615
+ theme: selectTheme
3616
+ },
3617
+ { escLabel: "exit" }
3618
+ );
3619
+ if (command === null) {
3620
+ escPressed = true;
3621
+ continue;
3622
+ }
3623
+ if (command === "exit") {
3624
+ showFarewell();
3625
+ break;
3626
+ }
3627
+ if (command.startsWith("action:")) {
3628
+ const parts = command.split(":");
3629
+ const group = parts[1] ?? "";
3630
+ const subCommand = parts[2] ?? "";
3631
+ log.newline();
3632
+ await executeCommand(group, subCommand);
3633
+ log.newline();
3634
+ await pressEnterToContinue();
3635
+ continue;
3636
+ }
3637
+ if (command === "wizard") {
3638
+ const { runWizard } = await import("./wizard-LRELAN2J.mjs");
3639
+ await runWizard();
3640
+ continue;
3641
+ }
3642
+ const subMenu = buildSubMenu(command, ctx);
3643
+ if (subMenu) {
3644
+ await handleSubMenu(command, subMenu);
3645
+ }
3646
+ } catch (err) {
3647
+ if (err.name === "ExitPromptError") {
3648
+ showFarewell();
3649
+ break;
3650
+ }
3651
+ throw err;
3652
+ }
3653
+ }
3654
+ }
3655
+ async function handleSubMenu(commandGroup, initialSubMenu) {
3656
+ let currentTitle = initialSubMenu.title;
3657
+ let currentItems = initialSubMenu.items;
3658
+ while (true) {
3659
+ try {
3660
+ log.newline();
3661
+ const subCommand = await escapableSelect({
3662
+ message: `${emoji.donut} ${currentTitle}`,
3663
+ choices: currentItems,
3664
+ pageSize: 30,
3665
+ loop: true,
3666
+ theme: selectTheme
3667
+ });
3668
+ if (subCommand === null || subCommand === "back") {
3669
+ break;
3670
+ }
3671
+ log.newline();
3672
+ await executeCommand(commandGroup, subCommand);
3673
+ log.newline();
3674
+ if (isWorkflowAction(commandGroup, subCommand)) {
3675
+ break;
3676
+ }
3677
+ const { ctx: refreshedCtx } = await getMenuContext();
3678
+ const refreshedMenu = buildSubMenu(commandGroup, refreshedCtx);
3679
+ if (refreshedMenu) {
3680
+ currentTitle = refreshedMenu.title;
3681
+ currentItems = refreshedMenu.items;
3682
+ }
3683
+ } catch (err) {
3684
+ if (err.name === "ExitPromptError") {
3685
+ break;
3686
+ }
3687
+ throw err;
3688
+ }
3689
+ }
3690
+ }
3691
+ async function executeCommand(group, subCommand) {
3692
+ const groupHandlers = commandMap[group];
3693
+ const handler = groupHandlers?.[subCommand];
3694
+ if (!handler) {
3695
+ log.error(`Unknown command: ${group} ${subCommand}`);
3696
+ return;
3697
+ }
3698
+ try {
3699
+ await handler();
3700
+ } catch (err) {
3701
+ if (err instanceof Error) {
3702
+ log.error(err.message);
3703
+ }
3704
+ }
3705
+ }
3706
+
3707
+ // src/commands/project/index.ts
3708
+ function registerProjectCommands(program2) {
3709
+ const project = program2.command("project").description("Manage projects");
3710
+ project.addHelpText(
3711
+ "after",
3712
+ `
3713
+ Examples:
3714
+ $ ralphctl project add --name api --display-name "API Server" --path ~/code/api
3715
+ $ ralphctl project list
3716
+ $ ralphctl project show api
3717
+ $ ralphctl project repo add api ~/code/api-v2
3718
+ `
3719
+ );
3720
+ project.command("add").description("Add/update project").option("--name <name>", "Slug (lowercase, numbers, hyphens)").option("--display-name <name>", "Human-readable name").option("--path <path...>", "Repository path (repeatable)").option("--description <desc>", "Optional description").option("--check-script <cmd>", "Check command (install + verify)").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(
3721
+ async (opts) => {
3722
+ await projectAddCommand({
3723
+ name: opts.name,
3724
+ displayName: opts.displayName,
3725
+ paths: opts.path,
3726
+ description: opts.description,
3727
+ checkScript: opts.checkScript,
3728
+ // --no-interactive sets interactive=false, otherwise true (prompt for missing)
3729
+ interactive: opts.interactive !== false
3730
+ });
3731
+ }
3732
+ );
3733
+ project.command("list").description("List all projects").action(projectListCommand);
3734
+ project.command("show [name]").description("Show project details").action(async (name) => {
3735
+ await projectShowCommand(name ? [name] : []);
3736
+ });
3737
+ project.command("remove [name]").description("Remove a project").option("-y, --yes", "Skip confirmation").action(async (name, opts) => {
3738
+ const args = [];
3739
+ if (name) args.push(name);
3740
+ if (opts?.yes) args.push("-y");
3741
+ await projectRemoveCommand(args);
3742
+ });
3743
+ const repo = project.command("repo").description("Manage project repositories");
3744
+ repo.addHelpText(
3745
+ "after",
3746
+ `
3747
+ Examples:
3748
+ $ ralphctl project repo add my-app ~/code/new-service
3749
+ $ ralphctl project repo remove my-app ~/code/old-service
3750
+ `
3751
+ );
3752
+ repo.command("add [name] [path]").description("Add repository to project").action(async (name, pathArg) => {
3753
+ const args = [];
3754
+ if (name) args.push(name);
3755
+ if (pathArg) args.push(pathArg);
3756
+ await projectRepoAddCommand(args);
3757
+ });
3758
+ repo.command("remove [name] [path]").description("Remove repository from project").option("-y, --yes", "Skip confirmation").action(async (name, pathArg, opts) => {
3759
+ const args = [];
3760
+ if (name) args.push(name);
3761
+ if (pathArg) args.push(pathArg);
3762
+ if (opts?.yes) args.push("-y");
3763
+ await projectRepoRemoveCommand(args);
3764
+ });
3765
+ }
3766
+
3767
+ // src/commands/sprint/switch.ts
3768
+ async function sprintSwitchCommand() {
3769
+ const selectedId = await selectSprint("Select sprint to switch to:");
3770
+ if (!selectedId) return;
3771
+ await setCurrentSprint(selectedId);
3772
+ const sprint = await getSprint(selectedId);
3773
+ showSuccess("Switched to sprint!", [
3774
+ ["ID", sprint.id],
3775
+ ["Name", sprint.name]
3776
+ ]);
3777
+ log.newline();
3778
+ }
3779
+
3780
+ // src/commands/sprint/index.ts
3781
+ function registerSprintCommands(program2) {
3782
+ const sprint = program2.command("sprint").description("Manage sprints");
3783
+ sprint.addHelpText(
3784
+ "after",
3785
+ `
3786
+ Examples:
3787
+ $ ralphctl sprint create --name "Sprint 1"
3788
+ $ ralphctl sprint refine # Refine ticket requirements with AI
3789
+ $ ralphctl sprint plan --auto # Generate tasks automatically
3790
+ $ ralphctl sprint start -s # Start with interactive session
3791
+ `
3792
+ );
3793
+ sprint.command("create").description("Create a new sprint").option("--name <name>", "Sprint name").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(async (opts) => {
3794
+ await sprintCreateCommand({
3795
+ name: opts.name,
3796
+ // --no-interactive sets interactive=false, otherwise true (prompt for missing)
3797
+ interactive: opts.interactive !== false
3798
+ });
3799
+ });
3800
+ sprint.command("list").description("List all sprints").option("--status <status>", "Filter by status (draft, active, closed)").action(async (opts) => {
3801
+ const args = [];
3802
+ if (opts.status) args.push("--status", opts.status);
3803
+ await sprintListCommand(args);
3804
+ });
3805
+ sprint.command("show [id]").description("Show sprint details").action(async (id) => {
3806
+ await sprintShowCommand(id ? [id] : []);
3807
+ });
3808
+ sprint.command("context [id]").description("Output full context for planning").action(async (id) => {
3809
+ await sprintContextCommand(id ? [id] : []);
3810
+ });
3811
+ sprint.command("current [id]").description('Show/set current sprint (use "-" to open selector)').action(async (id) => {
3812
+ await sprintCurrentCommand(id ? [id] : []);
3813
+ });
3814
+ sprint.command("switch").description("Quick sprint switcher (opens selector)").action(async () => {
3815
+ await sprintSwitchCommand();
3816
+ });
3817
+ sprint.command("refine [id]").description("Refine ticket specifications").option("--project <name>", "Only refine tickets for specific project").action(async (id, opts) => {
3818
+ const args = [];
3819
+ if (id) args.push(id);
3820
+ if (opts?.project) args.push("--project", opts.project);
3821
+ await sprintRefineCommand(args);
3822
+ });
3823
+ sprint.command("ideate [id]").description("Quick idea to tasks (refine + plan in one session)").option("--auto", "Run without user interaction (AI decides autonomously)").option("--all-paths", "Explore all project repositories instead of prompting for selection").option("--project <name>", "Pre-select project (skip interactive selection)").action(async (id, opts) => {
3824
+ const args = [];
3825
+ if (id) args.push(id);
3826
+ if (opts?.auto) args.push("--auto");
3827
+ if (opts?.allPaths) args.push("--all-paths");
3828
+ if (opts?.project) args.push("--project", opts.project);
3829
+ await sprintIdeateCommand(args);
3830
+ });
3831
+ sprint.command("plan [id]").description("Generate tasks using AI CLI").option("--auto", "Run without user interaction (AI decides autonomously)").option("--all-paths", "Explore all project repositories instead of prompting for selection").action(async (id, opts) => {
3832
+ const args = [];
3833
+ if (id) args.push(id);
3834
+ if (opts?.auto) args.push("--auto");
3835
+ if (opts?.allPaths) args.push("--all-paths");
3836
+ await sprintPlanCommand(args);
3837
+ });
3838
+ sprint.command("close [id]").description("Close an active sprint").option("--create-pr", "Create pull requests for sprint branches").action(async (id, opts) => {
3839
+ const args = [];
3840
+ if (id) args.push(id);
3841
+ if (opts?.createPr) args.push("--create-pr");
3842
+ await sprintCloseCommand(args);
3843
+ });
3844
+ sprint.command("delete [id]").description("Delete a sprint permanently").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
3845
+ const args = [];
3846
+ if (id) args.push(id);
3847
+ if (opts?.yes) args.push("-y");
3848
+ await sprintDeleteCommand(args);
3849
+ });
3850
+ sprint.command("requirements [id]").description("Export refined requirements to file").action(async (id) => {
3851
+ await sprintRequirementsCommand(id ? [id] : []);
3852
+ });
3853
+ sprint.command("health").description("Check sprint health").action(async () => {
3854
+ await sprintHealthCommand();
3855
+ });
3856
+ sprint.command("start [id]").description("Run automated implementation loop").option("-s, --session", "Interactive AI session (collaborate with your AI provider)").option("-t, --step", "Step through tasks with approval between each").option("-c, --count <n>", "Limit to N tasks").option("--no-commit", "Skip automatic git commit after each task completes").option("--concurrency <n>", "Max parallel tasks (default: auto based on unique repos)").option("--max-retries <n>", "Max rate-limit retries per task (default: 5)").option("--fail-fast", "Stop launching new tasks on first failure").option("-f, --force", "Skip precondition checks (e.g., unplanned tickets)").option("--refresh-check", "Force re-run check scripts even if they already ran this sprint").option("-b, --branch", "Create sprint branch (ralphctl/<sprint-id>) in all repos").option("--branch-name <name>", "Use a custom branch name for sprint execution").addHelpText(
3857
+ "after",
3858
+ `
3859
+ Exit Codes:
3860
+ 0 - Success (all requested operations completed)
3861
+ 1 - Error (validation, missing params, execution failed)
3862
+ 2 - No tasks available
3863
+ 3 - All remaining tasks blocked by dependencies
3864
+
3865
+ Parallel Execution:
3866
+ Tasks targeting different repos run concurrently by default.
3867
+ At most one task per repository runs at a time to avoid git conflicts.
3868
+ Use --concurrency 1 to force sequential execution.
3869
+ Session (--session) and step (--step) modes always run sequentially.
3870
+
3871
+ Branch Management:
3872
+ Use -b/--branch to auto-create a sprint branch in all repos.
3873
+ Use --branch-name <name> to specify a custom branch name.
3874
+ On first run, an interactive prompt offers branch strategy selection.
3875
+ The chosen branch is persisted and reused on subsequent runs.
3876
+ `
3877
+ ).action(
3878
+ async (id, opts) => {
3879
+ const args = [];
3880
+ if (id) args.push(id);
3881
+ if (opts?.session) args.push("--session");
3882
+ if (opts?.step) args.push("--step");
3883
+ if (opts?.count) args.push("--count", opts.count);
3884
+ if (opts?.commit === false) args.push("--no-commit");
3885
+ if (opts?.concurrency) args.push("--concurrency", opts.concurrency);
3886
+ if (opts?.maxRetries) args.push("--max-retries", opts.maxRetries);
3887
+ if (opts?.failFast) args.push("--fail-fast");
3888
+ if (opts?.force) args.push("--force");
3889
+ if (opts?.refreshCheck) args.push("--refresh-check");
3890
+ if (opts?.branch) args.push("--branch");
3891
+ if (opts?.branchName) args.push("--branch-name", opts.branchName);
3892
+ await sprintStartCommand(args);
3893
+ }
3894
+ );
3895
+ }
3896
+
3897
+ // src/commands/task/index.ts
3898
+ function registerTaskCommands(program2) {
3899
+ const task = program2.command("task").description("Manage tasks");
3900
+ task.addHelpText(
3901
+ "after",
3902
+ `
3903
+ Examples:
3904
+ $ ralphctl task add --name "Implement login" --ticket abc123
3905
+ $ ralphctl task list
3906
+ $ ralphctl task status abc123 done
3907
+ $ ralphctl task next
3908
+ `
3909
+ );
3910
+ task.command("add").description("Add task to current sprint").option("--name <name>", "Task name").option("-d, --description <desc>", "Description").option("--step <step...>", "Implementation step (repeatable)").option("--ticket <id>", "Link to ticket ID").option("-p, --project <path>", "Project path").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(
3911
+ async (opts) => {
3912
+ await taskAddCommand({
3913
+ name: opts.name,
3914
+ description: opts.description,
3915
+ steps: opts.step,
3916
+ ticket: opts.ticket,
3917
+ project: opts.project,
3918
+ // --no-interactive sets interactive=false, otherwise true (prompt for missing)
3919
+ interactive: opts.interactive !== false
3920
+ });
3921
+ }
3922
+ );
3923
+ task.command("import <file>").description("Import tasks from JSON file").action(async (file) => {
3924
+ await taskImportCommand([file]);
3925
+ });
3926
+ task.command("list").description("List tasks").option("-b, --brief", "Brief format").option("--status <status>", "Filter by status (todo, in_progress, done)").option("--project <name>", "Filter by project path").option("--ticket <id>", "Filter by ticket ID").option("--blocked", "Show only blocked tasks").action(
3927
+ async (opts) => {
3928
+ const args = [];
3929
+ if (opts.brief) args.push("-b");
3930
+ if (opts.status) args.push("--status", opts.status);
3931
+ if (opts.project) args.push("--project", opts.project);
3932
+ if (opts.ticket) args.push("--ticket", opts.ticket);
3933
+ if (opts.blocked) args.push("--blocked");
3934
+ await taskListCommand(args);
3935
+ }
3936
+ );
3937
+ task.command("show [id]").description("Show task details").action(async (id) => {
3938
+ await taskShowCommand(id ? [id] : []);
3939
+ });
3940
+ task.command("remove [id]").description("Remove a task").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
3941
+ const args = [];
3942
+ if (id) args.push(id);
3943
+ if (opts?.yes) args.push("-y");
3944
+ await taskRemoveCommand(args);
3945
+ });
3946
+ task.command("status [id] [status]").description("Update task status (todo/in_progress/done)").option("-n, --no-interactive", "Non-interactive mode (exit with error codes)").action(async (id, status, opts) => {
3947
+ await taskStatusCommand([], {
3948
+ taskId: id,
3949
+ status,
3950
+ noInteractive: opts?.interactive === false
3951
+ });
3952
+ });
3953
+ task.command("next").description("Get next task").action(taskNextCommand);
3954
+ task.command("reorder [id] [position]").description("Change task priority").action(async (id, position) => {
3955
+ const args = [];
3956
+ if (id) args.push(id);
3957
+ if (position) args.push(position);
3958
+ await taskReorderCommand(args);
3959
+ });
3960
+ }
3961
+
3962
+ // src/commands/ticket/index.ts
3963
+ function registerTicketCommands(program2) {
3964
+ const ticket = program2.command("ticket").description("Manage tickets");
3965
+ ticket.addHelpText(
3966
+ "after",
3967
+ `
3968
+ Examples:
3969
+ $ ralphctl ticket add --project api --title "Fix auth bug"
3970
+ $ ralphctl ticket edit abc123 --title "New title"
3971
+ $ ralphctl ticket list -b
3972
+ $ ralphctl ticket show abc123
3973
+ `
3974
+ );
3975
+ ticket.command("add").description("Add ticket to current sprint").option("-p, --project <name>", "Project name").option("-t, --title <title>", "Ticket title").option("-d, --description <desc>", "Description").option("--link <url>", "Link to external issue").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(
3976
+ async (opts) => {
3977
+ await ticketAddCommand({
3978
+ project: opts.project,
3979
+ title: opts.title,
3980
+ description: opts.description,
3981
+ link: opts.link,
3982
+ // --no-interactive sets interactive=false, otherwise true (prompt for missing)
3983
+ interactive: opts.interactive !== false
3984
+ });
3985
+ }
3986
+ );
3987
+ ticket.command("edit [id]").description("Edit an existing ticket").option("--title <title>", "New title").option("--description <desc>", "New description").option("--link <url>", "New link").option("-n, --no-interactive", "Non-interactive mode").action(
3988
+ async (id, opts) => {
3989
+ await ticketEditCommand(id, {
3990
+ title: opts?.title,
3991
+ description: opts?.description,
3992
+ link: opts?.link,
3993
+ interactive: opts?.interactive !== false
3994
+ });
3995
+ }
3996
+ );
3997
+ ticket.command("list").description("List tickets").option("-b, --brief", "Brief one-liner format").option("--project <name>", "Filter by project").option("--status <status>", "Filter by requirement status (pending, approved)").action(async (opts) => {
3998
+ const args = [];
3999
+ if (opts.brief) args.push("-b");
4000
+ if (opts.project) args.push("--project", opts.project);
4001
+ if (opts.status) args.push("--status", opts.status);
4002
+ await ticketListCommand(args);
4003
+ });
4004
+ ticket.command("show [id]").description("Show ticket details").action(async (id) => {
4005
+ await ticketShowCommand(id ? [id] : []);
4006
+ });
4007
+ ticket.command("refine [id]").description("Re-refine an approved ticket").action(async (id) => {
4008
+ await ticketRefineCommand(id);
4009
+ });
4010
+ ticket.command("remove [id]").description("Remove a ticket").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
4011
+ const args = [];
4012
+ if (id) args.push(id);
4013
+ if (opts?.yes) args.push("-y");
4014
+ await ticketRemoveCommand(args);
4015
+ });
4016
+ }
4017
+
4018
+ // src/commands/progress/index.ts
4019
+ function registerProgressCommands(program2) {
4020
+ const progress = program2.command("progress").description("Log and view progress");
4021
+ progress.addHelpText(
4022
+ "after",
4023
+ `
4024
+ Examples:
4025
+ $ ralphctl progress log "Completed auth flow"
4026
+ $ ralphctl progress show
4027
+ `
4028
+ );
4029
+ progress.command("log [message]").description("Append to progress log (opens editor if no message)").action(async (message) => {
4030
+ await progressLogCommand(message ? [message] : []);
4031
+ });
4032
+ progress.command("show").description("Display progress log").action(progressShowCommand);
4033
+ }
4034
+
4035
+ // src/commands/dashboard/dashboard.ts
4036
+ async function dashboardCommand() {
4037
+ await showDashboard();
4038
+ }
4039
+
4040
+ // src/commands/dashboard/index.ts
4041
+ function registerDashboardCommands(program2) {
4042
+ program2.command("status").description("Show current sprint overview").action(dashboardCommand);
4043
+ }
4044
+
4045
+ // src/commands/config/index.ts
4046
+ function registerConfigCommands(program2) {
4047
+ const config = program2.command("config").description("Manage configuration");
4048
+ config.addHelpText(
4049
+ "after",
4050
+ `
4051
+ Examples:
4052
+ $ ralphctl config show # Show current configuration
4053
+ $ ralphctl config set provider claude # Use Claude Code
4054
+ $ ralphctl config set provider copilot # Use GitHub Copilot
4055
+ $ ralphctl config set editor "subl -w" # Use Sublime Text for multiline input
4056
+ $ ralphctl config set editor "code --wait" # Use VS Code for multiline input
4057
+ $ ralphctl config set editor vim # Use Vim for multiline input
4058
+ `
4059
+ );
4060
+ config.command("show").description("Show current configuration").action(async () => {
4061
+ await configShowCommand();
4062
+ });
4063
+ config.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
4064
+ await configSetCommand([key, value]);
4065
+ });
4066
+ }
4067
+
4068
+ // src/commands/completion/index.ts
4069
+ function registerCompletionCommands(program2) {
4070
+ const completion = program2.command("completion").description("Manage shell tab-completion");
4071
+ completion.addHelpText(
4072
+ "after",
4073
+ `
4074
+ Examples:
4075
+ $ ralphctl completion install # Enable tab-completion for your shell
4076
+ $ ralphctl completion uninstall # Remove tab-completion
4077
+ `
4078
+ );
4079
+ completion.command("install").description("Install shell tab-completion (bash, zsh, fish)").action(async () => {
4080
+ const tabtab = (await import("tabtab")).default;
4081
+ await tabtab.install({ name: "ralphctl", completer: "ralphctl" });
4082
+ showSuccess("Shell completion installed. Restart your shell or source your profile to activate.");
4083
+ });
4084
+ completion.command("uninstall").description("Remove shell tab-completion").action(async () => {
4085
+ const tabtab = (await import("tabtab")).default;
4086
+ await tabtab.uninstall({ name: "ralphctl" });
4087
+ showSuccess("Shell completion removed.");
4088
+ });
4089
+ }
4090
+
4091
+ // src/commands/doctor/index.ts
4092
+ function registerDoctorCommands(program2) {
4093
+ program2.command("doctor").description("Check environment health and diagnose setup issues").addHelpText(
4094
+ "after",
4095
+ `
4096
+ Examples:
4097
+ $ ralphctl doctor # Run all health checks
4098
+
4099
+ Checks performed:
4100
+ - Node.js version (>= 24)
4101
+ - Git installation and identity
4102
+ - AI provider binary (claude or copilot)
4103
+ - Data directory accessibility
4104
+ - Project repository paths
4105
+ - Current sprint validity`
4106
+ ).action(async () => {
4107
+ await doctorCommand();
4108
+ });
4109
+ }
4110
+
4111
+ // package.json
4112
+ var package_default = {
4113
+ name: "ralphctl",
4114
+ version: "0.1.2",
4115
+ description: "Sprint and task management CLI for AI-assisted coding",
4116
+ homepage: "https://github.com/lukas-grigis/ralphctl",
4117
+ type: "module",
4118
+ license: "MIT",
4119
+ author: "Lukas Grigis",
4120
+ repository: {
4121
+ type: "git",
4122
+ url: "https://github.com/lukas-grigis/ralphctl.git"
4123
+ },
4124
+ bugs: {
4125
+ url: "https://github.com/lukas-grigis/ralphctl/issues"
4126
+ },
4127
+ keywords: [
4128
+ "cli",
4129
+ "claude",
4130
+ "ai",
4131
+ "sprint",
4132
+ "task-management",
4133
+ "planning",
4134
+ "anthropic",
4135
+ "developer-tools"
4136
+ ],
4137
+ bin: {
4138
+ ralphctl: "./dist/cli.mjs"
4139
+ },
4140
+ files: [
4141
+ "dist/",
4142
+ "schemas/"
4143
+ ],
4144
+ publishConfig: {
4145
+ access: "public"
4146
+ },
4147
+ scripts: {
4148
+ build: "tsup && mkdir -p dist/prompts && cp src/ai/prompts/*.md dist/prompts/",
4149
+ prepublishOnly: "pnpm build",
4150
+ dev: "tsx src/cli.ts",
4151
+ lint: "eslint .",
4152
+ "lint:fix": "eslint . --fix",
4153
+ format: "prettier --write .",
4154
+ "format:check": "prettier --check .",
4155
+ typecheck: "tsc --noEmit",
4156
+ test: "vitest run",
4157
+ "test:watch": "vitest",
4158
+ "test:coverage": "vitest run --coverage",
4159
+ prepare: "husky"
4160
+ },
4161
+ packageManager: "pnpm@10.29.3",
4162
+ engines: {
4163
+ node: ">=24.0.0"
4164
+ },
4165
+ dependencies: {
4166
+ "@inquirer/prompts": "^8.3.0",
4167
+ colorette: "^2.0.20",
4168
+ commander: "^14.0.3",
4169
+ "gradient-string": "^3.0.0",
4170
+ ora: "^9.3.0",
4171
+ tabtab: "^3.0.2",
4172
+ zod: "^4.3.6"
4173
+ },
4174
+ devDependencies: {
4175
+ "@eslint/js": "^10.0.1",
4176
+ "@types/node": "^25.3.3",
4177
+ "@types/tabtab": "^3.0.4",
4178
+ eslint: "^10.0.2",
4179
+ "eslint-config-prettier": "^10.1.8",
4180
+ globals: "^17.4.0",
4181
+ husky: "^9.1.7",
4182
+ "lint-staged": "^16.3.1",
4183
+ prettier: "^3.8.1",
4184
+ tsup: "^8.5.1",
4185
+ tsx: "^4.21.0",
4186
+ typescript: "^5.9.3",
4187
+ "typescript-eslint": "^8.56.1",
4188
+ vitest: "^4.0.18"
4189
+ },
4190
+ "lint-staged": {
4191
+ "*.ts": [
4192
+ "eslint --cache --fix",
4193
+ "prettier --write"
4194
+ ],
4195
+ "*.{md,json,yml,yaml}": "prettier --write"
4196
+ }
4197
+ };
4198
+
4199
+ // src/cli-metadata.ts
4200
+ var cliMetadata = {
4201
+ name: "ralphctl",
4202
+ version: package_default.version,
4203
+ description: "I'm helping! Plan sprints and execute tasks with AI"
4204
+ };
4205
+
4206
+ // src/cli.ts
4207
+ var program = new Command();
4208
+ program.name(cliMetadata.name).description(cliMetadata.description).version(cliMetadata.version).addHelpText(
4209
+ "after",
4210
+ `
4211
+ Examples:
4212
+ $ ralphctl # Interactive mode
4213
+ $ ralphctl status # Show current sprint status
4214
+ $ ralphctl sprint create --name "v1.0" # Create sprint
4215
+ $ ralphctl ticket add --project api # Add ticket
4216
+ $ ralphctl task list -b # Brief task list
4217
+
4218
+ Run any command with --help for details.
4219
+ `
4220
+ );
4221
+ registerProjectCommands(program);
4222
+ registerSprintCommands(program);
4223
+ registerTaskCommands(program);
4224
+ registerTicketCommands(program);
4225
+ registerProgressCommands(program);
4226
+ registerDashboardCommands(program);
4227
+ registerConfigCommands(program);
4228
+ registerCompletionCommands(program);
4229
+ registerDoctorCommands(program);
4230
+ async function main() {
4231
+ if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
4232
+ const { handleCompletionRequest } = await import("./handle-UG5M2OON.mjs");
4233
+ if (await handleCompletionRequest(program)) return;
4234
+ }
4235
+ if (process.argv.length <= 2 || process.argv[2] === "interactive") {
4236
+ await interactiveMode();
4237
+ } else {
4238
+ showBanner();
4239
+ await program.parseAsync(process.argv);
4240
+ }
4241
+ }
4242
+ main().catch((err) => {
4243
+ console.error(error("Fatal error:"), err);
4244
+ process.exit(1);
4245
+ });