@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 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: args.linearStatus.length ? args.linearStatus : cfg.linear.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() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.6",
3
+ "version": "2.7.7",
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",