@neriros/ralphy 2.11.2 → 2.12.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +343 -57
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -50837,7 +50837,7 @@ var require_axios = __commonJS((exports, module) => {
50837
50837
  });
50838
50838
 
50839
50839
  // apps/cli/src/index.ts
50840
- import { resolve, join as join19, dirname as dirname5 } from "path";
50840
+ import { resolve, join as join20, dirname as dirname5 } from "path";
50841
50841
  import { exists as exists2, mkdir as mkdir4, rm } from "fs/promises";
50842
50842
 
50843
50843
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
@@ -56407,7 +56407,7 @@ function log(msg) {
56407
56407
  // package.json
56408
56408
  var package_default = {
56409
56409
  name: "@neriros/ralphy",
56410
- version: "2.11.2",
56410
+ version: "2.12.0",
56411
56411
  description: "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
56412
56412
  keywords: [
56413
56413
  "agent",
@@ -56879,7 +56879,7 @@ function createDefaultContext() {
56879
56879
 
56880
56880
  // apps/cli/src/components/App.tsx
56881
56881
  var import_react58 = __toESM(require_react(), 1);
56882
- import { join as join18 } from "path";
56882
+ import { join as join19 } from "path";
56883
56883
 
56884
56884
  // packages/core/src/state.ts
56885
56885
  import { join as join2 } from "path";
@@ -70104,7 +70104,7 @@ function TaskLoop({ opts }) {
70104
70104
 
70105
70105
  // apps/cli/src/components/AgentMode.tsx
70106
70106
  var import_react57 = __toESM(require_react(), 1);
70107
- import { join as join16 } from "path";
70107
+ import { join as join17 } from "path";
70108
70108
 
70109
70109
  // apps/cli/src/agent/state.ts
70110
70110
  import { join as join10 } from "path";
@@ -70249,6 +70249,9 @@ async function ensureRalphyConfig(projectRoot) {
70249
70249
  return path;
70250
70250
  }
70251
70251
 
70252
+ // apps/cli/src/agent/wire.ts
70253
+ import { join as join16 } from "path";
70254
+
70252
70255
  // packages/core/src/layout.ts
70253
70256
  import { join as join12 } from "path";
70254
70257
  var STATE_FILE2 = ".ralph-state.json";
@@ -70864,8 +70867,32 @@ async function createPullRequest(input, runner) {
70864
70867
 
70865
70868
  // apps/cli/src/agent/ci.ts
70866
70869
  var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
70867
- async function getPrChecksStatus(prRef, runner, cwd2) {
70868
- const out = await runner.run(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], cwd2);
70870
+ var TRANSIENT_GH_RE = /HTTP 5\d\d|Gateway Timeout|Bad Gateway|Service Unavailable|connection reset|ECONNRESET|ETIMEDOUT|getaddrinfo|EAI_AGAIN|could not resolve host/i;
70871
+ var GH_RETRY_DELAYS = [5000, 15000, 45000];
70872
+ async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
70873
+ let lastErr;
70874
+ for (let i = 0;i <= GH_RETRY_DELAYS.length; i++) {
70875
+ try {
70876
+ return await runner.run(cmd, cwd2);
70877
+ } catch (err) {
70878
+ const e = err;
70879
+ const blob = `${e.message}
70880
+ ${e.stderr ?? ""}
70881
+ ${e.stdout ?? ""}`;
70882
+ if (!TRANSIENT_GH_RE.test(blob) || i === GH_RETRY_DELAYS.length)
70883
+ throw err;
70884
+ const delay2 = GH_RETRY_DELAYS[i];
70885
+ const firstLine = (e.stderr?.trim().split(`
70886
+ `)[0] ?? e.message).slice(0, 120);
70887
+ onRetry?.(i + 1, delay2, firstLine);
70888
+ await sleep2(delay2);
70889
+ lastErr = err;
70890
+ }
70891
+ }
70892
+ throw lastErr;
70893
+ }
70894
+ async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry) {
70895
+ const out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
70869
70896
  const checks = JSON.parse(out.stdout || "[]").filter((c) => c.bucket !== "skipping");
70870
70897
  if (checks.some((c) => c.bucket === "pending")) {
70871
70898
  return { bucket: "pending", failedRunIds: [] };
@@ -70902,17 +70929,28 @@ ${truncated}`);
70902
70929
  }
70903
70930
  async function fixCiUntilGreen(deps, opts) {
70904
70931
  for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
70932
+ let pollN = 0;
70905
70933
  while (true) {
70906
70934
  if (deps.cancelled?.())
70907
70935
  return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
70908
- const s = await deps.getStatus();
70936
+ pollN += 1;
70937
+ deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 poll ${pollN}`);
70938
+ let s;
70939
+ try {
70940
+ s = await deps.getStatus();
70941
+ } catch (err) {
70942
+ deps.log(`! gh pr checks failed permanently: ${err.message} \u2014 giving up CI watch`, "red");
70943
+ return { success: false, attempts: attempt2 - 1, reason: "gh-failed" };
70944
+ }
70909
70945
  if (s.bucket === "pass") {
70910
70946
  deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
70911
70947
  return { success: true, attempts: attempt2 - 1 };
70912
70948
  }
70913
70949
  if (s.bucket === "fail") {
70914
70950
  deps.log(`\u2717 CI failing (attempt ${attempt2}/${opts.maxAttempts}) \u2014 fetching logs and re-running task`, "yellow");
70951
+ deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 fetching logs`);
70915
70952
  const logs = await deps.getFailedLogs(s.failedRunIds);
70953
+ deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 re-running worker`);
70916
70954
  const steering = `CI is failing on this PR. Investigate and fix:
70917
70955
 
70918
70956
  \`\`\`
@@ -70923,6 +70961,7 @@ ${logs}
70923
70961
  deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
70924
70962
  }
70925
70963
  try {
70964
+ deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pushing fix`);
70926
70965
  await deps.pushBranch();
70927
70966
  } catch (err) {
70928
70967
  deps.log(`! push failed during CI fix: ${err.message}`, "red");
@@ -70930,6 +70969,7 @@ ${logs}
70930
70969
  }
70931
70970
  break;
70932
70971
  }
70972
+ deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pending, waiting`);
70933
70973
  await deps.sleep(opts.pollIntervalSeconds * 1000);
70934
70974
  }
70935
70975
  }
@@ -70957,6 +70997,7 @@ async function reactivateState(stateFilePath, log2, changeName) {
70957
70997
  }
70958
70998
  async function runPostTask(input, deps) {
70959
70999
  const { log: log2, cmd, git, runScript } = deps;
71000
+ const emit = (phase, detail) => deps.onPhase?.(phase, detail);
70960
71001
  const {
70961
71002
  changeName,
70962
71003
  cwd: cwd2,
@@ -70973,6 +71014,7 @@ async function runPostTask(input, deps) {
70973
71014
  respawnWorker
70974
71015
  } = input;
70975
71016
  if (cfg.teardownScript) {
71017
+ emit("teardown", cfg.teardownScript);
70976
71018
  try {
70977
71019
  await runScript("teardown", cfg.teardownScript, cwd2);
70978
71020
  } catch {}
@@ -70998,6 +71040,7 @@ async function runPostTask(input, deps) {
70998
71040
  let hookFixAttempt = 0;
70999
71041
  let commitGaveUp = false;
71000
71042
  while (true) {
71043
+ emit("committing", "git status");
71001
71044
  let dirty = "";
71002
71045
  try {
71003
71046
  const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
@@ -71009,7 +71052,9 @@ async function runPostTask(input, deps) {
71009
71052
  if (!dirty)
71010
71053
  break;
71011
71054
  try {
71055
+ emit("committing", "git add -A");
71012
71056
  await cmd.run(["git", "add", "-A"], cwd2);
71057
+ emit("committing", "git commit");
71013
71058
  await cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${changeName}`], cwd2);
71014
71059
  log2(` committed residual changes for ${changeName}`, "gray");
71015
71060
  break;
@@ -71028,6 +71073,7 @@ ${e.stderr ?? ""}`;
71028
71073
  break;
71029
71074
  }
71030
71075
  hookFixAttempt += 1;
71076
+ emit("commit-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
71031
71077
  log2(`! commit rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
71032
71078
  log2(` detail: ${detail}`, "yellow");
71033
71079
  const retryCode = await runWorkerWithFixTask("Fix host pre-commit hook rejection", `Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem, then the commit will be retried.
@@ -71043,8 +71089,10 @@ ${e.stderr ?? ""}`;
71043
71089
  }
71044
71090
  let pr = null;
71045
71091
  let prGaveUp = commitGaveUp;
71092
+ let nonFfRebaseAttempted = false;
71046
71093
  while (!prGaveUp) {
71047
71094
  try {
71095
+ emit("pr-create", "git push + gh pr create");
71048
71096
  pr = await createPullRequest({ cwd: cwd2, branch, issue, base: cfg.prBaseBranch }, cmd);
71049
71097
  break;
71050
71098
  } catch (err) {
@@ -71052,8 +71100,69 @@ ${e.stderr ?? ""}`;
71052
71100
  const detail = e.stderr?.trim() || e.message;
71053
71101
  const combined = `${e.stdout ?? ""}
71054
71102
  ${e.stderr ?? ""}`;
71055
- const pushRejected = /failed to push some refs|pre-push hook|hook declined/i.test(combined);
71056
- if (!pushRejected || hookFixAttempt >= maxHookFixAttempts) {
71103
+ const isNonFastForward = /non-fast-forward|Updates were rejected because the (tip of your current branch is behind|remote contains work)/i.test(combined) && !/pre-push hook|hook declined/i.test(combined);
71104
+ const isHookReject = /pre-push hook|hook declined/i.test(combined);
71105
+ const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
71106
+ if (isNonFastForward && !nonFfRebaseAttempted) {
71107
+ nonFfRebaseAttempted = true;
71108
+ emit("rebasing", `git pull --rebase origin ${branch}`);
71109
+ log2(` non-fast-forward push for ${changeName} \u2014 rebasing onto origin/${branch}`, "yellow");
71110
+ try {
71111
+ await cmd.run(["git", "fetch", "origin", branch], cwd2);
71112
+ await cmd.run(["git", "pull", "--rebase", "origin", branch], cwd2);
71113
+ continue;
71114
+ } catch (rebaseErr) {
71115
+ const re = rebaseErr;
71116
+ const reBlob = `${re.stdout ?? ""}
71117
+ ${re.stderr ?? ""}`;
71118
+ const isConflict = /CONFLICT|Merge conflict|could not apply|both modified/i.test(reBlob);
71119
+ if (!isConflict) {
71120
+ log2(`! rebase failed for ${changeName}: ${rebaseErr.message} \u2014 giving up`, "red");
71121
+ effectiveCode = PR_FAILED_EXIT;
71122
+ prGaveUp = true;
71123
+ break;
71124
+ }
71125
+ emit("rebasing", "conflicts detected \u2014 aborting + queueing fix task");
71126
+ try {
71127
+ await cmd.run(["git", "rebase", "--abort"], cwd2);
71128
+ } catch {}
71129
+ let conflictedFiles = "";
71130
+ try {
71131
+ const r = await cmd.run(["git", "diff", "--name-only", `HEAD..origin/${branch}`], cwd2);
71132
+ conflictedFiles = r.stdout.trim();
71133
+ } catch {}
71134
+ if (hookFixAttempt >= maxHookFixAttempts) {
71135
+ log2(`! merge conflict on rebase of ${branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${cwd2}`, "red");
71136
+ log2(` detail: ${reBlob.trim().split(`
71137
+ `).slice(0, 8).join(`
71138
+ `)}`, "red");
71139
+ effectiveCode = PR_FAILED_EXIT;
71140
+ prGaveUp = true;
71141
+ break;
71142
+ }
71143
+ hookFixAttempt += 1;
71144
+ emit("rebasing", `conflict-fix ${hookFixAttempt}/${maxHookFixAttempts}`);
71145
+ log2(`! merge conflict rebasing ${branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
71146
+ const retryCode2 = await runWorkerWithFixTask("Resolve merge conflict with origin/" + branch, `Push to origin/${branch} was rejected as non-fast-forward, and rebasing ` + `onto origin/${branch} produced merge conflicts.
71147
+
71148
+ ` + `Run \`git fetch origin ${branch}\` and \`git rebase origin/${branch}\`, ` + `resolve every conflict, \`git add\` the resolved files, and finish with ` + `\`git rebase --continue\`. The push will be retried after this loop ` + `iteration finishes.
71149
+
71150
+ ` + (conflictedFiles ? `Files that differ between your branch and origin/${branch}:
71151
+ ${conflictedFiles}
71152
+
71153
+ ` : "") + `Rebase output:
71154
+ ${reBlob.trim()}`);
71155
+ if (retryCode2 !== 0) {
71156
+ log2(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
71157
+ effectiveCode = PR_FAILED_EXIT;
71158
+ prGaveUp = true;
71159
+ break;
71160
+ }
71161
+ nonFfRebaseAttempted = false;
71162
+ continue;
71163
+ }
71164
+ }
71165
+ if (!isHookReject || hookFixAttempt >= maxHookFixAttempts) {
71057
71166
  if (pushRejected) {
71058
71167
  log2(`! push rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
71059
71168
  log2(` detail: ${detail}`, "red");
@@ -71065,6 +71174,7 @@ ${e.stderr ?? ""}`;
71065
71174
  break;
71066
71175
  }
71067
71176
  hookFixAttempt += 1;
71177
+ emit("push-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
71068
71178
  log2(`! push rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
71069
71179
  log2(` detail: ${detail}`, "yellow");
71070
71180
  const retryCode = await runWorkerWithFixTask("Fix host pre-push hook rejection", `Push to origin/${branch} was rejected by the host repo's pre-push hook. ` + `Fix the underlying problem, then the push will be retried.
@@ -71084,8 +71194,10 @@ ${e.stderr ?? ""}`;
71084
71194
  log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
71085
71195
  if (wantFixCi) {
71086
71196
  log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
71197
+ emit("ci-poll", "starting");
71087
71198
  const result2 = await fixCiUntilGreen({
71088
- getStatus: () => getPrChecksStatus(pr.url, cmd, cwd2),
71199
+ onPhase: (p, d) => emit(p, d),
71200
+ getStatus: () => getPrChecksStatus(pr.url, cmd, cwd2, (n, ms, why) => log2(` gh transient (try ${n}) \u2014 retry in ${Math.round(ms / 1000)}s \xB7 ${why}`, "yellow")),
71089
71201
  getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
71090
71202
  runTaskWithSteering: async (steering) => {
71091
71203
  try {
@@ -71112,7 +71224,12 @@ ${e.stderr ?? ""}`;
71112
71224
  }
71113
71225
  }
71114
71226
  }
71227
+ if (effectiveCode === 0)
71228
+ emit("done");
71229
+ else
71230
+ emit("gave-up", `exit ${effectiveCode}`);
71115
71231
  if (useWorktree && cwd2 !== projectRoot) {
71232
+ emit("cleanup", "checking worktree safety");
71116
71233
  if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
71117
71234
  const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
71118
71235
  safe: false,
@@ -71176,6 +71293,22 @@ var bunCmdRunner = {
71176
71293
  return { stdout, stderr };
71177
71294
  }
71178
71295
  };
71296
+ function traceCmdRunner(base2, onStart, onEnd) {
71297
+ return {
71298
+ run: async (cmd, cwd2) => {
71299
+ const t0 = Date.now();
71300
+ onStart(cmd);
71301
+ try {
71302
+ const r = await base2.run(cmd, cwd2);
71303
+ onEnd(cmd, Date.now() - t0, true);
71304
+ return r;
71305
+ } catch (err) {
71306
+ onEnd(cmd, Date.now() - t0, false);
71307
+ throw err;
71308
+ }
71309
+ }
71310
+ };
71311
+ }
71179
71312
  function buildAgentCoordinator(input) {
71180
71313
  const {
71181
71314
  args,
@@ -71188,8 +71321,12 @@ function buildAgentCoordinator(input) {
71188
71321
  onLog,
71189
71322
  onWorkersChanged,
71190
71323
  onWorkerStarted,
71191
- onWorkerExited
71324
+ onWorkerExited,
71325
+ onWorkerPhase,
71326
+ onWorkerOutput,
71327
+ onWorkerCmd
71192
71328
  } = input;
71329
+ const logsDir = join16(projectRoot, ".ralph", "logs");
71193
71330
  const concurrency = args.concurrency || cfg.concurrency;
71194
71331
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
71195
71332
  const inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
@@ -71300,24 +71437,83 @@ function buildAgentCoordinator(input) {
71300
71437
  }
71301
71438
  function spawnWorker(changeName) {
71302
71439
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
71303
- const respawn = () => {
71304
- const rp = Bun.spawn({
71440
+ const logFilePath = join16(logsDir, `${changeName}.log`);
71441
+ let logWriter = null;
71442
+ const ensureLogWriter = async () => {
71443
+ if (logWriter)
71444
+ return logWriter;
71445
+ try {
71446
+ await Bun.write(logFilePath, "");
71447
+ logWriter = Bun.file(logFilePath).writer();
71448
+ return logWriter;
71449
+ } catch (err) {
71450
+ onLog(`! could not open worker log ${logFilePath}: ${err.message}`, "yellow");
71451
+ return null;
71452
+ }
71453
+ };
71454
+ async function pump(stream, label) {
71455
+ if (!stream)
71456
+ return;
71457
+ const reader = stream.getReader();
71458
+ const decoder = new TextDecoder;
71459
+ let buf = "";
71460
+ const writer = await ensureLogWriter();
71461
+ try {
71462
+ while (true) {
71463
+ const { value, done } = await reader.read();
71464
+ if (done)
71465
+ break;
71466
+ const chunk2 = decoder.decode(value, { stream: true });
71467
+ buf += chunk2;
71468
+ let nl;
71469
+ while ((nl = buf.indexOf(`
71470
+ `)) >= 0) {
71471
+ const line = buf.slice(0, nl);
71472
+ buf = buf.slice(nl + 1);
71473
+ if (writer)
71474
+ writer.write(line + `
71475
+ `);
71476
+ if (line)
71477
+ onWorkerOutput?.(changeName, label === "err" ? `! ${line}` : line);
71478
+ }
71479
+ }
71480
+ if (buf) {
71481
+ if (writer)
71482
+ writer.write(buf + `
71483
+ `);
71484
+ onWorkerOutput?.(changeName, label === "err" ? `! ${buf}` : buf);
71485
+ }
71486
+ } catch {} finally {
71487
+ try {
71488
+ writer?.flush();
71489
+ } catch {}
71490
+ }
71491
+ }
71492
+ const launch = (note) => {
71493
+ const p = Bun.spawn({
71305
71494
  cmd: buildTaskCmdFor(changeName),
71306
71495
  cwd: cwd2,
71307
- stdout: "ignore",
71308
- stderr: "ignore",
71496
+ stdout: "pipe",
71497
+ stderr: "pipe",
71309
71498
  stdin: "ignore"
71310
71499
  });
71500
+ if (note && logWriter)
71501
+ logWriter.write(`
71502
+ --- ${note} ---
71503
+ `);
71504
+ pump(p.stdout, "out");
71505
+ pump(p.stderr, "err");
71506
+ return p;
71507
+ };
71508
+ const respawn = () => {
71509
+ onWorkerPhase?.(changeName, "working", "respawn");
71510
+ const rp = launch(`respawn at ${new Date().toISOString()}`);
71311
71511
  return rp.exited;
71312
71512
  };
71313
- const proc = Bun.spawn({
71314
- cmd: buildTaskCmdFor(changeName),
71315
- cwd: cwd2,
71316
- stdout: "ignore",
71317
- stderr: "ignore",
71318
- stdin: "ignore"
71319
- });
71320
- onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir);
71513
+ const proc = launch(`spawn at ${new Date().toISOString()}`);
71514
+ onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir, logFilePath);
71515
+ onWorkerPhase?.(changeName, "working");
71516
+ const tracedCmd = onWorkerCmd ? traceCmdRunner(bunCmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : bunCmdRunner;
71321
71517
  const wantPr = args.createPr || cfg.createPrOnSuccess;
71322
71518
  const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
71323
71519
  const wrapped = proc.exited.then(async (code) => {
@@ -71342,7 +71538,19 @@ function buildAgentCoordinator(input) {
71342
71538
  cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess
71343
71539
  },
71344
71540
  respawnWorker: respawn
71345
- }, { cmd: bunCmdRunner, git: bunGitRunner, log: onLog, runScript });
71541
+ }, {
71542
+ cmd: tracedCmd,
71543
+ git: bunGitRunner,
71544
+ log: onLog,
71545
+ runScript,
71546
+ ...onWorkerPhase && {
71547
+ onPhase: (phase, detail) => onWorkerPhase(changeName, phase, detail)
71548
+ }
71549
+ });
71550
+ try {
71551
+ logWriter?.flush();
71552
+ await logWriter?.end();
71553
+ } catch {}
71346
71554
  cwdByChange.delete(changeName);
71347
71555
  statesDirByChange.delete(changeName);
71348
71556
  branchByChange.delete(changeName);
@@ -71418,6 +71626,12 @@ function nextId() {
71418
71626
  lineCounter += 1;
71419
71627
  return `${Date.now()}-${lineCounter}`;
71420
71628
  }
71629
+ var TAIL_MAX_LINES = 5;
71630
+ var CMD_DISPLAY_MAX = 80;
71631
+ function fmtCmd(argv) {
71632
+ const joined = argv.join(" ");
71633
+ return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
71634
+ }
71421
71635
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
71422
71636
  function fmtElapsed(ms) {
71423
71637
  const s = Math.floor(ms / 1000);
@@ -71467,15 +71681,50 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
71467
71681
  store,
71468
71682
  onLog: appendLog,
71469
71683
  onWorkersChanged: () => setTick((t) => t + 1),
71470
- onWorkerStarted: (changeName, dir) => {
71684
+ onWorkerStarted: (changeName, dir, logFile) => {
71471
71685
  workerMetaRef.current.set(changeName, {
71472
71686
  startedAt: Date.now(),
71473
71687
  statesDir: dir,
71474
- iter: 0
71688
+ logFile,
71689
+ iter: 0,
71690
+ phase: "working",
71691
+ phaseDetail: "",
71692
+ phaseStartedAt: Date.now(),
71693
+ currentCmd: null,
71694
+ lastCmd: null,
71695
+ tail: []
71475
71696
  });
71476
71697
  },
71477
71698
  onWorkerExited: (changeName) => {
71478
71699
  workerMetaRef.current.delete(changeName);
71700
+ },
71701
+ onWorkerPhase: (changeName, phase, detail) => {
71702
+ const m = workerMetaRef.current.get(changeName);
71703
+ if (!m)
71704
+ return;
71705
+ if (m.phase !== phase)
71706
+ m.phaseStartedAt = Date.now();
71707
+ m.phase = phase;
71708
+ m.phaseDetail = detail ?? "";
71709
+ },
71710
+ onWorkerOutput: (changeName, line) => {
71711
+ const m = workerMetaRef.current.get(changeName);
71712
+ if (!m)
71713
+ return;
71714
+ m.tail.push(line);
71715
+ if (m.tail.length > TAIL_MAX_LINES)
71716
+ m.tail.splice(0, m.tail.length - TAIL_MAX_LINES);
71717
+ },
71718
+ onWorkerCmd: (changeName, cmd, state, durationMs, ok) => {
71719
+ const m = workerMetaRef.current.get(changeName);
71720
+ if (!m)
71721
+ return;
71722
+ if (state === "start") {
71723
+ m.currentCmd = { argv: cmd, startedAt: Date.now() };
71724
+ } else {
71725
+ m.currentCmd = null;
71726
+ m.lastCmd = { argv: cmd, durationMs: durationMs ?? 0, ok: ok ?? true };
71727
+ }
71479
71728
  }
71480
71729
  });
71481
71730
  appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
@@ -71531,7 +71780,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
71531
71780
  (async () => {
71532
71781
  for (const [changeName, meta] of workerMetaRef.current) {
71533
71782
  try {
71534
- const file = Bun.file(join16(meta.statesDir, changeName, ".ralph-state.json"));
71783
+ const file = Bun.file(join17(meta.statesDir, changeName, ".ralph-state.json"));
71535
71784
  if (await file.exists()) {
71536
71785
  const json = await file.json();
71537
71786
  meta.iter = json.iteration ?? meta.iter;
@@ -71589,19 +71838,56 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
71589
71838
  const meta = workerMetaRef.current.get(w.changeName);
71590
71839
  const elapsed = meta ? fmtElapsed(now2 - meta.startedAt) : "\u2013";
71591
71840
  const iter = meta?.iter ?? 0;
71592
- return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
71593
- color: "cyan",
71841
+ const phase = meta?.phase ?? "working";
71842
+ const phaseElapsed = meta ? fmtElapsed(now2 - meta.phaseStartedAt) : "\u2013";
71843
+ const phaseDetail = meta?.phaseDetail ? ` (${meta.phaseDetail})` : "";
71844
+ const cmd = meta?.currentCmd;
71845
+ const cmdElapsed = cmd ? fmtElapsed(now2 - cmd.startedAt) : null;
71846
+ const tail2 = meta?.tail ?? [];
71847
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
71848
+ flexDirection: "column",
71594
71849
  children: [
71595
- " ",
71596
- spinnerFrame,
71597
- " ",
71598
- w.issueIdentifier,
71599
- " (",
71600
- w.changeName,
71601
- ") \xB7 iter ",
71602
- iter,
71603
- " \xB7 ",
71604
- elapsed
71850
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
71851
+ color: "cyan",
71852
+ children: [
71853
+ " ",
71854
+ spinnerFrame,
71855
+ " ",
71856
+ w.issueIdentifier,
71857
+ " (",
71858
+ w.changeName,
71859
+ ") \xB7 iter ",
71860
+ iter,
71861
+ " \xB7 ",
71862
+ elapsed
71863
+ ]
71864
+ }, undefined, true, undefined, this),
71865
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
71866
+ dimColor: true,
71867
+ children: [
71868
+ " phase: ",
71869
+ phase,
71870
+ phaseDetail,
71871
+ " \xB7 ",
71872
+ phaseElapsed
71873
+ ]
71874
+ }, undefined, true, undefined, this),
71875
+ cmd && /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
71876
+ color: "yellow",
71877
+ children: [
71878
+ " \u23F5 ",
71879
+ fmtCmd(cmd.argv),
71880
+ " \xB7 ",
71881
+ cmdElapsed
71882
+ ]
71883
+ }, undefined, true, undefined, this),
71884
+ tail2.map((line, i) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
71885
+ dimColor: true,
71886
+ children: [
71887
+ " \u2502 ",
71888
+ line.length > 110 ? line.slice(0, 109) + "\u2026" : line
71889
+ ]
71890
+ }, `${w.changeName}-tail-${i}`, true, undefined, this))
71605
71891
  ]
71606
71892
  }, w.changeName, true, undefined, this);
71607
71893
  })
@@ -71612,11 +71898,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
71612
71898
  }
71613
71899
 
71614
71900
  // packages/openspec/src/openspec-change-store.ts
71615
- import { join as join17, dirname as dirname4 } from "path";
71901
+ import { join as join18, dirname as dirname4 } from "path";
71616
71902
  import { readdir, mkdir as mkdir3 } from "fs/promises";
71617
71903
  function resolveOpenspecBin() {
71618
71904
  const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
71619
- return join17(dirname4(pkgJsonPath), "bin", "openspec.js");
71905
+ return join18(dirname4(pkgJsonPath), "bin", "openspec.js");
71620
71906
  }
71621
71907
  function runOpenspec(args, options = {}) {
71622
71908
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -71642,7 +71928,7 @@ class OpenSpecChangeStore {
71642
71928
  }
71643
71929
  }
71644
71930
  getChangeDirectory(name) {
71645
- return join17("openspec", "changes", name);
71931
+ return join18("openspec", "changes", name);
71646
71932
  }
71647
71933
  async listChanges() {
71648
71934
  const result2 = runOpenspec(["list", "--json"]);
@@ -71656,7 +71942,7 @@ class OpenSpecChangeStore {
71656
71942
  }
71657
71943
  } catch {}
71658
71944
  }
71659
- const changesDir = join17("openspec", "changes");
71945
+ const changesDir = join18("openspec", "changes");
71660
71946
  if (!await Bun.file(changesDir).exists())
71661
71947
  return [];
71662
71948
  try {
@@ -71667,18 +71953,18 @@ class OpenSpecChangeStore {
71667
71953
  }
71668
71954
  }
71669
71955
  async readTaskList(name) {
71670
- const file = Bun.file(join17("openspec", "changes", name, "tasks.md"));
71956
+ const file = Bun.file(join18("openspec", "changes", name, "tasks.md"));
71671
71957
  if (!await file.exists())
71672
71958
  return "";
71673
71959
  return await file.text();
71674
71960
  }
71675
71961
  async writeTaskList(name, content) {
71676
- const path = join17("openspec", "changes", name, "tasks.md");
71962
+ const path = join18("openspec", "changes", name, "tasks.md");
71677
71963
  await mkdir3(dirname4(path), { recursive: true });
71678
71964
  await Bun.write(path, content);
71679
71965
  }
71680
71966
  async appendSteering(name, message) {
71681
- const path = join17("openspec", "changes", name, "steering.md");
71967
+ const path = join18("openspec", "changes", name, "steering.md");
71682
71968
  const file = Bun.file(path);
71683
71969
  const existing = await file.exists() ? await file.text() : null;
71684
71970
  const updated = existing ? `${message}
@@ -71689,7 +71975,7 @@ ${existing.trimStart()}` : `${message}
71689
71975
  await Bun.write(path, updated);
71690
71976
  }
71691
71977
  async readSection(name, artifact, heading) {
71692
- const file = Bun.file(join17("openspec", "changes", name, artifact));
71978
+ const file = Bun.file(join18("openspec", "changes", name, artifact));
71693
71979
  if (!await file.exists())
71694
71980
  return "";
71695
71981
  const content = await file.text();
@@ -71771,8 +72057,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
71771
72057
  message: "Error: --name is required for status mode"
71772
72058
  }, undefined, false, undefined, this);
71773
72059
  }
71774
- const stateDir = join18(statesDir, args.name);
71775
- if (getStorage().read(join18(stateDir, ".ralph-state.json")) === null) {
72060
+ const stateDir = join19(statesDir, args.name);
72061
+ if (getStorage().read(join19(stateDir, ".ralph-state.json")) === null) {
71776
72062
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
71777
72063
  message: `Error: change '${args.name}' not found`
71778
72064
  }, undefined, false, undefined, this);
@@ -71833,7 +72119,7 @@ if (typeof globalThis.Bun === "undefined") {
71833
72119
  async function findProjectRoot() {
71834
72120
  let dir = process.cwd();
71835
72121
  while (dir !== "/") {
71836
- if (await exists2(join19(dir, "openspec")))
72122
+ if (await exists2(join20(dir, "openspec")))
71837
72123
  return dir;
71838
72124
  dir = resolve(dir, "..");
71839
72125
  }
@@ -71873,7 +72159,7 @@ try {
71873
72159
  const tasksDir = layout.tasksDir;
71874
72160
  if (args.mode === "init") {
71875
72161
  await mkdir4(statesDir, { recursive: true });
71876
- const openspecBin = join19(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
72162
+ const openspecBin = join20(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
71877
72163
  Bun.spawnSync({
71878
72164
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
71879
72165
  stdio: ["inherit", "inherit", "inherit"],
@@ -71886,9 +72172,9 @@ try {
71886
72172
  `);
71887
72173
  process.exit(1);
71888
72174
  }
71889
- const worktreeDir = join19(worktreesDir(projectRoot), args.name);
71890
- const changeDir = join19(tasksDir, args.name);
71891
- const stateDir = join19(statesDir, args.name);
72175
+ const worktreeDir = join20(worktreesDir(projectRoot), args.name);
72176
+ const changeDir = join20(tasksDir, args.name);
72177
+ const stateDir = join20(statesDir, args.name);
71892
72178
  const branch = `ralph/${args.name}`;
71893
72179
  const removed = [];
71894
72180
  if (await exists2(worktreeDir)) {
@@ -71948,13 +72234,13 @@ try {
71948
72234
  process.exit(0);
71949
72235
  }
71950
72236
  if (args.mode === "task" && args.name) {
71951
- await mkdir4(join19(statesDir, args.name), { recursive: true });
71952
- await mkdir4(join19(tasksDir, args.name), { recursive: true });
72237
+ await mkdir4(join20(statesDir, args.name), { recursive: true });
72238
+ await mkdir4(join20(tasksDir, args.name), { recursive: true });
71953
72239
  }
71954
72240
  if (args.mode === "agent") {
71955
72241
  await mkdir4(statesDir, { recursive: true });
71956
72242
  await mkdir4(tasksDir, { recursive: true });
71957
- await mkdir4(join19(projectRoot, ".ralph"), { recursive: true });
72243
+ await mkdir4(join20(projectRoot, ".ralph"), { recursive: true });
71958
72244
  }
71959
72245
  await runWithContext(createDefaultContext(), async () => {
71960
72246
  const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.11.2",
3
+ "version": "2.12.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",