ralphctl 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -2,9 +2,8 @@
2
2
  import {
3
3
  addCheckScriptToRepository,
4
4
  projectAddCommand
5
- } from "./chunk-MRKOFVTM.mjs";
5
+ } from "./chunk-YIB7QYU4.mjs";
6
6
  import {
7
- TaskNotFoundError,
8
7
  addTask,
9
8
  areAllTasksDone,
10
9
  branchExists,
@@ -50,16 +49,14 @@ import {
50
49
  sprintStartCommand,
51
50
  updateTaskStatus,
52
51
  validateImportTasks
53
- } from "./chunk-LOR7QBXX.mjs";
52
+ } from "./chunk-M7JV6MKD.mjs";
54
53
  import {
55
54
  escapableSelect
56
- } from "./chunk-NTWO2LXB.mjs";
55
+ } from "./chunk-7LZ6GOGN.mjs";
57
56
  import {
58
57
  sprintCreateCommand
59
- } from "./chunk-JON4GCLR.mjs";
58
+ } from "./chunk-DZ6HHTM5.mjs";
60
59
  import {
61
- IssueFetchError,
62
- TicketNotFoundError,
63
60
  addTicket,
64
61
  allRequirementsApproved,
65
62
  editorInput,
@@ -73,23 +70,19 @@ import {
73
70
  removeTicket,
74
71
  ticketAddCommand,
75
72
  updateTicket
76
- } from "./chunk-MNMQC36F.mjs";
73
+ } from "./chunk-F2MMCTB5.mjs";
77
74
  import {
78
75
  EXIT_ERROR,
79
76
  exitWithCode
80
77
  } from "./chunk-7TG3EAQ2.mjs";
81
78
  import {
82
- ProjectNotFoundError,
83
79
  addProjectRepo,
84
80
  getProject,
85
81
  listProjects,
86
82
  removeProject,
87
83
  removeProjectRepo
88
- } from "./chunk-WGHJI3OI.mjs";
84
+ } from "./chunk-PDI6HBZ7.mjs";
89
85
  import {
90
- NoCurrentSprintError,
91
- SprintNotFoundError,
92
- SprintStatusError,
93
86
  assertSprintStatus,
94
87
  closeSprint,
95
88
  deleteSprint,
@@ -108,7 +101,11 @@ import {
108
101
  setCurrentSprint,
109
102
  setEditor,
110
103
  withFileLock
111
- } from "./chunk-EKMZZRWI.mjs";
104
+ } from "./chunk-LFDW6MWF.mjs";
105
+ import {
106
+ ensureError,
107
+ wrapAsync
108
+ } from "./chunk-OEUJDSHY.mjs";
112
109
  import {
113
110
  AiProviderSchema,
114
111
  IdeateOutputSchema,
@@ -130,7 +127,16 @@ import {
130
127
  getTasksFilePath,
131
128
  readValidatedJson,
132
129
  validateProjectPath
133
- } from "./chunk-6PYTKGB5.mjs";
130
+ } from "./chunk-W3TY22IS.mjs";
131
+ import {
132
+ DomainError,
133
+ NoCurrentSprintError,
134
+ ProjectNotFoundError,
135
+ SprintNotFoundError,
136
+ SprintStatusError,
137
+ TaskNotFoundError,
138
+ TicketNotFoundError
139
+ } from "./chunk-EDJX7TT6.mjs";
134
140
  import {
135
141
  DETAIL_LABEL_WIDTH,
136
142
  badge,
@@ -433,7 +439,7 @@ function buildSubMenu(group, ctx) {
433
439
  async function loadDashboardData() {
434
440
  const sprintId = await getCurrentSprint();
435
441
  if (!sprintId) return null;
436
- try {
442
+ const r = await wrapAsync(async () => {
437
443
  const sprint = await getSprint(sprintId);
438
444
  const tasks = await getTasks(sprintId);
439
445
  const pendingTickets = getPendingRequirements(sprint.tickets);
@@ -447,9 +453,8 @@ async function loadDashboardData() {
447
453
  const plannedTicketCount = sprint.tickets.filter((t) => ticketIdsWithTasks.has(t.id)).length;
448
454
  const aiProvider = await getAiProvider();
449
455
  return { sprint, tasks, approvedCount, pendingCount, blockedCount, plannedTicketCount, aiProvider };
450
- } catch {
451
- return null;
452
- }
456
+ }, ensureError);
457
+ return r.ok ? r.value : null;
453
458
  }
454
459
  function getNextAction(data) {
455
460
  const { sprint, tasks, pendingCount, approvedCount } = data;
@@ -640,34 +645,35 @@ async function projectShowCommand(args) {
640
645
  if (!selected) return;
641
646
  projectName = selected;
642
647
  }
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) {
648
+ const projectR = await wrapAsync(() => getProject(projectName), ensureError);
649
+ if (!projectR.ok) {
650
+ if (projectR.error instanceof ProjectNotFoundError) {
651
+ showError(`Project not found: ${projectName}`);
653
652
  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));
653
+ } else {
654
+ throw projectR.error;
661
655
  }
656
+ return;
657
+ }
658
+ const project = projectR.value;
659
+ const infoLines = [labelValue("Name", project.name), labelValue("Display Name", project.displayName)];
660
+ if (project.description) {
661
+ infoLines.push(labelValue("Description", project.description));
662
+ }
663
+ infoLines.push(labelValue("Repositories", String(project.repositories.length)));
664
+ log.newline();
665
+ console.log(renderCard(`${icons.project} ${project.displayName}`, infoLines));
666
+ for (const repo of project.repositories) {
662
667
  log.newline();
663
- } catch (err) {
664
- if (err instanceof ProjectNotFoundError) {
665
- showError(`Project not found: ${projectName}`);
666
- log.newline();
668
+ const repoLines = [labelValue("Path", repo.path)];
669
+ if (repo.checkScript) {
670
+ repoLines.push(labelValue("Check", colors.info(repo.checkScript)));
667
671
  } else {
668
- throw err;
672
+ repoLines.push(muted("No check script configured"));
669
673
  }
674
+ console.log(renderCard(` ${repo.name}`, repoLines));
670
675
  }
676
+ log.newline();
671
677
  }
672
678
 
673
679
  // src/commands/project/remove.ts
@@ -680,29 +686,30 @@ async function projectRemoveCommand(args) {
680
686
  if (!selected) return;
681
687
  projectName = selected;
682
688
  }
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) {
689
+ const projectR = await wrapAsync(() => getProject(projectName), ensureError);
690
+ if (!projectR.ok) {
691
+ if (projectR.error instanceof ProjectNotFoundError) {
700
692
  showError(`Project not found: ${projectName}`);
701
693
  console.log("");
702
694
  } else {
703
- throw err;
695
+ throw projectR.error;
696
+ }
697
+ return;
698
+ }
699
+ const project = projectR.value;
700
+ if (!skipConfirm) {
701
+ const confirmed = await confirm({
702
+ message: `Remove project "${project.displayName}" (${project.name})?`,
703
+ default: false
704
+ });
705
+ if (!confirmed) {
706
+ console.log(muted("\nProject removal cancelled.\n"));
707
+ return;
704
708
  }
705
709
  }
710
+ await removeProject(projectName);
711
+ showSuccess("Project removed", [["Name", projectName]]);
712
+ console.log("");
706
713
  }
707
714
 
708
715
  // src/commands/project/repo.ts
@@ -720,31 +727,28 @@ async function projectRepoAddCommand(args) {
720
727
  message: `${emoji.donut} Repository path to add:`,
721
728
  validate: (v) => v.trim().length > 0 ? true : "Path is required"
722
729
  });
723
- try {
724
- const resolvedPath = resolve(expandTilde(path));
725
- const bareRepo = { name: basename(resolvedPath), path: resolvedPath };
726
- log.info(`
730
+ const resolvedPath = resolve(expandTilde(path));
731
+ const bareRepo = { name: basename(resolvedPath), path: resolvedPath };
732
+ log.info(`
727
733
  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) {
734
+ const repoWithScripts = await addCheckScriptToRepository(bareRepo);
735
+ const addR = await wrapAsync(() => addProjectRepo(projectName, repoWithScripts), ensureError);
736
+ if (!addR.ok) {
737
+ if (addR.error instanceof ProjectNotFoundError) {
739
738
  showError(`Project not found: ${projectName}`);
740
- log.newline();
741
- } else if (err instanceof Error) {
742
- showError(err.message);
743
- log.newline();
744
739
  } else {
745
- throw err;
740
+ showError(addR.error.message);
746
741
  }
742
+ log.newline();
743
+ return;
744
+ }
745
+ showSuccess("Repository added", [["Project", projectName]]);
746
+ log.newline();
747
+ log.info("Current repositories:");
748
+ for (const repo of addR.value.repositories) {
749
+ log.item(`${repo.name} \u2192 ${repo.path}`);
747
750
  }
751
+ log.newline();
748
752
  }
749
753
  async function projectRepoRemoveCommand(args) {
750
754
  const skipConfirm = args.includes("-y") || args.includes("--yes");
@@ -756,50 +760,57 @@ async function projectRepoRemoveCommand(args) {
756
760
  if (!selected) return;
757
761
  projectName = selected;
758
762
  }
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
- }
763
+ const projectR = await wrapAsync(() => getProject(projectName), ensureError);
764
+ if (!projectR.ok) {
765
+ if (projectR.error instanceof ProjectNotFoundError) {
766
+ showError(`Project not found: ${projectName}`);
767
+ } else {
768
+ showError(projectR.error.message);
783
769
  }
784
- const updatedProject = await removeProjectRepo(projectName, path);
785
- showSuccess("Repository removed", [["Project", projectName]]);
786
770
  log.newline();
787
- log.info("Remaining repositories:");
788
- for (const repo of updatedProject.repositories) {
789
- log.item(`${repo.name} \u2192 ${repo.path}`);
771
+ return;
772
+ }
773
+ const project = projectR.value;
774
+ if (!path) {
775
+ if (project.repositories.length === 0) {
776
+ console.log(muted("\nNo repositories to remove.\n"));
777
+ return;
790
778
  }
791
- log.newline();
792
- } catch (err) {
793
- if (err instanceof ProjectNotFoundError) {
779
+ path = await select({
780
+ message: `${emoji.donut} Select repository to remove:`,
781
+ choices: project.repositories.map((r) => ({
782
+ name: `${r.name} (${r.path})`,
783
+ value: r.path
784
+ }))
785
+ });
786
+ }
787
+ if (!skipConfirm) {
788
+ const confirmed = await confirm2({
789
+ message: `Remove repository "${path}" from project "${project.displayName}"?`,
790
+ default: false
791
+ });
792
+ if (!confirmed) {
793
+ console.log(muted("\nRepository removal cancelled.\n"));
794
+ return;
795
+ }
796
+ }
797
+ const removeR = await wrapAsync(() => removeProjectRepo(projectName, path), ensureError);
798
+ if (!removeR.ok) {
799
+ if (removeR.error instanceof ProjectNotFoundError) {
794
800
  showError(`Project not found: ${projectName}`);
795
- log.newline();
796
- } else if (err instanceof Error) {
797
- showError(err.message);
798
- log.newline();
799
801
  } else {
800
- throw err;
802
+ showError(removeR.error.message);
801
803
  }
804
+ log.newline();
805
+ return;
802
806
  }
807
+ showSuccess("Repository removed", [["Project", projectName]]);
808
+ log.newline();
809
+ log.info("Remaining repositories:");
810
+ for (const repo of removeR.value.repositories) {
811
+ log.item(`${repo.name} \u2192 ${repo.path}`);
812
+ }
813
+ log.newline();
803
814
  }
804
815
 
805
816
  // src/commands/sprint/list.ts
@@ -864,12 +875,13 @@ async function sprintListCommand(args = []) {
864
875
  async function sprintShowCommand(args) {
865
876
  const sprintId = args[0];
866
877
  let id;
867
- try {
868
- id = await resolveSprintId(sprintId);
869
- } catch {
878
+ const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
879
+ if (!idR.ok) {
870
880
  const selected = await selectSprint("Select sprint to show:");
871
881
  if (!selected) return;
872
882
  id = selected;
883
+ } else {
884
+ id = idR.value;
873
885
  }
874
886
  const sprint = await getSprint(id);
875
887
  const tasks = await listTasks(id);
@@ -961,9 +973,8 @@ async function sprintShowCommand(args) {
961
973
  async function sprintContextCommand(args) {
962
974
  const sprintId = args[0];
963
975
  let id;
964
- try {
965
- id = await resolveSprintId(sprintId);
966
- } catch {
976
+ const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
977
+ if (!idR.ok) {
967
978
  const selected = await selectSprint("Select sprint to show context for:");
968
979
  if (!selected) {
969
980
  showWarning("No sprints available.");
@@ -972,6 +983,8 @@ async function sprintContextCommand(args) {
972
983
  return;
973
984
  }
974
985
  id = selected;
986
+ } else {
987
+ id = idR.value;
975
988
  }
976
989
  const sprint = await getSprint(id);
977
990
  const tasks = await listTasks(id);
@@ -987,11 +1000,11 @@ async function sprintContextCommand(args) {
987
1000
  const ticketsByProject = groupTicketsByProject(sprint.tickets);
988
1001
  for (const [projectName, tickets] of ticketsByProject) {
989
1002
  console.log(`### Project: ${projectName}`);
990
- try {
991
- const project = await getProject(projectName);
992
- const repoPaths = project.repositories.map((r) => `${r.name} (${r.path})`);
1003
+ const projectR = await wrapAsync(() => getProject(projectName), ensureError);
1004
+ if (projectR.ok) {
1005
+ const repoPaths = projectR.value.repositories.map((r) => `${r.name} (${r.path})`);
993
1006
  console.log(`Repositories: ${repoPaths.join(", ")}`);
994
- } catch {
1007
+ } else {
995
1008
  console.log("Repositories: (project not found)");
996
1009
  }
997
1010
  console.log("");
@@ -1055,14 +1068,14 @@ async function sprintCurrentCommand(args) {
1055
1068
  log.newline();
1056
1069
  return;
1057
1070
  }
1058
- try {
1059
- const sprint = await getSprint(currentSprintId);
1071
+ const sprintR = await wrapAsync(() => getSprint(currentSprintId), ensureError);
1072
+ if (sprintR.ok) {
1060
1073
  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)));
1074
+ console.log(field("ID", sprintR.value.id));
1075
+ console.log(field("Name", sprintR.value.name));
1076
+ console.log(field("Status", formatSprintStatus(sprintR.value.status)));
1064
1077
  log.newline();
1065
- } catch {
1078
+ } else {
1066
1079
  showWarning(`Current sprint "${currentSprintId}" no longer exists.`);
1067
1080
  showNextStep("ralphctl sprint current -", "select a different sprint");
1068
1081
  log.newline();
@@ -1080,22 +1093,23 @@ async function sprintCurrentCommand(args) {
1080
1093
  ]);
1081
1094
  log.newline();
1082
1095
  } else {
1083
- try {
1096
+ const setR = await wrapAsync(async () => {
1084
1097
  const sprint = await getSprint(sprintId);
1085
1098
  await setCurrentSprint(sprintId);
1099
+ return sprint;
1100
+ }, ensureError);
1101
+ if (setR.ok) {
1086
1102
  showSuccess("Current sprint set!", [
1087
- ["ID", sprint.id],
1088
- ["Name", sprint.name]
1103
+ ["ID", setR.value.id],
1104
+ ["Name", setR.value.name]
1089
1105
  ]);
1090
1106
  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
- }
1107
+ } else if (setR.error instanceof SprintNotFoundError) {
1108
+ showError(`Sprint not found: ${sprintId}`);
1109
+ showNextStep("ralphctl sprint list", "see available sprints");
1110
+ log.newline();
1111
+ } else {
1112
+ throw setR.error;
1099
1113
  }
1100
1114
  }
1101
1115
  }
@@ -1104,6 +1118,7 @@ async function sprintCurrentCommand(args) {
1104
1118
  import { mkdir, readFile, writeFile } from "fs/promises";
1105
1119
  import { join } from "path";
1106
1120
  import { input as input2, select as select2 } from "@inquirer/prompts";
1121
+ import { Result } from "typescript-result";
1107
1122
  function parseArgs(args) {
1108
1123
  const options = {
1109
1124
  auto: false,
@@ -1163,13 +1178,11 @@ async function invokeAiAuto(prompt, repoPaths, ideateDir) {
1163
1178
  }
1164
1179
  function parseIdeateOutput(output) {
1165
1180
  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 });
1181
+ const parseR = Result.try(() => JSON.parse(jsonStr));
1182
+ if (!parseR.ok) {
1183
+ throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
1171
1184
  }
1172
- const result = IdeateOutputSchema.safeParse(parsed);
1185
+ const result = IdeateOutputSchema.safeParse(parseR.value);
1173
1186
  if (!result.success) {
1174
1187
  const issues = result.error.issues.map((issue) => {
1175
1188
  const path = issue.path.length > 0 ? `[${issue.path.join(".")}]` : "";
@@ -1182,23 +1195,20 @@ ${issues}`);
1182
1195
  }
1183
1196
  async function sprintIdeateCommand(args) {
1184
1197
  const { sprintId, options } = parseArgs(args);
1185
- let id;
1186
- try {
1187
- id = await resolveSprintId(sprintId);
1188
- } catch {
1198
+ const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
1199
+ if (!idR.ok) {
1189
1200
  showWarning("No sprint specified and no current sprint set.");
1190
1201
  showNextStep("ralphctl sprint create", "create a new sprint");
1191
1202
  log.newline();
1192
1203
  return;
1193
1204
  }
1205
+ const id = idR.value;
1194
1206
  const sprint = await getSprint(id);
1195
1207
  try {
1196
1208
  assertSprintStatus(sprint, ["draft"], "ideate");
1197
1209
  } catch (err) {
1198
- if (err instanceof Error) {
1199
- showError(err.message);
1200
- log.newline();
1201
- }
1210
+ showError(err instanceof Error ? err.message : String(err));
1211
+ log.newline();
1202
1212
  return;
1203
1213
  }
1204
1214
  const projects = await listProjects();
@@ -1230,21 +1240,26 @@ async function sprintIdeateCommand(args) {
1230
1240
  log.newline();
1231
1241
  return;
1232
1242
  }
1233
- let project;
1234
- try {
1235
- project = await getProject(projectName);
1236
- } catch {
1243
+ const projectR = await wrapAsync(() => getProject(projectName), ensureError);
1244
+ if (!projectR.ok) {
1237
1245
  showError(`Project '${projectName}' not found.`);
1238
1246
  log.newline();
1239
1247
  return;
1240
1248
  }
1249
+ const project = projectR.value;
1241
1250
  const ideaTitle = await input2({
1242
1251
  message: "Idea title (short summary):",
1243
1252
  validate: (value) => value.trim().length > 0 ? true : "Title is required"
1244
1253
  });
1245
- const ideaDescription = await editorInput({
1254
+ const editorR = await editorInput({
1246
1255
  message: "Idea description (what you want to build):"
1247
1256
  });
1257
+ if (!editorR.ok) {
1258
+ showError(`Editor input failed: ${editorR.error.message}`);
1259
+ log.newline();
1260
+ return;
1261
+ }
1262
+ const ideaDescription = editorR.value;
1248
1263
  if (!ideaDescription.trim()) {
1249
1264
  showError("Description is required.");
1250
1265
  log.newline();
@@ -1291,19 +1306,16 @@ async function sprintIdeateCommand(args) {
1291
1306
  const prompt = buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositoriesText, schema);
1292
1307
  const spinner = createSpinner(`${providerName} is refining idea and planning tasks...`);
1293
1308
  spinner.start();
1294
- let output;
1295
- try {
1296
- output = await invokeAiAuto(prompt, selectedPaths, ideateDir);
1297
- spinner.succeed(`${providerName} finished`);
1298
- } catch (err) {
1309
+ const outputR = await wrapAsync(() => invokeAiAuto(prompt, selectedPaths, ideateDir), ensureError);
1310
+ if (!outputR.ok) {
1299
1311
  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
- }
1312
+ showError(`Failed to invoke ${providerName}: ${outputR.error.message}`);
1313
+ showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
1314
+ log.newline();
1305
1315
  return;
1306
1316
  }
1317
+ spinner.succeed(`${providerName} finished`);
1318
+ const output = outputR.value;
1307
1319
  const blockedReason = parsePlanningBlocked(output);
1308
1320
  if (blockedReason) {
1309
1321
  showWarning(`Planning blocked: ${blockedReason}`);
@@ -1311,18 +1323,15 @@ async function sprintIdeateCommand(args) {
1311
1323
  return;
1312
1324
  }
1313
1325
  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
- }
1326
+ const ideateR = Result.try(() => parseIdeateOutput(output));
1327
+ if (!ideateR.ok) {
1328
+ showError(`Failed to parse ${providerName} output: ${ideateR.error.message}`);
1329
+ log.dim("Raw output:");
1330
+ console.log(output);
1331
+ log.newline();
1324
1332
  return;
1325
1333
  }
1334
+ const ideateOutput = ideateR.value;
1326
1335
  const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
1327
1336
  const ticketToUpdate = sprint.tickets[ticketIdx];
1328
1337
  if (ticketIdx !== -1 && ticketToUpdate) {
@@ -1332,16 +1341,13 @@ async function sprintIdeateCommand(args) {
1332
1341
  await saveSprint(sprint);
1333
1342
  showSuccess("Requirements approved and saved!");
1334
1343
  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
- }
1344
+ const parsedTasksR = Result.try(() => parseTasksJson(JSON.stringify(ideateOutput.tasks)));
1345
+ if (!parsedTasksR.ok) {
1346
+ showError(`Failed to parse tasks: ${parsedTasksR.error.message}`);
1347
+ log.newline();
1343
1348
  return;
1344
1349
  }
1350
+ const parsedTasks = parsedTasksR.value;
1345
1351
  if (parsedTasks.length === 0) {
1346
1352
  showWarning("No tasks generated.");
1347
1353
  log.newline();
@@ -1376,37 +1382,29 @@ async function sprintIdeateCommand(args) {
1376
1382
  ${providerName} will guide you through requirements refinement and task planning.`));
1377
1383
  console.log(muted(` When done, ask ${providerName} to write the output to: ${outputFile}
1378
1384
  `));
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
- }
1385
+ const interactiveR = await wrapAsync(() => invokeAiInteractive(prompt, selectedPaths, ideateDir), ensureError);
1386
+ if (!interactiveR.ok) {
1387
+ showError(`Failed to invoke ${providerName}: ${interactiveR.error.message}`);
1388
+ showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
1389
+ log.newline();
1387
1390
  return;
1388
1391
  }
1389
1392
  console.log("");
1390
1393
  if (await fileExists(outputFile)) {
1391
1394
  showInfo("Output file found. Processing...");
1392
- let content;
1393
- try {
1394
- content = await readFile(outputFile, "utf-8");
1395
- } catch {
1395
+ const contentR = await wrapAsync(() => readFile(outputFile, "utf-8"), ensureError);
1396
+ if (!contentR.ok) {
1396
1397
  showError(`Failed to read output file: ${outputFile}`);
1397
1398
  log.newline();
1398
1399
  return;
1399
1400
  }
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
- }
1401
+ const ideateR = Result.try(() => parseIdeateOutput(contentR.value));
1402
+ if (!ideateR.ok) {
1403
+ showError(`Failed to parse output file: ${ideateR.error.message}`);
1404
+ log.newline();
1408
1405
  return;
1409
1406
  }
1407
+ const ideateOutput = ideateR.value;
1410
1408
  const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
1411
1409
  const ticketToUpdate = sprint.tickets[ticketIdx];
1412
1410
  if (ticketIdx !== -1 && ticketToUpdate) {
@@ -1416,16 +1414,13 @@ async function sprintIdeateCommand(args) {
1416
1414
  await saveSprint(sprint);
1417
1415
  showSuccess("Requirements approved and saved!");
1418
1416
  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
- }
1417
+ const parsedTasksR = Result.try(() => parseTasksJson(JSON.stringify(ideateOutput.tasks)));
1418
+ if (!parsedTasksR.ok) {
1419
+ showError(`Failed to parse tasks: ${parsedTasksR.error.message}`);
1420
+ log.newline();
1427
1421
  return;
1428
1422
  }
1423
+ const parsedTasks = parsedTasksR.value;
1429
1424
  if (parsedTasks.length === 0) {
1430
1425
  showWarning("No tasks in file.");
1431
1426
  log.newline();
@@ -1463,6 +1458,7 @@ async function sprintIdeateCommand(args) {
1463
1458
  // src/commands/sprint/close.ts
1464
1459
  import { spawnSync } from "child_process";
1465
1460
  import { confirm as confirm3 } from "@inquirer/prompts";
1461
+ import { Result as Result2 } from "typescript-result";
1466
1462
  async function sprintCloseCommand(args) {
1467
1463
  let sprintId;
1468
1464
  let createPr = false;
@@ -1510,23 +1506,13 @@ async function sprintCloseCommand(args) {
1510
1506
  return;
1511
1507
  }
1512
1508
  }
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) {
1509
+ const closeR = await wrapAsync(async () => {
1510
+ const sprintBeforeClose2 = await getSprint(sprintId);
1511
+ const sprint2 = await closeSprint(sprintId);
1512
+ return { sprintBeforeClose: sprintBeforeClose2, sprint: sprint2 };
1513
+ }, ensureError);
1514
+ if (!closeR.ok) {
1515
+ const err = closeR.error;
1530
1516
  if (err instanceof SprintNotFoundError) {
1531
1517
  showError(`Sprint not found: ${sprintId}`);
1532
1518
  log.newline();
@@ -1536,6 +1522,21 @@ async function sprintCloseCommand(args) {
1536
1522
  } else {
1537
1523
  throw err;
1538
1524
  }
1525
+ return;
1526
+ }
1527
+ const { sprintBeforeClose, sprint } = closeR.value;
1528
+ showSuccess("Sprint closed!", [
1529
+ ["ID", sprint.id],
1530
+ ["Name", sprint.name],
1531
+ ["Status", formatSprintStatus(sprint.status)]
1532
+ ]);
1533
+ showRandomQuote();
1534
+ log.newline();
1535
+ if (createPr && sprintBeforeClose.branch) {
1536
+ await createPullRequests(sprintId, sprintBeforeClose.branch, sprint.name);
1537
+ } else if (createPr && !sprintBeforeClose.branch) {
1538
+ log.dim("No sprint branch set \u2014 skipping PR creation.");
1539
+ log.newline();
1539
1540
  }
1540
1541
  }
1541
1542
  async function createPullRequests(sprintId, branchName, sprintName) {
@@ -1548,59 +1549,62 @@ async function createPullRequests(sprintId, branchName, sprintName) {
1548
1549
  const tasks = await listTasks(sprintId);
1549
1550
  const uniquePaths = [...new Set(tasks.map((t) => t.projectPath))];
1550
1551
  for (const projectPath of uniquePaths) {
1551
- try {
1552
+ const prR = Result2.try(() => {
1552
1553
  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], {
1554
+ return { branchExists: branchExists(projectPath, branchName) };
1555
+ });
1556
+ if (!prR.ok) {
1557
+ showWarning(`Error creating PR for ${projectPath}: ${prR.error.message}`);
1558
+ continue;
1559
+ }
1560
+ if (!prR.value.branchExists) {
1561
+ log.dim(`Branch '${branchName}' not found in ${projectPath} \u2014 skipping`);
1562
+ continue;
1563
+ }
1564
+ const baseBranch = getDefaultBranch(projectPath);
1565
+ const title = `Sprint: ${sprintName}`;
1566
+ log.info(`Creating PR in ${projectPath}...`);
1567
+ const pushResult = spawnSync("git", ["push", "-u", "origin", branchName], {
1568
+ cwd: projectPath,
1569
+ encoding: "utf-8",
1570
+ stdio: ["pipe", "pipe", "pipe"]
1571
+ });
1572
+ if (pushResult.status !== 0) {
1573
+ showWarning(`Failed to push branch in ${projectPath}: ${pushResult.stderr.trim()}`);
1574
+ log.dim(
1575
+ ` Manual: cd ${projectPath} && git push -u origin ${branchName} && gh pr create --base ${baseBranch} --head ${branchName} --title "${title}"`
1576
+ );
1577
+ continue;
1578
+ }
1579
+ const result = spawnSync(
1580
+ "gh",
1581
+ [
1582
+ "pr",
1583
+ "create",
1584
+ "--base",
1585
+ baseBranch,
1586
+ "--head",
1587
+ branchName,
1588
+ "--title",
1589
+ title,
1590
+ "--body",
1591
+ `Sprint: ${sprintName}
1592
+ ID: ${sprintId}`
1593
+ ],
1594
+ {
1561
1595
  cwd: projectPath,
1562
1596
  encoding: "utf-8",
1563
1597
  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
1598
  }
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
- }
1599
+ );
1600
+ if (result.status === 0) {
1601
+ const prUrl = result.stdout.trim();
1602
+ showSuccess(`PR created: ${prUrl}`);
1603
+ } else {
1604
+ showWarning(`Failed to create PR in ${projectPath}: ${result.stderr.trim()}`);
1605
+ log.dim(
1606
+ ` Manual: cd ${projectPath} && gh pr create --base ${baseBranch} --head ${branchName} --title "${title}"`
1592
1607
  );
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
1608
  }
1605
1609
  }
1606
1610
  log.newline();
@@ -1616,51 +1620,51 @@ async function sprintDeleteCommand(args) {
1616
1620
  if (!selected) return;
1617
1621
  sprintId = selected;
1618
1622
  }
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) {
1623
+ const sprintR = await wrapAsync(() => getSprint(sprintId), ensureError);
1624
+ if (!sprintR.ok) {
1625
+ if (sprintR.error instanceof SprintNotFoundError) {
1658
1626
  showError(`Sprint not found: ${sprintId}`);
1659
1627
  log.newline();
1660
1628
  } else {
1661
- throw err;
1629
+ throw sprintR.error;
1630
+ }
1631
+ return;
1632
+ }
1633
+ const sprint = sprintR.value;
1634
+ let taskCount = 0;
1635
+ const tasksR = await wrapAsync(() => listTasks(sprintId), ensureError);
1636
+ if (tasksR.ok) {
1637
+ taskCount = tasksR.value.length;
1638
+ }
1639
+ if (!skipConfirm) {
1640
+ log.newline();
1641
+ log.warn("This will permanently delete the sprint and all its data.");
1642
+ log.item(`Name: ${sprint.name}`);
1643
+ log.item(`Status: ${formatSprintStatus(sprint.status)}`);
1644
+ log.item(`Tickets: ${String(sprint.tickets.length)}`);
1645
+ log.item(`Tasks: ${String(taskCount)}`);
1646
+ log.newline();
1647
+ const confirmed = await confirm4({
1648
+ message: `Delete sprint "${sprint.name}"?`,
1649
+ default: false
1650
+ });
1651
+ if (!confirmed) {
1652
+ console.log(muted("\nSprint deletion cancelled.\n"));
1653
+ return;
1662
1654
  }
1663
1655
  }
1656
+ const currentSprintId = await getCurrentSprint();
1657
+ await deleteSprint(sprintId);
1658
+ if (currentSprintId === sprintId) {
1659
+ await setCurrentSprint(null);
1660
+ showTip('Current sprint was cleared. Use "ralphctl sprint current" to set a new one.');
1661
+ }
1662
+ showSuccess("Sprint deleted", [
1663
+ ["Name", sprint.name],
1664
+ ["ID", sprint.id]
1665
+ ]);
1666
+ showRandomQuote();
1667
+ log.newline();
1664
1668
  }
1665
1669
 
1666
1670
  // src/commands/sprint/requirements.ts
@@ -1668,12 +1672,13 @@ import { join as join2 } from "path";
1668
1672
  async function sprintRequirementsCommand(args = []) {
1669
1673
  const sprintId = args.find((a) => !a.startsWith("-"));
1670
1674
  let id;
1671
- try {
1672
- id = await resolveSprintId(sprintId);
1673
- } catch {
1675
+ const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
1676
+ if (!idR.ok) {
1674
1677
  const selected = await selectSprint("Select sprint to export requirements from:");
1675
1678
  if (!selected) return;
1676
1679
  id = selected;
1680
+ } else {
1681
+ id = idR.value;
1677
1682
  }
1678
1683
  const sprint = await getSprint(id);
1679
1684
  if (sprint.tickets.length === 0) {
@@ -1693,22 +1698,18 @@ async function sprintRequirementsCommand(args = []) {
1693
1698
  log.newline();
1694
1699
  const sprintDir = getSprintDir(id);
1695
1700
  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
- }
1701
+ const exportR = await wrapAsync(() => exportRequirementsToMarkdown(sprint, outputPath), ensureError);
1702
+ if (!exportR.ok) {
1703
+ showError(`Failed to write requirements: ${exportR.error.message}`);
1706
1704
  return;
1707
1705
  }
1706
+ showSuccess("Requirements written to:");
1707
+ log.item(outputPath);
1708
1708
  log.newline();
1709
1709
  }
1710
1710
 
1711
1711
  // src/commands/sprint/health.ts
1712
+ import { Result as Result3 } from "typescript-result";
1712
1713
  function checkBlockers(tasks) {
1713
1714
  const doneTasks = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
1714
1715
  const allTaskIds = new Set(tasks.map((t) => t.id));
@@ -1799,13 +1800,11 @@ function checkBranchConsistency(sprint, tasks) {
1799
1800
  const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
1800
1801
  const items = [];
1801
1802
  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 {
1803
+ const branchR = Result3.try(() => getCurrentBranch(projectPath));
1804
+ if (!branchR.ok) {
1808
1805
  items.push(`${projectPath} \u2014 unable to determine branch`);
1806
+ } else if (branchR.value !== sprint.branch) {
1807
+ items.push(`${projectPath} \u2014 on '${branchR.value}', expected '${sprint.branch}'`);
1809
1808
  }
1810
1809
  }
1811
1810
  return {
@@ -1837,17 +1836,12 @@ function renderCheckCard(check) {
1837
1836
  return renderCard(check.name, lines, { colorFn });
1838
1837
  }
1839
1838
  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
- }
1839
+ const sprintR = await wrapAsync(() => getCurrentSprintOrThrow(), ensureError);
1840
+ if (!sprintR.ok) {
1841
+ showError(sprintR.error.message);
1849
1842
  return;
1850
1843
  }
1844
+ const sprint = sprintR.value;
1851
1845
  const tasks = await getTasks(sprint.id);
1852
1846
  printHeader(`Sprint Health: ${sprint.name}`, icons.sprint);
1853
1847
  const checks = [
@@ -1899,18 +1893,17 @@ async function ticketEditCommand(ticketId, options = {}) {
1899
1893
  }
1900
1894
  resolvedId = selected;
1901
1895
  }
1902
- let ticket;
1903
- try {
1904
- ticket = await getTicket(resolvedId);
1905
- } catch (err) {
1906
- if (err instanceof TicketNotFoundError) {
1896
+ const ticketR = await wrapAsync(() => getTicket(resolvedId), ensureError);
1897
+ if (!ticketR.ok) {
1898
+ if (ticketR.error instanceof TicketNotFoundError) {
1907
1899
  showError(`Ticket not found: ${resolvedId}`);
1908
1900
  showNextStep("ralphctl ticket list", "see available tickets");
1909
1901
  if (!isInteractive) exitWithCode(EXIT_ERROR);
1910
1902
  return;
1911
1903
  }
1912
- throw err;
1904
+ throw ticketR.error;
1913
1905
  }
1906
+ const ticket = ticketR.value;
1914
1907
  let newTitle;
1915
1908
  let newDescription;
1916
1909
  let newLink;
@@ -1924,10 +1917,15 @@ async function ticketEditCommand(ticketId, options = {}) {
1924
1917
  default: ticket.title,
1925
1918
  validate: (v) => v.trim().length > 0 ? true : "Title is required"
1926
1919
  });
1927
- newDescription = await editorInput({
1920
+ const descR = await editorInput({
1928
1921
  message: "Description:",
1929
1922
  default: ticket.description
1930
1923
  });
1924
+ if (!descR.ok) {
1925
+ showError(`Editor input failed: ${descR.error.message}`);
1926
+ return;
1927
+ }
1928
+ newDescription = descR.value;
1931
1929
  newLink = await input3({
1932
1930
  message: `${icons.info} Link:`,
1933
1931
  default: ticket.link ?? "",
@@ -1978,28 +1976,29 @@ async function ticketEditCommand(ticketId, options = {}) {
1978
1976
  console.log(muted("\n No changes made.\n"));
1979
1977
  return;
1980
1978
  }
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);
1979
+ const updateR = await wrapAsync(() => updateTicket(ticket.id, updates), ensureError);
1980
+ if (!updateR.ok) {
1981
+ if (updateR.error instanceof SprintStatusError) {
1982
+ showError(updateR.error.message);
1998
1983
  } else {
1999
- throw err;
1984
+ throw updateR.error;
2000
1985
  }
2001
1986
  if (!isInteractive) exitWithCode(EXIT_ERROR);
1987
+ return;
1988
+ }
1989
+ const updated = updateR.value;
1990
+ showSuccess("Ticket updated!", [
1991
+ ["ID", updated.id],
1992
+ ["Title", updated.title],
1993
+ ["Project", updated.projectName]
1994
+ ]);
1995
+ if (updated.description) {
1996
+ console.log(fieldMultiline("Description", updated.description));
1997
+ }
1998
+ if (updated.link) {
1999
+ console.log(field("Link", updated.link));
2002
2000
  }
2001
+ console.log("");
2003
2002
  }
2004
2003
 
2005
2004
  // src/commands/ticket/list.ts
@@ -2067,12 +2066,12 @@ async function ticketListCommand(args) {
2067
2066
  printHeader(`Tickets (${String(filtered.length)})`, icons.ticket);
2068
2067
  for (const [projectName, projectTickets] of ticketsByProject) {
2069
2068
  log.raw(`${colors.info(icons.project)} ${colors.info(projectName)}`);
2070
- try {
2071
- const project = await getProject(projectName);
2072
- for (const repo of project.repositories) {
2069
+ const projectR = await wrapAsync(() => getProject(projectName), ensureError);
2070
+ if (projectR.ok) {
2071
+ for (const repo of projectR.value.repositories) {
2073
2072
  log.raw(` ${muted(repo.name)} ${muted("\u2192")} ${muted(repo.path)}`, 1);
2074
2073
  }
2075
- } catch {
2074
+ } else {
2076
2075
  log.raw(` ${muted("(project not found)")}`, 1);
2077
2076
  }
2078
2077
  log.newline();
@@ -2104,52 +2103,53 @@ async function ticketShowCommand(args) {
2104
2103
  if (!selected) return;
2105
2104
  ticketId = selected;
2106
2105
  }
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) {
2106
+ const ticketR = await wrapAsync(() => getTicket(ticketId), ensureError);
2107
+ if (!ticketR.ok) {
2108
+ if (ticketR.error instanceof TicketNotFoundError) {
2146
2109
  showError(`Ticket not found: ${ticketId}`);
2147
2110
  showNextStep("ralphctl ticket list", "see available tickets");
2148
2111
  log.newline();
2149
2112
  } else {
2150
- throw err;
2113
+ throw ticketR.error;
2114
+ }
2115
+ return;
2116
+ }
2117
+ const ticket = ticketR.value;
2118
+ const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "muted");
2119
+ const infoLines = [labelValue("ID", ticket.id)];
2120
+ infoLines.push(labelValue("Project", ticket.projectName));
2121
+ infoLines.push(labelValue("Requirements", reqBadge));
2122
+ if (ticket.link) {
2123
+ infoLines.push(labelValue("Link", ticket.link));
2124
+ }
2125
+ const projectR = await wrapAsync(() => getProject(ticket.projectName), ensureError);
2126
+ if (projectR.ok) {
2127
+ infoLines.push("");
2128
+ for (const repo of projectR.value.repositories) {
2129
+ infoLines.push(` ${icons.bullet} ${repo.name} ${muted("\u2192")} ${muted(repo.path)}`);
2151
2130
  }
2131
+ } else {
2132
+ infoLines.push(labelValue("Repositories", muted("(project not found)")));
2152
2133
  }
2134
+ log.newline();
2135
+ console.log(renderCard(`${icons.ticket} ${ticket.title}`, infoLines));
2136
+ if (ticket.description) {
2137
+ log.newline();
2138
+ const descLines = [];
2139
+ for (const line2 of ticket.description.split("\n")) {
2140
+ descLines.push(line2);
2141
+ }
2142
+ console.log(renderCard(`${icons.edit} Description`, descLines));
2143
+ }
2144
+ if (ticket.affectedRepositories && ticket.affectedRepositories.length > 0) {
2145
+ log.newline();
2146
+ const affectedLines = [];
2147
+ for (const repoPath of ticket.affectedRepositories) {
2148
+ affectedLines.push(`${icons.bullet} ${repoPath}`);
2149
+ }
2150
+ console.log(renderCard(`${icons.project} Affected Repositories`, affectedLines));
2151
+ }
2152
+ log.newline();
2153
2153
  }
2154
2154
 
2155
2155
  // src/commands/ticket/remove.ts
@@ -2162,7 +2162,7 @@ async function ticketRemoveCommand(args) {
2162
2162
  if (!selected) return;
2163
2163
  ticketId = selected;
2164
2164
  }
2165
- try {
2165
+ const opR = await wrapAsync(async () => {
2166
2166
  const ticket = await getTicket(ticketId);
2167
2167
  if (!skipConfirm) {
2168
2168
  const confirmed = await confirm5({
@@ -2171,23 +2171,28 @@ async function ticketRemoveCommand(args) {
2171
2171
  });
2172
2172
  if (!confirmed) {
2173
2173
  console.log(muted("\nTicket removal cancelled.\n"));
2174
- return;
2174
+ return null;
2175
2175
  }
2176
2176
  }
2177
2177
  await removeTicket(ticketId);
2178
- showSuccess("Ticket removed", [["ID", ticketId]]);
2179
- log.newline();
2180
- } catch (err) {
2181
- if (err instanceof TicketNotFoundError) {
2178
+ return ticket;
2179
+ }, ensureError);
2180
+ if (!opR.ok) {
2181
+ if (opR.error instanceof TicketNotFoundError) {
2182
2182
  showError(`Ticket not found: ${ticketId}`);
2183
2183
  showNextStep("ralphctl ticket list", "see available tickets");
2184
2184
  log.newline();
2185
- } else if (err instanceof SprintStatusError) {
2186
- showError(err.message);
2185
+ } else if (opR.error instanceof SprintStatusError) {
2186
+ showError(opR.error.message);
2187
2187
  log.newline();
2188
2188
  } else {
2189
- throw err;
2189
+ throw opR.error;
2190
2190
  }
2191
+ return;
2192
+ }
2193
+ if (opR.value !== null) {
2194
+ showSuccess("Ticket removed", [["ID", ticketId]]);
2195
+ log.newline();
2191
2196
  }
2192
2197
  }
2193
2198
 
@@ -2195,25 +2200,23 @@ async function ticketRemoveCommand(args) {
2195
2200
  import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2196
2201
  import { join as join3 } from "path";
2197
2202
  import { confirm as confirm6 } from "@inquirer/prompts";
2203
+ import { Result as Result4 } from "typescript-result";
2198
2204
  async function ticketRefineCommand(ticketId, options = {}) {
2199
2205
  const isInteractive = options.interactive !== false;
2200
- let sprintId;
2201
- try {
2202
- sprintId = await resolveSprintId();
2203
- } catch {
2206
+ const sprintIdR = await wrapAsync(() => resolveSprintId(), ensureError);
2207
+ if (!sprintIdR.ok) {
2204
2208
  showWarning("No current sprint set.");
2205
2209
  showTip("Create a sprint first or set one with: ralphctl sprint current");
2206
2210
  log.newline();
2207
2211
  return;
2208
2212
  }
2213
+ const sprintId = sprintIdR.value;
2209
2214
  const sprint = await getSprint(sprintId);
2210
2215
  try {
2211
2216
  assertSprintStatus(sprint, ["draft"], "refine ticket");
2212
2217
  } catch (err) {
2213
- if (err instanceof Error) {
2214
- showError(err.message);
2215
- log.newline();
2216
- }
2218
+ showError(err instanceof Error ? err.message : String(err));
2219
+ log.newline();
2217
2220
  return;
2218
2221
  }
2219
2222
  const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === "approved");
@@ -2269,21 +2272,18 @@ async function ticketRefineCommand(ticketId, options = {}) {
2269
2272
  }
2270
2273
  let issueContext = "";
2271
2274
  if (ticket.link) {
2275
+ const ticketLink = ticket.link;
2272
2276
  const fetchSpinner = createSpinner("Fetching issue data...");
2273
2277
  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) {
2278
+ const fetchR = Result4.try(() => fetchIssueFromUrl(ticketLink));
2279
+ if (!fetchR.ok) {
2283
2280
  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
- }
2281
+ showWarning(`${fetchR.error.message} \u2014 continuing without issue context`);
2282
+ } else if (fetchR.value) {
2283
+ issueContext = formatIssueContext(fetchR.value);
2284
+ fetchSpinner.succeed(`Issue data fetched (${String(fetchR.value.comments.length)} comment(s))`);
2285
+ } else {
2286
+ fetchSpinner.stop();
2287
2287
  }
2288
2288
  }
2289
2289
  const refineDir = getRefinementDir(sprintId, ticket.id);
@@ -2301,41 +2301,34 @@ async function ticketRefineCommand(ticketId, options = {}) {
2301
2301
  log.newline();
2302
2302
  const spinner = createSpinner(`Starting ${providerName} session...`);
2303
2303
  spinner.start();
2304
- try {
2305
- await runAiSession(refineDir, prompt, ticket.title);
2306
- spinner.succeed(`${providerName} session completed`);
2307
- } catch (err) {
2304
+ const sessionR = await wrapAsync(() => runAiSession(refineDir, prompt, ticket.title), ensureError);
2305
+ if (!sessionR.ok) {
2308
2306
  spinner.fail(`${providerName} session failed`);
2309
- if (err instanceof Error) {
2310
- showError(err.message);
2311
- }
2307
+ showError(sessionR.error.message);
2312
2308
  log.newline();
2313
2309
  return;
2314
2310
  }
2311
+ spinner.succeed(`${providerName} session completed`);
2315
2312
  log.newline();
2316
2313
  if (!await fileExists(outputFile)) {
2317
2314
  showWarning("No requirements file found from AI session.");
2318
2315
  log.newline();
2319
2316
  return;
2320
2317
  }
2321
- let content;
2322
- try {
2323
- content = await readFile2(outputFile, "utf-8");
2324
- } catch {
2318
+ const contentR = await wrapAsync(() => readFile2(outputFile, "utf-8"), ensureError);
2319
+ if (!contentR.ok) {
2325
2320
  showError(`Failed to read requirements file: ${outputFile}`);
2326
2321
  log.newline();
2327
2322
  return;
2328
2323
  }
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
- }
2324
+ const content = contentR.value;
2325
+ const parseR = Result4.try(() => parseRequirementsFile(content));
2326
+ if (!parseR.ok) {
2327
+ showError(`Failed to parse requirements file: ${parseR.error.message}`);
2336
2328
  log.newline();
2337
2329
  return;
2338
2330
  }
2331
+ const refinedRequirements = parseR.value;
2339
2332
  if (refinedRequirements.length === 0) {
2340
2333
  showWarning("No requirements found in output file.");
2341
2334
  log.newline();
@@ -2386,11 +2379,13 @@ import { resolve as resolve2 } from "path";
2386
2379
  import { confirm as confirm7, input as input4 } from "@inquirer/prompts";
2387
2380
  async function taskAddCommand(options = {}) {
2388
2381
  const isInteractive = options.interactive !== false;
2389
- try {
2382
+ const statusCheckR = await wrapAsync(async () => {
2390
2383
  const sprintId = await resolveSprintId();
2391
2384
  const sprint = await getSprint(sprintId);
2392
2385
  assertSprintStatus(sprint, ["draft"], "add tasks");
2393
- } catch (err) {
2386
+ }, ensureError);
2387
+ if (!statusCheckR.ok) {
2388
+ const err = statusCheckR.error;
2394
2389
  if (err instanceof SprintStatusError) {
2395
2390
  const mainError = err.message.split("\n")[0] ?? err.message;
2396
2391
  showError(mainError);
@@ -2441,27 +2436,31 @@ async function taskAddCommand(options = {}) {
2441
2436
  const trimmedTicket = options.ticket?.trim();
2442
2437
  ticketId = trimmedTicket === "" ? void 0 : trimmedTicket;
2443
2438
  if (ticketId) {
2444
- try {
2445
- const ticket = await getTicket(ticketId);
2439
+ const resolvedTicketId = ticketId;
2440
+ const ticketProjectR = await wrapAsync(async () => {
2441
+ const ticket = await getTicket(resolvedTicketId);
2446
2442
  const project = await getProject(ticket.projectName);
2447
- projectPath = project.repositories[0]?.path;
2448
- } catch {
2443
+ return project.repositories[0]?.path;
2444
+ }, ensureError);
2445
+ if (ticketProjectR.ok) {
2446
+ projectPath = ticketProjectR.value;
2447
+ } else {
2449
2448
  if (!trimmedProject) {
2450
2449
  showError(`Ticket not found: ${ticketId}`);
2451
2450
  console.log(muted(" Provide --project or a valid --ticket\n"));
2452
2451
  exitWithCode(EXIT_ERROR);
2453
2452
  }
2454
2453
  const validation = await validateProjectPath(trimmedProject);
2455
- if (validation !== true) {
2456
- showError(`Invalid project path: ${validation}`);
2454
+ if (!validation.ok) {
2455
+ showError(`Invalid project path: ${validation.error.message}`);
2457
2456
  exitWithCode(EXIT_ERROR);
2458
2457
  }
2459
2458
  projectPath = resolve2(trimmedProject);
2460
2459
  }
2461
2460
  } else if (trimmedProject) {
2462
2461
  const validation = await validateProjectPath(trimmedProject);
2463
- if (validation !== true) {
2464
- showError(`Invalid project path: ${validation}`);
2462
+ if (!validation.ok) {
2463
+ showError(`Invalid project path: ${validation.error.message}`);
2465
2464
  exitWithCode(EXIT_ERROR);
2466
2465
  }
2467
2466
  projectPath = resolve2(trimmedProject);
@@ -2475,10 +2474,15 @@ async function taskAddCommand(options = {}) {
2475
2474
  default: options.name?.trim(),
2476
2475
  validate: (v) => v.trim().length > 0 ? true : "Name is required"
2477
2476
  });
2478
- description = await editorInput({
2477
+ const descR = await editorInput({
2479
2478
  message: "Description (optional):",
2480
2479
  default: options.description?.trim()
2481
2480
  });
2481
+ if (!descR.ok) {
2482
+ showError(`Editor input failed: ${descR.error.message}`);
2483
+ return;
2484
+ }
2485
+ description = descR.value;
2482
2486
  steps = options.steps ? [...options.steps] : [];
2483
2487
  const addSteps = await confirm7({
2484
2488
  message: `${emoji.donut} ${steps.length > 0 ? `Add more steps? (${String(steps.length)} pre-filled)` : "Add implementation steps?"}`,
@@ -2518,8 +2522,9 @@ async function taskAddCommand(options = {}) {
2518
2522
  ticketId = ticketChoice;
2519
2523
  const ticket = tickets.find((t) => t.id === ticketChoice);
2520
2524
  if (ticket) {
2521
- try {
2522
- const project = await getProject(ticket.projectName);
2525
+ const projR = await wrapAsync(() => getProject(ticket.projectName), ensureError);
2526
+ if (projR.ok) {
2527
+ const project = projR.value;
2523
2528
  if (project.repositories.length === 1) {
2524
2529
  projectPath = project.repositories[0]?.path;
2525
2530
  } else {
@@ -2532,18 +2537,21 @@ async function taskAddCommand(options = {}) {
2532
2537
  }))
2533
2538
  });
2534
2539
  }
2535
- } catch {
2540
+ } else {
2536
2541
  log.warn(`Project '${ticket.projectName}' not found, will prompt for path.`);
2537
2542
  }
2538
2543
  }
2539
2544
  }
2540
2545
  } else if (options.ticket) {
2541
2546
  ticketId = options.ticket;
2542
- try {
2543
- const ticket = await getTicket(ticketId);
2547
+ const resolvedTicketId = ticketId;
2548
+ const tpR = await wrapAsync(async () => {
2549
+ const ticket = await getTicket(resolvedTicketId);
2544
2550
  const project = await getProject(ticket.projectName);
2545
- projectPath = project.repositories[0]?.path;
2546
- } catch {
2551
+ return project.repositories[0]?.path;
2552
+ }, ensureError);
2553
+ if (tpR.ok) {
2554
+ projectPath = tpR.value;
2547
2555
  }
2548
2556
  }
2549
2557
  if (projectPath === void 0) {
@@ -2563,7 +2571,7 @@ async function taskAddCommand(options = {}) {
2563
2571
  default: options.project?.trim() ?? process.cwd(),
2564
2572
  validate: async (v) => {
2565
2573
  const result = await validateProjectPath(v.trim());
2566
- return result;
2574
+ return result.ok ? true : result.error.message;
2567
2575
  }
2568
2576
  });
2569
2577
  projectPath = resolve2(expandTilde(projectPath.trim()));
@@ -2581,7 +2589,7 @@ async function taskAddCommand(options = {}) {
2581
2589
  default: options.project?.trim() ?? process.cwd(),
2582
2590
  validate: async (v) => {
2583
2591
  const result = await validateProjectPath(v.trim());
2584
- return result;
2592
+ return result.ok ? true : result.error.message;
2585
2593
  }
2586
2594
  });
2587
2595
  projectPath = resolve2(expandTilde(projectPath.trim()));
@@ -2595,33 +2603,10 @@ async function taskAddCommand(options = {}) {
2595
2603
  showError("Project path is required");
2596
2604
  exitWithCode(EXIT_ERROR);
2597
2605
  }
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;
2606
+ const addR = await wrapAsync(() => addTask({ name, description, steps, ticketId, projectPath }), ensureError);
2607
+ if (!addR.ok) {
2608
+ if (addR.error instanceof SprintStatusError) {
2609
+ const mainError = addR.error.message.split("\n")[0] ?? addR.error.message;
2625
2610
  showError(mainError);
2626
2611
  showNextSteps([
2627
2612
  ["ralphctl sprint close", "close current sprint"],
@@ -2631,12 +2616,30 @@ async function taskAddCommand(options = {}) {
2631
2616
  if (!isInteractive) exitWithCode(EXIT_ERROR);
2632
2617
  return;
2633
2618
  }
2634
- throw err;
2619
+ throw addR.error;
2620
+ }
2621
+ const task = addR.value;
2622
+ showSuccess("Task added!", [
2623
+ ["ID", task.id],
2624
+ ["Name", task.name],
2625
+ ["Project", task.projectPath],
2626
+ ["Order", String(task.order)]
2627
+ ]);
2628
+ if (task.ticketId) {
2629
+ console.log(field("Ticket", task.ticketId));
2635
2630
  }
2631
+ if (task.steps.length > 0) {
2632
+ console.log(field("Steps", ""));
2633
+ task.steps.forEach((step, i) => {
2634
+ console.log(muted(` ${String(i + 1)}. ${step}`));
2635
+ });
2636
+ }
2637
+ console.log("");
2636
2638
  }
2637
2639
 
2638
2640
  // src/commands/task/import.ts
2639
2641
  import { readFile as readFile3 } from "fs/promises";
2642
+ import { Result as Result5 } from "typescript-result";
2640
2643
  async function taskImportCommand(args) {
2641
2644
  const filePath = args[0];
2642
2645
  if (!filePath) {
@@ -2660,22 +2663,19 @@ async function taskImportCommand(args) {
2660
2663
  log.newline();
2661
2664
  return;
2662
2665
  }
2663
- let content;
2664
- try {
2665
- content = await readFile3(filePath, "utf-8");
2666
- } catch {
2666
+ const contentR = await wrapAsync(() => readFile3(filePath, "utf-8"), ensureError);
2667
+ if (!contentR.ok) {
2667
2668
  showError(`Failed to read file: ${filePath}`);
2668
2669
  log.newline();
2669
2670
  return;
2670
2671
  }
2671
- let data;
2672
- try {
2673
- data = JSON.parse(content);
2674
- } catch {
2672
+ const dataR = Result5.try(() => JSON.parse(contentR.value));
2673
+ if (!dataR.ok) {
2675
2674
  showError("Invalid JSON format.");
2676
2675
  log.newline();
2677
2676
  return;
2678
2677
  }
2678
+ const data = dataR.value;
2679
2679
  const result = ImportTasksSchema.safeParse(data);
2680
2680
  if (!result.success) {
2681
2681
  showError("Invalid task format");
@@ -2709,8 +2709,8 @@ async function taskImportCommand(args) {
2709
2709
  const spinner = createSpinner(`Importing ${String(tasks.length)} task(s)...`).start();
2710
2710
  let imported = 0;
2711
2711
  for (const taskInput of tasks) {
2712
- try {
2713
- const task = await addTask({
2712
+ const addR = await wrapAsync(
2713
+ () => addTask({
2714
2714
  name: taskInput.name,
2715
2715
  description: taskInput.description,
2716
2716
  steps: taskInput.steps ?? [],
@@ -2718,29 +2718,31 @@ async function taskImportCommand(args) {
2718
2718
  blockedBy: [],
2719
2719
  // Set later
2720
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) {
2721
+ }),
2722
+ ensureError
2723
+ );
2724
+ if (!addR.ok) {
2725
+ if (addR.error instanceof SprintStatusError) {
2730
2726
  spinner.fail("Import failed");
2731
- showError(err.message);
2727
+ showError(addR.error.message);
2732
2728
  log.newline();
2733
2729
  return;
2734
2730
  }
2735
2731
  log.itemError(`Failed to add: ${taskInput.name}`);
2736
- if (err instanceof Error) {
2737
- console.log(muted(` ${err.message}`));
2738
- }
2732
+ console.log(muted(` ${addR.error.message}`));
2733
+ continue;
2739
2734
  }
2735
+ const task = addR.value;
2736
+ if (taskInput.id) {
2737
+ localToRealId.set(taskInput.id, task.id);
2738
+ }
2739
+ createdTasks.push({ task: taskInput, realId: task.id });
2740
+ imported++;
2741
+ spinner.text = `Importing tasks... (${String(imported)}/${String(tasks.length)})`;
2740
2742
  }
2741
2743
  spinner.text = "Resolving task dependencies...";
2742
2744
  const tasksFilePath = getTasksFilePath(sprintId);
2743
- await withFileLock(tasksFilePath, async () => {
2745
+ const lockR = await withFileLock(tasksFilePath, async () => {
2744
2746
  const allTasks = await getTasks();
2745
2747
  for (const { task: taskInput, realId } of createdTasks) {
2746
2748
  const blockedBy = (taskInput.blockedBy ?? []).map((localId) => localToRealId.get(localId) ?? "").filter((id) => id !== "");
@@ -2753,6 +2755,11 @@ async function taskImportCommand(args) {
2753
2755
  }
2754
2756
  await saveTasks(allTasks);
2755
2757
  });
2758
+ if (!lockR.ok) {
2759
+ showError(`Failed to update dependencies: ${lockR.error.message}`);
2760
+ log.newline();
2761
+ return;
2762
+ }
2756
2763
  spinner.succeed(`Imported ${String(imported)}/${String(tasks.length)} tasks`);
2757
2764
  for (const { task: taskInput, realId } of createdTasks) {
2758
2765
  log.itemSuccess(`${realId}: ${taskInput.name}`);
@@ -2876,76 +2883,75 @@ async function taskShowCommand(args) {
2876
2883
  if (!selected) return;
2877
2884
  taskId = selected;
2878
2885
  }
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) {
2886
+ const taskR = await wrapAsync(() => getTask(taskId), ensureError);
2887
+ if (!taskR.ok) {
2888
+ if (taskR.error instanceof TaskNotFoundError) {
2889
+ showError(`Task not found: ${taskId}`);
2890
+ showNextStep("ralphctl task list", "see available tasks");
2900
2891
  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));
2892
+ } else {
2893
+ throw taskR.error;
2908
2894
  }
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));
2895
+ return;
2896
+ }
2897
+ const task = taskR.value;
2898
+ const infoLines = [
2899
+ labelValue("ID", task.id),
2900
+ labelValue("Status", formatTaskStatus(task.status)),
2901
+ labelValue("Order", String(task.order)),
2902
+ labelValue("Project", task.projectPath)
2903
+ ];
2904
+ if (task.ticketId) {
2905
+ infoLines.push(labelValue("Ticket", task.ticketId));
2906
+ }
2907
+ if (task.description) {
2908
+ infoLines.push("");
2909
+ infoLines.push(labelValue("Description", ""));
2910
+ for (const line2 of task.description.split("\n")) {
2911
+ infoLines.push(`${" ".repeat(DETAIL_LABEL_WIDTH + 1)}${line2}`);
2916
2912
  }
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
- }
2913
+ }
2914
+ log.newline();
2915
+ console.log(renderCard(`${icons.task} ${task.name}`, infoLines));
2916
+ if (task.steps.length > 0) {
2917
+ log.newline();
2918
+ const stepLines = [];
2919
+ for (let i = 0; i < task.steps.length; i++) {
2920
+ const step = task.steps[i] ?? "";
2921
+ const checkbox = task.status === "done" ? colors.success("[x]") : muted("[ ]");
2922
+ stepLines.push(`${checkbox} ${muted(String(i + 1) + ".")} ${step}`);
2923
+ }
2924
+ console.log(renderCard(`${icons.bullet} Steps (${String(task.steps.length)})`, stepLines));
2925
+ }
2926
+ if (task.blockedBy.length > 0) {
2927
+ log.newline();
2928
+ const depLines = [];
2929
+ for (const dep of task.blockedBy) {
2930
+ depLines.push(`${icons.bullet} ${dep}`);
2927
2931
  }
2928
- if (task.verified) {
2932
+ console.log(renderCard(`${icons.warning} Blocked By`, depLines));
2933
+ }
2934
+ if (task.ticketId) {
2935
+ const taskTicketId = task.ticketId;
2936
+ const ticketR = await wrapAsync(() => getTicket(taskTicketId), ensureError);
2937
+ if (ticketR.ok && ticketR.value.requirements) {
2929
2938
  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));
2939
+ const reqLines = ticketR.value.requirements.split("\n");
2940
+ console.log(renderCard(`${icons.ticket} Requirements`, reqLines));
2938
2941
  }
2942
+ }
2943
+ if (task.verified) {
2939
2944
  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;
2945
+ const verifyLines = [`${colors.success(icons.success)} Verified`];
2946
+ if (task.verificationOutput) {
2947
+ verifyLines.push(colors.muted(horizontalLine(30, "rounded")));
2948
+ for (const line2 of task.verificationOutput.split("\n").slice(0, 10)) {
2949
+ verifyLines.push(muted(line2));
2950
+ }
2947
2951
  }
2952
+ console.log(renderCard(`${icons.success} Verification`, verifyLines));
2948
2953
  }
2954
+ log.newline();
2949
2955
  }
2950
2956
 
2951
2957
  // src/commands/task/status.ts
@@ -2995,32 +3001,32 @@ async function taskStatusCommand(args, options = {}) {
2995
3001
  }
2996
3002
  return;
2997
3003
  }
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) {
3004
+ const updateR = await wrapAsync(() => updateTaskStatus(taskId, result.data), ensureError);
3005
+ if (!updateR.ok) {
3006
+ if (updateR.error instanceof TaskNotFoundError) {
3008
3007
  showError(`Task not found: ${taskId}`);
3009
3008
  showNextStep("ralphctl task list", "see available tasks");
3010
3009
  log.newline();
3011
3010
  if (options.noInteractive) {
3012
3011
  exitWithCode(EXIT_ERROR);
3013
3012
  }
3014
- } else if (err instanceof SprintStatusError) {
3015
- showError(err.message);
3013
+ } else if (updateR.error instanceof SprintStatusError) {
3014
+ showError(updateR.error.message);
3016
3015
  log.newline();
3017
3016
  if (options.noInteractive) {
3018
3017
  exitWithCode(EXIT_ERROR);
3019
3018
  }
3020
3019
  } else {
3021
- throw err;
3020
+ throw updateR.error;
3022
3021
  }
3022
+ return;
3023
3023
  }
3024
+ showSuccess("Task status updated!", [
3025
+ ["ID", updateR.value.id],
3026
+ ["Name", updateR.value.name],
3027
+ ["Status", formatTaskStatus(updateR.value.status)]
3028
+ ]);
3029
+ log.newline();
3024
3030
  }
3025
3031
 
3026
3032
  // src/commands/task/next.ts
@@ -3075,25 +3081,25 @@ async function taskReorderCommand(args) {
3075
3081
  if (newOrder === void 0 || isNaN(newOrder) || newOrder < 1) {
3076
3082
  newOrder = await inputPositiveInt("New position (1 = highest priority):");
3077
3083
  }
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) {
3084
+ const reorderR = await wrapAsync(() => reorderTask(taskId, newOrder), ensureError);
3085
+ if (!reorderR.ok) {
3086
+ if (reorderR.error instanceof TaskNotFoundError) {
3088
3087
  showError(`Task not found: ${taskId}`);
3089
3088
  log.newline();
3090
- } else if (err instanceof SprintStatusError) {
3091
- showError(err.message);
3089
+ } else if (reorderR.error instanceof SprintStatusError) {
3090
+ showError(reorderR.error.message);
3092
3091
  log.newline();
3093
3092
  } else {
3094
- throw err;
3093
+ throw reorderR.error;
3095
3094
  }
3095
+ return;
3096
3096
  }
3097
+ showSuccess("Task reordered!", [
3098
+ ["ID", reorderR.value.id],
3099
+ ["Name", reorderR.value.name],
3100
+ ["New Order", String(reorderR.value.order)]
3101
+ ]);
3102
+ log.newline();
3097
3103
  }
3098
3104
 
3099
3105
  // src/commands/task/remove.ts
@@ -3106,7 +3112,7 @@ async function taskRemoveCommand(args) {
3106
3112
  if (!selected) return;
3107
3113
  taskId = selected;
3108
3114
  }
3109
- try {
3115
+ const opR = await wrapAsync(async () => {
3110
3116
  const task = await getTask(taskId);
3111
3117
  if (!skipConfirm) {
3112
3118
  const confirmed = await confirm8({
@@ -3115,32 +3121,39 @@ async function taskRemoveCommand(args) {
3115
3121
  });
3116
3122
  if (!confirmed) {
3117
3123
  console.log(muted("\nTask removal cancelled.\n"));
3118
- return;
3124
+ return null;
3119
3125
  }
3120
3126
  }
3121
3127
  await removeTask(taskId);
3122
- showSuccess("Task removed", [["ID", taskId]]);
3123
- log.newline();
3124
- } catch (err) {
3125
- if (err instanceof TaskNotFoundError) {
3128
+ return task;
3129
+ }, ensureError);
3130
+ if (!opR.ok) {
3131
+ if (opR.error instanceof TaskNotFoundError) {
3126
3132
  showError(`Task not found: ${taskId}`);
3127
3133
  log.newline();
3128
- } else if (err instanceof SprintStatusError) {
3129
- showError(err.message);
3134
+ } else if (opR.error instanceof SprintStatusError) {
3135
+ showError(opR.error.message);
3130
3136
  log.newline();
3131
3137
  } else {
3132
- throw err;
3138
+ throw opR.error;
3133
3139
  }
3140
+ return;
3141
+ }
3142
+ if (opR.value !== null) {
3143
+ showSuccess("Task removed", [["ID", taskId]]);
3144
+ log.newline();
3134
3145
  }
3135
3146
  }
3136
3147
 
3137
3148
  // src/commands/progress/log.ts
3138
3149
  async function progressLogCommand(args) {
3139
- try {
3150
+ const statusCheckR = await wrapAsync(async () => {
3140
3151
  const sprintId = await resolveSprintId();
3141
3152
  const sprint = await getSprint(sprintId);
3142
3153
  assertSprintStatus(sprint, ["active"], "log progress");
3143
- } catch (err) {
3154
+ }, ensureError);
3155
+ if (!statusCheckR.ok) {
3156
+ const err = statusCheckR.error;
3144
3157
  if (err instanceof SprintStatusError) {
3145
3158
  const mainError = err.message.split("\n")[0] ?? err.message;
3146
3159
  showError(mainError);
@@ -3158,9 +3171,15 @@ async function progressLogCommand(args) {
3158
3171
  }
3159
3172
  let message = args.join(" ").trim();
3160
3173
  if (!message) {
3161
- message = await editorInput({
3174
+ const editorR = await editorInput({
3162
3175
  message: "Progress message:"
3163
3176
  });
3177
+ if (!editorR.ok) {
3178
+ showError(`Editor input failed: ${editorR.error.message}`);
3179
+ log.newline();
3180
+ return;
3181
+ }
3182
+ message = editorR.value;
3164
3183
  message = message.trim();
3165
3184
  }
3166
3185
  if (!message) {
@@ -3168,18 +3187,18 @@ async function progressLogCommand(args) {
3168
3187
  log.newline();
3169
3188
  return;
3170
3189
  }
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);
3190
+ const logR = await wrapAsync(() => logProgress(message), ensureError);
3191
+ if (!logR.ok) {
3192
+ if (logR.error instanceof SprintStatusError) {
3193
+ showError(logR.error.message);
3178
3194
  log.newline();
3179
3195
  } else {
3180
- throw err;
3196
+ throw logR.error;
3181
3197
  }
3198
+ return;
3182
3199
  }
3200
+ showSuccess("Progress logged.");
3201
+ log.newline();
3183
3202
  }
3184
3203
 
3185
3204
  // src/commands/progress/show.ts
@@ -3321,12 +3340,11 @@ function checkGlabInstalled() {
3321
3340
  }
3322
3341
  async function checkDataDirectory() {
3323
3342
  const dataDir = getDataDir();
3324
- try {
3325
- await access(dataDir, constants.R_OK | constants.W_OK);
3343
+ const accessR = await wrapAsync(() => access(dataDir, constants.R_OK | constants.W_OK), ensureError);
3344
+ if (accessR.ok) {
3326
3345
  return { name: "Data directory", status: "pass", detail: dataDir };
3327
- } catch {
3328
- return { name: "Data directory", status: "fail", detail: `${dataDir} not accessible or writable` };
3329
3346
  }
3347
+ return { name: "Data directory", status: "fail", detail: `${dataDir} not accessible or writable` };
3330
3348
  }
3331
3349
  async function checkProjectPaths() {
3332
3350
  const projects = await listProjects();
@@ -3337,8 +3355,8 @@ async function checkProjectPaths() {
3337
3355
  for (const project of projects) {
3338
3356
  for (const repo of project.repositories) {
3339
3357
  const validation = await validateProjectPath(repo.path);
3340
- if (validation !== true) {
3341
- issues.push(`${project.name}/${repo.name}: ${validation}`);
3358
+ if (!validation.ok) {
3359
+ issues.push(`${project.name}/${repo.name}: ${validation.error.message}`);
3342
3360
  continue;
3343
3361
  }
3344
3362
  const gitDir = join4(repo.path, ".git");
@@ -3367,13 +3385,11 @@ async function checkCurrentSprint() {
3367
3385
  if (!await fileExists(sprintPath)) {
3368
3386
  return { name: "Current sprint", status: "fail", detail: `sprint file missing: ${sprintId}` };
3369
3387
  }
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}` };
3388
+ const result = await readValidatedJson(sprintPath, SprintSchema);
3389
+ if (!result.ok) {
3390
+ return { name: "Current sprint", status: "fail", detail: `invalid sprint data: ${result.error.message}` };
3376
3391
  }
3392
+ return { name: "Current sprint", status: "pass", detail: `${result.value.name} (${result.value.status})` };
3377
3393
  }
3378
3394
  async function doctorCommand() {
3379
3395
  printHeader("System Health Check", icons.info);
@@ -3528,11 +3544,9 @@ function showWelcomeBanner() {
3528
3544
  showBanner();
3529
3545
  }
3530
3546
  async function readTasksSafe(sprintId) {
3531
- try {
3532
- return await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
3533
- } catch {
3534
- return [];
3535
- }
3547
+ const result = await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
3548
+ if (!result.ok) return [];
3549
+ return result.value;
3536
3550
  }
3537
3551
  async function getMenuContext() {
3538
3552
  let dashboardData = null;
@@ -3591,21 +3605,21 @@ async function getMenuContext() {
3591
3605
  async function interactiveMode() {
3592
3606
  let escPressed = false;
3593
3607
  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();
3608
+ const { ctx, dashboardData } = await getMenuContext();
3609
+ clearScreen();
3610
+ showWelcomeBanner();
3611
+ const statusLines = renderStatusHeader(dashboardData);
3612
+ if (statusLines.length > 0) {
3613
+ for (const line2 of statusLines) {
3614
+ console.log(line2);
3604
3615
  }
3605
- const { items: mainMenu, defaultValue } = buildMainMenu(ctx);
3606
- const effectiveDefault = escPressed ? "exit" : defaultValue;
3607
- escPressed = false;
3608
- const command = await escapableSelect(
3616
+ log.newline();
3617
+ }
3618
+ const { items: mainMenu, defaultValue } = buildMainMenu(ctx);
3619
+ const effectiveDefault = escPressed ? "exit" : defaultValue;
3620
+ escPressed = false;
3621
+ const commandResult = await wrapAsync(
3622
+ () => escapableSelect(
3609
3623
  {
3610
3624
  message: `${emoji.donut} What would you like to do?`,
3611
3625
  choices: mainMenu,
@@ -3615,40 +3629,43 @@ async function interactiveMode() {
3615
3629
  theme: selectTheme
3616
3630
  },
3617
3631
  { 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") {
3632
+ ),
3633
+ ensureError
3634
+ );
3635
+ if (!commandResult.ok) {
3636
+ if (commandResult.error.name === "ExitPromptError") {
3648
3637
  showFarewell();
3649
3638
  break;
3650
3639
  }
3651
- throw err;
3640
+ throw commandResult.error;
3641
+ }
3642
+ const command = commandResult.value;
3643
+ if (command === null) {
3644
+ escPressed = true;
3645
+ continue;
3646
+ }
3647
+ if (command === "exit") {
3648
+ showFarewell();
3649
+ break;
3650
+ }
3651
+ if (command.startsWith("action:")) {
3652
+ const parts = command.split(":");
3653
+ const group = parts[1] ?? "";
3654
+ const subCommand = parts[2] ?? "";
3655
+ log.newline();
3656
+ await executeCommand(group, subCommand);
3657
+ log.newline();
3658
+ await pressEnterToContinue();
3659
+ continue;
3660
+ }
3661
+ if (command === "wizard") {
3662
+ const { runWizard } = await import("./wizard-MCDDXLGE.mjs");
3663
+ await runWizard();
3664
+ continue;
3665
+ }
3666
+ const subMenu = buildSubMenu(command, ctx);
3667
+ if (subMenu) {
3668
+ await handleSubMenu(command, subMenu);
3652
3669
  }
3653
3670
  }
3654
3671
  }
@@ -3656,35 +3673,38 @@ async function handleSubMenu(commandGroup, initialSubMenu) {
3656
3673
  let currentTitle = initialSubMenu.title;
3657
3674
  let currentItems = initialSubMenu.items;
3658
3675
  while (true) {
3659
- try {
3660
- log.newline();
3661
- const subCommand = await escapableSelect({
3676
+ log.newline();
3677
+ const subCommandResult = await wrapAsync(
3678
+ () => escapableSelect({
3662
3679
  message: `${emoji.donut} ${currentTitle}`,
3663
3680
  choices: currentItems,
3664
3681
  pageSize: 30,
3665
3682
  loop: true,
3666
3683
  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") {
3684
+ }),
3685
+ ensureError
3686
+ );
3687
+ if (!subCommandResult.ok) {
3688
+ if (subCommandResult.error.name === "ExitPromptError") {
3685
3689
  break;
3686
3690
  }
3687
- throw err;
3691
+ throw subCommandResult.error;
3692
+ }
3693
+ const subCommand = subCommandResult.value;
3694
+ if (subCommand === null || subCommand === "back") {
3695
+ break;
3696
+ }
3697
+ log.newline();
3698
+ await executeCommand(commandGroup, subCommand);
3699
+ log.newline();
3700
+ if (isWorkflowAction(commandGroup, subCommand)) {
3701
+ break;
3702
+ }
3703
+ const { ctx: refreshedCtx } = await getMenuContext();
3704
+ const refreshedMenu = buildSubMenu(commandGroup, refreshedCtx);
3705
+ if (refreshedMenu) {
3706
+ currentTitle = refreshedMenu.title;
3707
+ currentItems = refreshedMenu.items;
3688
3708
  }
3689
3709
  }
3690
3710
  }
@@ -3695,12 +3715,9 @@ async function executeCommand(group, subCommand) {
3695
3715
  log.error(`Unknown command: ${group} ${subCommand}`);
3696
3716
  return;
3697
3717
  }
3698
- try {
3699
- await handler();
3700
- } catch (err) {
3701
- if (err instanceof Error) {
3702
- log.error(err.message);
3703
- }
3718
+ const r = await wrapAsync(() => handler(), ensureError);
3719
+ if (!r.ok) {
3720
+ log.error(r.error.message);
3704
3721
  }
3705
3722
  }
3706
3723
 
@@ -4111,7 +4128,7 @@ Checks performed:
4111
4128
  // package.json
4112
4129
  var package_default = {
4113
4130
  name: "ralphctl",
4114
- version: "0.1.2",
4131
+ version: "0.1.4",
4115
4132
  description: "Sprint and task management CLI for AI-assisted coding",
4116
4133
  homepage: "https://github.com/lukas-grigis/ralphctl",
4117
4134
  type: "module",
@@ -4169,6 +4186,7 @@ var package_default = {
4169
4186
  "gradient-string": "^3.0.0",
4170
4187
  ora: "^9.3.0",
4171
4188
  tabtab: "^3.0.2",
4189
+ "typescript-result": "^3.5.2",
4172
4190
  zod: "^4.3.6"
4173
4191
  },
4174
4192
  devDependencies: {
@@ -4229,7 +4247,7 @@ registerCompletionCommands(program);
4229
4247
  registerDoctorCommands(program);
4230
4248
  async function main() {
4231
4249
  if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
4232
- const { handleCompletionRequest } = await import("./handle-UG5M2OON.mjs");
4250
+ const { handleCompletionRequest } = await import("./handle-K2AZLTKU.mjs");
4233
4251
  if (await handleCompletionRequest(program)) return;
4234
4252
  }
4235
4253
  if (process.argv.length <= 2 || process.argv[2] === "interactive") {
@@ -4240,6 +4258,10 @@ async function main() {
4240
4258
  }
4241
4259
  }
4242
4260
  main().catch((err) => {
4243
- console.error(error("Fatal error:"), err);
4244
- process.exit(1);
4261
+ if (err instanceof DomainError) {
4262
+ showError(err.message);
4263
+ process.exit(EXIT_ERROR);
4264
+ }
4265
+ console.error(error("Unexpected error \u2014 please report this bug:"), err instanceof Error ? err.stack : String(err));
4266
+ process.exit(EXIT_ERROR);
4245
4267
  });