@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.
- package/README.md +26 -13
- package/dist/cli/index.js +169 -17
- 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
|
|
70325
|
-
|
|
70326
|
-
|
|
70327
|
-
|
|
70328
|
-
|
|
70329
|
-
|
|
70330
|
-
|
|
70331
|
-
|
|
70332
|
-
|
|
70333
|
-
|
|
70334
|
-
|
|
70335
|
-
|
|
70336
|
-
|
|
70337
|
-
|
|
70338
|
-
|
|
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");
|