@neriros/ralphy 2.7.5 → 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
@@ -104,11 +104,15 @@ Defaults are written to `ralphy.config.json` on first run; CLI flags override co
104
104
  "doneStatus": "In Review",
105
105
  "doneLabel": "ralphy-done",
106
106
  "postComments": true,
107
+ "updateEveryIterations": 10,
107
108
  },
108
109
  "useWorktree": true,
109
110
  "cleanupWorktreeOnSuccess": false,
110
111
  "setupScript": "bun install",
111
112
  "teardownScript": "git status",
113
+ "appendPrompt": "Always run lint before committing.",
114
+ "createPrOnSuccess": true,
115
+ "prBaseBranch": "main",
112
116
  }
113
117
  ```
114
118
 
@@ -120,6 +124,12 @@ With `--worktree` (or `useWorktree: true` in config) each task runs in an isolat
120
124
 
121
125
  Use `setupScript` (run inside the worktree right after scaffolding) to install dependencies, copy `.env`, etc. Use `teardownScript` (run after the loop exits, before any worktree cleanup) to gather artifacts or roll back local mutations. Both run via `sh -c`; failures are logged but never block the loop. With `cleanupWorktreeOnSuccess: true` the worktree is removed when the worker exits 0 — failed workers always keep their worktree (and branch) for human inspection.
122
126
 
127
+ **`appendPrompt`** (or `--prompt` in agent mode) is appended to every scaffolded `proposal.md` under an `## Additional instructions` section — use it for cross-cutting guidance every task should see.
128
+
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`.
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
+
123
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.
124
134
 
125
135
  ## CLI Options
@@ -143,18 +153,19 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
143
153
 
144
154
  ### Agent mode flags
145
155
 
146
- | Option | Description |
147
- | ----------------------------- | --------------------------------------------- |
148
- | `--linear-team <key>` | Linear team key (e.g. `ENG`) |
149
- | `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
150
- | `--linear-status <name>` | Filter by status name (repeatable) |
151
- | `--linear-label <name>` | Filter by label name (repeatable, any-of) |
152
- | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
153
- | `--concurrency <n>` | Max concurrent task loops (default: 1) |
154
- | `--worktree` | Run each task in its own git worktree |
155
- | `--in-progress-status <name>` | Linear status to set when work starts |
156
- | `--done-status <name>` | Linear status to set on successful completion |
157
- | `--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`) |
158
169
 
159
170
  ## OpenSpec Flow
160
171
 
package/dist/cli/index.js CHANGED
@@ -56131,7 +56131,7 @@ var HELP_TEXT = [
56131
56131
  "",
56132
56132
  "Options:",
56133
56133
  " --name <name> Change name (required for most commands)",
56134
- " --prompt <text> Task description",
56134
+ " --prompt <text> Task description (in agent mode: appended to every scaffolded proposal)",
56135
56135
  " --prompt-file <path> Read prompt from file",
56136
56136
  " --model <model> Set model (haiku|sonnet|opus)",
56137
56137
  " --claude [model] Use Claude engine (haiku|sonnet|opus, default: opus)",
@@ -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;
@@ -69772,7 +69777,7 @@ function changeNameForIssue(issue) {
69772
69777
  const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
69773
69778
  return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
69774
69779
  }
69775
- async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = []) {
69780
+ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
69776
69781
  const name = changeNameForIssue(issue);
69777
69782
  const changeDir = join11(tasksDir, name);
69778
69783
  const stateDir = join11(statesDir, name);
@@ -69802,6 +69807,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [])
69802
69807
  "",
69803
69808
  issue.description?.trim() || "_No description provided in Linear._",
69804
69809
  ...commentsBlock,
69810
+ ...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
69805
69811
  "",
69806
69812
  "## Steering",
69807
69813
  "",
@@ -69843,6 +69849,9 @@ var RalphyConfigSchema = exports_external.object({
69843
69849
  cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
69844
69850
  setupScript: exports_external.string().optional(),
69845
69851
  teardownScript: exports_external.string().optional(),
69852
+ appendPrompt: exports_external.string().optional(),
69853
+ createPrOnSuccess: exports_external.boolean().default(false),
69854
+ prBaseBranch: exports_external.string().default("main"),
69846
69855
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
69847
69856
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
69848
69857
  linear: exports_external.object({
@@ -69853,7 +69862,8 @@ var RalphyConfigSchema = exports_external.object({
69853
69862
  inProgressStatus: exports_external.string().optional(),
69854
69863
  doneStatus: exports_external.string().optional(),
69855
69864
  doneLabel: exports_external.string().optional(),
69856
- postComments: exports_external.boolean().default(true)
69865
+ postComments: exports_external.boolean().default(true),
69866
+ updateEveryIterations: exports_external.number().int().nonnegative().default(10)
69857
69867
  }).default({ statuses: [], labels: [], postComments: true })
69858
69868
  }).default({
69859
69869
  concurrency: 1,
@@ -69939,8 +69949,37 @@ class AgentCoordinator {
69939
69949
  state.lastPollAt = new Date().toISOString();
69940
69950
  await this.deps.saveState(state);
69941
69951
  this.spawnNext();
69952
+ await this.reportProgress();
69942
69953
  return { found: issues.length, added };
69943
69954
  }
69955
+ async reportProgress() {
69956
+ const updater = this.deps.updater;
69957
+ const everyN = this.opts.commentEveryIterations ?? 0;
69958
+ if (everyN <= 0 || !updater || this.opts.postComments === false || !this.deps.getIterationCount) {
69959
+ return;
69960
+ }
69961
+ for (const w of this.workers) {
69962
+ let count;
69963
+ try {
69964
+ count = await this.deps.getIterationCount(w.changeName);
69965
+ } catch (err) {
69966
+ this.deps.onLog(`! iteration count read failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
69967
+ continue;
69968
+ }
69969
+ if (count < everyN)
69970
+ continue;
69971
+ const currMilestone = Math.floor(count / everyN);
69972
+ const lastMilestone = Math.floor(w.lastReportedIteration / everyN);
69973
+ if (currMilestone <= lastMilestone)
69974
+ continue;
69975
+ try {
69976
+ await updater.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
69977
+ w.lastReportedIteration = count;
69978
+ } catch (err) {
69979
+ this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
69980
+ }
69981
+ }
69982
+ }
69944
69983
  spawnNext() {
69945
69984
  if (this.stopped || !this.state)
69946
69985
  return;
@@ -69970,7 +70009,9 @@ class AgentCoordinator {
69970
70009
  changeName,
69971
70010
  issueId: issue.id,
69972
70011
  issueIdentifier: issue.identifier,
69973
- kill: handle.kill
70012
+ issue,
70013
+ kill: handle.kill,
70014
+ lastReportedIteration: 0
69974
70015
  };
69975
70016
  this.workers.push(worker);
69976
70017
  this.pendingIds.delete(issue.id);
@@ -70099,6 +70140,53 @@ async function removeWorktree(projectRoot, cwd2, runner) {
70099
70140
  await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
70100
70141
  }
70101
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
+
70102
70190
  // apps/cli/src/components/AgentMode.tsx
70103
70191
  var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
70104
70192
  import { join as join14 } from "path";
@@ -70117,6 +70205,21 @@ var bunGitRunner = {
70117
70205
  return { stdout, stderr };
70118
70206
  }
70119
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
+ };
70120
70223
  var lineCounter = 0;
70121
70224
  function nextId() {
70122
70225
  lineCounter += 1;
@@ -70146,10 +70249,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70146
70249
  exit();
70147
70250
  return;
70148
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;
70149
70255
  const filter2 = {
70150
70256
  team: args.linearTeam || cfg.linear.team,
70151
70257
  assignee: args.linearAssignee || cfg.linear.assignee,
70152
- statuses: args.linearStatus.length ? args.linearStatus : cfg.linear.statuses,
70258
+ statuses: effectiveStatuses,
70153
70259
  labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
70154
70260
  };
70155
70261
  const stateCache = new Map;
@@ -70157,6 +70263,9 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70157
70263
  const teamKeyOf = (issue) => issue.identifier.split("-")[0];
70158
70264
  const useWorktree = args.worktree || cfg.useWorktree;
70159
70265
  const cwdByChange = new Map;
70266
+ const statesDirByChange = new Map;
70267
+ const branchByChange = new Map;
70268
+ const issueByChange = new Map;
70160
70269
  async function runScript(label, cmd, cwd2) {
70161
70270
  appendLog(` ${label}: ${cmd}`, "gray");
70162
70271
  const proc = Bun.spawn({
@@ -70185,11 +70294,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70185
70294
  let workerCwd = projectRoot;
70186
70295
  let scaffoldTasksDir = tasksDir;
70187
70296
  let scaffoldStatesDir = statesDir;
70297
+ let workerBranch = null;
70188
70298
  const probeName = issue.identifier.toLowerCase();
70189
70299
  if (useWorktree) {
70190
70300
  try {
70191
70301
  const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
70192
70302
  workerCwd = wt.cwd;
70303
+ workerBranch = wt.branch;
70193
70304
  scaffoldTasksDir = join14(wt.cwd, "openspec", "changes");
70194
70305
  scaffoldStatesDir = join14(wt.cwd, ".ralph", "tasks");
70195
70306
  appendLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
@@ -70197,8 +70308,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70197
70308
  appendLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
70198
70309
  }
70199
70310
  }
70200
- const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments);
70311
+ const appendPrompt = args.prompt || cfg.appendPrompt || "";
70312
+ const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
70201
70313
  cwdByChange.set(changeName, workerCwd);
70314
+ statesDirByChange.set(changeName, scaffoldStatesDir);
70315
+ issueByChange.set(changeName, issue);
70316
+ if (workerBranch)
70317
+ branchByChange.set(changeName, workerBranch);
70202
70318
  if (cfg.setupScript) {
70203
70319
  await runScript("setup", cfg.setupScript, workerCwd);
70204
70320
  }
@@ -70228,14 +70344,33 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70228
70344
  stderr: "ignore",
70229
70345
  stdin: "ignore"
70230
70346
  });
70347
+ const wantPr = args.createPr || cfg.createPrOnSuccess;
70231
70348
  const wrapped = proc.exited.then(async (code) => {
70232
70349
  if (cfg.teardownScript) {
70233
70350
  try {
70234
70351
  await runScript("teardown", cfg.teardownScript, cwd2);
70235
70352
  } catch {}
70236
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
+ }
70237
70373
  if (useWorktree && cwd2 !== projectRoot) {
70238
- const ok = code === 0;
70239
70374
  if (ok && cfg.cleanupWorktreeOnSuccess) {
70240
70375
  try {
70241
70376
  await removeWorktree(projectRoot, cwd2, bunGitRunner);
@@ -70246,6 +70381,9 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70246
70381
  }
70247
70382
  }
70248
70383
  cwdByChange.delete(changeName);
70384
+ statesDirByChange.delete(changeName);
70385
+ branchByChange.delete(changeName);
70386
+ issueByChange.delete(changeName);
70249
70387
  return code;
70250
70388
  });
70251
70389
  return { exited: wrapped, kill: () => proc.kill() };
@@ -70254,6 +70392,14 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70254
70392
  saveState: (s) => writeAgentState(projectRoot, s),
70255
70393
  onLog: appendLog,
70256
70394
  onWorkersChanged: () => setTick((t) => t + 1),
70395
+ getIterationCount: async (changeName) => {
70396
+ const dir = statesDirByChange.get(changeName) ?? statesDir;
70397
+ const file = Bun.file(join14(dir, changeName, ".ralph-state.json"));
70398
+ if (!await file.exists())
70399
+ return 0;
70400
+ const json = await file.json();
70401
+ return json.iteration ?? 0;
70402
+ },
70257
70403
  updater: {
70258
70404
  postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
70259
70405
  setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
@@ -70285,7 +70431,8 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70285
70431
  inProgressStatus: args.inProgressStatus || cfg.linear.inProgressStatus,
70286
70432
  doneStatus: args.doneStatus || cfg.linear.doneStatus,
70287
70433
  doneLabel: args.doneLabel || cfg.linear.doneLabel,
70288
- postComments: cfg.linear.postComments
70434
+ postComments: cfg.linear.postComments,
70435
+ commentEveryIterations: cfg.linear.updateEveryIterations
70289
70436
  });
70290
70437
  coordRef.current = coord2;
70291
70438
  await coord2.init();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.5",
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",