@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.
- package/README.md +30 -12
- package/dist/cli/index.js +271 -19
- 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:
|
|
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
|
|
70246
|
-
|
|
70247
|
-
|
|
70248
|
-
|
|
70249
|
-
|
|
70250
|
-
|
|
70251
|
-
|
|
70252
|
-
|
|
70253
|
-
|
|
70254
|
-
|
|
70255
|
-
|
|
70256
|
-
|
|
70257
|
-
|
|
70258
|
-
|
|
70259
|
-
|
|
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() };
|