ralphctl 0.3.0 → 0.3.1

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.
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  addSingleTicketInteractive,
4
4
  ticketAddCommand
5
- } from "./chunk-JXMHLW42.mjs";
5
+ } from "./chunk-7JLZQICD.mjs";
6
6
  import "./chunk-NUYQK5MN.mjs";
7
- import "./chunk-HL4ZMHCQ.mjs";
7
+ import "./chunk-JOQO4HMM.mjs";
8
8
  import "./chunk-CFUVE2BP.mjs";
9
9
  import "./chunk-747KW2RW.mjs";
10
10
  import "./chunk-YCDUVPRT.mjs";
@@ -4,8 +4,9 @@ import {
4
4
  } from "./chunk-NUYQK5MN.mjs";
5
5
  import {
6
6
  addTicket,
7
- fetchIssueFromUrl
8
- } from "./chunk-HL4ZMHCQ.mjs";
7
+ fetchIssueFromUrl,
8
+ truncate
9
+ } from "./chunk-JOQO4HMM.mjs";
9
10
  import {
10
11
  EXIT_ERROR,
11
12
  exitWithCode
@@ -82,7 +83,7 @@ function tryFetchIssue(url) {
82
83
  }
83
84
  spinner.succeed("Issue data fetched");
84
85
  log.newline();
85
- const bodyPreview = data.body.length > 200 ? data.body.slice(0, 200) + "..." : data.body;
86
+ const bodyPreview = truncate(data.body, 200);
86
87
  const cardLines = [`Title: ${data.title}`, "", bodyPreview];
87
88
  if (data.comments.length > 0) {
88
89
  cardLines.push("", `${String(data.comments.length)} comment(s)`);
@@ -2,8 +2,9 @@
2
2
  import {
3
3
  fetchIssueFromUrl,
4
4
  formatIssueContext,
5
- formatTicketDisplay
6
- } from "./chunk-HL4ZMHCQ.mjs";
5
+ formatTicketDisplay,
6
+ truncate
7
+ } from "./chunk-JOQO4HMM.mjs";
7
8
  import {
8
9
  EXIT_ERROR,
9
10
  EXIT_INTERRUPTED,
@@ -691,10 +692,11 @@ var RefineTicketRequirementsUseCase = class {
691
692
  return "skipped";
692
693
  }
693
694
  const combined = this.combineRequirements(matching);
694
- this.logger.info(`Refined requirements:
695
- ${combined.requirements}`);
696
- const approve = await this.ui.confirm("Approve these requirements?", true);
695
+ const approve = await this.ui.confirm("Approve these requirements?", true, combined.requirements);
697
696
  if (!approve) {
697
+ this.logger.warning(
698
+ `Requirements rejected for ticket [${ticket.id}]. Re-run \`sprint refine\` to start fresh (resume is not yet supported).`
699
+ );
698
700
  return "skipped";
699
701
  }
700
702
  const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
@@ -1856,32 +1858,56 @@ ${instructions}`;
1856
1858
  * `feedback-loop` step after execution completes with all tasks done and
1857
1859
  * the user hasn't opted out via `--no-feedback` / `--session`.
1858
1860
  *
1861
+ * Each iteration reads multi-line markdown feedback via the editor prompt
1862
+ * and, for each affected repo, runs a *synthetic task* — a `Task` built
1863
+ * in-memory (never persisted to `tasks.json`) that reuses the same
1864
+ * building blocks as `executeOneTask`:
1865
+ *
1866
+ * - `task-started` / `task-finished` emissions to the signal bus keep
1867
+ * the live dashboard ticking.
1868
+ * - `dispatchSignals` routes parsed progress / note / blocked signals
1869
+ * to the durable signal handler so entries land in `progress.md`.
1870
+ * - `runPostTaskCheck` gates each iteration through the same check
1871
+ * script used for real tasks.
1872
+ *
1859
1873
  * The hard cap `MAX_FEEDBACK_ITERATIONS` lives inside this method so the
1860
1874
  * calling step stays a thin adapter.
1861
1875
  */
1862
1876
  async runFeedbackLoopOnly(sprint, options) {
1863
1877
  const MAX_FEEDBACK_ITERATIONS = 10;
1864
- for (let iteration = 0; iteration < MAX_FEEDBACK_ITERATIONS; iteration++) {
1878
+ const tasks = await this.persistence.getTasks(sprint.id);
1879
+ const repoIds = [...new Set(tasks.map((t) => t.repoId))];
1880
+ const repoPathByRepoId = /* @__PURE__ */ new Map();
1881
+ for (const repoId of repoIds) {
1882
+ try {
1883
+ repoPathByRepoId.set(repoId, await this.persistence.resolveRepoPath(repoId));
1884
+ } catch {
1885
+ }
1886
+ }
1887
+ const completedSummary = tasks.filter((t) => t.status === "done").map((t) => `- ${t.name} (${repoPathByRepoId.get(t.repoId) ?? t.repoId})`).join("\n");
1888
+ let iteration = 0;
1889
+ for (; iteration < MAX_FEEDBACK_ITERATIONS; iteration++) {
1865
1890
  const feedback = await this.ui.getFeedback("All tasks complete. Enter feedback for changes (empty to approve):");
1866
1891
  if (!feedback) return;
1867
1892
  await this.persistence.logProgress(`User feedback: ${feedback}`, { sprintId: sprint.id });
1868
- const tasks = await this.persistence.getTasks(sprint.id);
1869
- const repoIds = [...new Set(tasks.map((t) => t.repoId))];
1870
- const repoPathByRepoId = /* @__PURE__ */ new Map();
1871
- for (const repoId of repoIds) {
1872
- try {
1873
- repoPathByRepoId.set(repoId, await this.persistence.resolveRepoPath(repoId));
1874
- } catch {
1875
- }
1876
- }
1877
- const completedSummary = tasks.filter((t) => t.status === "done").map((t) => `- ${t.name} (${repoPathByRepoId.get(t.repoId) ?? t.repoId})`).join("\n");
1878
1893
  for (const repoId of repoIds) {
1879
1894
  const repoPath = repoPathByRepoId.get(repoId);
1880
1895
  if (!repoPath) continue;
1896
+ const syntheticTask = this.makeFeedbackTask(feedback, repoId);
1897
+ this.signalBus.emit({
1898
+ type: "task-started",
1899
+ sprintId: sprint.id,
1900
+ taskId: syntheticTask.id,
1901
+ taskName: syntheticTask.name,
1902
+ timestamp: /* @__PURE__ */ new Date()
1903
+ });
1904
+ let finishStatus = "done";
1881
1905
  const prompt = this.promptBuilder.buildFeedbackPrompt(sprint.name, completedSummary, feedback, sprint.branch);
1882
- this.logger.info(`Implementing feedback in ${repoPath}...`);
1883
- const spinner = this.logger.spinner("AI is implementing feedback...");
1906
+ const spinner = this.logger.spinner(
1907
+ `${this.aiSession.getProviderDisplayName()} is working on: ${syntheticTask.name}`
1908
+ );
1884
1909
  try {
1910
+ await this.aiSession.ensureReady();
1885
1911
  const sprintDir = this.fs.getSprintDir(sprint.id);
1886
1912
  const result = await this.aiSession.spawnWithRetry(prompt, {
1887
1913
  cwd: repoPath,
@@ -1889,32 +1915,64 @@ ${instructions}`;
1889
1915
  env: this.aiSession.getSpawnEnv(),
1890
1916
  maxTurns: options?.maxTurns
1891
1917
  });
1892
- spinner.succeed("Feedback implementation completed");
1893
- const signals = this.parser.parseExecutionSignals(result.output);
1894
- if (signals.blocked) {
1895
- this.logger.warning(`Feedback blocked: ${signals.blocked}`);
1918
+ spinner.succeed(`${this.aiSession.getProviderDisplayName()} completed: ${syntheticTask.name}`);
1919
+ const ctx = { sprintId: sprint.id, taskId: syntheticTask.id, projectPath: repoPath };
1920
+ const signals = await this.dispatchSignals(result.output, ctx);
1921
+ const blocked = signals.find((s) => s.type === "task-blocked");
1922
+ if (blocked) {
1923
+ finishStatus = "blocked";
1924
+ this.logger.warning(`Feedback blocked in ${repoPath}: ${blocked.reason}`);
1896
1925
  }
1897
1926
  } catch (err) {
1898
- spinner.fail("Feedback implementation failed");
1927
+ spinner.fail(`${this.aiSession.getProviderDisplayName()} failed: ${syntheticTask.name}`);
1928
+ finishStatus = "failed";
1899
1929
  this.logger.warning(err instanceof Error ? err.message : String(err));
1900
1930
  }
1901
- }
1902
- for (const repoId of repoIds) {
1903
- const resolved = await findProjectForRepoId(this.persistence, repoId);
1904
- const checkScript = resolveCheckScriptForRepo(resolved?.repo);
1905
- if (resolved && checkScript) {
1906
- const { repo } = resolved;
1907
- this.logger.info(`Running checks after feedback: ${checkScript}`);
1908
- const result = this.external.runCheckScript(repo.path, checkScript, "taskComplete", repo.checkTimeout);
1909
- if (!result.passed) {
1910
- this.logger.warning(`Check failed after feedback in ${repo.path}`);
1911
- } else {
1912
- this.logger.success(`Checks passed: ${repo.path}`);
1931
+ try {
1932
+ const passed = await this.runPostTaskCheck(syntheticTask, sprint);
1933
+ if (!passed) {
1934
+ this.logger.warning(`Post-feedback check failed in ${repoPath}`);
1935
+ if (finishStatus === "done") finishStatus = "failed";
1913
1936
  }
1937
+ } catch (err) {
1938
+ this.logger.warning(
1939
+ `Post-feedback check error in ${repoPath}: ${err instanceof Error ? err.message : String(err)}`
1940
+ );
1941
+ if (finishStatus === "done") finishStatus = "failed";
1914
1942
  }
1943
+ this.signalBus.emit({
1944
+ type: "task-finished",
1945
+ sprintId: sprint.id,
1946
+ taskId: syntheticTask.id,
1947
+ status: finishStatus,
1948
+ timestamp: /* @__PURE__ */ new Date()
1949
+ });
1915
1950
  }
1916
1951
  }
1917
- this.logger.warning(`Reached maximum feedback iterations (${String(MAX_FEEDBACK_ITERATIONS)}). Proceeding.`);
1952
+ if (iteration >= MAX_FEEDBACK_ITERATIONS) {
1953
+ this.logger.warning(`Reached maximum feedback iterations (${String(MAX_FEEDBACK_ITERATIONS)}). Proceeding.`);
1954
+ }
1955
+ }
1956
+ /**
1957
+ * Build an in-memory `Task` representing a single feedback iteration for a
1958
+ * single repo. Never persisted — used only to drive the shared building
1959
+ * blocks (`runPostTaskCheck`, signal dispatch, bus lifecycle) without
1960
+ * polluting `tasks.json`.
1961
+ */
1962
+ makeFeedbackTask(feedback, repoId) {
1963
+ return {
1964
+ id: `feedback-${generateUuid8()}`,
1965
+ name: `Feedback: ${truncate(feedback, 60)}`,
1966
+ description: feedback,
1967
+ steps: [feedback],
1968
+ verificationCriteria: ["Project check script passes"],
1969
+ status: "todo",
1970
+ order: 0,
1971
+ blockedBy: [],
1972
+ repoId,
1973
+ verified: false,
1974
+ evaluated: false
1975
+ };
1918
1976
  }
1919
1977
  // -------------------------------------------------------------------------
1920
1978
  // Helpers (private)
@@ -4733,8 +4791,8 @@ var DefaultOutputParserAdapter = class {
4733
4791
 
4734
4792
  // src/integration/user-interaction-adapter.ts
4735
4793
  var InteractiveUserAdapter = class {
4736
- async confirm(message, defaultValue) {
4737
- return getPrompt().confirm({ message, default: defaultValue });
4794
+ async confirm(message, defaultValue, details) {
4795
+ return getPrompt().confirm({ message, default: defaultValue, details });
4738
4796
  }
4739
4797
  async selectPaths(reposByProject, message, preselected) {
4740
4798
  const choices = [];
@@ -4770,12 +4828,15 @@ var InteractiveUserAdapter = class {
4770
4828
  return name.trim();
4771
4829
  }
4772
4830
  async getFeedback(message) {
4773
- const response = await getPrompt().input({ message });
4774
- return response.trim().length > 0 ? response.trim() : null;
4831
+ const response = await getPrompt().editor({ message, kind: "markdown" });
4832
+ if (response == null) return null;
4833
+ const trimmed = response.trim();
4834
+ return trimmed.length > 0 ? trimmed : null;
4775
4835
  }
4776
4836
  };
4777
4837
  var AutoUserAdapter = class {
4778
- confirm(_message, defaultValue) {
4838
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4839
+ confirm(_message, defaultValue, _details) {
4779
4840
  return Promise.resolve(defaultValue ?? true);
4780
4841
  }
4781
4842
  selectPaths(reposByProject, _message, preselected) {
@@ -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-CSC4TBJB.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.3.1",
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",
@@ -491,32 +492,52 @@ 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";
495
496
  function ConfirmPrompt({ options, onSubmit }) {
496
497
  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,
498
+ const details = options.details?.trim();
499
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
500
+ details ? /* @__PURE__ */ jsx(
501
+ Box,
510
502
  {
511
- defaultChoice: options.default === false ? "cancel" : "confirm",
512
- onConfirm: () => {
513
- onSubmit(true);
514
- },
515
- onCancel: () => {
516
- onSubmit(false);
517
- }
503
+ flexDirection: "column",
504
+ borderStyle: "round",
505
+ borderColor: inkColors.muted,
506
+ paddingX: spacing.gutter,
507
+ marginBottom: spacing.section,
508
+ children: details.split("\n").map((line, idx) => /* @__PURE__ */ jsx(Text, { children: line.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
509
+ /* @__PURE__ */ jsxs(Text, { color: inkColors.muted, children: [
510
+ glyphs.quoteRail,
511
+ " "
512
+ ] }),
513
+ line
514
+ ] }) : " " }, idx))
518
515
  }
519
- )
516
+ ) : null,
517
+ /* @__PURE__ */ jsxs(Box, { children: [
518
+ /* @__PURE__ */ jsxs(Text, { children: [
519
+ emoji.donut,
520
+ " ",
521
+ options.message,
522
+ " "
523
+ ] }),
524
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
525
+ hint,
526
+ " "
527
+ ] }),
528
+ /* @__PURE__ */ jsx(
529
+ ConfirmInput,
530
+ {
531
+ defaultChoice: options.default === false ? "cancel" : "confirm",
532
+ onConfirm: () => {
533
+ onSubmit(true);
534
+ },
535
+ onCancel: () => {
536
+ onSubmit(false);
537
+ }
538
+ }
539
+ )
540
+ ] })
520
541
  ] });
521
542
  }
522
543
 
@@ -1897,7 +1918,7 @@ async function selectTicket(message = "Select ticket:", filter) {
1897
1918
  default: true
1898
1919
  });
1899
1920
  if (create) {
1900
- const { ticketAddCommand } = await import("./add-JGUOR4Z5.mjs");
1921
+ const { ticketAddCommand } = await import("./add-CIM72NE3.mjs");
1901
1922
  await ticketAddCommand({ interactive: true });
1902
1923
  const updated = await listTickets();
1903
1924
  const refiltered = filter ? updated.filter(filter) : updated;
@@ -4092,7 +4113,7 @@ async function ticketListCommand(args) {
4092
4113
  log.raw(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
4093
4114
  if (ticket.description) {
4094
4115
  const preview = ticket.description.split("\n")[0] ?? "";
4095
- const truncated = preview.length > 60 ? preview.slice(0, 57) + "..." : preview;
4116
+ const truncated = truncate(preview, 60);
4096
4117
  log.raw(` ${muted(truncated)}`, 1);
4097
4118
  }
4098
4119
  }
@@ -99,6 +99,13 @@ function formatTicketDisplay(ticket) {
99
99
  return `[${ticket.id}] ${ticket.title}`;
100
100
  }
101
101
 
102
+ // src/domain/strings.ts
103
+ function truncate(str, max) {
104
+ if (str.length <= max) return str;
105
+ if (max <= 1) return "\u2026".slice(0, Math.max(0, max));
106
+ return str.slice(0, max - 1) + "\u2026";
107
+ }
108
+
102
109
  // src/integration/external/issue-fetch.ts
103
110
  import { spawnSync } from "child_process";
104
111
  import { Result } from "typescript-result";
@@ -256,6 +263,7 @@ export {
256
263
  allRequirementsApproved,
257
264
  getPendingRequirements,
258
265
  formatTicketDisplay,
266
+ truncate,
259
267
  fetchIssueFromUrl,
260
268
  formatIssueContext
261
269
  };
package/dist/cli.mjs CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  ticketRefineCommand,
39
39
  ticketRemoveCommand,
40
40
  ticketShowCommand
41
- } from "./chunk-4GHVNKLV.mjs";
41
+ } from "./chunk-EPDR6VO5.mjs";
42
42
  import {
43
43
  projectAddCommand
44
44
  } from "./chunk-D2YGPLIV.mjs";
@@ -47,13 +47,15 @@ import {
47
47
  } from "./chunk-3QBEBKMZ.mjs";
48
48
  import {
49
49
  ticketAddCommand
50
- } from "./chunk-JXMHLW42.mjs";
50
+ } from "./chunk-7JLZQICD.mjs";
51
51
  import "./chunk-NUYQK5MN.mjs";
52
52
  import {
53
53
  getTasks,
54
54
  sprintStartCommand
55
- } from "./chunk-CDOPLXFK.mjs";
56
- import "./chunk-HL4ZMHCQ.mjs";
55
+ } from "./chunk-CSC4TBJB.mjs";
56
+ import {
57
+ truncate
58
+ } from "./chunk-JOQO4HMM.mjs";
57
59
  import {
58
60
  EXIT_ERROR
59
61
  } from "./chunk-CFUVE2BP.mjs";
@@ -195,7 +197,7 @@ async function sprintInsightsCommand(args) {
195
197
  console.log(` ${colors.accent("Evaluation output:")}`);
196
198
  for (const task of withOutput) {
197
199
  const output = task.evaluationOutput ?? "";
198
- const truncated = output.length > 200 ? output.slice(0, 200) + "..." : output;
200
+ const truncated = truncate(output, 200);
199
201
  console.log(` ${icons.bullet} ${colors.accent(task.name)}: ${colors.muted(truncated)}`);
200
202
  }
201
203
  log.newline();
@@ -610,7 +612,7 @@ async function main() {
610
612
  const isBare = argv.length <= 2;
611
613
  const isInteractive = argv[2] === "interactive";
612
614
  if (isBare || isInteractive) {
613
- const { mountInkApp } = await import("./mount-XZPBDRPZ.mjs");
615
+ const { mountInkApp } = await import("./mount-U7QXVB5Q.mjs");
614
616
  const { fallback } = await mountInkApp({ initialView: "repl" });
615
617
  if (!fallback) return;
616
618
  printBanner();
@@ -621,10 +623,10 @@ async function main() {
621
623
  return;
622
624
  }
623
625
  if (argv[2] === "sprint" && argv[3] === "start") {
624
- const { parseSprintStartArgs } = await import("./start-MMWC7QLI.mjs");
626
+ const { parseSprintStartArgs } = await import("./start-WG7VMEB2.mjs");
625
627
  const parsed = parseSprintStartArgs(argv.slice(4));
626
628
  if (parsed.ok) {
627
- const { mountInkApp } = await import("./mount-XZPBDRPZ.mjs");
629
+ const { mountInkApp } = await import("./mount-U7QXVB5Q.mjs");
628
630
  const { getSharedDeps } = await import("./bootstrap-FMHG6DRY.mjs");
629
631
  let sprintId;
630
632
  try {