@neriros/ralphy 2.16.6 → 2.17.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/README.md +60 -1
- package/dist/cli/index.js +389 -95
- package/dist/mcp/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,65 @@ graph LR
|
|
|
21
21
|
|
|
22
22
|
Each iteration reads the `## Steering` section of `proposal.md`, picks the first unchecked item from `tasks.md`, does the work, validates, and checks the item off. When all items are checked the loop archives the change automatically.
|
|
23
23
|
|
|
24
|
+
## Agent Mode Flow
|
|
25
|
+
|
|
26
|
+
The full orchestration cycle from Linear poll through teardown:
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
flowchart TD
|
|
30
|
+
LINEAR_POLL["Linear poll\n(todo / in-progress / conflicted)"]
|
|
31
|
+
|
|
32
|
+
LINEAR_POLL --> ISSUE_STATE{issue state}
|
|
33
|
+
ISSUE_STATE -- todo --> MODE_FRESH[mode: fresh\nscaffold change]
|
|
34
|
+
ISSUE_STATE -- in-progress --> MODE_RESUME[mode: resume]
|
|
35
|
+
ISSUE_STATE -- conflicted --> MODE_CONFLICT_FIX[mode: conflict-fix\nprepend fix task]
|
|
36
|
+
|
|
37
|
+
MODE_FRESH & MODE_RESUME & MODE_CONFLICT_FIX --> SET_IN_PROGRESS[Linear: setInProgress]
|
|
38
|
+
SET_IN_PROGRESS --> USE_WORKTREE{useWorktree?}
|
|
39
|
+
USE_WORKTREE -- yes --> SCAFFOLD[scaffolding\ncreate worktree + branch]
|
|
40
|
+
USE_WORKTREE -- no --> WORKER
|
|
41
|
+
SCAFFOLD --> WORKER([working\nClaude agent loop])
|
|
42
|
+
|
|
43
|
+
WORKER --> EXIT_CODE{exitCode?}
|
|
44
|
+
EXIT_CODE -- "non-zero" --> GAVE_UP
|
|
45
|
+
EXIT_CODE -- "0" --> WANT_PR{wantPr?}
|
|
46
|
+
|
|
47
|
+
WANT_PR -- no --> DONE
|
|
48
|
+
WANT_PR -- yes --> PUSH_AND_CREATE_PR["push + pr-create\n↺ rebase/hook-fix on rejection"]
|
|
49
|
+
|
|
50
|
+
PUSH_AND_CREATE_PR -- gave-up --> GAVE_UP
|
|
51
|
+
PUSH_AND_CREATE_PR -- no commits ahead --> DONE
|
|
52
|
+
|
|
53
|
+
PUSH_AND_CREATE_PR -- pr opened --> WATCH_LOOP
|
|
54
|
+
|
|
55
|
+
subgraph WATCH_LOOP["watch loop"]
|
|
56
|
+
direction LR
|
|
57
|
+
CONFLICT_CHECK[conflict-check] --> CI_POLL[ci-poll / ci-fix]
|
|
58
|
+
CI_POLL --> CONFLICT_CHECK
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
WATCH_LOOP -- green & clean --> DONE
|
|
62
|
+
WATCH_LOOP -- gave-up --> GAVE_UP
|
|
63
|
+
|
|
64
|
+
DONE([done]) --> WORKTREE_CLEANUP
|
|
65
|
+
GAVE_UP([gave-up]) --> WORKTREE_CLEANUP
|
|
66
|
+
|
|
67
|
+
subgraph WORKTREE_CLEANUP["cleanup"]
|
|
68
|
+
direction LR
|
|
69
|
+
SHOULD_REMOVE_WORKTREE{useWorktree\n& success\n& cleanupOnSuccess?}
|
|
70
|
+
SHOULD_REMOVE_WORKTREE -- yes --> REMOVE_WORKTREE[remove worktree]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
WORKTREE_CLEANUP --> TEARDOWN[teardown]
|
|
74
|
+
TEARDOWN --> OUTCOME{ok?}
|
|
75
|
+
|
|
76
|
+
OUTCOME -- "yes,\nnot conflict-fix" --> LINEAR_SET_DONE["Linear: setDone\nclearInProgress\npost comment"]
|
|
77
|
+
OUTCOME -- "yes,\nconflict-fix" --> LINEAR_CLEAR_CONFLICTED["Linear: clearConflicted\npost comment"]
|
|
78
|
+
OUTCOME -- no --> LINEAR_SET_ERROR["Linear: setError\nclearInProgress\npost comment"]
|
|
79
|
+
|
|
80
|
+
LINEAR_SET_DONE & LINEAR_CLEAR_CONFLICTED & LINEAR_SET_ERROR --> LINEAR_POLL
|
|
81
|
+
```
|
|
82
|
+
|
|
24
83
|
## Installation
|
|
25
84
|
|
|
26
85
|
### npm (global)
|
|
@@ -156,7 +215,7 @@ Marker types are `"label"` or `"status"`. Combine markers under `apply` when one
|
|
|
156
215
|
|
|
157
216
|
With `--worktree` (or `useWorktree: true` in config) each task runs in an isolated worktree at `.ralph/worktrees/<change-name>` checked out onto a fresh `ralph/<change-name>` branch. The change is scaffolded _inside_ the worktree, and the loop's cwd is the worktree, so concurrent workers can't stomp on each other.
|
|
158
217
|
|
|
159
|
-
Use `setupScript` (run inside the worktree right after scaffolding) to install dependencies, copy `.env`, etc. Use `teardownScript` (run after the loop exits
|
|
218
|
+
Use `setupScript` (run inside the worktree right after scaffolding) to install dependencies, copy `.env`, etc. Use `teardownScript` (run after the loop exits and worktree cleanup) to gather artifacts or roll back local mutations. Both run via `sh -c`; failures are logged but never block the loop. With `cleanupWorktreeOnSuccess: true` the worktree is removed when the worker exits 0 — failed workers always keep their worktree (and branch) for human inspection.
|
|
160
219
|
|
|
161
220
|
**`appendPrompt`** (or `--prompt` in agent mode) is appended to every scaffolded `proposal.md` under an `## Additional instructions` section — use it for cross-cutting guidance every task should see.
|
|
162
221
|
|
package/dist/cli/index.js
CHANGED
|
@@ -35029,8 +35029,8 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
35029
35029
|
import { resolve } from "path";
|
|
35030
35030
|
function getVersion() {
|
|
35031
35031
|
try {
|
|
35032
|
-
if ("2.
|
|
35033
|
-
return "2.
|
|
35032
|
+
if ("2.17.0")
|
|
35033
|
+
return "2.17.0";
|
|
35034
35034
|
} catch {}
|
|
35035
35035
|
const dirsToTry = [];
|
|
35036
35036
|
try {
|
|
@@ -35121,6 +35121,7 @@ async function parseArgs(argv) {
|
|
|
35121
35121
|
log: false,
|
|
35122
35122
|
verbose: false,
|
|
35123
35123
|
manualTest: false,
|
|
35124
|
+
fromAgent: false,
|
|
35124
35125
|
linearTeam: "",
|
|
35125
35126
|
linearAssignee: "",
|
|
35126
35127
|
pollInterval: 60,
|
|
@@ -35321,6 +35322,9 @@ async function parseArgs(argv) {
|
|
|
35321
35322
|
case "--manual-test":
|
|
35322
35323
|
result2.manualTest = true;
|
|
35323
35324
|
break;
|
|
35325
|
+
case "--from-agent":
|
|
35326
|
+
result2.fromAgent = true;
|
|
35327
|
+
break;
|
|
35324
35328
|
default:
|
|
35325
35329
|
if (VALID_MODES.has(arg)) {
|
|
35326
35330
|
result2.mode = arg;
|
|
@@ -35336,7 +35340,7 @@ var VERSION, VALID_MODES, VALID_MODELS, INDICATOR_KEYS, GET_KEYS, HELP_TEXT;
|
|
|
35336
35340
|
var init_cli = __esm(() => {
|
|
35337
35341
|
init_output();
|
|
35338
35342
|
VERSION = getVersion();
|
|
35339
|
-
VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean"]);
|
|
35343
|
+
VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean", "debug"]);
|
|
35340
35344
|
VALID_MODELS = new Set(["haiku", "sonnet", "opus"]);
|
|
35341
35345
|
INDICATOR_KEYS = new Set([
|
|
35342
35346
|
"getTodo",
|
|
@@ -35361,6 +35365,7 @@ var init_cli = __esm(() => {
|
|
|
35361
35365
|
" init Initialize OpenSpec in current directory",
|
|
35362
35366
|
" agent Poll Linear for new tasks and run loops concurrently",
|
|
35363
35367
|
" clean Remove worktree, branch, openspec change, and task state for --name",
|
|
35368
|
+
" debug Show agent log timeline, Linear state, and GitHub PR for --name",
|
|
35364
35369
|
"",
|
|
35365
35370
|
"Options:",
|
|
35366
35371
|
" --name <name> Change name (required for most commands)",
|
|
@@ -39426,6 +39431,7 @@ var init_types2 = __esm(() => {
|
|
|
39426
39431
|
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
39427
39432
|
model: exports_external.string().default("opus"),
|
|
39428
39433
|
manualTest: exports_external.boolean().default(false),
|
|
39434
|
+
createPr: exports_external.boolean().default(false),
|
|
39429
39435
|
usage: UsageSchema.default({}),
|
|
39430
39436
|
history: exports_external.array(HistoryEntrySchema).default([]),
|
|
39431
39437
|
metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
|
|
@@ -59944,6 +59950,9 @@ class AgentCoordinator {
|
|
|
59944
59950
|
capture("agent_linear_poll_failed", { error: err.message });
|
|
59945
59951
|
return { found: 0, added: 0 };
|
|
59946
59952
|
}
|
|
59953
|
+
if (todo.length + inProgress.length + conflicted.length > 0) {
|
|
59954
|
+
this.deps.onLog(` poll: ${todo.length} todo, ${inProgress.length} in-progress, ${conflicted.length} conflicted`, "gray");
|
|
59955
|
+
}
|
|
59947
59956
|
const queuedIds = new Set(this.queue.map((q) => q.issue.id));
|
|
59948
59957
|
const activeIds = new Set(this.workers.map((w) => w.issueId));
|
|
59949
59958
|
const eligible = (id) => !queuedIds.has(id) && !activeIds.has(id) && !this.pendingIds.has(id);
|
|
@@ -59965,6 +59974,7 @@ class AgentCoordinator {
|
|
|
59965
59974
|
this.queue.push({ issue, mode: "resume" });
|
|
59966
59975
|
queuedIds.add(issue.id);
|
|
59967
59976
|
added += 1;
|
|
59977
|
+
this.deps.onLog(` \u21B3 ${issue.identifier} queued (resume)`, "gray");
|
|
59968
59978
|
}
|
|
59969
59979
|
for (const issue of conflicted) {
|
|
59970
59980
|
if (atTicketLimit())
|
|
@@ -59974,6 +59984,7 @@ class AgentCoordinator {
|
|
|
59974
59984
|
this.queue.push({ issue, mode: "conflict-fix" });
|
|
59975
59985
|
queuedIds.add(issue.id);
|
|
59976
59986
|
added += 1;
|
|
59987
|
+
this.deps.onLog(` \u21B3 ${issue.identifier} queued (conflict-fix)`, "gray");
|
|
59977
59988
|
}
|
|
59978
59989
|
for (const issue of todo) {
|
|
59979
59990
|
if (atTicketLimit())
|
|
@@ -59985,6 +59996,7 @@ class AgentCoordinator {
|
|
|
59985
59996
|
this.queue.push({ issue, mode: "fresh" });
|
|
59986
59997
|
queuedIds.add(issue.id);
|
|
59987
59998
|
added += 1;
|
|
59999
|
+
this.deps.onLog(` \u21B3 ${issue.identifier} queued (fresh)`, "gray");
|
|
59988
60000
|
}
|
|
59989
60001
|
if (added > 0) {
|
|
59990
60002
|
const modeRank = {
|
|
@@ -60043,6 +60055,7 @@ class AgentCoordinator {
|
|
|
60043
60055
|
try {
|
|
60044
60056
|
await this.deps.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
|
|
60045
60057
|
w.lastReportedIteration = count;
|
|
60058
|
+
this.deps.onLog(` ${w.issueIdentifier}: posted progress comment (iteration ${count})`, "gray");
|
|
60046
60059
|
} catch (err) {
|
|
60047
60060
|
this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
|
|
60048
60061
|
}
|
|
@@ -60082,6 +60095,7 @@ class AgentCoordinator {
|
|
|
60082
60095
|
capture("agent_conflict_detected", { issue_identifier: issue.identifier });
|
|
60083
60096
|
try {
|
|
60084
60097
|
await this.deps.applyIndicator(issue, this.opts.setConflicted);
|
|
60098
|
+
this.deps.onLog(` ${issue.identifier}: setConflicted applied`, "gray");
|
|
60085
60099
|
} catch (err) {
|
|
60086
60100
|
this.deps.onLog(`! Linear setConflicted failed for ${issue.identifier}: ${err.message}`, "red");
|
|
60087
60101
|
capture("agent_indicator_failed", {
|
|
@@ -60095,6 +60109,7 @@ class AgentCoordinator {
|
|
|
60095
60109
|
if (this.opts.postComments !== false) {
|
|
60096
60110
|
try {
|
|
60097
60111
|
await this.deps.postComment(issue, `\u26A0 Ralph detected merge conflicts on this PR (${pr.url}) \u2014 re-running to resolve`);
|
|
60112
|
+
this.deps.onLog(` ${issue.identifier}: posted conflict comment`, "gray");
|
|
60098
60113
|
} catch (err) {
|
|
60099
60114
|
this.deps.onLog(`! Linear conflict comment failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
60100
60115
|
}
|
|
@@ -60134,6 +60149,7 @@ class AgentCoordinator {
|
|
|
60134
60149
|
if (mode !== "resume" && this.opts.setInProgress) {
|
|
60135
60150
|
try {
|
|
60136
60151
|
await this.deps.applyIndicator(issue, this.opts.setInProgress);
|
|
60152
|
+
this.deps.onLog(` ${issue.identifier}: setInProgress applied`, "gray");
|
|
60137
60153
|
} catch (err) {
|
|
60138
60154
|
this.deps.onLog(`! Linear setInProgress failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
60139
60155
|
capture("agent_indicator_failed", {
|
|
@@ -60154,6 +60170,7 @@ class AgentCoordinator {
|
|
|
60154
60170
|
if (!alreadyPosted) {
|
|
60155
60171
|
try {
|
|
60156
60172
|
await this.deps.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${prep.changeName}\``);
|
|
60173
|
+
this.deps.onLog(` ${issue.identifier}: posted "started" comment`, "gray");
|
|
60157
60174
|
} catch (err) {
|
|
60158
60175
|
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
60159
60176
|
}
|
|
@@ -60207,6 +60224,7 @@ class AgentCoordinator {
|
|
|
60207
60224
|
` + `This issue has been quarantined and will not be auto-resumed on the next poll. ` + `Inspect the worktree at \`~/.ralph/<project>/worktrees/${changeName}\`, fix the ` + `underlying failure, then remove the error marker on this Linear issue (or run ` + `\`ralph clean --name ${changeName}\`) to clear the quarantine.`;
|
|
60208
60225
|
try {
|
|
60209
60226
|
await this.deps.postComment(issue, body);
|
|
60227
|
+
this.deps.onLog(` ${issue.identifier}: posted completion comment`, "gray");
|
|
60210
60228
|
} catch (err) {
|
|
60211
60229
|
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
60212
60230
|
}
|
|
@@ -60216,6 +60234,7 @@ class AgentCoordinator {
|
|
|
60216
60234
|
if (this.opts.clearConflicted) {
|
|
60217
60235
|
try {
|
|
60218
60236
|
await this.deps.removeIndicator(issue, this.opts.clearConflicted);
|
|
60237
|
+
this.deps.onLog(` ${issue.identifier}: clearConflicted applied`, "gray");
|
|
60219
60238
|
} catch (err) {
|
|
60220
60239
|
this.deps.onLog(`! Linear clearConflicted failed for ${issue.identifier}: ${err.message}`, "red");
|
|
60221
60240
|
capture("agent_indicator_failed", {
|
|
@@ -60229,6 +60248,7 @@ class AgentCoordinator {
|
|
|
60229
60248
|
} else if (this.opts.setDone) {
|
|
60230
60249
|
try {
|
|
60231
60250
|
await this.deps.applyIndicator(issue, this.opts.setDone);
|
|
60251
|
+
this.deps.onLog(` ${issue.identifier}: setDone applied`, "gray");
|
|
60232
60252
|
} catch (err) {
|
|
60233
60253
|
this.deps.onLog(`! Linear setDone failed for ${issue.identifier}: ${err.message}`, "red");
|
|
60234
60254
|
capture("agent_indicator_failed", {
|
|
@@ -60240,12 +60260,14 @@ class AgentCoordinator {
|
|
|
60240
60260
|
if (this.opts.setInProgress) {
|
|
60241
60261
|
try {
|
|
60242
60262
|
await this.deps.removeIndicator(issue, this.opts.setInProgress);
|
|
60263
|
+
this.deps.onLog(` ${issue.identifier}: clearInProgress applied`, "gray");
|
|
60243
60264
|
} catch {}
|
|
60244
60265
|
}
|
|
60245
60266
|
}
|
|
60246
60267
|
} else if (this.opts.setError) {
|
|
60247
60268
|
try {
|
|
60248
60269
|
await this.deps.applyIndicator(issue, this.opts.setError);
|
|
60270
|
+
this.deps.onLog(` ${issue.identifier}: setError applied`, "gray");
|
|
60249
60271
|
} catch (err) {
|
|
60250
60272
|
this.deps.onLog(`! Linear setError failed for ${issue.identifier}: ${err.message}`, "red");
|
|
60251
60273
|
capture("agent_indicator_failed", {
|
|
@@ -60257,6 +60279,7 @@ class AgentCoordinator {
|
|
|
60257
60279
|
if (this.opts.setInProgress) {
|
|
60258
60280
|
try {
|
|
60259
60281
|
await this.deps.removeIndicator(issue, this.opts.setInProgress);
|
|
60282
|
+
this.deps.onLog(` ${issue.identifier}: clearInProgress applied`, "gray");
|
|
60260
60283
|
} catch {}
|
|
60261
60284
|
}
|
|
60262
60285
|
}
|
|
@@ -60833,6 +60856,88 @@ async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
|
|
|
60833
60856
|
}
|
|
60834
60857
|
return 0;
|
|
60835
60858
|
}
|
|
60859
|
+
async function runPrPhase(input, deps) {
|
|
60860
|
+
const { changeName, cwd: cwd2, branch, changeDir, stateFilePath, issue, wantFixCi, cfg } = input;
|
|
60861
|
+
const { cmd, log: log2, emit, respawnWorker, registerPr, checkPrConflict } = deps;
|
|
60862
|
+
if (!branch || !issue) {
|
|
60863
|
+
log2(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
|
|
60864
|
+
return PR_FAILED_EXIT;
|
|
60865
|
+
}
|
|
60866
|
+
const ctx = {
|
|
60867
|
+
changeName,
|
|
60868
|
+
cwd: cwd2,
|
|
60869
|
+
branch,
|
|
60870
|
+
changeDir,
|
|
60871
|
+
stateFilePath,
|
|
60872
|
+
cfg,
|
|
60873
|
+
cmd,
|
|
60874
|
+
log: log2,
|
|
60875
|
+
emit,
|
|
60876
|
+
respawnWorker
|
|
60877
|
+
};
|
|
60878
|
+
try {
|
|
60879
|
+
const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
|
|
60880
|
+
if (status.stdout.trim()) {
|
|
60881
|
+
log2(`! ${changeName} has uncommitted changes after worker exit \u2014 the agent should commit everything before finishing. These changes will not be included in the PR.`, "yellow");
|
|
60882
|
+
}
|
|
60883
|
+
} catch (err) {
|
|
60884
|
+
log2(`! git status check failed for ${changeName}: ${err.message}`, "yellow");
|
|
60885
|
+
}
|
|
60886
|
+
const { pr, gaveUp: prGaveUp } = await createPrWithRetry(ctx, issue);
|
|
60887
|
+
if (prGaveUp)
|
|
60888
|
+
return PR_FAILED_EXIT;
|
|
60889
|
+
if (!pr) {
|
|
60890
|
+
log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
60891
|
+
return 0;
|
|
60892
|
+
}
|
|
60893
|
+
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
60894
|
+
registerPr?.(changeName, pr.url);
|
|
60895
|
+
return fixConflictsAndCiLoop(ctx, pr.url, wantFixCi, checkPrConflict);
|
|
60896
|
+
}
|
|
60897
|
+
async function runWorktreeCleanupPhase(input, deps) {
|
|
60898
|
+
const { changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode, cfg } = input;
|
|
60899
|
+
const { git, log: log2, emit } = deps;
|
|
60900
|
+
if (!useWorktree || cwd2 === projectRoot)
|
|
60901
|
+
return;
|
|
60902
|
+
emit("cleanup", "checking worktree safety");
|
|
60903
|
+
if (effectiveCode !== 0 || !cfg.cleanupWorktreeOnSuccess)
|
|
60904
|
+
return;
|
|
60905
|
+
const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
|
|
60906
|
+
safe: false,
|
|
60907
|
+
reason: `safety check failed: ${err.message}`,
|
|
60908
|
+
dirty: "",
|
|
60909
|
+
unpushedCommits: ""
|
|
60910
|
+
}));
|
|
60911
|
+
if (!check.safe) {
|
|
60912
|
+
log2(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
|
|
60913
|
+
if (check.dirty)
|
|
60914
|
+
log2(` uncommitted:
|
|
60915
|
+
${check.dirty}`, "yellow");
|
|
60916
|
+
if (check.unpushedCommits)
|
|
60917
|
+
log2(` commits:
|
|
60918
|
+
${check.unpushedCommits}`, "yellow");
|
|
60919
|
+
log2(` path: ${cwd2}`, "yellow");
|
|
60920
|
+
return;
|
|
60921
|
+
}
|
|
60922
|
+
try {
|
|
60923
|
+
await removeWorktree(projectRoot, cwd2, git);
|
|
60924
|
+
log2(` removed worktree ${cwd2}`, "gray");
|
|
60925
|
+
} catch (err) {
|
|
60926
|
+
log2(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
|
|
60927
|
+
}
|
|
60928
|
+
}
|
|
60929
|
+
async function runTeardownPhase(input, deps) {
|
|
60930
|
+
const { cwd: cwd2, teardownScript } = input;
|
|
60931
|
+
const { runScript, log: log2, emit } = deps;
|
|
60932
|
+
if (!teardownScript)
|
|
60933
|
+
return;
|
|
60934
|
+
emit("teardown", teardownScript);
|
|
60935
|
+
try {
|
|
60936
|
+
await runScript("teardown", teardownScript, cwd2);
|
|
60937
|
+
} catch (err) {
|
|
60938
|
+
log2(`! teardown script threw: ${err.message}`, "yellow");
|
|
60939
|
+
}
|
|
60940
|
+
}
|
|
60836
60941
|
async function runPostTask(input, deps) {
|
|
60837
60942
|
const { log: log2, cmd, git, runScript } = deps;
|
|
60838
60943
|
const emit = (phase, detail) => deps.onPhase?.(phase, detail);
|
|
@@ -60852,88 +60957,22 @@ async function runPostTask(input, deps) {
|
|
|
60852
60957
|
respawnWorker
|
|
60853
60958
|
} = input;
|
|
60854
60959
|
let effectiveCode = exitCode;
|
|
60855
|
-
|
|
60856
|
-
|
|
60857
|
-
if (!branch || !issue) {
|
|
60858
|
-
log2(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
|
|
60859
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
60860
|
-
} else {
|
|
60861
|
-
const ctx = {
|
|
60862
|
-
changeName,
|
|
60863
|
-
cwd: cwd2,
|
|
60864
|
-
branch,
|
|
60865
|
-
changeDir,
|
|
60866
|
-
stateFilePath,
|
|
60867
|
-
cfg,
|
|
60868
|
-
cmd,
|
|
60869
|
-
log: log2,
|
|
60870
|
-
emit,
|
|
60871
|
-
respawnWorker
|
|
60872
|
-
};
|
|
60873
|
-
try {
|
|
60874
|
-
const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
|
|
60875
|
-
if (status.stdout.trim()) {
|
|
60876
|
-
log2(`! ${changeName} has uncommitted changes after worker exit \u2014 the agent should commit everything before finishing. These changes will not be included in the PR.`, "yellow");
|
|
60877
|
-
}
|
|
60878
|
-
} catch (err) {
|
|
60879
|
-
log2(`! git status check failed for ${changeName}: ${err.message}`, "yellow");
|
|
60880
|
-
}
|
|
60881
|
-
{
|
|
60882
|
-
const { pr, gaveUp: prGaveUp } = await createPrWithRetry(ctx, issue);
|
|
60883
|
-
if (prGaveUp) {
|
|
60884
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
60885
|
-
} else if (!pr) {
|
|
60886
|
-
log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
60887
|
-
} else {
|
|
60888
|
-
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
60889
|
-
deps.registerPr?.(changeName, pr.url);
|
|
60890
|
-
const loopCode = await fixConflictsAndCiLoop(ctx, pr.url, wantFixCi, deps.checkPrConflict);
|
|
60891
|
-
if (loopCode !== 0)
|
|
60892
|
-
effectiveCode = loopCode;
|
|
60893
|
-
}
|
|
60894
|
-
}
|
|
60895
|
-
}
|
|
60896
|
-
}
|
|
60897
|
-
if (effectiveCode === 0)
|
|
60898
|
-
emit("done");
|
|
60899
|
-
else
|
|
60900
|
-
emit("gave-up", `exit ${effectiveCode}`);
|
|
60901
|
-
if (effectiveCode === 0 && cfg.teardownScript) {
|
|
60902
|
-
emit("teardown", cfg.teardownScript);
|
|
60903
|
-
try {
|
|
60904
|
-
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
60905
|
-
} catch (err) {
|
|
60906
|
-
log2(`! teardown script threw: ${err.message}`, "yellow");
|
|
60907
|
-
}
|
|
60960
|
+
if (effectiveCode !== 0 && wantPr) {
|
|
60961
|
+
log2(` skipping PR phase for ${changeName} (worker exited with code ${effectiveCode})`, "gray");
|
|
60908
60962
|
}
|
|
60909
|
-
if (
|
|
60910
|
-
|
|
60911
|
-
|
|
60912
|
-
|
|
60913
|
-
|
|
60914
|
-
|
|
60915
|
-
|
|
60916
|
-
|
|
60917
|
-
|
|
60918
|
-
if (!check.safe) {
|
|
60919
|
-
log2(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
|
|
60920
|
-
if (check.dirty)
|
|
60921
|
-
log2(` uncommitted:
|
|
60922
|
-
${check.dirty}`, "yellow");
|
|
60923
|
-
if (check.unpushedCommits)
|
|
60924
|
-
log2(` commits:
|
|
60925
|
-
${check.unpushedCommits}`, "yellow");
|
|
60926
|
-
log2(` path: ${cwd2}`, "yellow");
|
|
60927
|
-
} else {
|
|
60928
|
-
try {
|
|
60929
|
-
await removeWorktree(projectRoot, cwd2, git);
|
|
60930
|
-
log2(` removed worktree ${cwd2}`, "gray");
|
|
60931
|
-
} catch (err) {
|
|
60932
|
-
log2(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
|
|
60933
|
-
}
|
|
60934
|
-
}
|
|
60935
|
-
}
|
|
60963
|
+
if (effectiveCode === 0 && wantPr) {
|
|
60964
|
+
effectiveCode = await runPrPhase({ changeName, cwd: cwd2, branch, changeDir, stateFilePath, issue, wantFixCi, cfg }, {
|
|
60965
|
+
cmd,
|
|
60966
|
+
log: log2,
|
|
60967
|
+
emit,
|
|
60968
|
+
respawnWorker,
|
|
60969
|
+
...deps.registerPr !== undefined ? { registerPr: deps.registerPr } : {},
|
|
60970
|
+
...deps.checkPrConflict !== undefined ? { checkPrConflict: deps.checkPrConflict } : {}
|
|
60971
|
+
});
|
|
60936
60972
|
}
|
|
60973
|
+
emit(effectiveCode === 0 ? "done" : "gave-up", effectiveCode !== 0 ? `exit ${effectiveCode}` : undefined);
|
|
60974
|
+
await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode, cfg }, { git, log: log2, emit });
|
|
60975
|
+
await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log2, emit });
|
|
60937
60976
|
return effectiveCode;
|
|
60938
60977
|
}
|
|
60939
60978
|
var CI_FAILED_EXIT = 70, PR_FAILED_EXIT = 71;
|
|
@@ -61282,6 +61321,7 @@ PR: ${prUrl}` : ""
|
|
|
61282
61321
|
c.push("--verbose");
|
|
61283
61322
|
if (args.manualTest || cfg.enableManualTest)
|
|
61284
61323
|
c.push("--manual-test");
|
|
61324
|
+
c.push("--from-agent");
|
|
61285
61325
|
return c;
|
|
61286
61326
|
}
|
|
61287
61327
|
function defaultSpawn(changeName, cmd, cwd2, note) {
|
|
@@ -61578,7 +61618,7 @@ var exports_json_runner = {};
|
|
|
61578
61618
|
__export(exports_json_runner, {
|
|
61579
61619
|
runAgentJson: () => runAgentJson
|
|
61580
61620
|
});
|
|
61581
|
-
import { join as
|
|
61621
|
+
import { join as join21 } from "path";
|
|
61582
61622
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
61583
61623
|
import { homedir as homedir4 } from "os";
|
|
61584
61624
|
function cleanOutputLine2(raw) {
|
|
@@ -61603,7 +61643,7 @@ async function runAgentJson({
|
|
|
61603
61643
|
statesDir,
|
|
61604
61644
|
tasksDir
|
|
61605
61645
|
}) {
|
|
61606
|
-
await mkdir6(
|
|
61646
|
+
await mkdir6(join21(homedir4(), ".ralph"), { recursive: true }).catch(() => {
|
|
61607
61647
|
return;
|
|
61608
61648
|
});
|
|
61609
61649
|
const cfgPath = await ensureRalphyConfig(projectRoot);
|
|
@@ -61709,7 +61749,7 @@ var init_json_runner = __esm(() => {
|
|
|
61709
61749
|
});
|
|
61710
61750
|
|
|
61711
61751
|
// apps/cli/src/index.ts
|
|
61712
|
-
import { resolve as resolve2, join as
|
|
61752
|
+
import { resolve as resolve2, join as join22, dirname as dirname6 } from "path";
|
|
61713
61753
|
import { exists as exists2, mkdir as mkdir7, rm } from "fs/promises";
|
|
61714
61754
|
|
|
61715
61755
|
// node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
|
|
@@ -66885,6 +66925,7 @@ function buildInitialState(options) {
|
|
|
66885
66925
|
engine: options.engine ?? "claude",
|
|
66886
66926
|
model: options.model ?? "opus",
|
|
66887
66927
|
manualTest: options.manualTest ?? false,
|
|
66928
|
+
createPr: options.createPr ?? false,
|
|
66888
66929
|
createdAt: now2,
|
|
66889
66930
|
lastModified: now2,
|
|
66890
66931
|
metadata: { branch }
|
|
@@ -71884,6 +71925,17 @@ function buildTaskPrompt(state, taskDir) {
|
|
|
71884
71925
|
`;
|
|
71885
71926
|
prompt += `Commit all changed files yourself before finishing \u2014 stage files individually (e.g. \`git add path/to/file\`), never \`git add -A\` or \`git commit -am\`. Nothing is committed automatically after you exit.
|
|
71886
71927
|
`;
|
|
71928
|
+
if (state.createPr) {
|
|
71929
|
+
prompt += `
|
|
71930
|
+
When all tasks are complete and all files are committed, push your branch and open a pull request:
|
|
71931
|
+
`;
|
|
71932
|
+
prompt += ` git push -u origin HEAD
|
|
71933
|
+
`;
|
|
71934
|
+
prompt += ` gh pr create --title "${state.name}" --body "Summary of changes for ${state.name}"
|
|
71935
|
+
`;
|
|
71936
|
+
prompt += `Use the change name as the PR title and write a concise summary of the implementation in the body.
|
|
71937
|
+
`;
|
|
71938
|
+
}
|
|
71887
71939
|
return prompt;
|
|
71888
71940
|
}
|
|
71889
71941
|
function checkStopSignal(taskDir, stateDir) {
|
|
@@ -72051,7 +72103,8 @@ function useLoop(opts) {
|
|
|
72051
72103
|
prompt: opts.prompt,
|
|
72052
72104
|
engine: opts.engine,
|
|
72053
72105
|
model: opts.model,
|
|
72054
|
-
manualTest: opts.manualTest
|
|
72106
|
+
manualTest: opts.manualTest,
|
|
72107
|
+
createPr: opts.createPr ?? false
|
|
72055
72108
|
});
|
|
72056
72109
|
writeState(stateDir, currentState);
|
|
72057
72110
|
}
|
|
@@ -73616,6 +73669,7 @@ function TaskModeWrapper({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
73616
73669
|
log: args.log,
|
|
73617
73670
|
verbose: args.verbose,
|
|
73618
73671
|
manualTest,
|
|
73672
|
+
createPr: args.fromAgent,
|
|
73619
73673
|
statesDir,
|
|
73620
73674
|
tasksDir,
|
|
73621
73675
|
changeStore: new OpenSpecChangeStore
|
|
@@ -73664,6 +73718,7 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
73664
73718
|
}, undefined, false, undefined, this)
|
|
73665
73719
|
}, undefined, false, undefined, this);
|
|
73666
73720
|
case "clean":
|
|
73721
|
+
case "debug":
|
|
73667
73722
|
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
|
|
73668
73723
|
children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {}, undefined, false, undefined, this)
|
|
73669
73724
|
}, undefined, false, undefined, this);
|
|
@@ -73686,6 +73741,235 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
73686
73741
|
// apps/cli/src/index.ts
|
|
73687
73742
|
init_layout();
|
|
73688
73743
|
init_worktree();
|
|
73744
|
+
|
|
73745
|
+
// apps/cli/src/debug.ts
|
|
73746
|
+
init_log();
|
|
73747
|
+
import { join as join20 } from "path";
|
|
73748
|
+
var LOG_LINE_RE = /^\[(.+?)\] \[(.+?)\] (.+)$/;
|
|
73749
|
+
function parseLog(content) {
|
|
73750
|
+
return content.split(`
|
|
73751
|
+
`).filter(Boolean).flatMap((line) => {
|
|
73752
|
+
const m = LOG_LINE_RE.exec(line);
|
|
73753
|
+
if (!m)
|
|
73754
|
+
return [];
|
|
73755
|
+
const ts = new Date(m[1]);
|
|
73756
|
+
if (isNaN(ts.getTime()))
|
|
73757
|
+
return [];
|
|
73758
|
+
return [{ ts, type: m[2], text: m[3] }];
|
|
73759
|
+
});
|
|
73760
|
+
}
|
|
73761
|
+
function fmtTs(d) {
|
|
73762
|
+
return d.toISOString().replace("T", " ").slice(0, 23);
|
|
73763
|
+
}
|
|
73764
|
+
var SPAWN_RE = /\u25B6 (\S+) \u2192 (\S+)/;
|
|
73765
|
+
async function resolveDebugTarget(opts) {
|
|
73766
|
+
const agentLogFile = Bun.file(AGENT_LOG_PATH);
|
|
73767
|
+
const agentLines = await agentLogFile.exists() ? parseLog(await agentLogFile.text()) : [];
|
|
73768
|
+
if (opts.name && !opts.issue) {
|
|
73769
|
+
for (const line of agentLines) {
|
|
73770
|
+
const m = SPAWN_RE.exec(line.text);
|
|
73771
|
+
if (m && m[2] === opts.name)
|
|
73772
|
+
return { changeName: opts.name, identifier: m[1] };
|
|
73773
|
+
}
|
|
73774
|
+
return { changeName: opts.name, identifier: undefined };
|
|
73775
|
+
}
|
|
73776
|
+
if (opts.issue && !opts.name) {
|
|
73777
|
+
for (const line of agentLines) {
|
|
73778
|
+
const m = SPAWN_RE.exec(line.text);
|
|
73779
|
+
if (m && m[1] === opts.issue)
|
|
73780
|
+
return { changeName: m[2], identifier: opts.issue };
|
|
73781
|
+
}
|
|
73782
|
+
return { changeName: opts.issue, identifier: opts.issue };
|
|
73783
|
+
}
|
|
73784
|
+
return { changeName: opts.name, identifier: opts.issue };
|
|
73785
|
+
}
|
|
73786
|
+
async function fetchLinearIssue(identifier) {
|
|
73787
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
73788
|
+
if (!apiKey)
|
|
73789
|
+
return null;
|
|
73790
|
+
const query = `
|
|
73791
|
+
query($identifier: String!) {
|
|
73792
|
+
issues(filter: { identifier: { eq: $identifier } }, first: 1) {
|
|
73793
|
+
nodes {
|
|
73794
|
+
identifier title url
|
|
73795
|
+
state { name type }
|
|
73796
|
+
labels { nodes { name } }
|
|
73797
|
+
}
|
|
73798
|
+
}
|
|
73799
|
+
}
|
|
73800
|
+
`;
|
|
73801
|
+
try {
|
|
73802
|
+
const res = await fetch("https://api.linear.app/graphql", {
|
|
73803
|
+
method: "POST",
|
|
73804
|
+
headers: { "Content-Type": "application/json", Authorization: apiKey },
|
|
73805
|
+
body: JSON.stringify({ query, variables: { identifier } })
|
|
73806
|
+
});
|
|
73807
|
+
const json = await res.json();
|
|
73808
|
+
return json.data?.issues?.nodes?.[0] ?? null;
|
|
73809
|
+
} catch {
|
|
73810
|
+
return null;
|
|
73811
|
+
}
|
|
73812
|
+
}
|
|
73813
|
+
function spawnGh(args) {
|
|
73814
|
+
const result2 = Bun.spawnSync(["gh", ...args], { stderr: "ignore" });
|
|
73815
|
+
if (result2.exitCode !== 0)
|
|
73816
|
+
return null;
|
|
73817
|
+
try {
|
|
73818
|
+
return JSON.parse(result2.stdout.toString());
|
|
73819
|
+
} catch {
|
|
73820
|
+
return null;
|
|
73821
|
+
}
|
|
73822
|
+
}
|
|
73823
|
+
async function fetchGithubPr(changeName) {
|
|
73824
|
+
const branch = `ralph/${changeName}`;
|
|
73825
|
+
const prs = spawnGh([
|
|
73826
|
+
"pr",
|
|
73827
|
+
"list",
|
|
73828
|
+
"--head",
|
|
73829
|
+
branch,
|
|
73830
|
+
"--state",
|
|
73831
|
+
"all",
|
|
73832
|
+
"--json",
|
|
73833
|
+
"number,title,url,state,mergeable"
|
|
73834
|
+
]);
|
|
73835
|
+
if (!prs?.length)
|
|
73836
|
+
return null;
|
|
73837
|
+
const pr = prs[0];
|
|
73838
|
+
const checks = spawnGh([
|
|
73839
|
+
"pr",
|
|
73840
|
+
"checks",
|
|
73841
|
+
String(pr.number),
|
|
73842
|
+
"--json",
|
|
73843
|
+
"name,state,conclusion"
|
|
73844
|
+
]) ?? [];
|
|
73845
|
+
return { ...pr, checks };
|
|
73846
|
+
}
|
|
73847
|
+
async function runDebug(opts) {
|
|
73848
|
+
const { projectRoot } = opts;
|
|
73849
|
+
const agentLogFile = Bun.file(AGENT_LOG_PATH);
|
|
73850
|
+
const agentLogContent = await agentLogFile.exists() ? await agentLogFile.text() : "";
|
|
73851
|
+
const agentLines = parseLog(agentLogContent);
|
|
73852
|
+
let { changeName, identifier: issueIdentifier } = await resolveDebugTarget({
|
|
73853
|
+
...opts.name !== undefined ? { name: opts.name } : {},
|
|
73854
|
+
...opts.issue !== undefined ? { issue: opts.issue } : {}
|
|
73855
|
+
});
|
|
73856
|
+
if (!changeName) {
|
|
73857
|
+
process.stderr.write(`! Could not resolve a change name for ${opts.issue ?? opts.name}. Has this issue been started?
|
|
73858
|
+
`);
|
|
73859
|
+
process.exit(1);
|
|
73860
|
+
}
|
|
73861
|
+
const relevant = agentLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
|
|
73862
|
+
if (!issueIdentifier) {
|
|
73863
|
+
for (const line of relevant) {
|
|
73864
|
+
const m = SPAWN_RE.exec(line.text);
|
|
73865
|
+
if (m && m[2] === changeName) {
|
|
73866
|
+
issueIdentifier = m[1];
|
|
73867
|
+
break;
|
|
73868
|
+
}
|
|
73869
|
+
}
|
|
73870
|
+
}
|
|
73871
|
+
const workerLogPath = join20(projectRoot, ".ralph", "logs", `${changeName}.log`);
|
|
73872
|
+
const workerLogFile = Bun.file(workerLogPath);
|
|
73873
|
+
const workerLines = await workerLogFile.exists() ? parseLog(await workerLogFile.text()) : [];
|
|
73874
|
+
const merged = [...relevant, ...workerLines].sort((a, b) => +a.ts - +b.ts);
|
|
73875
|
+
const seen = new Set;
|
|
73876
|
+
const timeline = merged.filter((l) => {
|
|
73877
|
+
const key = `${l.ts.getTime()}:${l.type}:${l.text}`;
|
|
73878
|
+
if (seen.has(key))
|
|
73879
|
+
return false;
|
|
73880
|
+
seen.add(key);
|
|
73881
|
+
return true;
|
|
73882
|
+
});
|
|
73883
|
+
const out = (s) => process.stdout.write(s + `
|
|
73884
|
+
`);
|
|
73885
|
+
out(`
|
|
73886
|
+
=== Ralph Debug: ${changeName}${issueIdentifier ? ` (${issueIdentifier})` : ""} ===
|
|
73887
|
+
`);
|
|
73888
|
+
out("\u2500\u2500 Timeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
73889
|
+
if (!timeline.length) {
|
|
73890
|
+
out(" (no log entries found)");
|
|
73891
|
+
} else {
|
|
73892
|
+
for (const line of timeline) {
|
|
73893
|
+
const prefix = line.type === "output" ? " \u2502" : " \xB7";
|
|
73894
|
+
out(`${prefix} ${fmtTs(line.ts)} [${line.type.padEnd(7)}] ${line.text}`);
|
|
73895
|
+
}
|
|
73896
|
+
}
|
|
73897
|
+
out("");
|
|
73898
|
+
out("\u2500\u2500 Current Linear state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
73899
|
+
if (!issueIdentifier) {
|
|
73900
|
+
out(" (unknown identifier \u2014 pass --issue to query Linear directly)");
|
|
73901
|
+
} else if (!process.env.LINEAR_API_KEY) {
|
|
73902
|
+
out(" (set LINEAR_API_KEY to fetch current Linear state)");
|
|
73903
|
+
} else {
|
|
73904
|
+
const issue = await fetchLinearIssue(issueIdentifier);
|
|
73905
|
+
if (!issue) {
|
|
73906
|
+
out(` ! Could not fetch ${issueIdentifier} from Linear`);
|
|
73907
|
+
} else {
|
|
73908
|
+
const labels = issue.labels.nodes.map((l) => l.name).join(", ") || "(none)";
|
|
73909
|
+
out(` Title : ${issue.title}`);
|
|
73910
|
+
out(` Status : ${issue.state.name} (${issue.state.type})`);
|
|
73911
|
+
out(` Labels : ${labels}`);
|
|
73912
|
+
out(` URL : ${issue.url}`);
|
|
73913
|
+
}
|
|
73914
|
+
}
|
|
73915
|
+
out("");
|
|
73916
|
+
out("\u2500\u2500 Current GitHub PR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
73917
|
+
const pr = await fetchGithubPr(changeName);
|
|
73918
|
+
if (!pr) {
|
|
73919
|
+
out(` (no PR found for branch ralph/${changeName})`);
|
|
73920
|
+
} else {
|
|
73921
|
+
const failing = pr.checks.filter((c) => c.conclusion === "FAILURE" || c.conclusion === "failure");
|
|
73922
|
+
const pending = pr.checks.filter((c) => c.state === "PENDING" || c.state === "IN_PROGRESS");
|
|
73923
|
+
out(` PR #${pr.number} : ${pr.url}`);
|
|
73924
|
+
out(` State : ${pr.state}`);
|
|
73925
|
+
out(` Mergeable : ${pr.mergeable}`);
|
|
73926
|
+
if (pr.checks.length) {
|
|
73927
|
+
out(` Checks : ${pr.checks.length} total, ${failing.length} failing, ${pending.length} pending`);
|
|
73928
|
+
for (const c of failing)
|
|
73929
|
+
out(` \u2717 ${c.name}`);
|
|
73930
|
+
for (const c of pending)
|
|
73931
|
+
out(` \u29D7 ${c.name}`);
|
|
73932
|
+
} else {
|
|
73933
|
+
out(" Checks : (none or not yet available)");
|
|
73934
|
+
}
|
|
73935
|
+
}
|
|
73936
|
+
out("");
|
|
73937
|
+
out("\u2500\u2500 Diagnosis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
73938
|
+
const lastEvent = timeline.at(-1);
|
|
73939
|
+
if (lastEvent)
|
|
73940
|
+
out(` Last event : ${fmtTs(lastEvent.ts)} ${lastEvent.text}`);
|
|
73941
|
+
const exitLine = relevant.find((l) => /exited \(code \d+\)/.test(l.text));
|
|
73942
|
+
if (exitLine) {
|
|
73943
|
+
const code = Number(/code (\d+)/.exec(exitLine.text)?.[1]);
|
|
73944
|
+
const meaning = code === 0 ? "success" : code === 70 ? "CI fix loop exhausted its attempt budget" : code === 71 ? "push or PR creation failed (pre-push hook or remote rejection)" : "worker subprocess failed";
|
|
73945
|
+
out(` Exit code : ${code} \u2014 ${meaning}`);
|
|
73946
|
+
}
|
|
73947
|
+
const logHas = (s) => relevant.some((l) => l.text.includes(s));
|
|
73948
|
+
if (logHas("setError applied"))
|
|
73949
|
+
out(" \u26A0 setError applied \u2014 issue is quarantined in Linear");
|
|
73950
|
+
if (logHas("setDone applied"))
|
|
73951
|
+
out(" \u2713 setDone applied \u2014 issue marked done in Linear");
|
|
73952
|
+
if (logHas("clearConflicted applied"))
|
|
73953
|
+
out(" \u2713 clearConflicted applied \u2014 conflicts resolved");
|
|
73954
|
+
if (logHas("setConflicted applied"))
|
|
73955
|
+
out(" \u26A0 setConflicted applied \u2014 merge conflicts detected");
|
|
73956
|
+
if (logHas("skipping PR phase"))
|
|
73957
|
+
out(" \u21A9 PR phase skipped \u2014 worker exited non-zero");
|
|
73958
|
+
if (pr?.mergeable === "CONFLICTING")
|
|
73959
|
+
out(" \u26A0 PR currently has merge conflicts");
|
|
73960
|
+
if (pr?.checks.some((c) => c.conclusion === "FAILURE" || c.conclusion === "failure")) {
|
|
73961
|
+
out(" \u26A0 PR has failing CI checks");
|
|
73962
|
+
}
|
|
73963
|
+
const worktreePath = join20(projectRoot, ".ralph", "worktrees", changeName);
|
|
73964
|
+
const worktreeExists = await Bun.file(join20(worktreePath, ".git")).exists();
|
|
73965
|
+
if (worktreeExists)
|
|
73966
|
+
out(` Worktree : ${worktreePath}`);
|
|
73967
|
+
if (!timeline.length)
|
|
73968
|
+
out(" (no log entries \u2014 has this change been started yet?)");
|
|
73969
|
+
out("");
|
|
73970
|
+
}
|
|
73971
|
+
|
|
73972
|
+
// apps/cli/src/index.ts
|
|
73689
73973
|
init_src();
|
|
73690
73974
|
if (typeof globalThis.Bun === "undefined") {
|
|
73691
73975
|
process.stderr.write(`ralph requires the Bun runtime (https://bun.sh/). It is not compatible with plain Node.js.
|
|
@@ -73695,7 +73979,7 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
73695
73979
|
async function findProjectRoot() {
|
|
73696
73980
|
let dir = process.cwd();
|
|
73697
73981
|
while (dir !== "/") {
|
|
73698
|
-
if (await exists2(
|
|
73982
|
+
if (await exists2(join22(dir, "openspec")))
|
|
73699
73983
|
return dir;
|
|
73700
73984
|
dir = resolve2(dir, "..");
|
|
73701
73985
|
}
|
|
@@ -73736,22 +74020,32 @@ try {
|
|
|
73736
74020
|
const tasksDir = layout.tasksDir;
|
|
73737
74021
|
if (args.mode === "init") {
|
|
73738
74022
|
await mkdir7(statesDir, { recursive: true });
|
|
73739
|
-
const openspecBin =
|
|
74023
|
+
const openspecBin = join22(dirname6(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
73740
74024
|
Bun.spawnSync({
|
|
73741
74025
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
73742
74026
|
stdio: ["inherit", "inherit", "inherit"],
|
|
73743
74027
|
cwd: process.cwd()
|
|
73744
74028
|
});
|
|
73745
74029
|
}
|
|
74030
|
+
if (args.mode === "debug") {
|
|
74031
|
+
if (!args.name) {
|
|
74032
|
+
process.stderr.write(`Error: --name is required for debug mode
|
|
74033
|
+
`);
|
|
74034
|
+
process.exit(1);
|
|
74035
|
+
}
|
|
74036
|
+
await runDebug({ name: args.name, projectRoot });
|
|
74037
|
+
await shutdown();
|
|
74038
|
+
process.exit(0);
|
|
74039
|
+
}
|
|
73746
74040
|
if (args.mode === "clean") {
|
|
73747
74041
|
if (!args.name) {
|
|
73748
74042
|
process.stderr.write(`Error: --name is required for clean mode
|
|
73749
74043
|
`);
|
|
73750
74044
|
process.exit(1);
|
|
73751
74045
|
}
|
|
73752
|
-
const worktreeDir =
|
|
73753
|
-
const changeDir =
|
|
73754
|
-
const stateDir =
|
|
74046
|
+
const worktreeDir = join22(worktreesDir(projectRoot), args.name);
|
|
74047
|
+
const changeDir = join22(tasksDir, args.name);
|
|
74048
|
+
const stateDir = join22(statesDir, args.name);
|
|
73755
74049
|
const branch = `ralph/${args.name}`;
|
|
73756
74050
|
const removed = [];
|
|
73757
74051
|
if (await exists2(worktreeDir)) {
|
|
@@ -73803,13 +74097,13 @@ try {
|
|
|
73803
74097
|
process.exit(0);
|
|
73804
74098
|
}
|
|
73805
74099
|
if (args.mode === "task" && args.name) {
|
|
73806
|
-
await mkdir7(
|
|
73807
|
-
await mkdir7(
|
|
74100
|
+
await mkdir7(join22(statesDir, args.name), { recursive: true });
|
|
74101
|
+
await mkdir7(join22(tasksDir, args.name), { recursive: true });
|
|
73808
74102
|
}
|
|
73809
74103
|
if (args.mode === "agent") {
|
|
73810
74104
|
await mkdir7(statesDir, { recursive: true });
|
|
73811
74105
|
await mkdir7(tasksDir, { recursive: true });
|
|
73812
|
-
await mkdir7(
|
|
74106
|
+
await mkdir7(join22(projectRoot, ".ralph"), { recursive: true });
|
|
73813
74107
|
}
|
|
73814
74108
|
if (args.mode === "agent" && args.jsonOutput) {
|
|
73815
74109
|
const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
|
package/dist/mcp/index.js
CHANGED
|
@@ -23967,6 +23967,7 @@ var StateSchema = exports_external.object({
|
|
|
23967
23967
|
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
23968
23968
|
model: exports_external.string().default("opus"),
|
|
23969
23969
|
manualTest: exports_external.boolean().default(false),
|
|
23970
|
+
createPr: exports_external.boolean().default(false),
|
|
23970
23971
|
usage: UsageSchema.default({}),
|
|
23971
23972
|
history: exports_external.array(HistoryEntrySchema).default([]),
|
|
23972
23973
|
metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
|
|
@@ -24033,6 +24034,7 @@ function buildInitialState(options) {
|
|
|
24033
24034
|
engine: options.engine ?? "claude",
|
|
24034
24035
|
model: options.model ?? "opus",
|
|
24035
24036
|
manualTest: options.manualTest ?? false,
|
|
24037
|
+
createPr: options.createPr ?? false,
|
|
24036
24038
|
createdAt: now,
|
|
24037
24039
|
lastModified: now,
|
|
24038
24040
|
metadata: { branch }
|