ralphctl 0.3.0 → 0.4.0

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.
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-D2YGPLIV.mjs";
7
7
  import {
8
8
  editorInput
9
- } from "./chunk-JXMHLW42.mjs";
9
+ } from "./chunk-7JLZQICD.mjs";
10
10
  import {
11
11
  addProjectRepo,
12
12
  getProject,
@@ -52,7 +52,7 @@ import {
52
52
  updateTask,
53
53
  updateTaskStatus,
54
54
  validateImportTasks
55
- } from "./chunk-CDOPLXFK.mjs";
55
+ } from "./chunk-JYCGQA2D.mjs";
56
56
  import {
57
57
  fetchIssueFromUrl,
58
58
  formatIssueContext,
@@ -61,8 +61,9 @@ import {
61
61
  getTicket,
62
62
  listTickets,
63
63
  removeTicket,
64
+ truncate,
64
65
  updateTicket
65
- } from "./chunk-HL4ZMHCQ.mjs";
66
+ } from "./chunk-JOQO4HMM.mjs";
66
67
  import {
67
68
  EXIT_ERROR,
68
69
  exitWithCode
@@ -176,7 +177,7 @@ import {
176
177
  // package.json
177
178
  var package_default = {
178
179
  name: "ralphctl",
179
- version: "0.3.0",
180
+ version: "0.4.0",
180
181
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
181
182
  homepage: "https://github.com/lukas-grigis/ralphctl",
182
183
  type: "module",
@@ -421,8 +422,8 @@ function useCurrentPrompt() {
421
422
  }
422
423
 
423
424
  // src/integration/ui/prompts/confirm-prompt.tsx
424
- import "react";
425
- import { Box, Text } from "ink";
425
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
426
+ import { Box, Text, useInput, useStdout } from "ink";
426
427
  import { ConfirmInput } from "@inkjs/ui";
427
428
 
428
429
  // src/integration/ui/theme/tokens.ts
@@ -491,43 +492,112 @@ var spacing = {
491
492
  var FIELD_LABEL_WIDTH = 12;
492
493
 
493
494
  // src/integration/ui/prompts/confirm-prompt.tsx
494
- import { jsx, jsxs } from "react/jsx-runtime";
495
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
496
+ var RESERVED_ROWS = 10;
497
+ var MIN_VIEWPORT = 6;
498
+ var MAX_VIEWPORT = 40;
499
+ function useTerminalRows() {
500
+ const { stdout } = useStdout();
501
+ const [rows, setRows] = useState2(stdout.rows);
502
+ useEffect2(() => {
503
+ const onResize = () => {
504
+ setRows(stdout.rows);
505
+ };
506
+ stdout.on("resize", onResize);
507
+ return () => {
508
+ stdout.off("resize", onResize);
509
+ };
510
+ }, [stdout]);
511
+ return rows;
512
+ }
495
513
  function ConfirmPrompt({ options, onSubmit }) {
496
514
  const hint = options.default === false ? "(y/N)" : "(Y/n)";
497
- return /* @__PURE__ */ jsxs(Box, { children: [
498
- /* @__PURE__ */ jsxs(Text, { children: [
499
- emoji.donut,
500
- " ",
501
- options.message,
502
- " "
503
- ] }),
504
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
505
- hint,
506
- " "
507
- ] }),
508
- /* @__PURE__ */ jsx(
509
- ConfirmInput,
515
+ const details = options.details?.trim();
516
+ const lines = useMemo(() => details ? details.split("\n") : [], [details]);
517
+ const terminalRows = useTerminalRows();
518
+ const viewport = Math.max(MIN_VIEWPORT, Math.min(MAX_VIEWPORT, terminalRows - RESERVED_ROWS));
519
+ const total = lines.length;
520
+ const maxOffset = Math.max(0, total - viewport);
521
+ const scrollable = total > viewport;
522
+ const [offset, setOffset] = useState2(0);
523
+ useInput((_input, key) => {
524
+ if (!scrollable) return;
525
+ if (key.upArrow) setOffset((o) => Math.max(0, o - 1));
526
+ else if (key.downArrow) setOffset((o) => Math.min(maxOffset, o + 1));
527
+ else if (key.pageUp) setOffset((o) => Math.max(0, o - viewport));
528
+ else if (key.pageDown) setOffset((o) => Math.min(maxOffset, o + viewport));
529
+ });
530
+ const visibleLines = scrollable ? lines.slice(offset, offset + viewport) : lines;
531
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
532
+ details ? /* @__PURE__ */ jsxs(
533
+ Box,
510
534
  {
511
- defaultChoice: options.default === false ? "cancel" : "confirm",
512
- onConfirm: () => {
513
- onSubmit(true);
514
- },
515
- onCancel: () => {
516
- onSubmit(false);
517
- }
535
+ flexDirection: "column",
536
+ borderStyle: "round",
537
+ borderColor: inkColors.muted,
538
+ paddingX: spacing.gutter,
539
+ marginBottom: spacing.section,
540
+ children: [
541
+ visibleLines.map((line, idx) => /* @__PURE__ */ jsx(Text, { children: line.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
542
+ /* @__PURE__ */ jsxs(Text, { color: inkColors.muted, children: [
543
+ glyphs.quoteRail,
544
+ " "
545
+ ] }),
546
+ line
547
+ ] }) : " " }, idx)),
548
+ scrollable ? /* @__PURE__ */ jsxs(Text, { color: inkColors.muted, children: [
549
+ glyphs.inlineDot,
550
+ " lines ",
551
+ String(offset + 1),
552
+ "\u2013",
553
+ String(Math.min(offset + viewport, total)),
554
+ " of",
555
+ " ",
556
+ String(total),
557
+ " ",
558
+ glyphs.inlineDot,
559
+ " \u2191/\u2193 scroll ",
560
+ glyphs.inlineDot,
561
+ " PgUp/PgDn page"
562
+ ] }) : null
563
+ ]
518
564
  }
519
- )
565
+ ) : null,
566
+ /* @__PURE__ */ jsxs(Box, { children: [
567
+ /* @__PURE__ */ jsxs(Text, { children: [
568
+ emoji.donut,
569
+ " ",
570
+ options.message,
571
+ " "
572
+ ] }),
573
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
574
+ hint,
575
+ " "
576
+ ] }),
577
+ /* @__PURE__ */ jsx(
578
+ ConfirmInput,
579
+ {
580
+ defaultChoice: options.default === false ? "cancel" : "confirm",
581
+ onConfirm: () => {
582
+ onSubmit(true);
583
+ },
584
+ onCancel: () => {
585
+ onSubmit(false);
586
+ }
587
+ }
588
+ )
589
+ ] })
520
590
  ] });
521
591
  }
522
592
 
523
593
  // src/integration/ui/prompts/input-prompt.tsx
524
- import { useState as useState2 } from "react";
525
- import { Box as Box2, Text as Text2, useInput } from "ink";
594
+ import { useState as useState3 } from "react";
595
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
526
596
  import { TextInput } from "@inkjs/ui";
527
597
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
528
598
  function InputPrompt({ options, onSubmit, onCancel }) {
529
- const [error2, setError] = useState2(null);
530
- useInput((_input, key) => {
599
+ const [error2, setError] = useState3(null);
600
+ useInput2((_input, key) => {
531
601
  if (key.escape) onCancel();
532
602
  });
533
603
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
@@ -565,13 +635,13 @@ function InputPrompt({ options, onSubmit, onCancel }) {
565
635
  }
566
636
 
567
637
  // src/integration/ui/prompts/select-prompt.tsx
568
- import { useState as useState3 } from "react";
569
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
638
+ import { useState as useState4 } from "react";
639
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
570
640
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
571
641
  function SelectPrompt({ options, onSubmit, onCancel }) {
572
642
  const initialIdx = findInitialIdx(options);
573
- const [focusedIdx, setFocusedIdx] = useState3(initialIdx);
574
- useInput2((_input, key) => {
643
+ const [focusedIdx, setFocusedIdx] = useState4(initialIdx);
644
+ useInput3((_input, key) => {
575
645
  if (key.escape) {
576
646
  onCancel();
577
647
  return;
@@ -634,14 +704,14 @@ function stepFocus(choices, from, delta) {
634
704
  }
635
705
 
636
706
  // src/integration/ui/prompts/checkbox-prompt.tsx
637
- import { useState as useState4 } from "react";
638
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
707
+ import { useState as useState5 } from "react";
708
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
639
709
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
640
710
  function CheckboxPrompt({ options, onSubmit, onCancel }) {
641
711
  const initialFocus = options.choices.findIndex((c) => !isDisabled2(c));
642
- const [focusedIdx, setFocusedIdx] = useState4(initialFocus >= 0 ? initialFocus : 0);
643
- const [checked, setChecked] = useState4(() => seedCheckedSet(options));
644
- useInput3((input, key) => {
712
+ const [focusedIdx, setFocusedIdx] = useState5(initialFocus >= 0 ? initialFocus : 0);
713
+ const [checked, setChecked] = useState5(() => seedCheckedSet(options));
714
+ useInput4((input, key) => {
645
715
  if (key.escape) {
646
716
  onCancel();
647
717
  return;
@@ -717,8 +787,8 @@ function stepFocus2(choices, from, delta) {
717
787
  }
718
788
 
719
789
  // src/integration/ui/prompts/editor-prompt.tsx
720
- import { useState as useState5, useMemo } from "react";
721
- import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
790
+ import { useState as useState6, useMemo as useMemo2 } from "react";
791
+ import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
722
792
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
723
793
  var MIN_EDIT_ROWS = 8;
724
794
  function splitLines(text) {
@@ -734,13 +804,13 @@ function clampCursor(lines, cursor) {
734
804
  return { row, col };
735
805
  }
736
806
  function EditorPrompt({ options, onSubmit, onCancel }) {
737
- const [lines, setLines] = useState5(() => splitLines(options.default ?? ""));
738
- const [cursor, setCursor] = useState5(() => {
807
+ const [lines, setLines] = useState6(() => splitLines(options.default ?? ""));
808
+ const [cursor, setCursor] = useState6(() => {
739
809
  const init = splitLines(options.default ?? "");
740
810
  const lastRow = init.length - 1;
741
811
  return { row: lastRow, col: (init[lastRow] ?? "").length };
742
812
  });
743
- useInput4((input, key) => {
813
+ useInput5((input, key) => {
744
814
  if (key.escape || key.ctrl && input === "c") {
745
815
  onCancel();
746
816
  return;
@@ -836,7 +906,7 @@ function EditorPrompt({ options, onSubmit, onCancel }) {
836
906
  });
837
907
  }
838
908
  });
839
- const renderedLines = useMemo(() => {
909
+ const renderedLines = useMemo2(() => {
840
910
  const padCount = Math.max(0, MIN_EDIT_ROWS - lines.length);
841
911
  const padded = lines.map((line, i) => {
842
912
  if (i !== cursor.row) return line.length > 0 ? line : " ";
@@ -883,11 +953,11 @@ function EditorPrompt({ options, onSubmit, onCancel }) {
883
953
  }
884
954
 
885
955
  // src/integration/ui/prompts/file-browser-prompt.tsx
886
- import { useEffect as useEffect2, useMemo as useMemo2, useState as useState6 } from "react";
956
+ import { useEffect as useEffect3, useMemo as useMemo3, useState as useState7 } from "react";
887
957
  import { readdirSync, statSync } from "fs";
888
958
  import { homedir } from "os";
889
959
  import { dirname, join, resolve } from "path";
890
- import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
960
+ import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
891
961
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
892
962
  function listDirectories(dirPath) {
893
963
  try {
@@ -905,20 +975,20 @@ function isGitRepo(dirPath) {
905
975
  }
906
976
  var PAGE_SIZE = 12;
907
977
  function FileBrowserPrompt({ options, onSubmit, onCancel }) {
908
- const [currentPath, setCurrentPath] = useState6(
978
+ const [currentPath, setCurrentPath] = useState7(
909
979
  () => options.startPath ? resolve(options.startPath) : homedir()
910
980
  );
911
- const [dirs, setDirs] = useState6([]);
912
- const [cursor, setCursor] = useState6(0);
913
- const [offset, setOffset] = useState6(0);
914
- useEffect2(() => {
981
+ const [dirs, setDirs] = useState7([]);
982
+ const [cursor, setCursor] = useState7(0);
983
+ const [offset, setOffset] = useState7(0);
984
+ useEffect3(() => {
915
985
  setDirs(listDirectories(currentPath));
916
986
  setCursor(0);
917
987
  setOffset(0);
918
988
  }, [currentPath]);
919
989
  const message = options.message ?? "Browse to directory:";
920
990
  const parent = dirname(currentPath);
921
- useInput5((input, key) => {
991
+ useInput6((input, key) => {
922
992
  if (key.escape) {
923
993
  onCancel();
924
994
  return;
@@ -952,11 +1022,11 @@ function FileBrowserPrompt({ options, onSubmit, onCancel }) {
952
1022
  return;
953
1023
  }
954
1024
  });
955
- useEffect2(() => {
1025
+ useEffect3(() => {
956
1026
  if (cursor < offset) setOffset(cursor);
957
1027
  else if (cursor >= offset + PAGE_SIZE) setOffset(cursor - PAGE_SIZE + 1);
958
1028
  }, [cursor, offset]);
959
- const visible = useMemo2(() => dirs.slice(offset, offset + PAGE_SIZE), [dirs, offset]);
1029
+ const visible = useMemo3(() => dirs.slice(offset, offset + PAGE_SIZE), [dirs, offset]);
960
1030
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
961
1031
  /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
962
1032
  emoji.donut,
@@ -1897,7 +1967,7 @@ async function selectTicket(message = "Select ticket:", filter) {
1897
1967
  default: true
1898
1968
  });
1899
1969
  if (create) {
1900
- const { ticketAddCommand } = await import("./add-JGUOR4Z5.mjs");
1970
+ const { ticketAddCommand } = await import("./add-CIM72NE3.mjs");
1901
1971
  await ticketAddCommand({ interactive: true });
1902
1972
  const updated = await listTickets();
1903
1973
  const refiltered = filter ? updated.filter(filter) : updated;
@@ -2688,6 +2758,8 @@ function parseArgs2(args) {
2688
2758
  if (arg === "--project") {
2689
2759
  options.project = nextArg;
2690
2760
  i++;
2761
+ } else if (arg === "--auto") {
2762
+ options.auto = true;
2691
2763
  } else if (!arg?.startsWith("-")) {
2692
2764
  sprintId = arg;
2693
2765
  }
@@ -2710,7 +2782,7 @@ async function sprintRefineCommand(args) {
2710
2782
  console.log(field("ID", sprint.id));
2711
2783
  log.newline();
2712
2784
  const shared = getSharedDeps();
2713
- const pipeline = createRefinePipeline(shared, { project: options.project });
2785
+ const pipeline = createRefinePipeline(shared, { project: options.project, auto: options.auto });
2714
2786
  const result = await executePipeline(pipeline, { sprintId: id });
2715
2787
  if (!result.ok) {
2716
2788
  showError(result.error.message);
@@ -3893,6 +3965,100 @@ async function taskImportCommand(args) {
3893
3965
  }
3894
3966
  }
3895
3967
 
3968
+ // src/integration/cli/commands/task/why.ts
3969
+ function collectBlockers(root, byId) {
3970
+ const out = [];
3971
+ const visited = /* @__PURE__ */ new Set([root.id]);
3972
+ function walk(task, depth) {
3973
+ for (const blockerId of task.blockedBy) {
3974
+ const blocker = byId.get(blockerId);
3975
+ if (!blocker) {
3976
+ out.push({
3977
+ task: { id: blockerId, name: `(missing ${blockerId})`, status: "todo" },
3978
+ depth,
3979
+ missing: true
3980
+ });
3981
+ continue;
3982
+ }
3983
+ if (visited.has(blocker.id)) continue;
3984
+ visited.add(blocker.id);
3985
+ out.push({ task: blocker, depth, missing: false });
3986
+ if (blocker.status !== "done") walk(blocker, depth + 1);
3987
+ }
3988
+ }
3989
+ walk(root, 0);
3990
+ return out;
3991
+ }
3992
+ function renderBlockerLine(node) {
3993
+ const indent = " " + " ".repeat(node.depth);
3994
+ const connector = colors.muted(node.depth === 0 ? "\u251C\u2500" : "\u21B3");
3995
+ const idPart = colors.muted(node.task.id);
3996
+ if (node.missing) {
3997
+ return `${indent}${connector} ${colors.error(icons.error)} ${idPart} ${colors.error("(referenced but missing)")}`;
3998
+ }
3999
+ const status = formatTaskStatus(node.task.status);
4000
+ const marker = node.task.status === "done" ? colors.success(icons.success) : colors.warning(icons.warning);
4001
+ return `${indent}${connector} ${marker} ${idPart} ${node.task.name} ${status}`;
4002
+ }
4003
+ async function taskWhyCommand(taskId) {
4004
+ let id = taskId;
4005
+ if (!id) {
4006
+ const selected = await selectTask("Which task is blocked?");
4007
+ if (!selected) return;
4008
+ id = selected;
4009
+ }
4010
+ const r = await wrapAsync(() => getTasks(), ensureError);
4011
+ if (!r.ok) {
4012
+ showError(r.error.message);
4013
+ return;
4014
+ }
4015
+ const tasks = r.value;
4016
+ const byId = new Map(tasks.map((t) => [t.id, t]));
4017
+ const root = byId.get(id);
4018
+ if (!root) {
4019
+ showError(new TaskNotFoundError(id).message);
4020
+ return;
4021
+ }
4022
+ printHeader("Why blocked?");
4023
+ log.newline();
4024
+ console.log(` ${icons.task} ${colors.highlight(root.name)} ${formatTaskStatus(root.status)}`);
4025
+ console.log(` ${colors.muted("id:")} ${colors.muted(root.id)}`);
4026
+ log.newline();
4027
+ if (root.status === "done") {
4028
+ console.log(` ${colors.success(icons.success)} ${colors.success("Task is done \u2014 nothing blocking it.")}`);
4029
+ log.newline();
4030
+ return;
4031
+ }
4032
+ if (root.blockedBy.length === 0) {
4033
+ console.log(` ${colors.success(icons.success)} ${colors.success("No blockers \u2014 ready to execute.")}`);
4034
+ log.newline();
4035
+ showNextStep(`ralphctl task status ${root.id} in_progress`, "Start working on this task");
4036
+ log.newline();
4037
+ return;
4038
+ }
4039
+ const nodes = collectBlockers(root, byId);
4040
+ const unmet = nodes.filter((n) => !n.missing && n.task.status !== "done");
4041
+ const leafUnmet = unmet.filter((n) => n.task.blockedBy.every((bid) => byId.get(bid)?.status === "done"));
4042
+ console.log(` ${colors.muted("Dependency chain:")}`);
4043
+ for (const node of nodes) console.log(renderBlockerLine(node));
4044
+ log.newline();
4045
+ if (unmet.length === 0) {
4046
+ console.log(` ${colors.success(icons.success)} ${colors.success("All blockers are done \u2014 ready to execute.")}`);
4047
+ log.newline();
4048
+ showNextStep(`ralphctl task status ${root.id} in_progress`, "Start working on this task");
4049
+ log.newline();
4050
+ return;
4051
+ }
4052
+ const actionable = leafUnmet.length > 0 ? leafUnmet : unmet;
4053
+ console.log(
4054
+ ` ${colors.warning(icons.warning)} ${colors.warning(`Unblock by completing ${String(actionable.length)} task${actionable.length !== 1 ? "s" : ""} first:`)}`
4055
+ );
4056
+ for (const node of actionable) {
4057
+ console.log(` ${colors.muted("\u2192")} ${colors.highlight(node.task.id)} ${node.task.name}`);
4058
+ }
4059
+ log.newline();
4060
+ }
4061
+
3896
4062
  // src/integration/cli/commands/ticket/edit.ts
3897
4063
  function validateUrl(url) {
3898
4064
  try {
@@ -4092,7 +4258,7 @@ async function ticketListCommand(args) {
4092
4258
  log.raw(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
4093
4259
  if (ticket.description) {
4094
4260
  const preview = ticket.description.split("\n")[0] ?? "";
4095
- const truncated = preview.length > 60 ? preview.slice(0, 57) + "..." : preview;
4261
+ const truncated = truncate(preview, 60);
4096
4262
  log.raw(` ${muted(truncated)}`, 1);
4097
4263
  }
4098
4264
  }
@@ -5048,6 +5214,7 @@ export {
5048
5214
  taskNextCommand,
5049
5215
  taskReorderCommand,
5050
5216
  taskImportCommand,
5217
+ taskWhyCommand,
5051
5218
  ticketEditCommand,
5052
5219
  ticketListCommand,
5053
5220
  ticketShowCommand,
package/dist/cli.mjs CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  configShowCommand,
6
6
  createSharedDeps,
7
7
  doctorCommand,
8
+ getNextAction,
9
+ loadDashboardData,
8
10
  progressLogCommand,
9
11
  progressShowCommand,
10
12
  projectListCommand,
@@ -33,12 +35,13 @@ import {
33
35
  taskReorderCommand,
34
36
  taskShowCommand,
35
37
  taskStatusCommand,
38
+ taskWhyCommand,
36
39
  ticketEditCommand,
37
40
  ticketListCommand,
38
41
  ticketRefineCommand,
39
42
  ticketRemoveCommand,
40
43
  ticketShowCommand
41
- } from "./chunk-4GHVNKLV.mjs";
44
+ } from "./chunk-MRN3Z2XC.mjs";
42
45
  import {
43
46
  projectAddCommand
44
47
  } from "./chunk-D2YGPLIV.mjs";
@@ -47,13 +50,15 @@ import {
47
50
  } from "./chunk-3QBEBKMZ.mjs";
48
51
  import {
49
52
  ticketAddCommand
50
- } from "./chunk-JXMHLW42.mjs";
53
+ } from "./chunk-7JLZQICD.mjs";
51
54
  import "./chunk-NUYQK5MN.mjs";
52
55
  import {
53
56
  getTasks,
54
57
  sprintStartCommand
55
- } from "./chunk-CDOPLXFK.mjs";
56
- import "./chunk-HL4ZMHCQ.mjs";
58
+ } from "./chunk-JYCGQA2D.mjs";
59
+ import {
60
+ truncate
61
+ } from "./chunk-JOQO4HMM.mjs";
57
62
  import {
58
63
  EXIT_ERROR
59
64
  } from "./chunk-CFUVE2BP.mjs";
@@ -68,6 +73,7 @@ import {
68
73
  import {
69
74
  colors,
70
75
  error,
76
+ formatSprintStatus,
71
77
  icons,
72
78
  log,
73
79
  printBanner,
@@ -195,7 +201,7 @@ async function sprintInsightsCommand(args) {
195
201
  console.log(` ${colors.accent("Evaluation output:")}`);
196
202
  for (const task of withOutput) {
197
203
  const output = task.evaluationOutput ?? "";
198
- const truncated = output.length > 200 ? output.slice(0, 200) + "..." : output;
204
+ const truncated = truncate(output, 200);
199
205
  console.log(` ${icons.bullet} ${colors.accent(task.name)}: ${colors.muted(truncated)}`);
200
206
  }
201
207
  log.newline();
@@ -281,10 +287,11 @@ Examples:
281
287
  sprint.command("switch").description("Quick sprint switcher (opens selector)").action(async () => {
282
288
  await sprintSwitchCommand();
283
289
  });
284
- sprint.command("refine [id]").description("Refine ticket specifications").option("--project <name>", "Only refine tickets for specific project").action(async (id, opts) => {
290
+ sprint.command("refine [id]").description("Refine ticket specifications").option("--project <name>", "Only refine tickets for specific project").option("--auto", "Run without approval prompts (AI drafts requirements autonomously)").action(async (id, opts) => {
285
291
  const args = [];
286
292
  if (id) args.push(id);
287
293
  if (opts?.project) args.push("--project", opts.project);
294
+ if (opts?.auto) args.push("--auto");
288
295
  await sprintRefineCommand(args);
289
296
  });
290
297
  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) => {
@@ -424,6 +431,9 @@ Examples:
424
431
  });
425
432
  });
426
433
  task.command("next").description("Get next task").action(taskNextCommand);
434
+ task.command("why [id]").description("Explain why a task is blocked (walks the dependency chain)").action(async (id) => {
435
+ await taskWhyCommand(id);
436
+ });
427
437
  task.command("reorder [id] [position]").description("Change task priority").action(async (id, position) => {
428
438
  const args = [];
429
439
  if (id) args.push(id);
@@ -576,6 +586,85 @@ Checks performed:
576
586
  });
577
587
  }
578
588
 
589
+ // src/integration/cli/commands/next/next.ts
590
+ function toCommand(action) {
591
+ return `ralphctl ${action.group} ${action.subCommand}`;
592
+ }
593
+ function computePayload(data) {
594
+ if (!data) {
595
+ return { sprint: null, action: null, reason: "no-sprint" };
596
+ }
597
+ const sprint = { id: data.sprint.id, name: data.sprint.name, status: data.sprint.status };
598
+ if (data.sprint.status === "closed") {
599
+ return { sprint, action: null, reason: "sprint-closed" };
600
+ }
601
+ const next = getNextAction(data);
602
+ if (!next) {
603
+ return { sprint, action: null, reason: "all-done" };
604
+ }
605
+ return {
606
+ sprint,
607
+ action: { command: toCommand(next), label: next.label, description: next.description },
608
+ reason: "action-ready"
609
+ };
610
+ }
611
+ function renderPorcelain(payload) {
612
+ if (payload.action) {
613
+ console.log(payload.action.command);
614
+ return;
615
+ }
616
+ console.log("");
617
+ }
618
+ function renderHuman(payload) {
619
+ log.newline();
620
+ if (payload.reason === "no-sprint") {
621
+ console.log(` ${colors.muted(icons.inactive)} ${colors.muted("No current sprint.")}`);
622
+ console.log(` ${colors.muted(icons.tip)} ${colors.muted("Create one to get started:")}`);
623
+ console.log(` ${colors.highlight("ralphctl sprint create")}`);
624
+ log.newline();
625
+ return;
626
+ }
627
+ if (!payload.sprint) return;
628
+ const sprintLine = `${icons.sprint} ${colors.highlight(payload.sprint.name)} ${formatSprintStatus(payload.sprint.status)}`;
629
+ console.log(` ${sprintLine}`);
630
+ if (payload.reason === "sprint-closed") {
631
+ console.log(` ${colors.muted(icons.info)} ${colors.muted("Sprint is closed. Start a new one:")}`);
632
+ console.log(` ${colors.highlight("ralphctl sprint create")}`);
633
+ log.newline();
634
+ return;
635
+ }
636
+ if (payload.reason === "all-done") {
637
+ console.log(` ${colors.success(icons.success)} ${colors.success("Nothing left to do.")}`);
638
+ log.newline();
639
+ return;
640
+ }
641
+ const action = payload.action;
642
+ if (!action) return;
643
+ console.log(` ${colors.muted(icons.tip)} ${colors.muted(action.label + ":")} ${colors.muted(action.description)}`);
644
+ console.log(` ${colors.highlight(action.command)}`);
645
+ log.newline();
646
+ }
647
+ async function nextCommand(options = {}) {
648
+ const data = await loadDashboardData();
649
+ const payload = computePayload(data);
650
+ if (options.json) {
651
+ console.log(JSON.stringify(payload));
652
+ return;
653
+ }
654
+ if (options.porcelain) {
655
+ renderPorcelain(payload);
656
+ return;
657
+ }
658
+ renderHuman(payload);
659
+ }
660
+
661
+ // src/integration/cli/commands/next/register.ts
662
+ function registerNextCommands(program2) {
663
+ program2.command("next").description("Suggest the next workflow action for the current sprint").option("--porcelain", "Print only the suggested command (for shell/tmux integration)").option("--json", "Emit a machine-readable JSON payload").action(async (options) => {
664
+ await nextCommand(options);
665
+ });
666
+ }
667
+
579
668
  // src/application/entrypoint.ts
580
669
  setSharedDeps(createSharedDeps());
581
670
  var program = new Command();
@@ -601,6 +690,12 @@ registerDashboardCommands(program);
601
690
  registerConfigCommands(program);
602
691
  registerCompletionCommands(program);
603
692
  registerDoctorCommands(program);
693
+ registerNextCommands(program);
694
+ function isQuietCommand(argv) {
695
+ const cmd = argv[2];
696
+ if (cmd === "next" && (argv.includes("--porcelain") || argv.includes("--json"))) return true;
697
+ return false;
698
+ }
604
699
  async function main() {
605
700
  if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
606
701
  const { handleCompletionRequest } = await import("./handle-BBAZJ44Y.mjs");
@@ -610,7 +705,7 @@ async function main() {
610
705
  const isBare = argv.length <= 2;
611
706
  const isInteractive = argv[2] === "interactive";
612
707
  if (isBare || isInteractive) {
613
- const { mountInkApp } = await import("./mount-XZPBDRPZ.mjs");
708
+ const { mountInkApp } = await import("./mount-XMN3S4W6.mjs");
614
709
  const { fallback } = await mountInkApp({ initialView: "repl" });
615
710
  if (!fallback) return;
616
711
  printBanner();
@@ -621,10 +716,10 @@ async function main() {
621
716
  return;
622
717
  }
623
718
  if (argv[2] === "sprint" && argv[3] === "start") {
624
- const { parseSprintStartArgs } = await import("./start-MMWC7QLI.mjs");
719
+ const { parseSprintStartArgs } = await import("./start-D35SOXMM.mjs");
625
720
  const parsed = parseSprintStartArgs(argv.slice(4));
626
721
  if (parsed.ok) {
627
- const { mountInkApp } = await import("./mount-XZPBDRPZ.mjs");
722
+ const { mountInkApp } = await import("./mount-XMN3S4W6.mjs");
628
723
  const { getSharedDeps } = await import("./bootstrap-FMHG6DRY.mjs");
629
724
  let sprintId;
630
725
  try {
@@ -642,7 +737,7 @@ async function main() {
642
737
  }
643
738
  }
644
739
  }
645
- printBanner();
740
+ if (!isQuietCommand(argv)) printBanner();
646
741
  await program.parseAsync(argv);
647
742
  }
648
743
  main().catch((err) => {