ralphctl 0.3.1 → 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.
- package/dist/{chunk-CSC4TBJB.mjs → chunk-JYCGQA2D.mjs} +54 -20
- package/dist/{chunk-EPDR6VO5.mjs → chunk-MRN3Z2XC.mjs} +188 -42
- package/dist/cli.mjs +100 -7
- package/dist/{mount-U7QXVB5Q.mjs → mount-XMN3S4W6.mjs} +129 -43
- package/dist/{start-WG7VMEB2.mjs → start-D35SOXMM.mjs} +1 -1
- package/package.json +1 -1
|
@@ -2801,28 +2801,63 @@ function markDone(deps) {
|
|
|
2801
2801
|
|
|
2802
2802
|
// src/business/pipelines/execute/per-task-pipeline.ts
|
|
2803
2803
|
function createPerTaskPipeline(deps, useCase, options = {}) {
|
|
2804
|
+
const trace = withStepTrace(deps.signalBus);
|
|
2804
2805
|
return pipeline("per-task", [
|
|
2805
|
-
branchPreflight({ external: deps.external, persistence: deps.persistence }),
|
|
2806
|
-
contractNegotiate({ persistence: deps.persistence, fs: deps.fs }),
|
|
2807
|
-
markInProgress({ persistence: deps.persistence, signalBus: deps.signalBus }),
|
|
2808
|
-
executeTask({ useCase, options, taskSessionIds: deps.taskSessionIds, logger: deps.logger }),
|
|
2809
|
-
storeVerification({ persistence: deps.persistence, logger: deps.logger }),
|
|
2810
|
-
postTaskCheck({ useCase }),
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2806
|
+
trace(branchPreflight({ external: deps.external, persistence: deps.persistence })),
|
|
2807
|
+
trace(contractNegotiate({ persistence: deps.persistence, fs: deps.fs })),
|
|
2808
|
+
trace(markInProgress({ persistence: deps.persistence, signalBus: deps.signalBus })),
|
|
2809
|
+
trace(executeTask({ useCase, options, taskSessionIds: deps.taskSessionIds, logger: deps.logger })),
|
|
2810
|
+
trace(storeVerification({ persistence: deps.persistence, logger: deps.logger })),
|
|
2811
|
+
trace(postTaskCheck({ useCase })),
|
|
2812
|
+
trace(
|
|
2813
|
+
evaluateTask({
|
|
2814
|
+
persistence: deps.persistence,
|
|
2815
|
+
fs: deps.fs,
|
|
2816
|
+
aiSession: deps.aiSession,
|
|
2817
|
+
promptBuilder: deps.promptBuilder,
|
|
2818
|
+
parser: deps.parser,
|
|
2819
|
+
ui: deps.ui,
|
|
2820
|
+
logger: deps.logger,
|
|
2821
|
+
external: deps.external,
|
|
2822
|
+
useCase,
|
|
2823
|
+
options
|
|
2824
|
+
})
|
|
2825
|
+
),
|
|
2826
|
+
trace(markDone({ persistence: deps.persistence, logger: deps.logger, signalBus: deps.signalBus }))
|
|
2824
2827
|
]);
|
|
2825
2828
|
}
|
|
2829
|
+
function withStepTrace(signalBus) {
|
|
2830
|
+
return (inner) => ({
|
|
2831
|
+
name: inner.name,
|
|
2832
|
+
execute: inner.execute,
|
|
2833
|
+
hooks: {
|
|
2834
|
+
pre: async (ctx) => {
|
|
2835
|
+
signalBus.emit({
|
|
2836
|
+
type: "task-step",
|
|
2837
|
+
sprintId: ctx.sprint.id,
|
|
2838
|
+
taskId: ctx.task.id,
|
|
2839
|
+
stepName: inner.name,
|
|
2840
|
+
phase: "start",
|
|
2841
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2842
|
+
});
|
|
2843
|
+
const prior = await inner.hooks?.pre?.(ctx);
|
|
2844
|
+
return prior ?? Result.ok(ctx);
|
|
2845
|
+
},
|
|
2846
|
+
post: async (ctx, result) => {
|
|
2847
|
+
const prior = await inner.hooks?.post?.(ctx, result);
|
|
2848
|
+
signalBus.emit({
|
|
2849
|
+
type: "task-step",
|
|
2850
|
+
sprintId: ctx.sprint.id,
|
|
2851
|
+
taskId: ctx.task.id,
|
|
2852
|
+
stepName: inner.name,
|
|
2853
|
+
phase: "finish",
|
|
2854
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2855
|
+
});
|
|
2856
|
+
return prior ?? Result.ok({});
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2826
2861
|
|
|
2827
2862
|
// src/business/pipelines/execute.ts
|
|
2828
2863
|
var EXIT_SUCCESS = 0;
|
|
@@ -3260,7 +3295,6 @@ function executeTasksStep(deps, options) {
|
|
|
3260
3295
|
onSettle: (task, result) => {
|
|
3261
3296
|
if (result === "success") {
|
|
3262
3297
|
taskSessionIds.delete(task.id);
|
|
3263
|
-
deps.logger.success(`Completed: ${task.name}`);
|
|
3264
3298
|
}
|
|
3265
3299
|
}
|
|
3266
3300
|
},
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
updateTask,
|
|
53
53
|
updateTaskStatus,
|
|
54
54
|
validateImportTasks
|
|
55
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-JYCGQA2D.mjs";
|
|
56
56
|
import {
|
|
57
57
|
fetchIssueFromUrl,
|
|
58
58
|
formatIssueContext,
|
|
@@ -177,7 +177,7 @@ import {
|
|
|
177
177
|
// package.json
|
|
178
178
|
var package_default = {
|
|
179
179
|
name: "ralphctl",
|
|
180
|
-
version: "0.
|
|
180
|
+
version: "0.4.0",
|
|
181
181
|
description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
|
|
182
182
|
homepage: "https://github.com/lukas-grigis/ralphctl",
|
|
183
183
|
type: "module",
|
|
@@ -422,8 +422,8 @@ function useCurrentPrompt() {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
// src/integration/ui/prompts/confirm-prompt.tsx
|
|
425
|
-
import "react";
|
|
426
|
-
import { Box, Text } from "ink";
|
|
425
|
+
import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
426
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
427
427
|
import { ConfirmInput } from "@inkjs/ui";
|
|
428
428
|
|
|
429
429
|
// src/integration/ui/theme/tokens.ts
|
|
@@ -493,11 +493,43 @@ var FIELD_LABEL_WIDTH = 12;
|
|
|
493
493
|
|
|
494
494
|
// src/integration/ui/prompts/confirm-prompt.tsx
|
|
495
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
|
+
}
|
|
496
513
|
function ConfirmPrompt({ options, onSubmit }) {
|
|
497
514
|
const hint = options.default === false ? "(y/N)" : "(Y/n)";
|
|
498
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;
|
|
499
531
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
500
|
-
details ? /* @__PURE__ */
|
|
532
|
+
details ? /* @__PURE__ */ jsxs(
|
|
501
533
|
Box,
|
|
502
534
|
{
|
|
503
535
|
flexDirection: "column",
|
|
@@ -505,13 +537,30 @@ function ConfirmPrompt({ options, onSubmit }) {
|
|
|
505
537
|
borderColor: inkColors.muted,
|
|
506
538
|
paddingX: spacing.gutter,
|
|
507
539
|
marginBottom: spacing.section,
|
|
508
|
-
children:
|
|
509
|
-
/* @__PURE__ */
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
]
|
|
515
564
|
}
|
|
516
565
|
) : null,
|
|
517
566
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
@@ -542,13 +591,13 @@ function ConfirmPrompt({ options, onSubmit }) {
|
|
|
542
591
|
}
|
|
543
592
|
|
|
544
593
|
// src/integration/ui/prompts/input-prompt.tsx
|
|
545
|
-
import { useState as
|
|
546
|
-
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";
|
|
547
596
|
import { TextInput } from "@inkjs/ui";
|
|
548
597
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
549
598
|
function InputPrompt({ options, onSubmit, onCancel }) {
|
|
550
|
-
const [error2, setError] =
|
|
551
|
-
|
|
599
|
+
const [error2, setError] = useState3(null);
|
|
600
|
+
useInput2((_input, key) => {
|
|
552
601
|
if (key.escape) onCancel();
|
|
553
602
|
});
|
|
554
603
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
@@ -586,13 +635,13 @@ function InputPrompt({ options, onSubmit, onCancel }) {
|
|
|
586
635
|
}
|
|
587
636
|
|
|
588
637
|
// src/integration/ui/prompts/select-prompt.tsx
|
|
589
|
-
import { useState as
|
|
590
|
-
import { Box as Box3, Text as Text3, useInput as
|
|
638
|
+
import { useState as useState4 } from "react";
|
|
639
|
+
import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
|
|
591
640
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
592
641
|
function SelectPrompt({ options, onSubmit, onCancel }) {
|
|
593
642
|
const initialIdx = findInitialIdx(options);
|
|
594
|
-
const [focusedIdx, setFocusedIdx] =
|
|
595
|
-
|
|
643
|
+
const [focusedIdx, setFocusedIdx] = useState4(initialIdx);
|
|
644
|
+
useInput3((_input, key) => {
|
|
596
645
|
if (key.escape) {
|
|
597
646
|
onCancel();
|
|
598
647
|
return;
|
|
@@ -655,14 +704,14 @@ function stepFocus(choices, from, delta) {
|
|
|
655
704
|
}
|
|
656
705
|
|
|
657
706
|
// src/integration/ui/prompts/checkbox-prompt.tsx
|
|
658
|
-
import { useState as
|
|
659
|
-
import { Box as Box4, Text as Text4, useInput as
|
|
707
|
+
import { useState as useState5 } from "react";
|
|
708
|
+
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
660
709
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
661
710
|
function CheckboxPrompt({ options, onSubmit, onCancel }) {
|
|
662
711
|
const initialFocus = options.choices.findIndex((c) => !isDisabled2(c));
|
|
663
|
-
const [focusedIdx, setFocusedIdx] =
|
|
664
|
-
const [checked, setChecked] =
|
|
665
|
-
|
|
712
|
+
const [focusedIdx, setFocusedIdx] = useState5(initialFocus >= 0 ? initialFocus : 0);
|
|
713
|
+
const [checked, setChecked] = useState5(() => seedCheckedSet(options));
|
|
714
|
+
useInput4((input, key) => {
|
|
666
715
|
if (key.escape) {
|
|
667
716
|
onCancel();
|
|
668
717
|
return;
|
|
@@ -738,8 +787,8 @@ function stepFocus2(choices, from, delta) {
|
|
|
738
787
|
}
|
|
739
788
|
|
|
740
789
|
// src/integration/ui/prompts/editor-prompt.tsx
|
|
741
|
-
import { useState as
|
|
742
|
-
import { Box as Box5, Text as Text5, useInput as
|
|
790
|
+
import { useState as useState6, useMemo as useMemo2 } from "react";
|
|
791
|
+
import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
|
|
743
792
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
744
793
|
var MIN_EDIT_ROWS = 8;
|
|
745
794
|
function splitLines(text) {
|
|
@@ -755,13 +804,13 @@ function clampCursor(lines, cursor) {
|
|
|
755
804
|
return { row, col };
|
|
756
805
|
}
|
|
757
806
|
function EditorPrompt({ options, onSubmit, onCancel }) {
|
|
758
|
-
const [lines, setLines] =
|
|
759
|
-
const [cursor, setCursor] =
|
|
807
|
+
const [lines, setLines] = useState6(() => splitLines(options.default ?? ""));
|
|
808
|
+
const [cursor, setCursor] = useState6(() => {
|
|
760
809
|
const init = splitLines(options.default ?? "");
|
|
761
810
|
const lastRow = init.length - 1;
|
|
762
811
|
return { row: lastRow, col: (init[lastRow] ?? "").length };
|
|
763
812
|
});
|
|
764
|
-
|
|
813
|
+
useInput5((input, key) => {
|
|
765
814
|
if (key.escape || key.ctrl && input === "c") {
|
|
766
815
|
onCancel();
|
|
767
816
|
return;
|
|
@@ -857,7 +906,7 @@ function EditorPrompt({ options, onSubmit, onCancel }) {
|
|
|
857
906
|
});
|
|
858
907
|
}
|
|
859
908
|
});
|
|
860
|
-
const renderedLines =
|
|
909
|
+
const renderedLines = useMemo2(() => {
|
|
861
910
|
const padCount = Math.max(0, MIN_EDIT_ROWS - lines.length);
|
|
862
911
|
const padded = lines.map((line, i) => {
|
|
863
912
|
if (i !== cursor.row) return line.length > 0 ? line : " ";
|
|
@@ -904,11 +953,11 @@ function EditorPrompt({ options, onSubmit, onCancel }) {
|
|
|
904
953
|
}
|
|
905
954
|
|
|
906
955
|
// src/integration/ui/prompts/file-browser-prompt.tsx
|
|
907
|
-
import { useEffect as
|
|
956
|
+
import { useEffect as useEffect3, useMemo as useMemo3, useState as useState7 } from "react";
|
|
908
957
|
import { readdirSync, statSync } from "fs";
|
|
909
958
|
import { homedir } from "os";
|
|
910
959
|
import { dirname, join, resolve } from "path";
|
|
911
|
-
import { Box as Box6, Text as Text6, useInput as
|
|
960
|
+
import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
|
|
912
961
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
913
962
|
function listDirectories(dirPath) {
|
|
914
963
|
try {
|
|
@@ -926,20 +975,20 @@ function isGitRepo(dirPath) {
|
|
|
926
975
|
}
|
|
927
976
|
var PAGE_SIZE = 12;
|
|
928
977
|
function FileBrowserPrompt({ options, onSubmit, onCancel }) {
|
|
929
|
-
const [currentPath, setCurrentPath] =
|
|
978
|
+
const [currentPath, setCurrentPath] = useState7(
|
|
930
979
|
() => options.startPath ? resolve(options.startPath) : homedir()
|
|
931
980
|
);
|
|
932
|
-
const [dirs, setDirs] =
|
|
933
|
-
const [cursor, setCursor] =
|
|
934
|
-
const [offset, setOffset] =
|
|
935
|
-
|
|
981
|
+
const [dirs, setDirs] = useState7([]);
|
|
982
|
+
const [cursor, setCursor] = useState7(0);
|
|
983
|
+
const [offset, setOffset] = useState7(0);
|
|
984
|
+
useEffect3(() => {
|
|
936
985
|
setDirs(listDirectories(currentPath));
|
|
937
986
|
setCursor(0);
|
|
938
987
|
setOffset(0);
|
|
939
988
|
}, [currentPath]);
|
|
940
989
|
const message = options.message ?? "Browse to directory:";
|
|
941
990
|
const parent = dirname(currentPath);
|
|
942
|
-
|
|
991
|
+
useInput6((input, key) => {
|
|
943
992
|
if (key.escape) {
|
|
944
993
|
onCancel();
|
|
945
994
|
return;
|
|
@@ -973,11 +1022,11 @@ function FileBrowserPrompt({ options, onSubmit, onCancel }) {
|
|
|
973
1022
|
return;
|
|
974
1023
|
}
|
|
975
1024
|
});
|
|
976
|
-
|
|
1025
|
+
useEffect3(() => {
|
|
977
1026
|
if (cursor < offset) setOffset(cursor);
|
|
978
1027
|
else if (cursor >= offset + PAGE_SIZE) setOffset(cursor - PAGE_SIZE + 1);
|
|
979
1028
|
}, [cursor, offset]);
|
|
980
|
-
const visible =
|
|
1029
|
+
const visible = useMemo3(() => dirs.slice(offset, offset + PAGE_SIZE), [dirs, offset]);
|
|
981
1030
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
|
|
982
1031
|
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
983
1032
|
emoji.donut,
|
|
@@ -2709,6 +2758,8 @@ function parseArgs2(args) {
|
|
|
2709
2758
|
if (arg === "--project") {
|
|
2710
2759
|
options.project = nextArg;
|
|
2711
2760
|
i++;
|
|
2761
|
+
} else if (arg === "--auto") {
|
|
2762
|
+
options.auto = true;
|
|
2712
2763
|
} else if (!arg?.startsWith("-")) {
|
|
2713
2764
|
sprintId = arg;
|
|
2714
2765
|
}
|
|
@@ -2731,7 +2782,7 @@ async function sprintRefineCommand(args) {
|
|
|
2731
2782
|
console.log(field("ID", sprint.id));
|
|
2732
2783
|
log.newline();
|
|
2733
2784
|
const shared = getSharedDeps();
|
|
2734
|
-
const pipeline = createRefinePipeline(shared, { project: options.project });
|
|
2785
|
+
const pipeline = createRefinePipeline(shared, { project: options.project, auto: options.auto });
|
|
2735
2786
|
const result = await executePipeline(pipeline, { sprintId: id });
|
|
2736
2787
|
if (!result.ok) {
|
|
2737
2788
|
showError(result.error.message);
|
|
@@ -3914,6 +3965,100 @@ async function taskImportCommand(args) {
|
|
|
3914
3965
|
}
|
|
3915
3966
|
}
|
|
3916
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
|
+
|
|
3917
4062
|
// src/integration/cli/commands/ticket/edit.ts
|
|
3918
4063
|
function validateUrl(url) {
|
|
3919
4064
|
try {
|
|
@@ -5069,6 +5214,7 @@ export {
|
|
|
5069
5214
|
taskNextCommand,
|
|
5070
5215
|
taskReorderCommand,
|
|
5071
5216
|
taskImportCommand,
|
|
5217
|
+
taskWhyCommand,
|
|
5072
5218
|
ticketEditCommand,
|
|
5073
5219
|
ticketListCommand,
|
|
5074
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-
|
|
44
|
+
} from "./chunk-MRN3Z2XC.mjs";
|
|
42
45
|
import {
|
|
43
46
|
projectAddCommand
|
|
44
47
|
} from "./chunk-D2YGPLIV.mjs";
|
|
@@ -52,7 +55,7 @@ import "./chunk-NUYQK5MN.mjs";
|
|
|
52
55
|
import {
|
|
53
56
|
getTasks,
|
|
54
57
|
sprintStartCommand
|
|
55
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-JYCGQA2D.mjs";
|
|
56
59
|
import {
|
|
57
60
|
truncate
|
|
58
61
|
} from "./chunk-JOQO4HMM.mjs";
|
|
@@ -70,6 +73,7 @@ import {
|
|
|
70
73
|
import {
|
|
71
74
|
colors,
|
|
72
75
|
error,
|
|
76
|
+
formatSprintStatus,
|
|
73
77
|
icons,
|
|
74
78
|
log,
|
|
75
79
|
printBanner,
|
|
@@ -283,10 +287,11 @@ Examples:
|
|
|
283
287
|
sprint.command("switch").description("Quick sprint switcher (opens selector)").action(async () => {
|
|
284
288
|
await sprintSwitchCommand();
|
|
285
289
|
});
|
|
286
|
-
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) => {
|
|
287
291
|
const args = [];
|
|
288
292
|
if (id) args.push(id);
|
|
289
293
|
if (opts?.project) args.push("--project", opts.project);
|
|
294
|
+
if (opts?.auto) args.push("--auto");
|
|
290
295
|
await sprintRefineCommand(args);
|
|
291
296
|
});
|
|
292
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) => {
|
|
@@ -426,6 +431,9 @@ Examples:
|
|
|
426
431
|
});
|
|
427
432
|
});
|
|
428
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
|
+
});
|
|
429
437
|
task.command("reorder [id] [position]").description("Change task priority").action(async (id, position) => {
|
|
430
438
|
const args = [];
|
|
431
439
|
if (id) args.push(id);
|
|
@@ -578,6 +586,85 @@ Checks performed:
|
|
|
578
586
|
});
|
|
579
587
|
}
|
|
580
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
|
+
|
|
581
668
|
// src/application/entrypoint.ts
|
|
582
669
|
setSharedDeps(createSharedDeps());
|
|
583
670
|
var program = new Command();
|
|
@@ -603,6 +690,12 @@ registerDashboardCommands(program);
|
|
|
603
690
|
registerConfigCommands(program);
|
|
604
691
|
registerCompletionCommands(program);
|
|
605
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
|
+
}
|
|
606
699
|
async function main() {
|
|
607
700
|
if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
|
|
608
701
|
const { handleCompletionRequest } = await import("./handle-BBAZJ44Y.mjs");
|
|
@@ -612,7 +705,7 @@ async function main() {
|
|
|
612
705
|
const isBare = argv.length <= 2;
|
|
613
706
|
const isInteractive = argv[2] === "interactive";
|
|
614
707
|
if (isBare || isInteractive) {
|
|
615
|
-
const { mountInkApp } = await import("./mount-
|
|
708
|
+
const { mountInkApp } = await import("./mount-XMN3S4W6.mjs");
|
|
616
709
|
const { fallback } = await mountInkApp({ initialView: "repl" });
|
|
617
710
|
if (!fallback) return;
|
|
618
711
|
printBanner();
|
|
@@ -623,10 +716,10 @@ async function main() {
|
|
|
623
716
|
return;
|
|
624
717
|
}
|
|
625
718
|
if (argv[2] === "sprint" && argv[3] === "start") {
|
|
626
|
-
const { parseSprintStartArgs } = await import("./start-
|
|
719
|
+
const { parseSprintStartArgs } = await import("./start-D35SOXMM.mjs");
|
|
627
720
|
const parsed = parseSprintStartArgs(argv.slice(4));
|
|
628
721
|
if (parsed.ok) {
|
|
629
|
-
const { mountInkApp } = await import("./mount-
|
|
722
|
+
const { mountInkApp } = await import("./mount-XMN3S4W6.mjs");
|
|
630
723
|
const { getSharedDeps } = await import("./bootstrap-FMHG6DRY.mjs");
|
|
631
724
|
let sprintId;
|
|
632
725
|
try {
|
|
@@ -644,7 +737,7 @@ async function main() {
|
|
|
644
737
|
}
|
|
645
738
|
}
|
|
646
739
|
}
|
|
647
|
-
printBanner();
|
|
740
|
+
if (!isQuietCommand(argv)) printBanner();
|
|
648
741
|
await program.parseAsync(argv);
|
|
649
742
|
}
|
|
650
743
|
main().catch((err) => {
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
taskReorderCommand,
|
|
55
55
|
taskShowCommand,
|
|
56
56
|
taskStatusCommand,
|
|
57
|
+
taskWhyCommand,
|
|
57
58
|
ticketEditCommand,
|
|
58
59
|
ticketListCommand,
|
|
59
60
|
ticketRefineCommand,
|
|
@@ -61,7 +62,7 @@ import {
|
|
|
61
62
|
ticketShowCommand,
|
|
62
63
|
useCurrentPrompt,
|
|
63
64
|
validateConfigValue
|
|
64
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-MRN3Z2XC.mjs";
|
|
65
66
|
import {
|
|
66
67
|
PromptCancelledError,
|
|
67
68
|
projectAddCommand
|
|
@@ -108,7 +109,7 @@ import {
|
|
|
108
109
|
sprintStartCommand,
|
|
109
110
|
updateTaskStatus,
|
|
110
111
|
withSuspendedTui
|
|
111
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-JYCGQA2D.mjs";
|
|
112
113
|
import {
|
|
113
114
|
addTicket,
|
|
114
115
|
allRequirementsApproved,
|
|
@@ -554,6 +555,7 @@ function buildTaskSubMenu(ctx) {
|
|
|
554
555
|
items.push(titled("VIEW"));
|
|
555
556
|
items.push({ name: "List", value: "list", description: "List all tasks" });
|
|
556
557
|
items.push({ name: "Next", value: "next", description: "Get next task" });
|
|
558
|
+
items.push({ name: "Why Blocked?", value: "why", description: "Explain why a task is blocked" });
|
|
557
559
|
items.push(blank());
|
|
558
560
|
items.push(titled("MANAGE"));
|
|
559
561
|
items.push({ name: "Add", value: "add", description: "Add a new task" });
|
|
@@ -1035,9 +1037,27 @@ var commandMap = {
|
|
|
1035
1037
|
show: () => sprintShowCommand([]),
|
|
1036
1038
|
context: () => sprintContextCommand([]),
|
|
1037
1039
|
current: () => sprintCurrentCommand(["-"]),
|
|
1038
|
-
refine: () =>
|
|
1040
|
+
refine: async () => {
|
|
1041
|
+
const mode = await getPrompt().select({
|
|
1042
|
+
message: "How should refinement run?",
|
|
1043
|
+
choices: [
|
|
1044
|
+
{ label: "Interactive \u2014 approve requirements for each ticket", value: "interactive" },
|
|
1045
|
+
{ label: "Auto \u2014 AI drafts requirements without prompts", value: "auto" }
|
|
1046
|
+
]
|
|
1047
|
+
});
|
|
1048
|
+
await sprintRefineCommand(mode === "auto" ? ["--auto"] : []);
|
|
1049
|
+
},
|
|
1039
1050
|
ideate: () => sprintIdeateCommand([]),
|
|
1040
|
-
plan: () =>
|
|
1051
|
+
plan: async () => {
|
|
1052
|
+
const mode = await getPrompt().select({
|
|
1053
|
+
message: "How should planning run?",
|
|
1054
|
+
choices: [
|
|
1055
|
+
{ label: "Interactive \u2014 pick affected repos manually", value: "interactive" },
|
|
1056
|
+
{ label: "Auto \u2014 AI explores all repos autonomously", value: "auto" }
|
|
1057
|
+
]
|
|
1058
|
+
});
|
|
1059
|
+
await sprintPlanCommand(mode === "auto" ? ["--auto", "--all-paths"] : []);
|
|
1060
|
+
},
|
|
1041
1061
|
start: () => sprintStartCommand([]),
|
|
1042
1062
|
requirements: () => sprintRequirementsCommand([]),
|
|
1043
1063
|
health: () => sprintHealthCommand(),
|
|
@@ -1062,6 +1082,7 @@ var commandMap = {
|
|
|
1062
1082
|
show: () => taskShowCommand([]),
|
|
1063
1083
|
status: () => taskStatusCommand([]),
|
|
1064
1084
|
next: () => taskNextCommand(),
|
|
1085
|
+
why: () => taskWhyCommand(),
|
|
1065
1086
|
reorder: () => taskReorderCommand([]),
|
|
1066
1087
|
remove: () => taskRemoveCommand([])
|
|
1067
1088
|
},
|
|
@@ -1977,6 +1998,7 @@ function initialState() {
|
|
|
1977
1998
|
running: /* @__PURE__ */ new Set(),
|
|
1978
1999
|
blocked: /* @__PURE__ */ new Set(),
|
|
1979
2000
|
activity: /* @__PURE__ */ new Map(),
|
|
2001
|
+
currentStep: /* @__PURE__ */ new Map(),
|
|
1980
2002
|
summary: null,
|
|
1981
2003
|
error: null,
|
|
1982
2004
|
rateLimit: null
|
|
@@ -2038,7 +2060,16 @@ function ExecuteView({ sprintId, executionOptions }) {
|
|
|
2038
2060
|
const fresh = signalEvents.slice(processedCountRef.current);
|
|
2039
2061
|
processedCountRef.current = signalEvents.length;
|
|
2040
2062
|
setState((prev) => reduceEvents(prev, fresh));
|
|
2041
|
-
|
|
2063
|
+
if (fresh.some((e) => e.type === "task-finished")) {
|
|
2064
|
+
void (async () => {
|
|
2065
|
+
try {
|
|
2066
|
+
const tasks = await shared.persistence.getTasks(sprintId);
|
|
2067
|
+
setState((s) => ({ ...s, tasks }));
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
})();
|
|
2071
|
+
}
|
|
2072
|
+
}, [signalEvents, shared, sprintId]);
|
|
2042
2073
|
const [closePromptRun, setClosePromptRun] = useState8(false);
|
|
2043
2074
|
useEffect8(() => {
|
|
2044
2075
|
if (!done) return;
|
|
@@ -2092,6 +2123,11 @@ function ExecuteView({ sprintId, executionOptions }) {
|
|
|
2092
2123
|
activityByTask: state.activity
|
|
2093
2124
|
}
|
|
2094
2125
|
) }),
|
|
2126
|
+
!done && state.currentStep.size > 0 ? /* @__PURE__ */ jsx18(Box17, { marginTop: spacing.section, flexDirection: "column", children: Array.from(state.currentStep.entries()).map(([taskId, label]) => {
|
|
2127
|
+
const task = state.tasks.find((t) => t.id === taskId);
|
|
2128
|
+
const taskName = task?.name ?? taskId.slice(0, 8);
|
|
2129
|
+
return /* @__PURE__ */ jsx18(Spinner, { label: `${taskName} ${glyphs.emDash} ${label}` }, taskId);
|
|
2130
|
+
}) }) : null,
|
|
2095
2131
|
/* @__PURE__ */ jsx18(Box17, { marginTop: spacing.section, children: /* @__PURE__ */ jsx18(LogTail, { events: logEvents }) }),
|
|
2096
2132
|
state.error ? /* @__PURE__ */ jsx18(Box17, { marginTop: spacing.section, children: /* @__PURE__ */ jsxs16(Text16, { color: inkColors.error, children: [
|
|
2097
2133
|
glyphs.cross,
|
|
@@ -2123,10 +2159,24 @@ function ExecuteView({ sprintId, executionOptions }) {
|
|
|
2123
2159
|
] }) : null
|
|
2124
2160
|
] });
|
|
2125
2161
|
}
|
|
2162
|
+
var STEP_LABELS = {
|
|
2163
|
+
"branch-preflight": "Verifying branch\u2026",
|
|
2164
|
+
"contract-negotiate": "Writing contract\u2026",
|
|
2165
|
+
"mark-in-progress": "Starting\u2026",
|
|
2166
|
+
"execute-task": "Running Claude\u2026",
|
|
2167
|
+
"store-verification": "Storing verification\u2026",
|
|
2168
|
+
"post-task-check": "Running post-task check\u2026",
|
|
2169
|
+
"evaluate-task": "Evaluating\u2026",
|
|
2170
|
+
"mark-done": "Finalizing\u2026"
|
|
2171
|
+
};
|
|
2172
|
+
function labelForStep(stepName) {
|
|
2173
|
+
return STEP_LABELS[stepName] ?? stepName;
|
|
2174
|
+
}
|
|
2126
2175
|
function reduceEvents(state, events) {
|
|
2127
2176
|
const running = new Set(state.running);
|
|
2128
2177
|
const blocked = new Set(state.blocked);
|
|
2129
2178
|
const activity = new Map(state.activity);
|
|
2179
|
+
const currentStep = new Map(state.currentStep);
|
|
2130
2180
|
let rateLimit = state.rateLimit;
|
|
2131
2181
|
for (const event of events) {
|
|
2132
2182
|
switch (event.type) {
|
|
@@ -2135,10 +2185,22 @@ function reduceEvents(state, events) {
|
|
|
2135
2185
|
break;
|
|
2136
2186
|
case "task-finished":
|
|
2137
2187
|
running.delete(event.taskId);
|
|
2188
|
+
activity.delete(event.taskId);
|
|
2189
|
+
currentStep.delete(event.taskId);
|
|
2138
2190
|
if (event.status === "blocked" || event.status === "failed") {
|
|
2139
2191
|
blocked.add(event.taskId);
|
|
2140
2192
|
}
|
|
2141
2193
|
break;
|
|
2194
|
+
case "task-step":
|
|
2195
|
+
if (event.phase === "start") {
|
|
2196
|
+
activity.set(event.taskId, labelForStep(event.stepName));
|
|
2197
|
+
currentStep.set(event.taskId, labelForStep(event.stepName));
|
|
2198
|
+
} else {
|
|
2199
|
+
if (currentStep.get(event.taskId) === labelForStep(event.stepName)) {
|
|
2200
|
+
currentStep.delete(event.taskId);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
break;
|
|
2142
2204
|
case "rate-limit-paused":
|
|
2143
2205
|
rateLimit = { pausedSince: event.timestamp, delayMs: event.delayMs };
|
|
2144
2206
|
break;
|
|
@@ -2161,7 +2223,7 @@ function reduceEvents(state, events) {
|
|
|
2161
2223
|
}
|
|
2162
2224
|
}
|
|
2163
2225
|
}
|
|
2164
|
-
return { ...state, running, blocked, activity, rateLimit };
|
|
2226
|
+
return { ...state, running, blocked, activity, currentStep, rateLimit };
|
|
2165
2227
|
}
|
|
2166
2228
|
|
|
2167
2229
|
// src/integration/ui/tui/views/dashboard-view.tsx
|
|
@@ -2436,6 +2498,15 @@ function RefinePhaseView({ sprintId }) {
|
|
|
2436
2498
|
useEffect10(() => {
|
|
2437
2499
|
void loadSprint();
|
|
2438
2500
|
}, [loadSprint]);
|
|
2501
|
+
useEffect10(() => {
|
|
2502
|
+
if (!state.running) return;
|
|
2503
|
+
const handle = setInterval(() => {
|
|
2504
|
+
void loadSprint();
|
|
2505
|
+
}, 1e3);
|
|
2506
|
+
return () => {
|
|
2507
|
+
clearInterval(handle);
|
|
2508
|
+
};
|
|
2509
|
+
}, [state.running, loadSprint]);
|
|
2439
2510
|
const runRefine = useCallback5(async () => {
|
|
2440
2511
|
setState((s) => ({ ...s, running: true, error: null, records: [] }));
|
|
2441
2512
|
try {
|
|
@@ -3470,7 +3541,8 @@ var STEP_LABEL = {
|
|
|
3470
3541
|
fetching: "Fetching issue data\u2026",
|
|
3471
3542
|
title: "Awaiting ticket title\u2026",
|
|
3472
3543
|
description: "Awaiting ticket description\u2026",
|
|
3473
|
-
saving: "Saving ticket\u2026"
|
|
3544
|
+
saving: "Saving ticket\u2026",
|
|
3545
|
+
another: "Add another ticket?"
|
|
3474
3546
|
};
|
|
3475
3547
|
function isValidUrl(value) {
|
|
3476
3548
|
try {
|
|
@@ -3505,40 +3577,52 @@ function TicketAddView() {
|
|
|
3505
3577
|
setPhase({ kind: "no-project" });
|
|
3506
3578
|
return;
|
|
3507
3579
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3580
|
+
let count = 0;
|
|
3581
|
+
while (true) {
|
|
3582
|
+
setPhase({ kind: "running", step: "link" });
|
|
3583
|
+
const link = await prompt.input({
|
|
3584
|
+
message: "Issue link (optional):",
|
|
3585
|
+
validate: (v) => {
|
|
3586
|
+
const trimmed = v.trim();
|
|
3587
|
+
if (trimmed.length === 0) return true;
|
|
3588
|
+
return isValidUrl(trimmed) ? true : "Must be a valid URL (or leave blank)";
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
const trimmedLink = link.trim();
|
|
3592
|
+
let prefill = null;
|
|
3593
|
+
if (trimmedLink.length > 0) {
|
|
3594
|
+
setPhase({ kind: "running", step: "fetching" });
|
|
3595
|
+
prefill = tryFetchIssue(trimmedLink);
|
|
3596
|
+
}
|
|
3597
|
+
setPhase({ kind: "running", step: "title" });
|
|
3598
|
+
const title = await prompt.input({
|
|
3599
|
+
message: "Title:",
|
|
3600
|
+
default: prefill?.title,
|
|
3601
|
+
validate: (v) => v.trim().length > 0 ? true : "Title is required"
|
|
3602
|
+
});
|
|
3603
|
+
setPhase({ kind: "running", step: "description" });
|
|
3604
|
+
const description = await prompt.editor({
|
|
3605
|
+
message: "Description (recommended)",
|
|
3606
|
+
default: prefill?.body
|
|
3607
|
+
});
|
|
3608
|
+
setPhase({ kind: "running", step: "saving" });
|
|
3609
|
+
const trimmedDescription = description?.trim() ?? "";
|
|
3610
|
+
const ticket = await addTicket({
|
|
3611
|
+
title: title.trim(),
|
|
3612
|
+
description: trimmedDescription.length > 0 ? trimmedDescription : void 0,
|
|
3613
|
+
link: trimmedLink.length > 0 ? trimmedLink : void 0
|
|
3614
|
+
});
|
|
3615
|
+
count++;
|
|
3616
|
+
setPhase({ kind: "running", step: "another" });
|
|
3617
|
+
const another = await prompt.confirm({
|
|
3618
|
+
message: `Add another ticket? (${String(count)} added)`,
|
|
3619
|
+
default: true
|
|
3620
|
+
});
|
|
3621
|
+
if (!another) {
|
|
3622
|
+
setPhase({ kind: "done", ticket, project, prefilled: prefill !== null, count });
|
|
3623
|
+
return;
|
|
3515
3624
|
}
|
|
3516
|
-
});
|
|
3517
|
-
const trimmedLink = link.trim();
|
|
3518
|
-
let prefill = null;
|
|
3519
|
-
if (trimmedLink.length > 0) {
|
|
3520
|
-
setPhase({ kind: "running", step: "fetching" });
|
|
3521
|
-
prefill = tryFetchIssue(trimmedLink);
|
|
3522
3625
|
}
|
|
3523
|
-
setPhase({ kind: "running", step: "title" });
|
|
3524
|
-
const title = await prompt.input({
|
|
3525
|
-
message: "Title:",
|
|
3526
|
-
default: prefill?.title,
|
|
3527
|
-
validate: (v) => v.trim().length > 0 ? true : "Title is required"
|
|
3528
|
-
});
|
|
3529
|
-
setPhase({ kind: "running", step: "description" });
|
|
3530
|
-
const description = await prompt.editor({
|
|
3531
|
-
message: "Description (recommended)",
|
|
3532
|
-
default: prefill?.body
|
|
3533
|
-
});
|
|
3534
|
-
setPhase({ kind: "running", step: "saving" });
|
|
3535
|
-
const trimmedDescription = description?.trim() ?? "";
|
|
3536
|
-
const ticket = await addTicket({
|
|
3537
|
-
title: title.trim(),
|
|
3538
|
-
description: trimmedDescription.length > 0 ? trimmedDescription : void 0,
|
|
3539
|
-
link: trimmedLink.length > 0 ? trimmedLink : void 0
|
|
3540
|
-
});
|
|
3541
|
-
setPhase({ kind: "done", ticket, project, prefilled: prefill !== null });
|
|
3542
3626
|
}
|
|
3543
3627
|
});
|
|
3544
3628
|
const hints = useMemo12(() => phase.kind === "running" ? HINTS_RUNNING6 : HINTS_DONE6, [phase.kind]);
|
|
@@ -3570,21 +3654,23 @@ function renderBody6(phase) {
|
|
|
3570
3654
|
);
|
|
3571
3655
|
case "error":
|
|
3572
3656
|
return /* @__PURE__ */ jsx31(ResultCard, { kind: "error", title: "Could not add ticket", lines: [phase.message] });
|
|
3573
|
-
case "done":
|
|
3657
|
+
case "done": {
|
|
3658
|
+
const title = phase.count > 1 ? `${String(phase.count)} tickets added` : phase.prefilled ? "Ticket added (prefilled from issue)" : "Ticket added";
|
|
3574
3659
|
return /* @__PURE__ */ jsx31(
|
|
3575
3660
|
ResultCard,
|
|
3576
3661
|
{
|
|
3577
3662
|
kind: "success",
|
|
3578
|
-
title
|
|
3663
|
+
title,
|
|
3579
3664
|
fields: [
|
|
3580
|
-
["ID", phase.ticket.id],
|
|
3581
|
-
["
|
|
3665
|
+
["Last ID", phase.ticket.id],
|
|
3666
|
+
["Last title", phase.ticket.title],
|
|
3582
3667
|
["Project", `${phase.project.displayName} (${phase.project.name})`],
|
|
3583
3668
|
["Status", `requirement: ${phase.ticket.requirementStatus}`]
|
|
3584
3669
|
],
|
|
3585
3670
|
nextSteps: [{ action: "Refine requirements", description: "Home \u2192 Next: Refine Requirements" }]
|
|
3586
3671
|
}
|
|
3587
3672
|
);
|
|
3673
|
+
}
|
|
3588
3674
|
}
|
|
3589
3675
|
}
|
|
3590
3676
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralphctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent harness for long-running AI coding tasks — orchestrates Claude Code & GitHub Copilot across repositories",
|
|
5
5
|
"homepage": "https://github.com/lukas-grigis/ralphctl",
|
|
6
6
|
"type": "module",
|