@neriros/ralphy 2.7.3 → 2.7.4

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
@@ -105,11 +105,21 @@ Defaults are written to `ralphy.config.json` on first run; CLI flags override co
105
105
  "doneLabel": "ralphy-done",
106
106
  "postComments": true,
107
107
  },
108
+ "useWorktree": true,
109
+ "cleanupWorktreeOnSuccess": false,
110
+ "setupScript": "bun install",
111
+ "teardownScript": "git status",
108
112
  }
109
113
  ```
110
114
 
111
115
  `doneStatus` and `doneLabel` are independent — set either, both, or neither. Use `doneLabel` if your team marks completion via a label rather than a workflow state.
112
116
 
117
+ #### Per-task git worktrees
118
+
119
+ With `--worktree` (or `useWorktree: true` in config) each task runs in an isolated worktree at `.ralph/worktrees/<change-name>` checked out onto a fresh `ralph/<change-name>` branch. The change is scaffolded _inside_ the worktree, and the loop's cwd is the worktree, so concurrent workers can't stomp on each other.
120
+
121
+ 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
+
113
123
  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.
114
124
 
115
125
  ## CLI Options
@@ -141,6 +151,7 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
141
151
  | `--linear-label <name>` | Filter by label name (repeatable, any-of) |
142
152
  | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
143
153
  | `--concurrency <n>` | Max concurrent task loops (default: 1) |
154
+ | `--worktree` | Run each task in its own git worktree |
144
155
 
145
156
  ## OpenSpec Flow
146
157
 
package/dist/cli/index.js CHANGED
@@ -49811,10 +49811,10 @@ var require_axios = __commonJS((exports, module) => {
49811
49811
  } = utils$1;
49812
49812
  var globalFetchAPI = (({
49813
49813
  Request,
49814
- Response
49814
+ Response: Response2
49815
49815
  }) => ({
49816
49816
  Request,
49817
- Response
49817
+ Response: Response2
49818
49818
  }))(utils$1.global);
49819
49819
  var {
49820
49820
  ReadableStream: ReadableStream$1,
@@ -49834,11 +49834,11 @@ var require_axios = __commonJS((exports, module) => {
49834
49834
  const {
49835
49835
  fetch: envFetch,
49836
49836
  Request,
49837
- Response
49837
+ Response: Response2
49838
49838
  } = env3;
49839
49839
  const isFetchSupported = envFetch ? isFunction2(envFetch) : typeof fetch === "function";
49840
49840
  const isRequestSupported = isFunction2(Request);
49841
- const isResponseSupported = isFunction2(Response);
49841
+ const isResponseSupported = isFunction2(Response2);
49842
49842
  if (!isFetchSupported) {
49843
49843
  return false;
49844
49844
  }
@@ -49860,7 +49860,7 @@ var require_axios = __commonJS((exports, module) => {
49860
49860
  }
49861
49861
  return duplexAccessed && !hasContentType;
49862
49862
  });
49863
- const supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(() => utils$1.isReadableStream(new Response("").body));
49863
+ const supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(() => utils$1.isReadableStream(new Response2("").body));
49864
49864
  const resolvers = {
49865
49865
  stream: supportsResponseStream && ((res) => res.body)
49866
49866
  };
@@ -49971,7 +49971,7 @@ var require_axios = __commonJS((exports, module) => {
49971
49971
  });
49972
49972
  const responseContentLength = utils$1.toFiniteNumber(response.headers.get("content-length"));
49973
49973
  const [onProgress, flush] = onDownloadProgress && progressEventDecorator(responseContentLength, progressEventReducer(asyncDecorator(onDownloadProgress), true)) || [];
49974
- response = new Response(trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
49974
+ response = new Response2(trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
49975
49975
  flush && flush();
49976
49976
  unsubscribe && unsubscribe();
49977
49977
  }), options);
@@ -50006,9 +50006,9 @@ var require_axios = __commonJS((exports, module) => {
50006
50006
  const {
50007
50007
  fetch: fetch2,
50008
50008
  Request,
50009
- Response
50009
+ Response: Response2
50010
50010
  } = env3;
50011
- const seeds = [Request, Response, fetch2];
50011
+ const seeds = [Request, Response2, fetch2];
50012
50012
  let len = seeds.length, i = len, seed, target, map2 = seedCache;
50013
50013
  while (i--) {
50014
50014
  seed = seeds[i];
@@ -50548,7 +50548,7 @@ var require_axios = __commonJS((exports, module) => {
50548
50548
  });
50549
50549
 
50550
50550
  // apps/cli/src/index.ts
50551
- import { resolve, join as join15, dirname as dirname4 } from "path";
50551
+ import { resolve, join as join17, dirname as dirname4 } from "path";
50552
50552
  import { exists, mkdir as mkdir3 } from "fs/promises";
50553
50553
 
50554
50554
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
@@ -56152,6 +56152,7 @@ var HELP_TEXT = [
56152
56152
  " --linear-label <name> Filter by label name (repeatable, any-of)",
56153
56153
  " --poll-interval <s> Seconds between Linear polls (default: 60)",
56154
56154
  " --concurrency <n> Max concurrent task loops (default: 1)",
56155
+ " --worktree Run each task in its own git worktree (.ralph/worktrees/<name>)",
56155
56156
  "",
56156
56157
  " --help, -h Show this help message",
56157
56158
  "",
@@ -56187,7 +56188,8 @@ async function parseArgs(argv) {
56187
56188
  linearStatus: [],
56188
56189
  linearLabel: [],
56189
56190
  pollInterval: 60,
56190
- concurrency: 1
56191
+ concurrency: 1,
56192
+ worktree: false
56191
56193
  };
56192
56194
  let expectModel = false;
56193
56195
  let expectModelFlag = false;
@@ -56378,6 +56380,9 @@ async function parseArgs(argv) {
56378
56380
  case "--concurrency":
56379
56381
  expectConcurrency = true;
56380
56382
  break;
56383
+ case "--worktree":
56384
+ result2.worktree = true;
56385
+ break;
56381
56386
  default:
56382
56387
  if (VALID_MODES.has(arg)) {
56383
56388
  result2.mode = arg;
@@ -56445,7 +56450,7 @@ function createDefaultContext() {
56445
56450
 
56446
56451
  // apps/cli/src/components/App.tsx
56447
56452
  var import_react58 = __toESM(require_react(), 1);
56448
- import { join as join14 } from "path";
56453
+ import { join as join16 } from "path";
56449
56454
 
56450
56455
  // packages/core/src/state.ts
56451
56456
  import { join as join2 } from "path";
@@ -69801,6 +69806,10 @@ var RalphyConfigSchema = exports_external.object({
69801
69806
  pollIntervalSeconds: exports_external.number().int().positive().default(60),
69802
69807
  maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
69803
69808
  maxCostUsdPerTask: exports_external.number().nonnegative().default(0),
69809
+ useWorktree: exports_external.boolean().default(false),
69810
+ cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
69811
+ setupScript: exports_external.string().optional(),
69812
+ teardownScript: exports_external.string().optional(),
69804
69813
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
69805
69814
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
69806
69815
  linear: exports_external.object({
@@ -70026,8 +70035,55 @@ class AgentCoordinator {
70026
70035
  }
70027
70036
  }
70028
70037
 
70038
+ // apps/cli/src/agent/worktree.ts
70039
+ import { join as join13 } from "path";
70040
+ function worktreesDir(projectRoot) {
70041
+ return join13(projectRoot, ".ralph", "worktrees");
70042
+ }
70043
+ function branchForChange(changeName) {
70044
+ return `ralph/${changeName}`;
70045
+ }
70046
+ async function createWorktree(projectRoot, changeName, runner) {
70047
+ const dir = worktreesDir(projectRoot);
70048
+ const cwd2 = join13(dir, changeName);
70049
+ const branch = branchForChange(changeName);
70050
+ const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
70051
+ if (list.stdout.includes(`worktree ${cwd2}
70052
+ `)) {
70053
+ return { cwd: cwd2, branch };
70054
+ }
70055
+ let branchExists = true;
70056
+ try {
70057
+ await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
70058
+ } catch {
70059
+ branchExists = false;
70060
+ }
70061
+ const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
70062
+ await runner.run(cmd, projectRoot);
70063
+ return { cwd: cwd2, branch };
70064
+ }
70065
+ async function removeWorktree(projectRoot, cwd2, runner) {
70066
+ await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
70067
+ }
70068
+
70029
70069
  // apps/cli/src/components/AgentMode.tsx
70030
70070
  var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
70071
+ import { join as join14 } from "path";
70072
+ var bunGitRunner = {
70073
+ run: async (args, cwd2) => {
70074
+ const proc = Bun.spawn({ cmd: ["git", ...args], cwd: cwd2, stdout: "pipe", stderr: "pipe" });
70075
+ const stdout = await new Response(proc.stdout).text();
70076
+ const stderr = await new Response(proc.stderr).text();
70077
+ const code = await proc.exited;
70078
+ if (code !== 0) {
70079
+ const err = new Error("git command failed");
70080
+ err.stderr = stderr;
70081
+ err.code = code;
70082
+ throw err;
70083
+ }
70084
+ return { stdout, stderr };
70085
+ }
70086
+ };
70031
70087
  var lineCounter = 0;
70032
70088
  function nextId() {
70033
70089
  lineCounter += 1;
@@ -70066,6 +70122,24 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70066
70122
  const stateCache = new Map;
70067
70123
  const labelCache = new Map;
70068
70124
  const teamKeyOf = (issue) => issue.identifier.split("-")[0];
70125
+ const useWorktree = args.worktree || cfg.useWorktree;
70126
+ const cwdByChange = new Map;
70127
+ async function runScript(label, cmd, cwd2) {
70128
+ appendLog(` ${label}: ${cmd}`, "gray");
70129
+ const proc = Bun.spawn({
70130
+ cmd: ["sh", "-c", cmd],
70131
+ cwd: cwd2,
70132
+ stdout: "ignore",
70133
+ stderr: "pipe",
70134
+ stdin: "ignore"
70135
+ });
70136
+ const code = await proc.exited;
70137
+ if (code !== 0) {
70138
+ const stderr = await new Response(proc.stderr).text();
70139
+ appendLog(`! ${label} exited code ${code}${stderr ? `: ${stderr.trim().split(`
70140
+ `)[0]}` : ""}`, "yellow");
70141
+ }
70142
+ }
70069
70143
  const coord2 = new AgentCoordinator({
70070
70144
  fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
70071
70145
  scaffold: async (issue) => {
@@ -70075,7 +70149,27 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70075
70149
  } catch (err) {
70076
70150
  appendLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
70077
70151
  }
70078
- return scaffoldChangeForIssue(tasksDir, statesDir, issue, comments);
70152
+ let workerCwd = projectRoot;
70153
+ let scaffoldTasksDir = tasksDir;
70154
+ let scaffoldStatesDir = statesDir;
70155
+ const probeName = issue.identifier.toLowerCase();
70156
+ if (useWorktree) {
70157
+ try {
70158
+ const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
70159
+ workerCwd = wt.cwd;
70160
+ scaffoldTasksDir = join14(wt.cwd, "openspec", "changes");
70161
+ scaffoldStatesDir = join14(wt.cwd, ".ralph", "tasks");
70162
+ appendLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
70163
+ } catch (err) {
70164
+ appendLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
70165
+ }
70166
+ }
70167
+ const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments);
70168
+ cwdByChange.set(changeName, workerCwd);
70169
+ if (cfg.setupScript) {
70170
+ await runScript("setup", cfg.setupScript, workerCwd);
70171
+ }
70172
+ return changeName;
70079
70173
  },
70080
70174
  spawnWorker: (changeName) => {
70081
70175
  const cmd = [
@@ -70093,14 +70187,35 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70093
70187
  const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
70094
70188
  if (maxCost > 0)
70095
70189
  cmd.push("--max-cost", String(maxCost));
70190
+ const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
70096
70191
  const proc = Bun.spawn({
70097
70192
  cmd,
70098
- cwd: projectRoot,
70193
+ cwd: cwd2,
70099
70194
  stdout: "ignore",
70100
70195
  stderr: "ignore",
70101
70196
  stdin: "ignore"
70102
70197
  });
70103
- return { exited: proc.exited, kill: () => proc.kill() };
70198
+ const wrapped = proc.exited.then(async (code) => {
70199
+ if (cfg.teardownScript) {
70200
+ try {
70201
+ await runScript("teardown", cfg.teardownScript, cwd2);
70202
+ } catch {}
70203
+ }
70204
+ if (useWorktree && cwd2 !== projectRoot) {
70205
+ const ok = code === 0;
70206
+ if (ok && cfg.cleanupWorktreeOnSuccess) {
70207
+ try {
70208
+ await removeWorktree(projectRoot, cwd2, bunGitRunner);
70209
+ appendLog(` removed worktree ${cwd2}`, "gray");
70210
+ } catch (err) {
70211
+ appendLog(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
70212
+ }
70213
+ }
70214
+ }
70215
+ cwdByChange.delete(changeName);
70216
+ return code;
70217
+ });
70218
+ return { exited: wrapped, kill: () => proc.kill() };
70104
70219
  },
70105
70220
  loadState: () => readAgentState(projectRoot),
70106
70221
  saveState: (s) => writeAgentState(projectRoot, s),
@@ -70218,11 +70333,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70218
70333
  }
70219
70334
 
70220
70335
  // packages/openspec/src/openspec-change-store.ts
70221
- import { join as join13, dirname as dirname3 } from "path";
70336
+ import { join as join15, dirname as dirname3 } from "path";
70222
70337
  import { readdir, mkdir as mkdir2 } from "fs/promises";
70223
70338
  function resolveOpenspecBin() {
70224
70339
  const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
70225
- return join13(dirname3(pkgJsonPath), "bin", "openspec.js");
70340
+ return join15(dirname3(pkgJsonPath), "bin", "openspec.js");
70226
70341
  }
70227
70342
  function runOpenspec(args, options = {}) {
70228
70343
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -70248,7 +70363,7 @@ class OpenSpecChangeStore {
70248
70363
  }
70249
70364
  }
70250
70365
  getChangeDirectory(name) {
70251
- return join13("openspec", "changes", name);
70366
+ return join15("openspec", "changes", name);
70252
70367
  }
70253
70368
  async listChanges() {
70254
70369
  const result2 = runOpenspec(["list", "--json"]);
@@ -70262,7 +70377,7 @@ class OpenSpecChangeStore {
70262
70377
  }
70263
70378
  } catch {}
70264
70379
  }
70265
- const changesDir = join13("openspec", "changes");
70380
+ const changesDir = join15("openspec", "changes");
70266
70381
  if (!await Bun.file(changesDir).exists())
70267
70382
  return [];
70268
70383
  try {
@@ -70273,18 +70388,18 @@ class OpenSpecChangeStore {
70273
70388
  }
70274
70389
  }
70275
70390
  async readTaskList(name) {
70276
- const file = Bun.file(join13("openspec", "changes", name, "tasks.md"));
70391
+ const file = Bun.file(join15("openspec", "changes", name, "tasks.md"));
70277
70392
  if (!await file.exists())
70278
70393
  return "";
70279
70394
  return await file.text();
70280
70395
  }
70281
70396
  async writeTaskList(name, content) {
70282
- const path = join13("openspec", "changes", name, "tasks.md");
70397
+ const path = join15("openspec", "changes", name, "tasks.md");
70283
70398
  await mkdir2(dirname3(path), { recursive: true });
70284
70399
  await Bun.write(path, content);
70285
70400
  }
70286
70401
  async appendSteering(name, message) {
70287
- const path = join13("openspec", "changes", name, "steering.md");
70402
+ const path = join15("openspec", "changes", name, "steering.md");
70288
70403
  const file = Bun.file(path);
70289
70404
  const existing = await file.exists() ? await file.text() : null;
70290
70405
  const updated = existing ? `${message}
@@ -70295,7 +70410,7 @@ ${existing.trimStart()}` : `${message}
70295
70410
  await Bun.write(path, updated);
70296
70411
  }
70297
70412
  async readSection(name, artifact, heading) {
70298
- const file = Bun.file(join13("openspec", "changes", name, artifact));
70413
+ const file = Bun.file(join15("openspec", "changes", name, artifact));
70299
70414
  if (!await file.exists())
70300
70415
  return "";
70301
70416
  const content = await file.text();
@@ -70376,8 +70491,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
70376
70491
  message: "Error: --name is required for status mode"
70377
70492
  }, undefined, false, undefined, this);
70378
70493
  }
70379
- const stateDir = join14(statesDir, args.name);
70380
- if (getStorage().read(join14(stateDir, ".ralph-state.json")) === null) {
70494
+ const stateDir = join16(statesDir, args.name);
70495
+ if (getStorage().read(join16(stateDir, ".ralph-state.json")) === null) {
70381
70496
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
70382
70497
  message: `Error: change '${args.name}' not found`
70383
70498
  }, undefined, false, undefined, this);
@@ -70434,7 +70549,7 @@ if (typeof globalThis.Bun === "undefined") {
70434
70549
  async function findProjectRoot() {
70435
70550
  let dir = process.cwd();
70436
70551
  while (dir !== "/") {
70437
- if (await exists(join15(dir, "openspec")))
70552
+ if (await exists(join17(dir, "openspec")))
70438
70553
  return dir;
70439
70554
  dir = resolve(dir, "..");
70440
70555
  }
@@ -70469,11 +70584,11 @@ try {
70469
70584
  capture("command_run", { mode: args.mode, engine: args.engine, model: args.model });
70470
70585
  try {
70471
70586
  const projectRoot = await findProjectRoot();
70472
- const statesDir = join15(projectRoot, ".ralph", "tasks");
70473
- const tasksDir = join15(projectRoot, "openspec", "changes");
70587
+ const statesDir = join17(projectRoot, ".ralph", "tasks");
70588
+ const tasksDir = join17(projectRoot, "openspec", "changes");
70474
70589
  if (args.mode === "init") {
70475
70590
  await mkdir3(statesDir, { recursive: true });
70476
- const openspecBin = join15(dirname4(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
70591
+ const openspecBin = join17(dirname4(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
70477
70592
  Bun.spawnSync({
70478
70593
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
70479
70594
  stdio: ["inherit", "inherit", "inherit"],
@@ -70481,13 +70596,13 @@ try {
70481
70596
  });
70482
70597
  }
70483
70598
  if (args.mode === "task" && args.name) {
70484
- await mkdir3(join15(statesDir, args.name), { recursive: true });
70485
- await mkdir3(join15(tasksDir, args.name), { recursive: true });
70599
+ await mkdir3(join17(statesDir, args.name), { recursive: true });
70600
+ await mkdir3(join17(tasksDir, args.name), { recursive: true });
70486
70601
  }
70487
70602
  if (args.mode === "agent") {
70488
70603
  await mkdir3(statesDir, { recursive: true });
70489
70604
  await mkdir3(tasksDir, { recursive: true });
70490
- await mkdir3(join15(projectRoot, ".ralph"), { recursive: true });
70605
+ await mkdir3(join17(projectRoot, ".ralph"), { recursive: true });
70491
70606
  }
70492
70607
  await runWithContext(createDefaultContext(), async () => {
70493
70608
  const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.3",
3
+ "version": "2.7.4",
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",