@neriros/ralphy 2.16.7 → 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.7")
35033
- return "2.16.7";
35032
+ if ("2.17.0")
35033
+ return "2.17.0";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -35340,7 +35340,7 @@ var VERSION, VALID_MODES, VALID_MODELS, INDICATOR_KEYS, GET_KEYS, HELP_TEXT;
35340
35340
  var init_cli = __esm(() => {
35341
35341
  init_output();
35342
35342
  VERSION = getVersion();
35343
- VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean"]);
35343
+ VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean", "debug"]);
35344
35344
  VALID_MODELS = new Set(["haiku", "sonnet", "opus"]);
35345
35345
  INDICATOR_KEYS = new Set([
35346
35346
  "getTodo",
@@ -35365,6 +35365,7 @@ var init_cli = __esm(() => {
35365
35365
  " init Initialize OpenSpec in current directory",
35366
35366
  " agent Poll Linear for new tasks and run loops concurrently",
35367
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",
35368
35369
  "",
35369
35370
  "Options:",
35370
35371
  " --name <name> Change name (required for most commands)",
@@ -59949,6 +59950,9 @@ class AgentCoordinator {
59949
59950
  capture("agent_linear_poll_failed", { error: err.message });
59950
59951
  return { found: 0, added: 0 };
59951
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
+ }
59952
59956
  const queuedIds = new Set(this.queue.map((q) => q.issue.id));
59953
59957
  const activeIds = new Set(this.workers.map((w) => w.issueId));
59954
59958
  const eligible = (id) => !queuedIds.has(id) && !activeIds.has(id) && !this.pendingIds.has(id);
@@ -59970,6 +59974,7 @@ class AgentCoordinator {
59970
59974
  this.queue.push({ issue, mode: "resume" });
59971
59975
  queuedIds.add(issue.id);
59972
59976
  added += 1;
59977
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (resume)`, "gray");
59973
59978
  }
59974
59979
  for (const issue of conflicted) {
59975
59980
  if (atTicketLimit())
@@ -59979,6 +59984,7 @@ class AgentCoordinator {
59979
59984
  this.queue.push({ issue, mode: "conflict-fix" });
59980
59985
  queuedIds.add(issue.id);
59981
59986
  added += 1;
59987
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (conflict-fix)`, "gray");
59982
59988
  }
59983
59989
  for (const issue of todo) {
59984
59990
  if (atTicketLimit())
@@ -59990,6 +59996,7 @@ class AgentCoordinator {
59990
59996
  this.queue.push({ issue, mode: "fresh" });
59991
59997
  queuedIds.add(issue.id);
59992
59998
  added += 1;
59999
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (fresh)`, "gray");
59993
60000
  }
59994
60001
  if (added > 0) {
59995
60002
  const modeRank = {
@@ -60048,6 +60055,7 @@ class AgentCoordinator {
60048
60055
  try {
60049
60056
  await this.deps.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
60050
60057
  w.lastReportedIteration = count;
60058
+ this.deps.onLog(` ${w.issueIdentifier}: posted progress comment (iteration ${count})`, "gray");
60051
60059
  } catch (err) {
60052
60060
  this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
60053
60061
  }
@@ -60087,6 +60095,7 @@ class AgentCoordinator {
60087
60095
  capture("agent_conflict_detected", { issue_identifier: issue.identifier });
60088
60096
  try {
60089
60097
  await this.deps.applyIndicator(issue, this.opts.setConflicted);
60098
+ this.deps.onLog(` ${issue.identifier}: setConflicted applied`, "gray");
60090
60099
  } catch (err) {
60091
60100
  this.deps.onLog(`! Linear setConflicted failed for ${issue.identifier}: ${err.message}`, "red");
60092
60101
  capture("agent_indicator_failed", {
@@ -60100,6 +60109,7 @@ class AgentCoordinator {
60100
60109
  if (this.opts.postComments !== false) {
60101
60110
  try {
60102
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");
60103
60113
  } catch (err) {
60104
60114
  this.deps.onLog(`! Linear conflict comment failed for ${issue.identifier}: ${err.message}`, "yellow");
60105
60115
  }
@@ -60139,6 +60149,7 @@ class AgentCoordinator {
60139
60149
  if (mode !== "resume" && this.opts.setInProgress) {
60140
60150
  try {
60141
60151
  await this.deps.applyIndicator(issue, this.opts.setInProgress);
60152
+ this.deps.onLog(` ${issue.identifier}: setInProgress applied`, "gray");
60142
60153
  } catch (err) {
60143
60154
  this.deps.onLog(`! Linear setInProgress failed for ${issue.identifier}: ${err.message}`, "yellow");
60144
60155
  capture("agent_indicator_failed", {
@@ -60159,6 +60170,7 @@ class AgentCoordinator {
60159
60170
  if (!alreadyPosted) {
60160
60171
  try {
60161
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");
60162
60174
  } catch (err) {
60163
60175
  this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
60164
60176
  }
@@ -60212,6 +60224,7 @@ class AgentCoordinator {
60212
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.`;
60213
60225
  try {
60214
60226
  await this.deps.postComment(issue, body);
60227
+ this.deps.onLog(` ${issue.identifier}: posted completion comment`, "gray");
60215
60228
  } catch (err) {
60216
60229
  this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
60217
60230
  }
@@ -60221,6 +60234,7 @@ class AgentCoordinator {
60221
60234
  if (this.opts.clearConflicted) {
60222
60235
  try {
60223
60236
  await this.deps.removeIndicator(issue, this.opts.clearConflicted);
60237
+ this.deps.onLog(` ${issue.identifier}: clearConflicted applied`, "gray");
60224
60238
  } catch (err) {
60225
60239
  this.deps.onLog(`! Linear clearConflicted failed for ${issue.identifier}: ${err.message}`, "red");
60226
60240
  capture("agent_indicator_failed", {
@@ -60234,6 +60248,7 @@ class AgentCoordinator {
60234
60248
  } else if (this.opts.setDone) {
60235
60249
  try {
60236
60250
  await this.deps.applyIndicator(issue, this.opts.setDone);
60251
+ this.deps.onLog(` ${issue.identifier}: setDone applied`, "gray");
60237
60252
  } catch (err) {
60238
60253
  this.deps.onLog(`! Linear setDone failed for ${issue.identifier}: ${err.message}`, "red");
60239
60254
  capture("agent_indicator_failed", {
@@ -60245,12 +60260,14 @@ class AgentCoordinator {
60245
60260
  if (this.opts.setInProgress) {
60246
60261
  try {
60247
60262
  await this.deps.removeIndicator(issue, this.opts.setInProgress);
60263
+ this.deps.onLog(` ${issue.identifier}: clearInProgress applied`, "gray");
60248
60264
  } catch {}
60249
60265
  }
60250
60266
  }
60251
60267
  } else if (this.opts.setError) {
60252
60268
  try {
60253
60269
  await this.deps.applyIndicator(issue, this.opts.setError);
60270
+ this.deps.onLog(` ${issue.identifier}: setError applied`, "gray");
60254
60271
  } catch (err) {
60255
60272
  this.deps.onLog(`! Linear setError failed for ${issue.identifier}: ${err.message}`, "red");
60256
60273
  capture("agent_indicator_failed", {
@@ -60262,6 +60279,7 @@ class AgentCoordinator {
60262
60279
  if (this.opts.setInProgress) {
60263
60280
  try {
60264
60281
  await this.deps.removeIndicator(issue, this.opts.setInProgress);
60282
+ this.deps.onLog(` ${issue.identifier}: clearInProgress applied`, "gray");
60265
60283
  } catch {}
60266
60284
  }
60267
60285
  }
@@ -60838,6 +60856,88 @@ async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
60838
60856
  }
60839
60857
  return 0;
60840
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
+ }
60841
60941
  async function runPostTask(input, deps) {
60842
60942
  const { log: log2, cmd, git, runScript } = deps;
60843
60943
  const emit = (phase, detail) => deps.onPhase?.(phase, detail);
@@ -60857,88 +60957,22 @@ async function runPostTask(input, deps) {
60857
60957
  respawnWorker
60858
60958
  } = input;
60859
60959
  let effectiveCode = exitCode;
60860
- const ok = exitCode === 0;
60861
- if (ok && wantPr) {
60862
- if (!branch || !issue) {
60863
- log2(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
60864
- effectiveCode = PR_FAILED_EXIT;
60865
- } else {
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
- {
60887
- const { pr, gaveUp: prGaveUp } = await createPrWithRetry(ctx, issue);
60888
- if (prGaveUp) {
60889
- effectiveCode = PR_FAILED_EXIT;
60890
- } else if (!pr) {
60891
- log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
60892
- } else {
60893
- log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
60894
- deps.registerPr?.(changeName, pr.url);
60895
- const loopCode = await fixConflictsAndCiLoop(ctx, pr.url, wantFixCi, deps.checkPrConflict);
60896
- if (loopCode !== 0)
60897
- effectiveCode = loopCode;
60898
- }
60899
- }
60900
- }
60901
- }
60902
- if (effectiveCode === 0)
60903
- emit("done");
60904
- else
60905
- emit("gave-up", `exit ${effectiveCode}`);
60906
- if (effectiveCode === 0 && cfg.teardownScript) {
60907
- emit("teardown", cfg.teardownScript);
60908
- try {
60909
- await runScript("teardown", cfg.teardownScript, cwd2);
60910
- } catch (err) {
60911
- log2(`! teardown script threw: ${err.message}`, "yellow");
60912
- }
60960
+ if (effectiveCode !== 0 && wantPr) {
60961
+ log2(` skipping PR phase for ${changeName} (worker exited with code ${effectiveCode})`, "gray");
60913
60962
  }
60914
- if (useWorktree && cwd2 !== projectRoot) {
60915
- emit("cleanup", "checking worktree safety");
60916
- if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
60917
- const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
60918
- safe: false,
60919
- reason: `safety check failed: ${err.message}`,
60920
- dirty: "",
60921
- unpushedCommits: ""
60922
- }));
60923
- if (!check.safe) {
60924
- log2(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
60925
- if (check.dirty)
60926
- log2(` uncommitted:
60927
- ${check.dirty}`, "yellow");
60928
- if (check.unpushedCommits)
60929
- log2(` commits:
60930
- ${check.unpushedCommits}`, "yellow");
60931
- log2(` path: ${cwd2}`, "yellow");
60932
- } else {
60933
- try {
60934
- await removeWorktree(projectRoot, cwd2, git);
60935
- log2(` removed worktree ${cwd2}`, "gray");
60936
- } catch (err) {
60937
- log2(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
60938
- }
60939
- }
60940
- }
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
+ });
60941
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 });
60942
60976
  return effectiveCode;
60943
60977
  }
60944
60978
  var CI_FAILED_EXIT = 70, PR_FAILED_EXIT = 71;
@@ -61584,7 +61618,7 @@ var exports_json_runner = {};
61584
61618
  __export(exports_json_runner, {
61585
61619
  runAgentJson: () => runAgentJson
61586
61620
  });
61587
- import { join as join20 } from "path";
61621
+ import { join as join21 } from "path";
61588
61622
  import { mkdir as mkdir6 } from "fs/promises";
61589
61623
  import { homedir as homedir4 } from "os";
61590
61624
  function cleanOutputLine2(raw) {
@@ -61609,7 +61643,7 @@ async function runAgentJson({
61609
61643
  statesDir,
61610
61644
  tasksDir
61611
61645
  }) {
61612
- await mkdir6(join20(homedir4(), ".ralph"), { recursive: true }).catch(() => {
61646
+ await mkdir6(join21(homedir4(), ".ralph"), { recursive: true }).catch(() => {
61613
61647
  return;
61614
61648
  });
61615
61649
  const cfgPath = await ensureRalphyConfig(projectRoot);
@@ -61715,7 +61749,7 @@ var init_json_runner = __esm(() => {
61715
61749
  });
61716
61750
 
61717
61751
  // apps/cli/src/index.ts
61718
- 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";
61719
61753
  import { exists as exists2, mkdir as mkdir7, rm } from "fs/promises";
61720
61754
 
61721
61755
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
@@ -73684,6 +73718,7 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
73684
73718
  }, undefined, false, undefined, this)
73685
73719
  }, undefined, false, undefined, this);
73686
73720
  case "clean":
73721
+ case "debug":
73687
73722
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
73688
73723
  children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {}, undefined, false, undefined, this)
73689
73724
  }, undefined, false, undefined, this);
@@ -73706,6 +73741,235 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
73706
73741
  // apps/cli/src/index.ts
73707
73742
  init_layout();
73708
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
73709
73973
  init_src();
73710
73974
  if (typeof globalThis.Bun === "undefined") {
73711
73975
  process.stderr.write(`ralph requires the Bun runtime (https://bun.sh/). It is not compatible with plain Node.js.
@@ -73715,7 +73979,7 @@ if (typeof globalThis.Bun === "undefined") {
73715
73979
  async function findProjectRoot() {
73716
73980
  let dir = process.cwd();
73717
73981
  while (dir !== "/") {
73718
- if (await exists2(join21(dir, "openspec")))
73982
+ if (await exists2(join22(dir, "openspec")))
73719
73983
  return dir;
73720
73984
  dir = resolve2(dir, "..");
73721
73985
  }
@@ -73756,22 +74020,32 @@ try {
73756
74020
  const tasksDir = layout.tasksDir;
73757
74021
  if (args.mode === "init") {
73758
74022
  await mkdir7(statesDir, { recursive: true });
73759
- 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");
73760
74024
  Bun.spawnSync({
73761
74025
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
73762
74026
  stdio: ["inherit", "inherit", "inherit"],
73763
74027
  cwd: process.cwd()
73764
74028
  });
73765
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
+ }
73766
74040
  if (args.mode === "clean") {
73767
74041
  if (!args.name) {
73768
74042
  process.stderr.write(`Error: --name is required for clean mode
73769
74043
  `);
73770
74044
  process.exit(1);
73771
74045
  }
73772
- const worktreeDir = join21(worktreesDir(projectRoot), args.name);
73773
- const changeDir = join21(tasksDir, args.name);
73774
- 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);
73775
74049
  const branch = `ralph/${args.name}`;
73776
74050
  const removed = [];
73777
74051
  if (await exists2(worktreeDir)) {
@@ -73823,13 +74097,13 @@ try {
73823
74097
  process.exit(0);
73824
74098
  }
73825
74099
  if (args.mode === "task" && args.name) {
73826
- await mkdir7(join21(statesDir, args.name), { recursive: true });
73827
- 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 });
73828
74102
  }
73829
74103
  if (args.mode === "agent") {
73830
74104
  await mkdir7(statesDir, { recursive: true });
73831
74105
  await mkdir7(tasksDir, { recursive: true });
73832
- await mkdir7(join21(projectRoot, ".ralph"), { recursive: true });
74106
+ await mkdir7(join22(projectRoot, ".ralph"), { recursive: true });
73833
74107
  }
73834
74108
  if (args.mode === "agent" && args.jsonOutput) {
73835
74109
  const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.16.7",
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",