@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 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, before any 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.
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.16.6")
35033
- return "2.16.6";
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
- const ok = exitCode === 0;
60856
- if (ok && wantPr) {
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 (useWorktree && cwd2 !== projectRoot) {
60910
- emit("cleanup", "checking worktree safety");
60911
- if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
60912
- const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
60913
- safe: false,
60914
- reason: `safety check failed: ${err.message}`,
60915
- dirty: "",
60916
- unpushedCommits: ""
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 join20 } from "path";
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(join20(homedir4(), ".ralph"), { recursive: true }).catch(() => {
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 join21, dirname as dirname6 } from "path";
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(join21(dir, "openspec")))
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 = join21(dirname6(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
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 = join21(worktreesDir(projectRoot), args.name);
73753
- const changeDir = join21(tasksDir, args.name);
73754
- const stateDir = join21(statesDir, args.name);
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(join21(statesDir, args.name), { recursive: true });
73807
- await mkdir7(join21(tasksDir, args.name), { recursive: true });
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(join21(projectRoot, ".ralph"), { recursive: true });
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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.16.6",
3
+ "version": "2.17.0",
4
4
  "description": "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
5
5
  "keywords": [
6
6
  "agent",