@neriros/ralphy 2.7.6 → 2.7.8

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 (3) hide show
  1. package/README.md +30 -12
  2. package/dist/cli/index.js +271 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -111,6 +111,16 @@ Defaults are written to `ralphy.config.json` on first run; CLI flags override co
111
111
  "setupScript": "bun install",
112
112
  "teardownScript": "git status",
113
113
  "appendPrompt": "Always run lint before committing.",
114
+ "createPrOnSuccess": true,
115
+ "prBaseBranch": "main",
116
+ "fixCiOnFailure": true,
117
+ "maxCiFixAttempts": 5,
118
+ "ciPollIntervalSeconds": 30,
119
+ "maxRuntimeMinutesPerTask": 0,
120
+ "maxConsecutiveFailuresPerTask": 5,
121
+ "iterationDelaySeconds": 0,
122
+ "logRawStream": false,
123
+ "taskVerbose": false,
114
124
  }
115
125
  ```
116
126
 
@@ -126,6 +136,12 @@ Use `setupScript` (run inside the worktree right after scaffolding) to install d
126
136
 
127
137
  **`updateEveryIterations`** (default `10`, `0` disables) posts a "🔄 Ralph progress update: iteration N" comment on the Linear issue every N task iterations. Requires `postComments: true`.
128
138
 
139
+ **`createPrOnSuccess`** (or `--create-pr`) pushes the worker's branch and opens a GitHub PR via `gh` after a clean exit. Requires `--worktree` (the PR needs a branch to point at) and the `gh` CLI authenticated. The PR title is `<ID>: <title>`, the body links the Linear issue. If a PR already exists for the branch the existing URL is reported (idempotent for retries). `prBaseBranch` defaults to `main`.
140
+
141
+ **`fixCiOnFailure`** (or `--fix-ci`) watches the PR's checks via `gh pr checks` and, on failure, fetches the failed-run logs (`gh run view --log-failed`), appends them to `proposal.md` under `## Steering`, re-spawns the task loop in the worktree, and pushes the new commits — repeating until checks go green or `maxCiFixAttempts` is hit (default 5, polling interval `ciPollIntervalSeconds` defaults to 30s). Requires `--create-pr`.
142
+
143
+ Every CLI flag is also configurable in `ralphy.config.json`; CLI values override config when both are set. The agent forwards `maxRuntimeMinutesPerTask` / `maxConsecutiveFailuresPerTask` / `iterationDelaySeconds` / `logRawStream` / `taskVerbose` to each spawned `ralph task` worker.
144
+
129
145
  Failed workers (non-zero exit) are not marked processed, so they'll be retried on the next poll. SIGINT/SIGTERM cleanly stops polling and kills active workers. All Linear side effects are best-effort — failures log a warning but never block the task loop.
130
146
 
131
147
  ## CLI Options
@@ -149,18 +165,20 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
149
165
 
150
166
  ### Agent mode flags
151
167
 
152
- | Option | Description |
153
- | ----------------------------- | --------------------------------------------- |
154
- | `--linear-team <key>` | Linear team key (e.g. `ENG`) |
155
- | `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
156
- | `--linear-status <name>` | Filter by status name (repeatable) |
157
- | `--linear-label <name>` | Filter by label name (repeatable, any-of) |
158
- | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
159
- | `--concurrency <n>` | Max concurrent task loops (default: 1) |
160
- | `--worktree` | Run each task in its own git worktree |
161
- | `--in-progress-status <name>` | Linear status to set when work starts |
162
- | `--done-status <name>` | Linear status to set on successful completion |
163
- | `--done-label <name>` | Linear label to add on successful completion |
168
+ | Option | Description |
169
+ | ----------------------------- | ---------------------------------------------------------------------------- |
170
+ | `--linear-team <key>` | Linear team key (e.g. `ENG`) |
171
+ | `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
172
+ | `--linear-status <name>` | Filter by status name (repeatable) |
173
+ | `--linear-label <name>` | Filter by label name (repeatable, any-of) |
174
+ | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
175
+ | `--concurrency <n>` | Max concurrent task loops (default: 1) |
176
+ | `--worktree` | Run each task in its own git worktree |
177
+ | `--in-progress-status <name>` | Linear status to set when work starts |
178
+ | `--done-status <name>` | Linear status to set on successful completion |
179
+ | `--done-label <name>` | Linear label to add on successful completion |
180
+ | `--create-pr` | Push worker branch + open a GitHub PR on success (needs `--worktree`) |
181
+ | `--fix-ci` | After PR opens, re-run task on CI failures until green (needs `--create-pr`) |
164
182
 
165
183
  ## OpenSpec Flow
166
184
 
package/dist/cli/index.js CHANGED
@@ -56156,6 +56156,8 @@ var HELP_TEXT = [
56156
56156
  " --in-progress-status <name> Linear status to set when work starts on an issue",
56157
56157
  " --done-status <name> Linear status to set when work completes successfully",
56158
56158
  " --done-label <name> Linear label to add when work completes successfully",
56159
+ " --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
56160
+ " --fix-ci After opening the PR, re-run the task on CI failures until green (needs --create-pr)",
56159
56161
  "",
56160
56162
  " --help, -h Show this help message",
56161
56163
  "",
@@ -56195,7 +56197,9 @@ async function parseArgs(argv) {
56195
56197
  worktree: false,
56196
56198
  inProgressStatus: "",
56197
56199
  doneStatus: "",
56198
- doneLabel: ""
56200
+ doneLabel: "",
56201
+ createPr: false,
56202
+ fixCi: false
56199
56203
  };
56200
56204
  let expectModel = false;
56201
56205
  let expectModelFlag = false;
@@ -56416,6 +56420,12 @@ async function parseArgs(argv) {
56416
56420
  case "--done-label":
56417
56421
  expectDoneLabel = true;
56418
56422
  break;
56423
+ case "--create-pr":
56424
+ result2.createPr = true;
56425
+ break;
56426
+ case "--fix-ci":
56427
+ result2.fixCi = true;
56428
+ break;
56419
56429
  default:
56420
56430
  if (VALID_MODES.has(arg)) {
56421
56431
  result2.mode = arg;
@@ -69840,11 +69850,21 @@ var RalphyConfigSchema = exports_external.object({
69840
69850
  pollIntervalSeconds: exports_external.number().int().positive().default(60),
69841
69851
  maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
69842
69852
  maxCostUsdPerTask: exports_external.number().nonnegative().default(0),
69853
+ maxRuntimeMinutesPerTask: exports_external.number().nonnegative().default(0),
69854
+ maxConsecutiveFailuresPerTask: exports_external.number().int().nonnegative().default(5),
69855
+ iterationDelaySeconds: exports_external.number().int().nonnegative().default(0),
69856
+ logRawStream: exports_external.boolean().default(false),
69857
+ taskVerbose: exports_external.boolean().default(false),
69843
69858
  useWorktree: exports_external.boolean().default(false),
69844
69859
  cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
69845
69860
  setupScript: exports_external.string().optional(),
69846
69861
  teardownScript: exports_external.string().optional(),
69847
69862
  appendPrompt: exports_external.string().optional(),
69863
+ createPrOnSuccess: exports_external.boolean().default(false),
69864
+ prBaseBranch: exports_external.string().default("main"),
69865
+ fixCiOnFailure: exports_external.boolean().default(false),
69866
+ maxCiFixAttempts: exports_external.number().int().positive().default(5),
69867
+ ciPollIntervalSeconds: exports_external.number().int().positive().default(30),
69848
69868
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
69849
69869
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
69850
69870
  linear: exports_external.object({
@@ -70133,6 +70153,127 @@ async function removeWorktree(projectRoot, cwd2, runner) {
70133
70153
  await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
70134
70154
  }
70135
70155
 
70156
+ // apps/cli/src/agent/pr.ts
70157
+ function defaultTitle(issue) {
70158
+ return `${issue.identifier}: ${issue.title}`;
70159
+ }
70160
+ function defaultBody(issue, branch) {
70161
+ return [
70162
+ `Auto-generated by Ralph for ${issue.identifier}.`,
70163
+ "",
70164
+ `Source: ${issue.url}`,
70165
+ `Branch: \`${branch}\``,
70166
+ "",
70167
+ issue.description?.trim() ? `## Description
70168
+
70169
+ ${issue.description.trim()}` : ""
70170
+ ].filter(Boolean).join(`
70171
+ `);
70172
+ }
70173
+ async function createPullRequest(input, runner) {
70174
+ const base2 = input.base ?? "main";
70175
+ const log2 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
70176
+ if (log2.stdout.trim() === "")
70177
+ return null;
70178
+ await runner.run(["git", "push", "-u", "origin", input.branch], input.cwd);
70179
+ const existing = await runner.run([
70180
+ "gh",
70181
+ "pr",
70182
+ "list",
70183
+ "--head",
70184
+ input.branch,
70185
+ "--state",
70186
+ "open",
70187
+ "--json",
70188
+ "url",
70189
+ "--jq",
70190
+ ".[0].url // empty"
70191
+ ], input.cwd);
70192
+ const existingUrl = existing.stdout.trim();
70193
+ if (existingUrl)
70194
+ return { url: existingUrl, created: false };
70195
+ const title = defaultTitle(input.issue);
70196
+ const body = defaultBody(input.issue, input.branch);
70197
+ const created = await runner.run(["gh", "pr", "create", "--base", base2, "--title", title, "--body", body], input.cwd);
70198
+ const url = created.stdout.trim().split(`
70199
+ `).pop() ?? "";
70200
+ return { url, created: true };
70201
+ }
70202
+
70203
+ // apps/cli/src/agent/ci.ts
70204
+ var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
70205
+ async function getPrChecksStatus(prRef, runner, cwd2) {
70206
+ const out = await runner.run(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], cwd2);
70207
+ const checks = JSON.parse(out.stdout || "[]").filter((c) => c.bucket !== "skipping");
70208
+ if (checks.some((c) => c.bucket === "pending")) {
70209
+ return { bucket: "pending", failedRunIds: [] };
70210
+ }
70211
+ const failed = checks.filter((c) => c.bucket === "fail" || c.bucket === "cancel");
70212
+ if (failed.length === 0)
70213
+ return { bucket: "pass", failedRunIds: [] };
70214
+ const ids = new Set;
70215
+ for (const c of failed) {
70216
+ const m = c.link?.match(/\/actions\/runs\/(\d+)/);
70217
+ if (m)
70218
+ ids.add(m[1]);
70219
+ }
70220
+ return { bucket: "fail", failedRunIds: [...ids] };
70221
+ }
70222
+ async function fetchFailedRunLogs(runIds, runner, cwd2, maxCharsPerRun = 4000) {
70223
+ const chunks = [];
70224
+ for (const id of runIds) {
70225
+ try {
70226
+ const r = await runner.run(["gh", "run", "view", id, "--log-failed"], cwd2);
70227
+ const text = r.stdout.trim();
70228
+ const truncated = text.length > maxCharsPerRun ? text.slice(0, maxCharsPerRun) + `
70229
+ \u2026[truncated ${text.length - maxCharsPerRun} chars]` : text;
70230
+ chunks.push(`--- run ${id} ---
70231
+ ${truncated}`);
70232
+ } catch (err) {
70233
+ chunks.push(`--- run ${id} ---
70234
+ (failed to fetch logs: ${err.message})`);
70235
+ }
70236
+ }
70237
+ return chunks.join(`
70238
+
70239
+ `);
70240
+ }
70241
+ async function fixCiUntilGreen(deps, opts) {
70242
+ for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
70243
+ while (true) {
70244
+ if (deps.cancelled?.())
70245
+ return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
70246
+ const s = await deps.getStatus();
70247
+ if (s.bucket === "pass") {
70248
+ deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
70249
+ return { success: true, attempts: attempt2 - 1 };
70250
+ }
70251
+ if (s.bucket === "fail") {
70252
+ deps.log(`\u2717 CI failing (attempt ${attempt2}/${opts.maxAttempts}) \u2014 fetching logs and re-running task`, "yellow");
70253
+ const logs = await deps.getFailedLogs(s.failedRunIds);
70254
+ const steering = `CI is failing on this PR. Investigate and fix:
70255
+
70256
+ \`\`\`
70257
+ ${logs}
70258
+ \`\`\``;
70259
+ const code = await deps.runTaskWithSteering(steering);
70260
+ if (code !== 0) {
70261
+ deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
70262
+ }
70263
+ try {
70264
+ await deps.pushBranch();
70265
+ } catch (err) {
70266
+ deps.log(`! push failed during CI fix: ${err.message}`, "red");
70267
+ return { success: false, attempts: attempt2, reason: "push-failed" };
70268
+ }
70269
+ break;
70270
+ }
70271
+ await deps.sleep(opts.pollIntervalSeconds * 1000);
70272
+ }
70273
+ }
70274
+ return { success: false, attempts: opts.maxAttempts, reason: "max-attempts" };
70275
+ }
70276
+
70136
70277
  // apps/cli/src/components/AgentMode.tsx
70137
70278
  var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
70138
70279
  import { join as join14 } from "path";
@@ -70151,6 +70292,21 @@ var bunGitRunner = {
70151
70292
  return { stdout, stderr };
70152
70293
  }
70153
70294
  };
70295
+ var bunCmdRunner = {
70296
+ run: async (cmd, cwd2) => {
70297
+ const proc = Bun.spawn({ cmd, cwd: cwd2, stdout: "pipe", stderr: "pipe" });
70298
+ const stdout = await new Response(proc.stdout).text();
70299
+ const stderr = await new Response(proc.stderr).text();
70300
+ const code = await proc.exited;
70301
+ if (code !== 0) {
70302
+ const err = new Error(`command \`${cmd[0]}\` failed`);
70303
+ err.stderr = stderr;
70304
+ err.code = code;
70305
+ throw err;
70306
+ }
70307
+ return { stdout, stderr };
70308
+ }
70309
+ };
70154
70310
  var lineCounter = 0;
70155
70311
  function nextId() {
70156
70312
  lineCounter += 1;
@@ -70180,10 +70336,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70180
70336
  exit();
70181
70337
  return;
70182
70338
  }
70339
+ const inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
70340
+ const baseStatuses = args.linearStatus.length ? args.linearStatus : cfg.linear.statuses;
70341
+ const effectiveStatuses = inProgressName && baseStatuses.length > 0 && !baseStatuses.includes(inProgressName) ? [...baseStatuses, inProgressName] : baseStatuses;
70183
70342
  const filter2 = {
70184
70343
  team: args.linearTeam || cfg.linear.team,
70185
70344
  assignee: args.linearAssignee || cfg.linear.assignee,
70186
- statuses: args.linearStatus.length ? args.linearStatus : cfg.linear.statuses,
70345
+ statuses: effectiveStatuses,
70187
70346
  labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
70188
70347
  };
70189
70348
  const stateCache = new Map;
@@ -70192,6 +70351,8 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70192
70351
  const useWorktree = args.worktree || cfg.useWorktree;
70193
70352
  const cwdByChange = new Map;
70194
70353
  const statesDirByChange = new Map;
70354
+ const branchByChange = new Map;
70355
+ const issueByChange = new Map;
70195
70356
  async function runScript(label, cmd, cwd2) {
70196
70357
  appendLog(` ${label}: ${cmd}`, "gray");
70197
70358
  const proc = Bun.spawn({
@@ -70220,11 +70381,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70220
70381
  let workerCwd = projectRoot;
70221
70382
  let scaffoldTasksDir = tasksDir;
70222
70383
  let scaffoldStatesDir = statesDir;
70384
+ let workerBranch = null;
70223
70385
  const probeName = issue.identifier.toLowerCase();
70224
70386
  if (useWorktree) {
70225
70387
  try {
70226
70388
  const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
70227
70389
  workerCwd = wt.cwd;
70390
+ workerBranch = wt.branch;
70228
70391
  scaffoldTasksDir = join14(wt.cwd, "openspec", "changes");
70229
70392
  scaffoldStatesDir = join14(wt.cwd, ".ralph", "tasks");
70230
70393
  appendLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
@@ -70236,43 +70399,130 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70236
70399
  const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
70237
70400
  cwdByChange.set(changeName, workerCwd);
70238
70401
  statesDirByChange.set(changeName, scaffoldStatesDir);
70402
+ issueByChange.set(changeName, issue);
70403
+ if (workerBranch)
70404
+ branchByChange.set(changeName, workerBranch);
70239
70405
  if (cfg.setupScript) {
70240
70406
  await runScript("setup", cfg.setupScript, workerCwd);
70241
70407
  }
70242
70408
  return changeName;
70243
70409
  },
70244
70410
  spawnWorker: (changeName) => {
70245
- const cmd = [
70246
- process.execPath,
70247
- process.argv[1] ?? "",
70248
- "task",
70249
- "--name",
70250
- changeName,
70251
- "--" + (args.engineSet ? args.engine : cfg.engine),
70252
- args.engineSet ? args.model : cfg.model
70253
- ];
70254
- const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
70255
- if (maxIter > 0)
70256
- cmd.push("--max-iterations", String(maxIter));
70257
- const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
70258
- if (maxCost > 0)
70259
- cmd.push("--max-cost", String(maxCost));
70411
+ const buildTaskCmd = () => {
70412
+ const c = [
70413
+ process.execPath,
70414
+ process.argv[1] ?? "",
70415
+ "task",
70416
+ "--name",
70417
+ changeName,
70418
+ "--" + (args.engineSet ? args.engine : cfg.engine),
70419
+ args.engineSet ? args.model : cfg.model
70420
+ ];
70421
+ const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
70422
+ if (maxIter > 0)
70423
+ c.push("--max-iterations", String(maxIter));
70424
+ const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
70425
+ if (maxCost > 0)
70426
+ c.push("--max-cost", String(maxCost));
70427
+ const maxRuntime = args.maxRuntimeMinutes || cfg.maxRuntimeMinutesPerTask;
70428
+ if (maxRuntime > 0)
70429
+ c.push("--max-runtime", String(maxRuntime));
70430
+ const maxFailures = args.maxConsecutiveFailures !== 5 ? args.maxConsecutiveFailures : cfg.maxConsecutiveFailuresPerTask;
70431
+ if (maxFailures !== 5)
70432
+ c.push("--max-failures", String(maxFailures));
70433
+ const delay2 = args.delay || cfg.iterationDelaySeconds;
70434
+ if (delay2 > 0)
70435
+ c.push("--delay", String(delay2));
70436
+ if (args.log || cfg.logRawStream)
70437
+ c.push("--log");
70438
+ if (args.verbose || cfg.taskVerbose)
70439
+ c.push("--verbose");
70440
+ return c;
70441
+ };
70260
70442
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
70261
70443
  const proc = Bun.spawn({
70262
- cmd,
70444
+ cmd: buildTaskCmd(),
70263
70445
  cwd: cwd2,
70264
70446
  stdout: "ignore",
70265
70447
  stderr: "ignore",
70266
70448
  stdin: "ignore"
70267
70449
  });
70450
+ const wantPr = args.createPr || cfg.createPrOnSuccess;
70268
70451
  const wrapped = proc.exited.then(async (code) => {
70269
70452
  if (cfg.teardownScript) {
70270
70453
  try {
70271
70454
  await runScript("teardown", cfg.teardownScript, cwd2);
70272
70455
  } catch {}
70273
70456
  }
70457
+ const ok = code === 0;
70458
+ if (ok && wantPr) {
70459
+ const branch = branchByChange.get(changeName);
70460
+ const prIssue = issueByChange.get(changeName);
70461
+ if (!branch || !prIssue) {
70462
+ appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
70463
+ } else {
70464
+ try {
70465
+ const pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
70466
+ if (!pr) {
70467
+ appendLog(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
70468
+ } else {
70469
+ appendLog(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
70470
+ const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
70471
+ if (wantFixCi) {
70472
+ appendLog(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
70473
+ const proposalPath = join14(statesDirByChange.get(changeName) ?? statesDir, "..", "..", "openspec", "changes", changeName, "proposal.md");
70474
+ const result2 = await fixCiUntilGreen({
70475
+ getStatus: () => getPrChecksStatus(pr.url, bunCmdRunner, cwd2),
70476
+ getFailedLogs: (ids) => fetchFailedRunLogs(ids, bunCmdRunner, cwd2),
70477
+ runTaskWithSteering: async (steering) => {
70478
+ try {
70479
+ const file = Bun.file(proposalPath);
70480
+ const prev = await file.exists() ? await file.text() : "";
70481
+ const stamped = `
70482
+
70483
+ ### CI feedback (${new Date().toISOString()})
70484
+
70485
+ ${steering}
70486
+ `;
70487
+ const next = prev.includes("## Steering") ? prev.replace(/## Steering\n+([\s\S]*?)$/, (_m, rest2) => `## Steering
70488
+ ${stamped}
70489
+ ${rest2}`) : prev + `
70490
+ ## Steering
70491
+ ${stamped}
70492
+ `;
70493
+ await Bun.write(proposalPath, next);
70494
+ } catch (err) {
70495
+ appendLog(`! could not append steering: ${err.message}`, "red");
70496
+ }
70497
+ const p = Bun.spawn({
70498
+ cmd: buildTaskCmd(),
70499
+ cwd: cwd2,
70500
+ stdout: "ignore",
70501
+ stderr: "ignore",
70502
+ stdin: "ignore"
70503
+ });
70504
+ return p.exited;
70505
+ },
70506
+ pushBranch: async () => {
70507
+ await bunCmdRunner.run(["git", "push", "origin", branch], cwd2);
70508
+ },
70509
+ log: appendLog,
70510
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms))
70511
+ }, {
70512
+ maxAttempts: cfg.maxCiFixAttempts,
70513
+ pollIntervalSeconds: cfg.ciPollIntervalSeconds
70514
+ });
70515
+ if (!result2.success) {
70516
+ appendLog(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"})`, "red");
70517
+ }
70518
+ }
70519
+ }
70520
+ } catch (err) {
70521
+ appendLog(`! PR create failed for ${changeName}: ${err.message}`, "red");
70522
+ }
70523
+ }
70524
+ }
70274
70525
  if (useWorktree && cwd2 !== projectRoot) {
70275
- const ok = code === 0;
70276
70526
  if (ok && cfg.cleanupWorktreeOnSuccess) {
70277
70527
  try {
70278
70528
  await removeWorktree(projectRoot, cwd2, bunGitRunner);
@@ -70284,6 +70534,8 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70284
70534
  }
70285
70535
  cwdByChange.delete(changeName);
70286
70536
  statesDirByChange.delete(changeName);
70537
+ branchByChange.delete(changeName);
70538
+ issueByChange.delete(changeName);
70287
70539
  return code;
70288
70540
  });
70289
70541
  return { exited: wrapped, kill: () => proc.kill() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.6",
3
+ "version": "2.7.8",
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",