@neriros/ralphy 2.7.7 → 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 +26 -13
  2. package/dist/cli/index.js +169 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -113,6 +113,14 @@ Defaults are written to `ralphy.config.json` on first run; CLI flags override co
113
113
  "appendPrompt": "Always run lint before committing.",
114
114
  "createPrOnSuccess": true,
115
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,
116
124
  }
117
125
  ```
118
126
 
@@ -130,6 +138,10 @@ Use `setupScript` (run inside the worktree right after scaffolding) to install d
130
138
 
131
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`.
132
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
+
133
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.
134
146
 
135
147
  ## CLI Options
@@ -153,19 +165,20 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
153
165
 
154
166
  ### Agent mode flags
155
167
 
156
- | Option | Description |
157
- | ----------------------------- | --------------------------------------------------------------------- |
158
- | `--linear-team <key>` | Linear team key (e.g. `ENG`) |
159
- | `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
160
- | `--linear-status <name>` | Filter by status name (repeatable) |
161
- | `--linear-label <name>` | Filter by label name (repeatable, any-of) |
162
- | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
163
- | `--concurrency <n>` | Max concurrent task loops (default: 1) |
164
- | `--worktree` | Run each task in its own git worktree |
165
- | `--in-progress-status <name>` | Linear status to set when work starts |
166
- | `--done-status <name>` | Linear status to set on successful completion |
167
- | `--done-label <name>` | Linear label to add on successful completion |
168
- | `--create-pr` | Push worker branch + open a GitHub PR on success (needs `--worktree`) |
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`) |
169
182
 
170
183
  ## OpenSpec Flow
171
184
 
package/dist/cli/index.js CHANGED
@@ -56157,6 +56157,7 @@ var HELP_TEXT = [
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
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)",
56160
56161
  "",
56161
56162
  " --help, -h Show this help message",
56162
56163
  "",
@@ -56197,7 +56198,8 @@ async function parseArgs(argv) {
56197
56198
  inProgressStatus: "",
56198
56199
  doneStatus: "",
56199
56200
  doneLabel: "",
56200
- createPr: false
56201
+ createPr: false,
56202
+ fixCi: false
56201
56203
  };
56202
56204
  let expectModel = false;
56203
56205
  let expectModelFlag = false;
@@ -56421,6 +56423,9 @@ async function parseArgs(argv) {
56421
56423
  case "--create-pr":
56422
56424
  result2.createPr = true;
56423
56425
  break;
56426
+ case "--fix-ci":
56427
+ result2.fixCi = true;
56428
+ break;
56424
56429
  default:
56425
56430
  if (VALID_MODES.has(arg)) {
56426
56431
  result2.mode = arg;
@@ -69845,6 +69850,11 @@ var RalphyConfigSchema = exports_external.object({
69845
69850
  pollIntervalSeconds: exports_external.number().int().positive().default(60),
69846
69851
  maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
69847
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),
69848
69858
  useWorktree: exports_external.boolean().default(false),
69849
69859
  cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
69850
69860
  setupScript: exports_external.string().optional(),
@@ -69852,6 +69862,9 @@ var RalphyConfigSchema = exports_external.object({
69852
69862
  appendPrompt: exports_external.string().optional(),
69853
69863
  createPrOnSuccess: exports_external.boolean().default(false),
69854
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),
69855
69868
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
69856
69869
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
69857
69870
  linear: exports_external.object({
@@ -70187,6 +70200,80 @@ async function createPullRequest(input, runner) {
70187
70200
  return { url, created: true };
70188
70201
  }
70189
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
+
70190
70277
  // apps/cli/src/components/AgentMode.tsx
70191
70278
  var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
70192
70279
  import { join as join14 } from "path";
@@ -70321,24 +70408,40 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70321
70408
  return changeName;
70322
70409
  },
70323
70410
  spawnWorker: (changeName) => {
70324
- const cmd = [
70325
- process.execPath,
70326
- process.argv[1] ?? "",
70327
- "task",
70328
- "--name",
70329
- changeName,
70330
- "--" + (args.engineSet ? args.engine : cfg.engine),
70331
- args.engineSet ? args.model : cfg.model
70332
- ];
70333
- const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
70334
- if (maxIter > 0)
70335
- cmd.push("--max-iterations", String(maxIter));
70336
- const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
70337
- if (maxCost > 0)
70338
- 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
+ };
70339
70442
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
70340
70443
  const proc = Bun.spawn({
70341
- cmd,
70444
+ cmd: buildTaskCmd(),
70342
70445
  cwd: cwd2,
70343
70446
  stdout: "ignore",
70344
70447
  stderr: "ignore",
@@ -70364,6 +70467,55 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70364
70467
  appendLog(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
70365
70468
  } else {
70366
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
+ }
70367
70519
  }
70368
70520
  } catch (err) {
70369
70521
  appendLog(`! PR create failed for ${changeName}: ${err.message}`, "red");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.7",
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",