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.
@@ -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)`);
@@ -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
  };
@@ -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)
@@ -2743,28 +2801,63 @@ function markDone(deps) {
2743
2801
 
2744
2802
  // src/business/pipelines/execute/per-task-pipeline.ts
2745
2803
  function createPerTaskPipeline(deps, useCase, options = {}) {
2804
+ const trace = withStepTrace(deps.signalBus);
2746
2805
  return pipeline("per-task", [
2747
- branchPreflight({ external: deps.external, persistence: deps.persistence }),
2748
- contractNegotiate({ persistence: deps.persistence, fs: deps.fs }),
2749
- markInProgress({ persistence: deps.persistence, signalBus: deps.signalBus }),
2750
- executeTask({ useCase, options, taskSessionIds: deps.taskSessionIds, logger: deps.logger }),
2751
- storeVerification({ persistence: deps.persistence, logger: deps.logger }),
2752
- postTaskCheck({ useCase }),
2753
- evaluateTask({
2754
- persistence: deps.persistence,
2755
- fs: deps.fs,
2756
- aiSession: deps.aiSession,
2757
- promptBuilder: deps.promptBuilder,
2758
- parser: deps.parser,
2759
- ui: deps.ui,
2760
- logger: deps.logger,
2761
- external: deps.external,
2762
- useCase,
2763
- options
2764
- }),
2765
- markDone({ persistence: deps.persistence, logger: deps.logger, signalBus: deps.signalBus })
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 }))
2766
2827
  ]);
2767
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
+ }
2768
2861
 
2769
2862
  // src/business/pipelines/execute.ts
2770
2863
  var EXIT_SUCCESS = 0;
@@ -3202,7 +3295,6 @@ function executeTasksStep(deps, options) {
3202
3295
  onSettle: (task, result) => {
3203
3296
  if (result === "success") {
3204
3297
  taskSessionIds.delete(task.id);
3205
- deps.logger.success(`Completed: ${task.name}`);
3206
3298
  }
3207
3299
  }
3208
3300
  },
@@ -4733,8 +4825,8 @@ var DefaultOutputParserAdapter = class {
4733
4825
 
4734
4826
  // src/integration/user-interaction-adapter.ts
4735
4827
  var InteractiveUserAdapter = class {
4736
- async confirm(message, defaultValue) {
4737
- return getPrompt().confirm({ message, default: defaultValue });
4828
+ async confirm(message, defaultValue, details) {
4829
+ return getPrompt().confirm({ message, default: defaultValue, details });
4738
4830
  }
4739
4831
  async selectPaths(reposByProject, message, preselected) {
4740
4832
  const choices = [];
@@ -4770,12 +4862,15 @@ var InteractiveUserAdapter = class {
4770
4862
  return name.trim();
4771
4863
  }
4772
4864
  async getFeedback(message) {
4773
- const response = await getPrompt().input({ message });
4774
- return response.trim().length > 0 ? response.trim() : null;
4865
+ const response = await getPrompt().editor({ message, kind: "markdown" });
4866
+ if (response == null) return null;
4867
+ const trimmed = response.trim();
4868
+ return trimmed.length > 0 ? trimmed : null;
4775
4869
  }
4776
4870
  };
4777
4871
  var AutoUserAdapter = class {
4778
- confirm(_message, defaultValue) {
4872
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4873
+ confirm(_message, defaultValue, _details) {
4779
4874
  return Promise.resolve(defaultValue ?? true);
4780
4875
  }
4781
4876
  selectPaths(reposByProject, _message, preselected) {