@neriros/ralphy 2.7.2 → 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
@@ -83,7 +83,7 @@ What it does on each tick:
83
83
  2. Dedupes against `.ralph/agent-state.json` (already processed) plus any in-flight workers
84
84
  3. For each new issue: fetches existing comments, scaffolds `openspec/changes/<id-slug>/{proposal.md,tasks.md,design.md}` (with the comments embedded so the worker sees prior discussion), then spawns `ralph task --name <id-slug>` up to the concurrency cap
85
85
  4. Posts a "🤖 started" comment on the Linear issue and (optionally) moves it to `inProgressStatus`
86
- 5. On worker exit, posts a success/failure comment and (on success) moves the issue to `doneStatus`
86
+ 5. On worker exit, posts a success/failure comment and (on success) moves the issue to `doneStatus` and/or applies `doneLabel`
87
87
 
88
88
  Defaults are written to `ralphy.config.json` on first run; CLI flags override config values per invocation.
89
89
 
@@ -102,11 +102,24 @@ Defaults are written to `ralphy.config.json` on first run; CLI flags override co
102
102
  "labels": ["ralph", "automation"],
103
103
  "inProgressStatus": "In Progress",
104
104
  "doneStatus": "In Review",
105
+ "doneLabel": "ralphy-done",
105
106
  "postComments": true,
106
107
  },
108
+ "useWorktree": true,
109
+ "cleanupWorktreeOnSuccess": false,
110
+ "setupScript": "bun install",
111
+ "teardownScript": "git status",
107
112
  }
108
113
  ```
109
114
 
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.
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
+
110
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.
111
124
 
112
125
  ## CLI Options
@@ -138,6 +151,7 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
138
151
  | `--linear-label <name>` | Filter by label name (repeatable, any-of) |
139
152
  | `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
140
153
  | `--concurrency <n>` | Max concurrent task loops (default: 1) |
154
+ | `--worktree` | Run each task in its own git worktree |
141
155
 
142
156
  ## OpenSpec Flow
143
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";
@@ -69685,6 +69690,26 @@ async function updateIssueState(apiKey, issueId, stateId) {
69685
69690
  stateId
69686
69691
  });
69687
69692
  }
69693
+ async function fetchIssueLabels(apiKey, teamKey) {
69694
+ const query = `query Labels($team: String!) {
69695
+ issueLabels(filter: { team: { key: { eq: $team } } }, first: 250) {
69696
+ nodes { id name }
69697
+ }
69698
+ }`;
69699
+ const data = await linearRequest(apiKey, query, {
69700
+ team: teamKey
69701
+ });
69702
+ return data.issueLabels.nodes;
69703
+ }
69704
+ async function addLabelToIssue(apiKey, issueId, labelId) {
69705
+ const mutation = `mutation AddLabel($id: String!, $labelId: String!) {
69706
+ issueAddLabel(id: $id, labelId: $labelId) { success }
69707
+ }`;
69708
+ await linearRequest(apiKey, mutation, {
69709
+ id: issueId,
69710
+ labelId
69711
+ });
69712
+ }
69688
69713
 
69689
69714
  // apps/cli/src/agent/state.ts
69690
69715
  import { join as join10 } from "path";
@@ -69781,6 +69806,10 @@ var RalphyConfigSchema = exports_external.object({
69781
69806
  pollIntervalSeconds: exports_external.number().int().positive().default(60),
69782
69807
  maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
69783
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(),
69784
69813
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
69785
69814
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
69786
69815
  linear: exports_external.object({
@@ -69790,6 +69819,7 @@ var RalphyConfigSchema = exports_external.object({
69790
69819
  labels: exports_external.union([exports_external.array(exports_external.string()), exports_external.string()]).transform((v) => typeof v === "string" ? [v] : v).default([]),
69791
69820
  inProgressStatus: exports_external.string().optional(),
69792
69821
  doneStatus: exports_external.string().optional(),
69822
+ doneLabel: exports_external.string().optional(),
69793
69823
  postComments: exports_external.boolean().default(true)
69794
69824
  }).default({ statuses: [], labels: [], postComments: true })
69795
69825
  }).default({
@@ -69959,6 +69989,27 @@ class AgentCoordinator {
69959
69989
  if (ok && this.opts.doneStatus) {
69960
69990
  await this.moveIssue(issue, this.opts.doneStatus);
69961
69991
  }
69992
+ if (ok && this.opts.doneLabel) {
69993
+ await this.tagIssue(issue, this.opts.doneLabel);
69994
+ }
69995
+ }
69996
+ async tagIssue(issue, labelName) {
69997
+ const updater = this.deps.updater;
69998
+ if (!updater.resolveLabelId || !updater.addLabel) {
69999
+ this.deps.onLog(`! Linear updater does not support labels (cannot tag ${issue.identifier} with '${labelName}')`, "yellow");
70000
+ return;
70001
+ }
70002
+ try {
70003
+ const labelId = await updater.resolveLabelId(issue, labelName);
70004
+ if (!labelId) {
70005
+ this.deps.onLog(`! Linear label '${labelName}' not found for ${issue.identifier}`, "yellow");
70006
+ return;
70007
+ }
70008
+ await updater.addLabel(issue, labelId);
70009
+ this.deps.onLog(` \u2192 ${issue.identifier} tagged with '${labelName}'`, "gray");
70010
+ } catch (err) {
70011
+ this.deps.onLog(`! Linear label add failed for ${issue.identifier}: ${err.message}`, "red");
70012
+ }
69962
70013
  }
69963
70014
  async moveIssue(issue, stateName) {
69964
70015
  const updater = this.deps.updater;
@@ -69984,8 +70035,55 @@ class AgentCoordinator {
69984
70035
  }
69985
70036
  }
69986
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
+
69987
70069
  // apps/cli/src/components/AgentMode.tsx
69988
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
+ };
69989
70087
  var lineCounter = 0;
69990
70088
  function nextId() {
69991
70089
  lineCounter += 1;
@@ -70022,7 +70120,26 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70022
70120
  labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
70023
70121
  };
70024
70122
  const stateCache = new Map;
70123
+ const labelCache = new Map;
70025
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
+ }
70026
70143
  const coord2 = new AgentCoordinator({
70027
70144
  fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
70028
70145
  scaffold: async (issue) => {
@@ -70032,7 +70149,27 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70032
70149
  } catch (err) {
70033
70150
  appendLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
70034
70151
  }
70035
- 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;
70036
70173
  },
70037
70174
  spawnWorker: (changeName) => {
70038
70175
  const cmd = [
@@ -70050,14 +70187,35 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70050
70187
  const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
70051
70188
  if (maxCost > 0)
70052
70189
  cmd.push("--max-cost", String(maxCost));
70190
+ const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
70053
70191
  const proc = Bun.spawn({
70054
70192
  cmd,
70055
- cwd: projectRoot,
70193
+ cwd: cwd2,
70056
70194
  stdout: "ignore",
70057
70195
  stderr: "ignore",
70058
70196
  stdin: "ignore"
70059
70197
  });
70060
- 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() };
70061
70219
  },
70062
70220
  loadState: () => readAgentState(projectRoot),
70063
70221
  saveState: (s) => writeAgentState(projectRoot, s),
@@ -70075,6 +70233,17 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70075
70233
  stateCache.set(team, map2);
70076
70234
  }
70077
70235
  return map2.get(stateName.toLowerCase()) ?? null;
70236
+ },
70237
+ addLabel: (issue, labelId) => addLabelToIssue(apiKey, issue.id, labelId),
70238
+ resolveLabelId: async (issue, labelName) => {
70239
+ const team = teamKeyOf(issue);
70240
+ let map2 = labelCache.get(team);
70241
+ if (!map2) {
70242
+ const labels = await fetchIssueLabels(apiKey, team);
70243
+ map2 = new Map(labels.map((l) => [l.name.toLowerCase(), l.id]));
70244
+ labelCache.set(team, map2);
70245
+ }
70246
+ return map2.get(labelName.toLowerCase()) ?? null;
70078
70247
  }
70079
70248
  }
70080
70249
  }, {
@@ -70082,6 +70251,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70082
70251
  filter: filter2,
70083
70252
  inProgressStatus: cfg.linear.inProgressStatus,
70084
70253
  doneStatus: cfg.linear.doneStatus,
70254
+ doneLabel: cfg.linear.doneLabel,
70085
70255
  postComments: cfg.linear.postComments
70086
70256
  });
70087
70257
  coordRef.current = coord2;
@@ -70163,11 +70333,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70163
70333
  }
70164
70334
 
70165
70335
  // packages/openspec/src/openspec-change-store.ts
70166
- import { join as join13, dirname as dirname3 } from "path";
70336
+ import { join as join15, dirname as dirname3 } from "path";
70167
70337
  import { readdir, mkdir as mkdir2 } from "fs/promises";
70168
70338
  function resolveOpenspecBin() {
70169
70339
  const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
70170
- return join13(dirname3(pkgJsonPath), "bin", "openspec.js");
70340
+ return join15(dirname3(pkgJsonPath), "bin", "openspec.js");
70171
70341
  }
70172
70342
  function runOpenspec(args, options = {}) {
70173
70343
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -70193,7 +70363,7 @@ class OpenSpecChangeStore {
70193
70363
  }
70194
70364
  }
70195
70365
  getChangeDirectory(name) {
70196
- return join13("openspec", "changes", name);
70366
+ return join15("openspec", "changes", name);
70197
70367
  }
70198
70368
  async listChanges() {
70199
70369
  const result2 = runOpenspec(["list", "--json"]);
@@ -70207,7 +70377,7 @@ class OpenSpecChangeStore {
70207
70377
  }
70208
70378
  } catch {}
70209
70379
  }
70210
- const changesDir = join13("openspec", "changes");
70380
+ const changesDir = join15("openspec", "changes");
70211
70381
  if (!await Bun.file(changesDir).exists())
70212
70382
  return [];
70213
70383
  try {
@@ -70218,18 +70388,18 @@ class OpenSpecChangeStore {
70218
70388
  }
70219
70389
  }
70220
70390
  async readTaskList(name) {
70221
- const file = Bun.file(join13("openspec", "changes", name, "tasks.md"));
70391
+ const file = Bun.file(join15("openspec", "changes", name, "tasks.md"));
70222
70392
  if (!await file.exists())
70223
70393
  return "";
70224
70394
  return await file.text();
70225
70395
  }
70226
70396
  async writeTaskList(name, content) {
70227
- const path = join13("openspec", "changes", name, "tasks.md");
70397
+ const path = join15("openspec", "changes", name, "tasks.md");
70228
70398
  await mkdir2(dirname3(path), { recursive: true });
70229
70399
  await Bun.write(path, content);
70230
70400
  }
70231
70401
  async appendSteering(name, message) {
70232
- const path = join13("openspec", "changes", name, "steering.md");
70402
+ const path = join15("openspec", "changes", name, "steering.md");
70233
70403
  const file = Bun.file(path);
70234
70404
  const existing = await file.exists() ? await file.text() : null;
70235
70405
  const updated = existing ? `${message}
@@ -70240,7 +70410,7 @@ ${existing.trimStart()}` : `${message}
70240
70410
  await Bun.write(path, updated);
70241
70411
  }
70242
70412
  async readSection(name, artifact, heading) {
70243
- const file = Bun.file(join13("openspec", "changes", name, artifact));
70413
+ const file = Bun.file(join15("openspec", "changes", name, artifact));
70244
70414
  if (!await file.exists())
70245
70415
  return "";
70246
70416
  const content = await file.text();
@@ -70321,8 +70491,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
70321
70491
  message: "Error: --name is required for status mode"
70322
70492
  }, undefined, false, undefined, this);
70323
70493
  }
70324
- const stateDir = join14(statesDir, args.name);
70325
- 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) {
70326
70496
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
70327
70497
  message: `Error: change '${args.name}' not found`
70328
70498
  }, undefined, false, undefined, this);
@@ -70379,7 +70549,7 @@ if (typeof globalThis.Bun === "undefined") {
70379
70549
  async function findProjectRoot() {
70380
70550
  let dir = process.cwd();
70381
70551
  while (dir !== "/") {
70382
- if (await exists(join15(dir, "openspec")))
70552
+ if (await exists(join17(dir, "openspec")))
70383
70553
  return dir;
70384
70554
  dir = resolve(dir, "..");
70385
70555
  }
@@ -70414,11 +70584,11 @@ try {
70414
70584
  capture("command_run", { mode: args.mode, engine: args.engine, model: args.model });
70415
70585
  try {
70416
70586
  const projectRoot = await findProjectRoot();
70417
- const statesDir = join15(projectRoot, ".ralph", "tasks");
70418
- const tasksDir = join15(projectRoot, "openspec", "changes");
70587
+ const statesDir = join17(projectRoot, ".ralph", "tasks");
70588
+ const tasksDir = join17(projectRoot, "openspec", "changes");
70419
70589
  if (args.mode === "init") {
70420
70590
  await mkdir3(statesDir, { recursive: true });
70421
- 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");
70422
70592
  Bun.spawnSync({
70423
70593
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
70424
70594
  stdio: ["inherit", "inherit", "inherit"],
@@ -70426,13 +70596,13 @@ try {
70426
70596
  });
70427
70597
  }
70428
70598
  if (args.mode === "task" && args.name) {
70429
- await mkdir3(join15(statesDir, args.name), { recursive: true });
70430
- 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 });
70431
70601
  }
70432
70602
  if (args.mode === "agent") {
70433
70603
  await mkdir3(statesDir, { recursive: true });
70434
70604
  await mkdir3(tasksDir, { recursive: true });
70435
- await mkdir3(join15(projectRoot, ".ralph"), { recursive: true });
70605
+ await mkdir3(join17(projectRoot, ".ralph"), { recursive: true });
70436
70606
  }
70437
70607
  await runWithContext(createDefaultContext(), async () => {
70438
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.2",
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",