@neriros/ralphy 2.13.14 → 2.14.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 +457 -268
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -50837,8 +50837,8 @@ var require_axios = __commonJS((exports, module) => {
50837
50837
  });
50838
50838
 
50839
50839
  // apps/cli/src/index.ts
50840
- import { resolve as resolve2, join as join19, dirname as dirname5 } from "path";
50841
- import { exists as exists2, mkdir as mkdir5, rm } from "fs/promises";
50840
+ import { resolve as resolve2, join as join19, dirname as dirname6 } from "path";
50841
+ import { exists as exists2, mkdir as mkdir6, rm } from "fs/promises";
50842
50842
 
50843
50843
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
50844
50844
  import { Stream } from "stream";
@@ -56116,6 +56116,12 @@ function Static(props) {
56116
56116
  }
56117
56117
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/components/Transform.js
56118
56118
  var import_react12 = __toESM(require_react(), 1);
56119
+ function Transform({ children, transform: transform2 }) {
56120
+ if (children === undefined || children === null) {
56121
+ return null;
56122
+ }
56123
+ return import_react12.default.createElement("ink-text", { style: { flexGrow: 0, flexShrink: 1, flexDirection: "row" }, internal_transform: transform2 }, children);
56124
+ }
56119
56125
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/components/Newline.js
56120
56126
  var import_react13 = __toESM(require_react(), 1);
56121
56127
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/components/Spacer.js
@@ -56411,10 +56417,13 @@ function log(msg) {
56411
56417
  import { readFileSync as readFileSync2 } from "fs";
56412
56418
  import { resolve } from "path";
56413
56419
  function getVersion() {
56420
+ try {
56421
+ if ("2.14.0")
56422
+ return "2.14.0";
56423
+ } catch {}
56414
56424
  const dirsToTry = [];
56415
56425
  try {
56416
- const cliDir = import.meta.dir;
56417
- dirsToTry.push(cliDir);
56426
+ dirsToTry.push(import.meta.dir);
56418
56427
  } catch {}
56419
56428
  dirsToTry.push(process.cwd());
56420
56429
  for (const startDir of dirsToTry) {
@@ -70108,7 +70117,9 @@ function TaskLoop({ opts }) {
70108
70117
 
70109
70118
  // apps/cli/src/components/AgentMode.tsx
70110
70119
  var import_react57 = __toESM(require_react(), 1);
70111
- import { join as join16, relative } from "path";
70120
+ import { join as join16, relative, dirname as dirname4 } from "path";
70121
+ import { homedir as homedir3 } from "os";
70122
+ import { appendFile, mkdir as mkdir4 } from "fs/promises";
70112
70123
 
70113
70124
  // apps/cli/src/agent/config.ts
70114
70125
  import { join as join10 } from "path";
@@ -70279,45 +70290,34 @@ var DEFAULT_CONFIG_TEMPLATE = `{
70279
70290
  // Post a progress update every N iterations. 0 disables. Requires postComments.
70280
70291
  "updateEveryIterations": 10,
70281
70292
 
70282
- // ---------------------------------------------------------------------------
70283
- // Linear indicators \u2014 COMMENTED OUT BY DEFAULT
70284
- //
70285
70293
  // Indicators map Ralph lifecycle events to Linear labels/statuses.
70286
- // WARNING: Activating indicators will query AND mutate your Linear workspace.
70287
- // Labels or statuses that do not already exist may be created automatically.
70288
- // Review every value against your actual Linear workspace before enabling,
70289
- // then replace the empty object below with the full indicators block.
70290
- //
70291
- // To activate, replace "indicators": {} with:
70292
- //
70293
- // "indicators": {
70294
- // // Issues to pick up (any-of filter \u2014 Ralph will start working on these).
70295
- // "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
70296
- //
70297
- // // Issues already in flight (resume after restart).
70298
- // "getInProgress": { "filter": [{ "type": "label", "value": "ralph:in-progress" }] },
70299
- //
70300
- // // Issues whose PR has a merge conflict (Ralph will attempt a re-fix run).
70301
- // "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflict" }] },
70302
- //
70303
- // // Applied when Ralph picks up an issue.
70304
- // "setInProgress": { "type": "label", "value": "ralph:in-progress" },
70305
- //
70306
- // // Applied on clean success.
70307
- // "setDone": { "type": "status", "value": "In Review" },
70308
- //
70309
- // // Applied when the task exits with an error (quarantine signal).
70310
- // "setError": { "type": "label", "value": "ralph:error" },
70311
- //
70312
- // // Applied when a PR merge conflict is detected.
70313
- // "setConflicted": { "type": "label", "value": "ralph:conflict" },
70314
- //
70315
- // // Label-only marker(s) removed once the conflict is fixed.
70316
- // // Note: only label-typed markers are valid here \u2014 status removal is not supported.
70317
- // "clearConflicted": { "type": "label", "value": "ralph:conflict" }
70318
- // }
70319
- // ---------------------------------------------------------------------------
70320
- "indicators": {}
70294
+ // WARNING: activating indicators will query AND mutate your Linear workspace.
70295
+ // Uncomment each entry after confirming the label/status names match your workspace.
70296
+ "indicators": {
70297
+ // Issues to pick up (any-of filter \u2014 Ralph will start working on these).
70298
+ // "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
70299
+
70300
+ // Issues already in flight (resume after restart).
70301
+ // "getInProgress": { "filter": [{ "type": "label", "value": "ralph:in-progress" }] },
70302
+
70303
+ // Issues whose PR has a merge conflict (Ralph will attempt a re-fix run).
70304
+ // "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflict" }] },
70305
+
70306
+ // Applied when Ralph picks up an issue.
70307
+ // "setInProgress": { "type": "label", "value": "ralph:in-progress" },
70308
+
70309
+ // Applied on clean success.
70310
+ // "setDone": { "type": "status", "value": "In Review" },
70311
+
70312
+ // Applied when the task exits with an error (quarantine signal).
70313
+ // "setError": { "type": "label", "value": "ralph:error" },
70314
+
70315
+ // Applied when a PR merge conflict is detected.
70316
+ // "setConflicted": { "type": "label", "value": "ralph:conflict" },
70317
+
70318
+ // Label removed once the conflict is fixed (status removal is not supported here).
70319
+ // "clearConflicted": { "type": "label", "value": "ralph:conflict" }
70320
+ }
70321
70321
  }
70322
70322
  }
70323
70323
  `;
@@ -70529,14 +70529,19 @@ async function fetchTeamIdByKey(apiKey, teamKey) {
70529
70529
  });
70530
70530
  return data.teams.nodes[0]?.id ?? null;
70531
70531
  }
70532
- async function createIssueLabel(apiKey, teamId, name) {
70533
- const mutation = `mutation CreateLabel($teamId: String!, $name: String!) {
70532
+ async function createIssueLabel(apiKey, teamId, name, parentId) {
70533
+ const mutation = parentId ? `mutation CreateLabel($teamId: String!, $name: String!, $parentId: String!) {
70534
+ issueLabelCreate(input: { teamId: $teamId, name: $name, parentId: $parentId }) {
70535
+ success
70536
+ issueLabel { id }
70537
+ }
70538
+ }` : `mutation CreateLabel($teamId: String!, $name: String!) {
70534
70539
  issueLabelCreate(input: { teamId: $teamId, name: $name }) {
70535
70540
  success
70536
70541
  issueLabel { id }
70537
70542
  }
70538
70543
  }`;
70539
- const data = await linearRequest(apiKey, mutation, { teamId, name });
70544
+ const data = await linearRequest(apiKey, mutation, parentId ? { teamId, name, parentId } : { teamId, name });
70540
70545
  return data.issueLabelCreate.issueLabel?.id ?? null;
70541
70546
  }
70542
70547
  async function addLabelToIssue(apiKey, issueId, labelId) {
@@ -70789,7 +70794,7 @@ class AgentCoordinator {
70789
70794
  this.pendingIds.delete(issue.id);
70790
70795
  return;
70791
70796
  }
70792
- if (mode === "fresh" && this.opts.setInProgress) {
70797
+ if (mode !== "resume" && this.opts.setInProgress) {
70793
70798
  try {
70794
70799
  await this.deps.applyIndicator(issue, this.opts.setInProgress);
70795
70800
  } catch (err) {
@@ -70895,6 +70900,11 @@ class AgentCoordinator {
70895
70900
  error: err.message
70896
70901
  });
70897
70902
  }
70903
+ if (this.opts.setInProgress) {
70904
+ try {
70905
+ await this.deps.removeIndicator(issue, this.opts.setInProgress);
70906
+ } catch {}
70907
+ }
70898
70908
  }
70899
70909
  } else if (this.opts.setError) {
70900
70910
  try {
@@ -70907,6 +70917,11 @@ class AgentCoordinator {
70907
70917
  error: err.message
70908
70918
  });
70909
70919
  }
70920
+ if (this.opts.setInProgress) {
70921
+ try {
70922
+ await this.deps.removeIndicator(issue, this.opts.setInProgress);
70923
+ } catch {}
70924
+ }
70910
70925
  }
70911
70926
  }
70912
70927
  stop() {
@@ -71134,6 +71149,7 @@ async function createPullRequest(input, runner) {
71134
71149
  // apps/cli/src/agent/ci.ts
71135
71150
  var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
71136
71151
  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;
71152
+ var NO_CHECKS_RE = /no checks reported/i;
71137
71153
  var GH_RETRY_DELAYS = [5000, 15000, 45000];
71138
71154
  async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
71139
71155
  let lastErr;
@@ -71158,7 +71174,18 @@ ${e.stdout ?? ""}`;
71158
71174
  throw lastErr;
71159
71175
  }
71160
71176
  async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry) {
71161
- const out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
71177
+ let out;
71178
+ try {
71179
+ out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
71180
+ } catch (err) {
71181
+ const e = err;
71182
+ const blob = `${e.message}
71183
+ ${e.stderr ?? ""}
71184
+ ${e.stdout ?? ""}`;
71185
+ if (NO_CHECKS_RE.test(blob))
71186
+ return { bucket: "pass", failedRunIds: [] };
71187
+ throw err;
71188
+ }
71162
71189
  const checks = JSON.parse(out.stdout || "[]").filter((c) => c.bucket !== "skipping");
71163
71190
  if (checks.some((c) => c.bucket === "pending")) {
71164
71191
  return { bucket: "pending", failedRunIds: [] };
@@ -71261,6 +71288,261 @@ async function reactivateState(stateFilePath, log2, changeName) {
71261
71288
  log2(`! could not reactivate state for ${changeName}: ${err.message}`, "yellow");
71262
71289
  }
71263
71290
  }
71291
+ async function runWorkerWithFixTask(ctx, heading, body) {
71292
+ try {
71293
+ await prependFixTask(join14(ctx.changeDir, "tasks.md"), heading, body);
71294
+ } catch (err) {
71295
+ ctx.log(`! could not prepend fix task: ${err.message}`, "red");
71296
+ return 1;
71297
+ }
71298
+ await reactivateState(ctx.stateFilePath, ctx.log, ctx.changeName);
71299
+ return ctx.respawnWorker();
71300
+ }
71301
+ async function pushWithLeases(ctx) {
71302
+ try {
71303
+ ctx.emit("pushing", "after conflict resolution");
71304
+ await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
71305
+ return true;
71306
+ } catch (pushErr) {
71307
+ const pe = pushErr;
71308
+ const blob = `${pe.message}
71309
+ ${pe.stderr ?? ""}`;
71310
+ if (!/non-fast-forward|Updates were rejected/i.test(blob)) {
71311
+ ctx.log(`! push after conflict fix failed: ${pe.message}`, "red");
71312
+ return false;
71313
+ }
71314
+ try {
71315
+ await ctx.cmd.run(["git", "push", "--force-with-lease", "origin", ctx.branch], ctx.cwd);
71316
+ return true;
71317
+ } catch (forceErr) {
71318
+ ctx.log(`! force-push after conflict fix failed: ${forceErr.message}`, "red");
71319
+ return false;
71320
+ }
71321
+ }
71322
+ }
71323
+ async function commitResidualChanges(ctx, maxAttempts) {
71324
+ let hookFixAttempt = 0;
71325
+ while (true) {
71326
+ ctx.emit("committing", "git status");
71327
+ let dirty = "";
71328
+ try {
71329
+ const status = await ctx.cmd.run(["git", "status", "--porcelain"], ctx.cwd);
71330
+ dirty = status.stdout.trim();
71331
+ } catch (err) {
71332
+ ctx.log(`! git status failed for ${ctx.changeName}: ${err.message}`, "yellow");
71333
+ break;
71334
+ }
71335
+ if (!dirty)
71336
+ break;
71337
+ try {
71338
+ ctx.emit("committing", "git add -A");
71339
+ await ctx.cmd.run(["git", "add", "-A"], ctx.cwd);
71340
+ ctx.emit("committing", "git commit");
71341
+ await ctx.cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${ctx.changeName}`], ctx.cwd);
71342
+ ctx.log(` committed residual changes for ${ctx.changeName}`, "gray");
71343
+ break;
71344
+ } catch (err) {
71345
+ const e = err;
71346
+ const detail = e.stderr?.trim() || e.message;
71347
+ const combined = `${e.stdout ?? ""}
71348
+ ${e.stderr ?? ""}`;
71349
+ if (/nothing to commit/i.test(combined) || /empty git commit/i.test(combined))
71350
+ break;
71351
+ if (hookFixAttempt >= maxAttempts) {
71352
+ ctx.log(`! commit rejected for ${ctx.changeName} after ${hookFixAttempt} hook-fix attempts (host pre-commit hook still failing) \u2014 worktree preserved at ${ctx.cwd}`, "red");
71353
+ ctx.log(` detail: ${detail}`, "red");
71354
+ return { gaveUp: true, hookFixAttempt };
71355
+ }
71356
+ hookFixAttempt += 1;
71357
+ ctx.emit("commit-retry", `${hookFixAttempt}/${maxAttempts}`);
71358
+ ctx.log(`! commit rejected for ${ctx.changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxAttempts})`, "yellow");
71359
+ ctx.log(` detail: ${detail}`, "yellow");
71360
+ const retryCode = await runWorkerWithFixTask(ctx, "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.
71361
+
71362
+ ` + combined.trim());
71363
+ if (retryCode !== 0) {
71364
+ ctx.log(`! worker re-run after commit rejection exited code ${retryCode} \u2014 giving up`, "red");
71365
+ return { gaveUp: true, hookFixAttempt };
71366
+ }
71367
+ }
71368
+ }
71369
+ return { gaveUp: false, hookFixAttempt };
71370
+ }
71371
+ async function createPrWithRetry(ctx, issue, initialHookFixAttempt) {
71372
+ const maxAttempts = ctx.cfg.maxCiFixAttempts;
71373
+ let hookFixAttempt = initialHookFixAttempt;
71374
+ let nonFfRebaseAttempted = false;
71375
+ let pr = null;
71376
+ while (true) {
71377
+ try {
71378
+ ctx.emit("pr-create", "git push + gh pr create");
71379
+ pr = await createPullRequest({ cwd: ctx.cwd, branch: ctx.branch, issue, base: ctx.cfg.prBaseBranch }, ctx.cmd);
71380
+ return { pr, gaveUp: false };
71381
+ } catch (err) {
71382
+ const e = err;
71383
+ const detail = e.stderr?.trim() || e.message;
71384
+ const combined = `${e.stdout ?? ""}
71385
+ ${e.stderr ?? ""}`;
71386
+ 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);
71387
+ const isHookReject = /pre-push hook|hook declined/i.test(combined);
71388
+ const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
71389
+ if (isNonFastForward && !nonFfRebaseAttempted) {
71390
+ nonFfRebaseAttempted = true;
71391
+ ctx.emit("rebasing", `git pull --rebase origin ${ctx.branch}`);
71392
+ ctx.log(` non-fast-forward push for ${ctx.changeName} \u2014 rebasing onto origin/${ctx.branch}`, "yellow");
71393
+ try {
71394
+ await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
71395
+ await ctx.cmd.run(["git", "pull", "--rebase", "origin", ctx.branch], ctx.cwd);
71396
+ continue;
71397
+ } catch (rebaseErr) {
71398
+ const re = rebaseErr;
71399
+ const reBlob = `${re.stdout ?? ""}
71400
+ ${re.stderr ?? ""}`;
71401
+ const isConflict = /CONFLICT|Merge conflict|could not apply|both modified/i.test(reBlob);
71402
+ if (!isConflict) {
71403
+ ctx.log(`! rebase failed for ${ctx.changeName}: ${rebaseErr.message} \u2014 giving up`, "red");
71404
+ return { pr: null, gaveUp: true };
71405
+ }
71406
+ ctx.emit("rebasing", "conflicts detected \u2014 aborting + queueing fix task");
71407
+ try {
71408
+ await ctx.cmd.run(["git", "rebase", "--abort"], ctx.cwd);
71409
+ } catch {}
71410
+ let conflictedFiles = "";
71411
+ try {
71412
+ const r = await ctx.cmd.run(["git", "diff", "--name-only", `HEAD..origin/${ctx.branch}`], ctx.cwd);
71413
+ conflictedFiles = r.stdout.trim();
71414
+ } catch {}
71415
+ if (hookFixAttempt >= maxAttempts) {
71416
+ ctx.log(`! merge conflict on rebase of ${ctx.branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${ctx.cwd}`, "red");
71417
+ ctx.log(` detail: ${reBlob.trim().split(`
71418
+ `).slice(0, 8).join(`
71419
+ `)}`, "red");
71420
+ return { pr: null, gaveUp: true };
71421
+ }
71422
+ hookFixAttempt += 1;
71423
+ ctx.emit("rebasing", `conflict-fix ${hookFixAttempt}/${maxAttempts}`);
71424
+ ctx.log(`! merge conflict rebasing ${ctx.branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxAttempts})`, "yellow");
71425
+ const retryCode2 = await runWorkerWithFixTask(ctx, "Resolve merge conflict with origin/" + ctx.branch, `Push to origin/${ctx.branch} was rejected as non-fast-forward, and rebasing ` + `onto origin/${ctx.branch} produced merge conflicts.
71426
+
71427
+ ` + `Run \`git fetch origin ${ctx.branch}\` and \`git rebase origin/${ctx.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.
71428
+
71429
+ ` + (conflictedFiles ? `Files that differ between your branch and origin/${ctx.branch}:
71430
+ ${conflictedFiles}
71431
+
71432
+ ` : "") + `Rebase output:
71433
+ ${reBlob.trim()}`);
71434
+ if (retryCode2 !== 0) {
71435
+ ctx.log(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
71436
+ return { pr: null, gaveUp: true };
71437
+ }
71438
+ nonFfRebaseAttempted = false;
71439
+ continue;
71440
+ }
71441
+ }
71442
+ if (!pushRejected || hookFixAttempt >= maxAttempts) {
71443
+ if (pushRejected) {
71444
+ ctx.log(`! push rejected for ${ctx.changeName} after ${hookFixAttempt} fix attempts (push still failing) \u2014 worktree preserved at ${ctx.cwd}`, "red");
71445
+ ctx.log(` detail: ${detail}`, "red");
71446
+ } else {
71447
+ ctx.log(`! PR create failed for ${ctx.changeName}: ${detail}`, "red");
71448
+ }
71449
+ return { pr: null, gaveUp: true };
71450
+ }
71451
+ hookFixAttempt += 1;
71452
+ ctx.emit("push-retry", `${hookFixAttempt}/${maxAttempts}`);
71453
+ ctx.log(`! push rejected for ${ctx.changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxAttempts})`, "yellow");
71454
+ ctx.log(` detail: ${detail}`, "yellow");
71455
+ const retryCode = await runWorkerWithFixTask(ctx, "Fix push rejection", `Push to origin/${ctx.branch} was rejected. Fix the underlying problem ` + `(e.g. failing pre-push hook checks), then the push will be retried.
71456
+
71457
+ ` + combined.trim());
71458
+ if (retryCode !== 0) {
71459
+ ctx.log(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
71460
+ return { pr: null, gaveUp: true };
71461
+ }
71462
+ }
71463
+ }
71464
+ }
71465
+ async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
71466
+ const wantConflictLoop = !!checkPrConflict;
71467
+ const maxOuterAttempts = ctx.cfg.maxCiFixAttempts;
71468
+ let outerAttempt = 0;
71469
+ let ciConfirmedGreen = false;
71470
+ while (outerAttempt < maxOuterAttempts) {
71471
+ if (wantConflictLoop) {
71472
+ ctx.emit("conflict-check");
71473
+ let conflicting = false;
71474
+ try {
71475
+ conflicting = await checkPrConflict(prUrl);
71476
+ } catch (err) {
71477
+ ctx.log(`! conflict check failed: ${err.message}`, "yellow");
71478
+ }
71479
+ if (conflicting) {
71480
+ outerAttempt++;
71481
+ ciConfirmedGreen = false;
71482
+ ctx.emit("conflict-fix-inner", `attempt ${outerAttempt}/${maxOuterAttempts}`);
71483
+ ctx.log(` merge conflicts on PR (attempt ${outerAttempt}/${maxOuterAttempts}) \u2014 spawning resolution task`, "yellow");
71484
+ const conflictCode = await runWorkerWithFixTask(ctx, "Resolve PR merge conflicts", [
71485
+ `The PR ${prUrl} has merge conflicts with \`${ctx.cfg.prBaseBranch}\`.`,
71486
+ "",
71487
+ "Steps:",
71488
+ `1. \`git fetch origin ${ctx.cfg.prBaseBranch}\` then rebase or merge \`${ctx.cfg.prBaseBranch}\` into the current branch.`,
71489
+ "2. Resolve conflicts in the files git lists.",
71490
+ "3. Stage and commit the resolution."
71491
+ ].join(`
71492
+ `));
71493
+ if (conflictCode !== 0) {
71494
+ ctx.log(`! conflict resolution worker exited code ${conflictCode} \u2014 giving up`, "red");
71495
+ return PR_FAILED_EXIT;
71496
+ }
71497
+ const pushed = await pushWithLeases(ctx);
71498
+ if (!pushed)
71499
+ return PR_FAILED_EXIT;
71500
+ continue;
71501
+ }
71502
+ }
71503
+ if (!wantFixCi)
71504
+ break;
71505
+ if (!ciConfirmedGreen) {
71506
+ ctx.log(` watching CI for ${prUrl} (max ${ctx.cfg.maxCiFixAttempts} fix attempts)`, "gray");
71507
+ ctx.emit("ci-poll", "starting");
71508
+ const result2 = await fixCiUntilGreen({
71509
+ onPhase: (p, d) => ctx.emit(p, d),
71510
+ getStatus: () => getPrChecksStatus(prUrl, ctx.cmd, ctx.cwd, (n, ms, why) => ctx.log(` gh transient (try ${n}) \u2014 retry in ${Math.round(ms / 1000)}s \xB7 ${why}`, "yellow")),
71511
+ getFailedLogs: (ids) => fetchFailedRunLogs(ids, ctx.cmd, ctx.cwd),
71512
+ runTaskWithSteering: async (steering) => {
71513
+ try {
71514
+ await prependFixTask(join14(ctx.changeDir, "tasks.md"), "Fix failing CI checks", steering);
71515
+ } catch (err) {
71516
+ ctx.log(`! could not prepend fix task: ${err.message}`, "red");
71517
+ }
71518
+ return ctx.respawnWorker();
71519
+ },
71520
+ pushBranch: async () => {
71521
+ await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
71522
+ },
71523
+ log: ctx.log,
71524
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms))
71525
+ }, {
71526
+ maxAttempts: ctx.cfg.maxCiFixAttempts,
71527
+ pollIntervalSeconds: ctx.cfg.ciPollIntervalSeconds
71528
+ });
71529
+ if (!result2.success) {
71530
+ ctx.log(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
71531
+ return CI_FAILED_EXIT;
71532
+ }
71533
+ ciConfirmedGreen = true;
71534
+ }
71535
+ if (wantConflictLoop) {
71536
+ continue;
71537
+ }
71538
+ return 0;
71539
+ }
71540
+ if (outerAttempt >= maxOuterAttempts) {
71541
+ ctx.log(`! outer fix loop exhausted ${maxOuterAttempts} attempts \u2014 giving up`, "red");
71542
+ return CI_FAILED_EXIT;
71543
+ }
71544
+ return 0;
71545
+ }
71264
71546
  async function runPostTask(input, deps) {
71265
71547
  const { log: log2, cmd, git, runScript } = deps;
71266
71548
  const emit = (phase, detail) => deps.onPhase?.(phase, detail);
@@ -71292,201 +71574,33 @@ async function runPostTask(input, deps) {
71292
71574
  log2(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
71293
71575
  effectiveCode = PR_FAILED_EXIT;
71294
71576
  } else {
71295
- const maxHookFixAttempts = cfg.maxCiFixAttempts;
71296
- const runWorkerWithFixTask = async (heading, failureOutput) => {
71297
- try {
71298
- await prependFixTask(join14(changeDir, "tasks.md"), heading, failureOutput);
71299
- } catch (err) {
71300
- log2(`! could not prepend fix task: ${err.message}`, "red");
71301
- return 1;
71302
- }
71303
- await reactivateState(stateFilePath, log2, changeName);
71304
- return respawnWorker();
71577
+ const ctx = {
71578
+ changeName,
71579
+ cwd: cwd2,
71580
+ branch,
71581
+ changeDir,
71582
+ stateFilePath,
71583
+ cfg,
71584
+ cmd,
71585
+ log: log2,
71586
+ emit,
71587
+ respawnWorker
71305
71588
  };
71306
- let hookFixAttempt = 0;
71307
- let commitGaveUp = false;
71308
- while (true) {
71309
- emit("committing", "git status");
71310
- let dirty = "";
71311
- try {
71312
- const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
71313
- dirty = status.stdout.trim();
71314
- } catch (err) {
71315
- log2(`! git status failed for ${changeName}: ${err.message}`, "yellow");
71316
- break;
71317
- }
71318
- if (!dirty)
71319
- break;
71320
- try {
71321
- emit("committing", "git add -A");
71322
- await cmd.run(["git", "add", "-A"], cwd2);
71323
- emit("committing", "git commit");
71324
- await cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${changeName}`], cwd2);
71325
- log2(` committed residual changes for ${changeName}`, "gray");
71326
- break;
71327
- } catch (err) {
71328
- const e = err;
71329
- const detail = e.stderr?.trim() || e.message;
71330
- const combined = `${e.stdout ?? ""}
71331
- ${e.stderr ?? ""}`;
71332
- if (/nothing to commit/i.test(combined))
71333
- break;
71334
- if (hookFixAttempt >= maxHookFixAttempts) {
71335
- log2(`! commit rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-commit hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
71336
- log2(` detail: ${detail}`, "red");
71337
- effectiveCode = PR_FAILED_EXIT;
71338
- commitGaveUp = true;
71339
- break;
71340
- }
71341
- hookFixAttempt += 1;
71342
- emit("commit-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
71343
- log2(`! commit rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
71344
- log2(` detail: ${detail}`, "yellow");
71345
- 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.
71346
-
71347
- ` + combined.trim());
71348
- if (retryCode !== 0) {
71349
- log2(`! worker re-run after commit rejection exited code ${retryCode} \u2014 giving up`, "red");
71350
- effectiveCode = PR_FAILED_EXIT;
71351
- commitGaveUp = true;
71352
- break;
71353
- }
71354
- }
71355
- }
71356
- let pr = null;
71357
- let prGaveUp = commitGaveUp;
71358
- let nonFfRebaseAttempted = false;
71359
- while (!prGaveUp) {
71360
- try {
71361
- emit("pr-create", "git push + gh pr create");
71362
- pr = await createPullRequest({ cwd: cwd2, branch, issue, base: cfg.prBaseBranch }, cmd);
71363
- break;
71364
- } catch (err) {
71365
- const e = err;
71366
- const detail = e.stderr?.trim() || e.message;
71367
- const combined = `${e.stdout ?? ""}
71368
- ${e.stderr ?? ""}`;
71369
- 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);
71370
- const isHookReject = /pre-push hook|hook declined/i.test(combined);
71371
- const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
71372
- if (isNonFastForward && !nonFfRebaseAttempted) {
71373
- nonFfRebaseAttempted = true;
71374
- emit("rebasing", `git pull --rebase origin ${branch}`);
71375
- log2(` non-fast-forward push for ${changeName} \u2014 rebasing onto origin/${branch}`, "yellow");
71376
- try {
71377
- await cmd.run(["git", "fetch", "origin", branch], cwd2);
71378
- await cmd.run(["git", "pull", "--rebase", "origin", branch], cwd2);
71379
- continue;
71380
- } catch (rebaseErr) {
71381
- const re = rebaseErr;
71382
- const reBlob = `${re.stdout ?? ""}
71383
- ${re.stderr ?? ""}`;
71384
- const isConflict = /CONFLICT|Merge conflict|could not apply|both modified/i.test(reBlob);
71385
- if (!isConflict) {
71386
- log2(`! rebase failed for ${changeName}: ${rebaseErr.message} \u2014 giving up`, "red");
71387
- effectiveCode = PR_FAILED_EXIT;
71388
- prGaveUp = true;
71389
- break;
71390
- }
71391
- emit("rebasing", "conflicts detected \u2014 aborting + queueing fix task");
71392
- try {
71393
- await cmd.run(["git", "rebase", "--abort"], cwd2);
71394
- } catch {}
71395
- let conflictedFiles = "";
71396
- try {
71397
- const r = await cmd.run(["git", "diff", "--name-only", `HEAD..origin/${branch}`], cwd2);
71398
- conflictedFiles = r.stdout.trim();
71399
- } catch {}
71400
- if (hookFixAttempt >= maxHookFixAttempts) {
71401
- log2(`! merge conflict on rebase of ${branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${cwd2}`, "red");
71402
- log2(` detail: ${reBlob.trim().split(`
71403
- `).slice(0, 8).join(`
71404
- `)}`, "red");
71405
- effectiveCode = PR_FAILED_EXIT;
71406
- prGaveUp = true;
71407
- break;
71408
- }
71409
- hookFixAttempt += 1;
71410
- emit("rebasing", `conflict-fix ${hookFixAttempt}/${maxHookFixAttempts}`);
71411
- log2(`! merge conflict rebasing ${branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
71412
- 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.
71413
-
71414
- ` + `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.
71415
-
71416
- ` + (conflictedFiles ? `Files that differ between your branch and origin/${branch}:
71417
- ${conflictedFiles}
71418
-
71419
- ` : "") + `Rebase output:
71420
- ${reBlob.trim()}`);
71421
- if (retryCode2 !== 0) {
71422
- log2(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
71423
- effectiveCode = PR_FAILED_EXIT;
71424
- prGaveUp = true;
71425
- break;
71426
- }
71427
- nonFfRebaseAttempted = false;
71428
- continue;
71429
- }
71430
- }
71431
- if (!pushRejected || hookFixAttempt >= maxHookFixAttempts) {
71432
- if (pushRejected) {
71433
- log2(`! push rejected for ${changeName} after ${hookFixAttempt} fix attempts (push still failing) \u2014 worktree preserved at ${cwd2}`, "red");
71434
- log2(` detail: ${detail}`, "red");
71435
- } else {
71436
- log2(`! PR create failed for ${changeName}: ${detail}`, "red");
71437
- }
71438
- effectiveCode = PR_FAILED_EXIT;
71439
- prGaveUp = true;
71440
- break;
71441
- }
71442
- hookFixAttempt += 1;
71443
- emit("push-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
71444
- log2(`! push rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
71445
- log2(` detail: ${detail}`, "yellow");
71446
- const retryCode = await runWorkerWithFixTask("Fix push rejection", `Push to origin/${branch} was rejected. Fix the underlying problem ` + `(e.g. failing pre-push hook checks), then the push will be retried.
71447
-
71448
- ` + combined.trim());
71449
- if (retryCode !== 0) {
71450
- log2(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
71451
- effectiveCode = PR_FAILED_EXIT;
71452
- prGaveUp = true;
71453
- break;
71454
- }
71455
- }
71456
- }
71457
- if (prGaveUp) {} else if (!pr) {
71458
- log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
71589
+ const { gaveUp: commitGaveUp, hookFixAttempt } = await commitResidualChanges(ctx, cfg.maxCiFixAttempts);
71590
+ if (commitGaveUp) {
71591
+ effectiveCode = PR_FAILED_EXIT;
71459
71592
  } else {
71460
- log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
71461
- deps.registerPr?.(changeName, pr.url);
71462
- if (wantFixCi) {
71463
- log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
71464
- emit("ci-poll", "starting");
71465
- const result2 = await fixCiUntilGreen({
71466
- onPhase: (p, d) => emit(p, d),
71467
- 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")),
71468
- getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
71469
- runTaskWithSteering: async (steering) => {
71470
- try {
71471
- await prependFixTask(join14(changeDir, "tasks.md"), "Fix failing CI checks", steering);
71472
- } catch (err) {
71473
- log2(`! could not prepend fix task: ${err.message}`, "red");
71474
- }
71475
- return respawnWorker();
71476
- },
71477
- pushBranch: async () => {
71478
- await cmd.run(["git", "push", "origin", branch], cwd2);
71479
- },
71480
- log: log2,
71481
- sleep: (ms) => new Promise((r) => setTimeout(r, ms))
71482
- }, {
71483
- maxAttempts: cfg.maxCiFixAttempts,
71484
- pollIntervalSeconds: cfg.ciPollIntervalSeconds
71485
- });
71486
- if (!result2.success) {
71487
- log2(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
71488
- effectiveCode = CI_FAILED_EXIT;
71489
- }
71593
+ const { pr, gaveUp: prGaveUp } = await createPrWithRetry(ctx, issue, hookFixAttempt);
71594
+ if (prGaveUp) {
71595
+ effectiveCode = PR_FAILED_EXIT;
71596
+ } else if (!pr) {
71597
+ log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
71598
+ } else {
71599
+ log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
71600
+ deps.registerPr?.(changeName, pr.url);
71601
+ const loopCode = await fixConflictsAndCiLoop(ctx, pr.url, wantFixCi, deps.checkPrConflict);
71602
+ if (loopCode !== 0)
71603
+ effectiveCode = loopCode;
71490
71604
  }
71491
71605
  }
71492
71606
  }
@@ -71663,13 +71777,31 @@ function buildAgentCoordinator(input) {
71663
71777
  teamId = fetched;
71664
71778
  teamIdCache.set(t, teamId);
71665
71779
  }
71666
- const newId = await createIssueLabel(apiKey, teamId, name);
71780
+ const colonIdx = name.indexOf(":");
71781
+ let parentId;
71782
+ let childName = name;
71783
+ if (colonIdx > 0) {
71784
+ const groupName = name.slice(0, colonIdx);
71785
+ childName = name.slice(colonIdx + 1);
71786
+ const existingGroup = map2.get(groupName.toLowerCase());
71787
+ if (existingGroup) {
71788
+ parentId = existingGroup;
71789
+ } else {
71790
+ const groupId = await createIssueLabel(apiKey, teamId, groupName);
71791
+ if (groupId) {
71792
+ map2.set(groupName.toLowerCase(), groupId);
71793
+ parentId = groupId;
71794
+ }
71795
+ }
71796
+ }
71797
+ const newId = await createIssueLabel(apiKey, teamId, childName, parentId);
71667
71798
  if (!newId)
71668
71799
  return null;
71669
71800
  map2.set(name.toLowerCase(), newId);
71670
71801
  onLog(` created Linear label '${name}' for team ${t}`, "gray");
71671
71802
  return newId;
71672
- } catch {
71803
+ } catch (err) {
71804
+ onLog(`! Linear label '${name}' creation threw: ${err.message}`, "yellow");
71673
71805
  return null;
71674
71806
  }
71675
71807
  }
@@ -71896,6 +72028,13 @@ PR: ${prUrl}` : ""
71896
72028
  return null;
71897
72029
  }
71898
72030
  };
72031
+ const ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
72032
+ const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
72033
+ const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
72034
+ const ITER_HEADER_LINE_RE = /^\u2500\u2500/;
72035
+ function isLogWorthy(clean) {
72036
+ return !BOX_ONLY_RE.test(clean) && !STATUS_BAR_LINE_RE.test(clean) && !ITER_HEADER_LINE_RE.test(clean);
72037
+ }
71899
72038
  async function pump(stream, label) {
71900
72039
  if (!stream)
71901
72040
  return;
@@ -71915,16 +72054,18 @@ PR: ${prUrl}` : ""
71915
72054
  `)) >= 0) {
71916
72055
  const line = buf.slice(0, nl);
71917
72056
  buf = buf.slice(nl + 1);
71918
- if (writer)
71919
- writer.write(line + `
72057
+ const clean = line.replace(ANSI_RE, "").trim();
72058
+ if (writer && clean && isLogWorthy(clean))
72059
+ writer.write(clean + `
71920
72060
  `);
71921
72061
  if (line)
71922
72062
  onWorkerOutput?.(changeName, label === "err" ? `! ${line}` : line);
71923
72063
  }
71924
72064
  }
71925
72065
  if (buf) {
71926
- if (writer)
71927
- writer.write(buf + `
72066
+ const clean = buf.replace(ANSI_RE, "").trim();
72067
+ if (writer && clean && isLogWorthy(clean))
72068
+ writer.write(clean + `
71928
72069
  `);
71929
72070
  onWorkerOutput?.(changeName, label === "err" ? `! ${buf}` : buf);
71930
72071
  }
@@ -72010,6 +72151,14 @@ PR: ${prUrl}` : ""
72010
72151
  },
72011
72152
  ...onWorkerPhase && {
72012
72153
  onPhase: (phase, detail) => onWorkerPhase(changeName, phase, detail)
72154
+ },
72155
+ checkPrConflict: async (prUrl) => {
72156
+ try {
72157
+ const res = await tracedCmd.run(["gh", "pr", "view", prUrl, "--json", "mergeable", "--jq", ".mergeable"], cwd2);
72158
+ return res.stdout.trim() === "CONFLICTING";
72159
+ } catch {
72160
+ return false;
72161
+ }
72013
72162
  }
72014
72163
  });
72015
72164
  cwdByChange.delete(changeName);
@@ -72164,6 +72313,22 @@ function prLabel(prUrl) {
72164
72313
  const m = prUrl.match(/\/pull\/(\d+)/);
72165
72314
  return m ? `#${m[1]}` : "PR";
72166
72315
  }
72316
+ var HYPERLINKS_SUPPORTED = !process.env["TMUX"];
72317
+ function Link({ url, label, color }) {
72318
+ if (!HYPERLINKS_SUPPORTED)
72319
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
72320
+ color,
72321
+ children: label
72322
+ }, undefined, false, undefined, this);
72323
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Transform, {
72324
+ transform: (output) => `\x1B]8;;${url}\x07${output}\x1B]8;;\x07`,
72325
+ children: /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
72326
+ color,
72327
+ underline: true,
72328
+ children: label
72329
+ }, undefined, false, undefined, this)
72330
+ }, undefined, false, undefined, this);
72331
+ }
72167
72332
  var ANSI_STRIP_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
72168
72333
  var BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
72169
72334
  var STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -72265,6 +72430,21 @@ function displayTailLines(activeCount) {
72265
72430
  return 8;
72266
72431
  return 5;
72267
72432
  }
72433
+ var AGENT_LOG_PATH = join16(homedir3(), ".ralph", "agent-mode.log");
72434
+ var SESSION_START = new Date().toISOString();
72435
+ mkdir4(dirname4(AGENT_LOG_PATH), { recursive: true }).catch(() => {
72436
+ return;
72437
+ });
72438
+ function writeAgentLog(text) {
72439
+ const clean = text.replace(ANSI_STRIP_RE, "").trim();
72440
+ if (!clean)
72441
+ return;
72442
+ const line = `[${new Date().toISOString()}] ${clean}
72443
+ `;
72444
+ appendFile(AGENT_LOG_PATH, line).catch(() => {
72445
+ return;
72446
+ });
72447
+ }
72268
72448
  function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72269
72449
  const { exit } = use_app_default();
72270
72450
  const { stdout } = use_stdout_default();
@@ -72280,11 +72460,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72280
72460
  const [pollStatus, setPollStatus] = import_react57.useState({ state: "idle", lastFound: null, lastAdded: null, lastAt: null, filterDesc: "" });
72281
72461
  function appendLog(text, color) {
72282
72462
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
72463
+ writeAgentLog(text);
72283
72464
  }
72284
72465
  import_react57.useEffect(() => {
72285
72466
  let pollTimer = null;
72286
72467
  let cancelled = false;
72287
72468
  async function init2() {
72469
+ writeAgentLog(`=== session start ${SESSION_START} ===`);
72288
72470
  const cfgPath = await ensureRalphyConfig(projectRoot);
72289
72471
  const cfg2 = await loadRalphyConfig(projectRoot);
72290
72472
  cfgRef.current = cfg2;
@@ -72305,6 +72487,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72305
72487
  onLog: appendLog,
72306
72488
  onWorkersChanged: () => setTick((t) => t + 1),
72307
72489
  onWorkerStarted: (changeName, dir, logFile, changeDir) => {
72490
+ writeAgentLog(`worker-started ${changeName} log=${logFile}`);
72308
72491
  workerMetaRef.current.set(changeName, {
72309
72492
  startedAt: Date.now(),
72310
72493
  statesDir: dir,
@@ -72321,14 +72504,17 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72321
72504
  });
72322
72505
  },
72323
72506
  onWorkerExited: (changeName) => {
72507
+ writeAgentLog(`worker-exited ${changeName}`);
72324
72508
  workerMetaRef.current.delete(changeName);
72325
72509
  },
72326
72510
  onWorkerPhase: (changeName, phase, detail) => {
72327
72511
  const m = workerMetaRef.current.get(changeName);
72328
72512
  if (!m)
72329
72513
  return;
72330
- if (m.phase !== phase)
72514
+ if (m.phase !== phase) {
72515
+ writeAgentLog(`phase ${changeName}: ${phase}${detail ? ` (${detail})` : ""}`);
72331
72516
  m.phaseStartedAt = Date.now();
72517
+ }
72332
72518
  m.phase = phase;
72333
72519
  m.phaseDetail = detail ?? "";
72334
72520
  },
@@ -72459,9 +72645,10 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72459
72645
  setFocusedIdx(n - 1);
72460
72646
  }
72461
72647
  }, { isActive: isRawModeSupported && activeCount > 1 });
72462
- const FIXED_OVERHEAD = 22;
72463
72648
  const nonFocusedCount = Math.max(0, activeCount - 1);
72464
- const focusedTailLines = Math.max(5, termHeight - FIXED_OVERHEAD - nonFocusedCount);
72649
+ const tasksBoxLines = activeCount > 0 ? 5 : 0;
72650
+ const FIXED_OVERHEAD = 5 + 5 + tasksBoxLines + 8 + nonFocusedCount * 4;
72651
+ const focusedTailLines = Math.max(3, termHeight - FIXED_OVERHEAD);
72465
72652
  const compactTailLines = displayTailLines(activeCount);
72466
72653
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
72467
72654
  flexDirection: "column",
@@ -72961,9 +73148,10 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72961
73148
  dimColor: true,
72962
73149
  children: "\u2197 LINEAR"
72963
73150
  }, undefined, false, undefined, this),
72964
- /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
72965
- color: "blue",
72966
- children: `\x1B]8;;${w.issue.url}\x07${w.issueIdentifier}\x1B]8;;\x07`
73151
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Link, {
73152
+ url: w.issue.url,
73153
+ label: w.issueIdentifier,
73154
+ color: "blue"
72967
73155
  }, undefined, false, undefined, this)
72968
73156
  ]
72969
73157
  }, undefined, true, undefined, this),
@@ -72974,9 +73162,10 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72974
73162
  dimColor: true,
72975
73163
  children: "\u2197 PR"
72976
73164
  }, undefined, false, undefined, this),
72977
- /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
72978
- color: "green",
72979
- children: `\x1B]8;;${prUrl}\x07${prLabel(prUrl)}\x1B]8;;\x07`
73165
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Link, {
73166
+ url: prUrl,
73167
+ label: prLabel(prUrl),
73168
+ color: "green"
72980
73169
  }, undefined, false, undefined, this)
72981
73170
  ]
72982
73171
  }, undefined, true, undefined, this)
@@ -73085,11 +73274,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
73085
73274
  }
73086
73275
 
73087
73276
  // packages/openspec/src/openspec-change-store.ts
73088
- import { join as join17, dirname as dirname4 } from "path";
73089
- import { readdir, mkdir as mkdir4 } from "fs/promises";
73277
+ import { join as join17, dirname as dirname5 } from "path";
73278
+ import { readdir, mkdir as mkdir5 } from "fs/promises";
73090
73279
  function resolveOpenspecBin() {
73091
73280
  const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
73092
- return join17(dirname4(pkgJsonPath), "bin", "openspec.js");
73281
+ return join17(dirname5(pkgJsonPath), "bin", "openspec.js");
73093
73282
  }
73094
73283
  function runOpenspec(args, options = {}) {
73095
73284
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -73147,7 +73336,7 @@ class OpenSpecChangeStore {
73147
73336
  }
73148
73337
  async writeTaskList(name, content) {
73149
73338
  const path = join17("openspec", "changes", name, "tasks.md");
73150
- await mkdir4(dirname4(path), { recursive: true });
73339
+ await mkdir5(dirname5(path), { recursive: true });
73151
73340
  await Bun.write(path, content);
73152
73341
  }
73153
73342
  async appendSteering(name, message) {
@@ -73158,7 +73347,7 @@ class OpenSpecChangeStore {
73158
73347
 
73159
73348
  ${existing.trimStart()}` : `${message}
73160
73349
  `;
73161
- await mkdir4(dirname4(path), { recursive: true });
73350
+ await mkdir5(dirname5(path), { recursive: true });
73162
73351
  await Bun.write(path, updated);
73163
73352
  }
73164
73353
  async readSection(name, artifact, heading) {
@@ -73369,8 +73558,8 @@ try {
73369
73558
  const statesDir = layout.statesDir;
73370
73559
  const tasksDir = layout.tasksDir;
73371
73560
  if (args.mode === "init") {
73372
- await mkdir5(statesDir, { recursive: true });
73373
- const openspecBin = join19(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
73561
+ await mkdir6(statesDir, { recursive: true });
73562
+ const openspecBin = join19(dirname6(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
73374
73563
  Bun.spawnSync({
73375
73564
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
73376
73565
  stdio: ["inherit", "inherit", "inherit"],
@@ -73437,13 +73626,13 @@ try {
73437
73626
  process.exit(0);
73438
73627
  }
73439
73628
  if (args.mode === "task" && args.name) {
73440
- await mkdir5(join19(statesDir, args.name), { recursive: true });
73441
- await mkdir5(join19(tasksDir, args.name), { recursive: true });
73629
+ await mkdir6(join19(statesDir, args.name), { recursive: true });
73630
+ await mkdir6(join19(tasksDir, args.name), { recursive: true });
73442
73631
  }
73443
73632
  if (args.mode === "agent") {
73444
- await mkdir5(statesDir, { recursive: true });
73445
- await mkdir5(tasksDir, { recursive: true });
73446
- await mkdir5(join19(projectRoot, ".ralph"), { recursive: true });
73633
+ await mkdir6(statesDir, { recursive: true });
73634
+ await mkdir6(tasksDir, { recursive: true });
73635
+ await mkdir6(join19(projectRoot, ".ralph"), { recursive: true });
73447
73636
  }
73448
73637
  await runWithContext(createDefaultContext(), async () => {
73449
73638
  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.13.14",
3
+ "version": "2.14.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",