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.
- package/dist/{add-JGUOR4Z5.mjs → add-CIM72NE3.mjs} +2 -2
- package/dist/{chunk-JXMHLW42.mjs → chunk-7JLZQICD.mjs} +4 -3
- package/dist/{chunk-HL4ZMHCQ.mjs → chunk-JOQO4HMM.mjs} +8 -0
- package/dist/{chunk-CDOPLXFK.mjs → chunk-JYCGQA2D.mjs} +156 -61
- package/dist/{chunk-4GHVNKLV.mjs → chunk-MRN3Z2XC.mjs} +227 -60
- package/dist/cli.mjs +105 -10
- package/dist/{mount-XZPBDRPZ.mjs → mount-XMN3S4W6.mjs} +315 -176
- package/dist/prompts/plan-common.md +23 -4
- package/dist/prompts/task-evaluation-resume.md +6 -1
- package/dist/prompts/validation-checklist.md +1 -1
- package/dist/{start-MMWC7QLI.mjs → start-D35SOXMM.mjs} +2 -2
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
addSingleTicketInteractive,
|
|
4
4
|
ticketAddCommand
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7JLZQICD.mjs";
|
|
6
6
|
import "./chunk-NUYQK5MN.mjs";
|
|
7
|
-
import "./chunk-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1883
|
-
|
|
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(
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
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(
|
|
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
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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().
|
|
4774
|
-
|
|
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
|
-
|
|
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) {
|