@neriros/ralphy 2.7.6 → 2.7.7
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 +17 -12
- package/dist/cli/index.js +103 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,6 +111,8 @@ 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",
|
|
114
116
|
}
|
|
115
117
|
```
|
|
116
118
|
|
|
@@ -126,6 +128,8 @@ Use `setupScript` (run inside the worktree right after scaffolding) to install d
|
|
|
126
128
|
|
|
127
129
|
**`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
130
|
|
|
131
|
+
**`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
|
+
|
|
129
133
|
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
134
|
|
|
131
135
|
## CLI Options
|
|
@@ -149,18 +153,19 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
|
|
|
149
153
|
|
|
150
154
|
### Agent mode flags
|
|
151
155
|
|
|
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
|
|
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`) |
|
|
164
169
|
|
|
165
170
|
## OpenSpec Flow
|
|
166
171
|
|
package/dist/cli/index.js
CHANGED
|
@@ -56156,6 +56156,7 @@ 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)",
|
|
56159
56160
|
"",
|
|
56160
56161
|
" --help, -h Show this help message",
|
|
56161
56162
|
"",
|
|
@@ -56195,7 +56196,8 @@ async function parseArgs(argv) {
|
|
|
56195
56196
|
worktree: false,
|
|
56196
56197
|
inProgressStatus: "",
|
|
56197
56198
|
doneStatus: "",
|
|
56198
|
-
doneLabel: ""
|
|
56199
|
+
doneLabel: "",
|
|
56200
|
+
createPr: false
|
|
56199
56201
|
};
|
|
56200
56202
|
let expectModel = false;
|
|
56201
56203
|
let expectModelFlag = false;
|
|
@@ -56416,6 +56418,9 @@ async function parseArgs(argv) {
|
|
|
56416
56418
|
case "--done-label":
|
|
56417
56419
|
expectDoneLabel = true;
|
|
56418
56420
|
break;
|
|
56421
|
+
case "--create-pr":
|
|
56422
|
+
result2.createPr = true;
|
|
56423
|
+
break;
|
|
56419
56424
|
default:
|
|
56420
56425
|
if (VALID_MODES.has(arg)) {
|
|
56421
56426
|
result2.mode = arg;
|
|
@@ -69845,6 +69850,8 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
69845
69850
|
setupScript: exports_external.string().optional(),
|
|
69846
69851
|
teardownScript: exports_external.string().optional(),
|
|
69847
69852
|
appendPrompt: exports_external.string().optional(),
|
|
69853
|
+
createPrOnSuccess: exports_external.boolean().default(false),
|
|
69854
|
+
prBaseBranch: exports_external.string().default("main"),
|
|
69848
69855
|
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
69849
69856
|
model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
|
|
69850
69857
|
linear: exports_external.object({
|
|
@@ -70133,6 +70140,53 @@ async function removeWorktree(projectRoot, cwd2, runner) {
|
|
|
70133
70140
|
await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
|
|
70134
70141
|
}
|
|
70135
70142
|
|
|
70143
|
+
// apps/cli/src/agent/pr.ts
|
|
70144
|
+
function defaultTitle(issue) {
|
|
70145
|
+
return `${issue.identifier}: ${issue.title}`;
|
|
70146
|
+
}
|
|
70147
|
+
function defaultBody(issue, branch) {
|
|
70148
|
+
return [
|
|
70149
|
+
`Auto-generated by Ralph for ${issue.identifier}.`,
|
|
70150
|
+
"",
|
|
70151
|
+
`Source: ${issue.url}`,
|
|
70152
|
+
`Branch: \`${branch}\``,
|
|
70153
|
+
"",
|
|
70154
|
+
issue.description?.trim() ? `## Description
|
|
70155
|
+
|
|
70156
|
+
${issue.description.trim()}` : ""
|
|
70157
|
+
].filter(Boolean).join(`
|
|
70158
|
+
`);
|
|
70159
|
+
}
|
|
70160
|
+
async function createPullRequest(input, runner) {
|
|
70161
|
+
const base2 = input.base ?? "main";
|
|
70162
|
+
const log2 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
|
|
70163
|
+
if (log2.stdout.trim() === "")
|
|
70164
|
+
return null;
|
|
70165
|
+
await runner.run(["git", "push", "-u", "origin", input.branch], input.cwd);
|
|
70166
|
+
const existing = await runner.run([
|
|
70167
|
+
"gh",
|
|
70168
|
+
"pr",
|
|
70169
|
+
"list",
|
|
70170
|
+
"--head",
|
|
70171
|
+
input.branch,
|
|
70172
|
+
"--state",
|
|
70173
|
+
"open",
|
|
70174
|
+
"--json",
|
|
70175
|
+
"url",
|
|
70176
|
+
"--jq",
|
|
70177
|
+
".[0].url // empty"
|
|
70178
|
+
], input.cwd);
|
|
70179
|
+
const existingUrl = existing.stdout.trim();
|
|
70180
|
+
if (existingUrl)
|
|
70181
|
+
return { url: existingUrl, created: false };
|
|
70182
|
+
const title = defaultTitle(input.issue);
|
|
70183
|
+
const body = defaultBody(input.issue, input.branch);
|
|
70184
|
+
const created = await runner.run(["gh", "pr", "create", "--base", base2, "--title", title, "--body", body], input.cwd);
|
|
70185
|
+
const url = created.stdout.trim().split(`
|
|
70186
|
+
`).pop() ?? "";
|
|
70187
|
+
return { url, created: true };
|
|
70188
|
+
}
|
|
70189
|
+
|
|
70136
70190
|
// apps/cli/src/components/AgentMode.tsx
|
|
70137
70191
|
var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
|
|
70138
70192
|
import { join as join14 } from "path";
|
|
@@ -70151,6 +70205,21 @@ var bunGitRunner = {
|
|
|
70151
70205
|
return { stdout, stderr };
|
|
70152
70206
|
}
|
|
70153
70207
|
};
|
|
70208
|
+
var bunCmdRunner = {
|
|
70209
|
+
run: async (cmd, cwd2) => {
|
|
70210
|
+
const proc = Bun.spawn({ cmd, cwd: cwd2, stdout: "pipe", stderr: "pipe" });
|
|
70211
|
+
const stdout = await new Response(proc.stdout).text();
|
|
70212
|
+
const stderr = await new Response(proc.stderr).text();
|
|
70213
|
+
const code = await proc.exited;
|
|
70214
|
+
if (code !== 0) {
|
|
70215
|
+
const err = new Error(`command \`${cmd[0]}\` failed`);
|
|
70216
|
+
err.stderr = stderr;
|
|
70217
|
+
err.code = code;
|
|
70218
|
+
throw err;
|
|
70219
|
+
}
|
|
70220
|
+
return { stdout, stderr };
|
|
70221
|
+
}
|
|
70222
|
+
};
|
|
70154
70223
|
var lineCounter = 0;
|
|
70155
70224
|
function nextId() {
|
|
70156
70225
|
lineCounter += 1;
|
|
@@ -70180,10 +70249,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70180
70249
|
exit();
|
|
70181
70250
|
return;
|
|
70182
70251
|
}
|
|
70252
|
+
const inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
|
|
70253
|
+
const baseStatuses = args.linearStatus.length ? args.linearStatus : cfg.linear.statuses;
|
|
70254
|
+
const effectiveStatuses = inProgressName && baseStatuses.length > 0 && !baseStatuses.includes(inProgressName) ? [...baseStatuses, inProgressName] : baseStatuses;
|
|
70183
70255
|
const filter2 = {
|
|
70184
70256
|
team: args.linearTeam || cfg.linear.team,
|
|
70185
70257
|
assignee: args.linearAssignee || cfg.linear.assignee,
|
|
70186
|
-
statuses:
|
|
70258
|
+
statuses: effectiveStatuses,
|
|
70187
70259
|
labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
|
|
70188
70260
|
};
|
|
70189
70261
|
const stateCache = new Map;
|
|
@@ -70192,6 +70264,8 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70192
70264
|
const useWorktree = args.worktree || cfg.useWorktree;
|
|
70193
70265
|
const cwdByChange = new Map;
|
|
70194
70266
|
const statesDirByChange = new Map;
|
|
70267
|
+
const branchByChange = new Map;
|
|
70268
|
+
const issueByChange = new Map;
|
|
70195
70269
|
async function runScript(label, cmd, cwd2) {
|
|
70196
70270
|
appendLog(` ${label}: ${cmd}`, "gray");
|
|
70197
70271
|
const proc = Bun.spawn({
|
|
@@ -70220,11 +70294,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70220
70294
|
let workerCwd = projectRoot;
|
|
70221
70295
|
let scaffoldTasksDir = tasksDir;
|
|
70222
70296
|
let scaffoldStatesDir = statesDir;
|
|
70297
|
+
let workerBranch = null;
|
|
70223
70298
|
const probeName = issue.identifier.toLowerCase();
|
|
70224
70299
|
if (useWorktree) {
|
|
70225
70300
|
try {
|
|
70226
70301
|
const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
|
|
70227
70302
|
workerCwd = wt.cwd;
|
|
70303
|
+
workerBranch = wt.branch;
|
|
70228
70304
|
scaffoldTasksDir = join14(wt.cwd, "openspec", "changes");
|
|
70229
70305
|
scaffoldStatesDir = join14(wt.cwd, ".ralph", "tasks");
|
|
70230
70306
|
appendLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
|
|
@@ -70236,6 +70312,9 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70236
70312
|
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
70237
70313
|
cwdByChange.set(changeName, workerCwd);
|
|
70238
70314
|
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
70315
|
+
issueByChange.set(changeName, issue);
|
|
70316
|
+
if (workerBranch)
|
|
70317
|
+
branchByChange.set(changeName, workerBranch);
|
|
70239
70318
|
if (cfg.setupScript) {
|
|
70240
70319
|
await runScript("setup", cfg.setupScript, workerCwd);
|
|
70241
70320
|
}
|
|
@@ -70265,14 +70344,33 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70265
70344
|
stderr: "ignore",
|
|
70266
70345
|
stdin: "ignore"
|
|
70267
70346
|
});
|
|
70347
|
+
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
70268
70348
|
const wrapped = proc.exited.then(async (code) => {
|
|
70269
70349
|
if (cfg.teardownScript) {
|
|
70270
70350
|
try {
|
|
70271
70351
|
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
70272
70352
|
} catch {}
|
|
70273
70353
|
}
|
|
70354
|
+
const ok = code === 0;
|
|
70355
|
+
if (ok && wantPr) {
|
|
70356
|
+
const branch = branchByChange.get(changeName);
|
|
70357
|
+
const prIssue = issueByChange.get(changeName);
|
|
70358
|
+
if (!branch || !prIssue) {
|
|
70359
|
+
appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
|
|
70360
|
+
} else {
|
|
70361
|
+
try {
|
|
70362
|
+
const pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
|
|
70363
|
+
if (!pr) {
|
|
70364
|
+
appendLog(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
70365
|
+
} else {
|
|
70366
|
+
appendLog(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
70367
|
+
}
|
|
70368
|
+
} catch (err) {
|
|
70369
|
+
appendLog(`! PR create failed for ${changeName}: ${err.message}`, "red");
|
|
70370
|
+
}
|
|
70371
|
+
}
|
|
70372
|
+
}
|
|
70274
70373
|
if (useWorktree && cwd2 !== projectRoot) {
|
|
70275
|
-
const ok = code === 0;
|
|
70276
70374
|
if (ok && cfg.cleanupWorktreeOnSuccess) {
|
|
70277
70375
|
try {
|
|
70278
70376
|
await removeWorktree(projectRoot, cwd2, bunGitRunner);
|
|
@@ -70284,6 +70382,8 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70284
70382
|
}
|
|
70285
70383
|
cwdByChange.delete(changeName);
|
|
70286
70384
|
statesDirByChange.delete(changeName);
|
|
70385
|
+
branchByChange.delete(changeName);
|
|
70386
|
+
issueByChange.delete(changeName);
|
|
70287
70387
|
return code;
|
|
70288
70388
|
});
|
|
70289
70389
|
return { exited: wrapped, kill: () => proc.kill() };
|