@neriros/ralphy 2.21.0 → 2.21.2

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
@@ -148,6 +148,7 @@ Linear is the source of truth for which issues Ralph has touched. The `linear.in
148
148
  | `getInProgress` | `{filter: Marker[]}` | Issues to resume after restart |
149
149
  | `getConflicted` | `{filter: Marker[]}` | Issues whose PR is conflicted (re-fix run) |
150
150
  | `getReview` | `{filter: Marker[]}` | Done issues flagged for review follow-up |
151
+ | `getAutoMerge` | `{filter: Marker[]}` | Issues whose PR should be auto-merged once required checks pass |
151
152
  | `setInProgress` | `Marker` or `{apply: Marker[]}` | Applied when a worker spawns (any non-resume mode) |
152
153
  | `setDone` | `Marker` or `{apply: Marker[]}` | Applied on clean exit |
153
154
  | `setError` | `Marker` or `{apply: Marker[]}` | Applied on non-zero exit (quarantine signal — issue is _not_ auto-resumed) |
@@ -167,6 +168,7 @@ Example `ralphy.config.json`:
167
168
  "model": "opus",
168
169
  "useWorktree": true,
169
170
  "createPrOnSuccess": true,
171
+ "autoMergeStrategy": "squash",
170
172
  "fixCiOnFailure": true,
171
173
  "linear": {
172
174
  "team": "ENG",
@@ -182,6 +184,7 @@ Example `ralphy.config.json`:
182
184
  "getInProgress": { "filter": [{ "type": "status", "value": "In Progress" }] },
183
185
  "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflicted" }] },
184
186
  "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
187
+ "getAutoMerge": { "filter": [{ "type": "label", "value": "ralph:auto-merge" }] },
185
188
  "setInProgress": { "type": "status", "value": "In Progress" },
186
189
  "setDone": {
187
190
  "apply": [
@@ -225,13 +228,14 @@ Done issues whose PR `gh pr view --json mergeable` reports as `CONFLICTING` get
225
228
 
226
229
  ### PR + CI integration
227
230
 
228
- | Flag / config | Behavior |
229
- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
230
- | `createPrOnSuccess` / `--create-pr` | After a clean exit, push the worker's branch and `gh pr create`. Title: `<ID>: <title>`. Idempotent — surfaces the existing URL if the PR is already open. Requires `--worktree` and `gh` authenticated. `prBaseBranch` defaults to `main`. |
231
- | `fixCiOnFailure` / `--fix-ci` | After the PR opens, poll `gh pr checks`. On failure, pull failed logs via `gh run view --log-failed`, append them to `## Steering`, re-spawn the worker, and push the new commits repeat until green or `maxCiFixAttempts` (default `5`) is hit. While this loop runs, `setDone` is **not** applied; if CI is never green the worker is treated as failed. |
232
- | `ciPollIntervalSeconds` | Seconds between CI status polls (default `30`). |
233
- | `ignoreCiChecks` | Array of check names to ignore when computing pass/fail. |
234
- | `codeReviewTrigger` / `--code-review` | See [Code-review iteration](#code-review-iteration). |
231
+ | Flag / config | Behavior |
232
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
233
+ | `createPrOnSuccess` / `--create-pr` | After a clean exit, push the worker's branch and `gh pr create`. Title: `<ID>: <title>`. Idempotent — surfaces the existing URL if the PR is already open. Requires `--worktree` and `gh` authenticated. `prBaseBranch` defaults to `main`; override per-issue by labelling the Linear issue with `ralph:branch:<branch-name>`. |
234
+ | `getAutoMerge` indicator | Opt an issue in for GitHub auto-merge (any-of label/status filter, same shape as `getReview`). When matched, Ralph runs `gh pr merge <url> --auto --<strategy>` right after opening the PR so GitHub merges as soon as required checks pass. Strategy comes from `autoMergeStrategy` (`squash` \| `merge` \| `rebase`, default `squash`). Failures are logged but non-fatal the CI/conflict watch loop continues. |
235
+ | `fixCiOnFailure` / `--fix-ci` | After the PR opens, poll `gh pr checks`. On failure, pull failed logs via `gh run view --log-failed`, append them to `## Steering`, re-spawn the worker, and push the new commits — repeat until green or `maxCiFixAttempts` (default `5`) is hit. While this loop runs, `setDone` is **not** applied; if CI is never green the worker is treated as failed. |
236
+ | `ciPollIntervalSeconds` | Seconds between CI status polls (default `30`). |
237
+ | `ignoreCiChecks` | Array of check names to ignore when computing pass/fail. |
238
+ | `codeReviewTrigger` / `--code-review` | See [Code-review iteration](#code-review-iteration). |
235
239
 
236
240
  ### Worktrees, setup, teardown
237
241
 
package/dist/cli/index.js CHANGED
@@ -35029,8 +35029,8 @@ import { readFileSync as readFileSync2 } from "fs";
35029
35029
  import { resolve } from "path";
35030
35030
  function getVersion() {
35031
35031
  try {
35032
- if ("2.21.0")
35033
- return "2.21.0";
35032
+ if ("2.21.2")
35033
+ return "2.21.2";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -62245,7 +62245,7 @@ var exports_json_runner = {};
62245
62245
  __export(exports_json_runner, {
62246
62246
  runAgentJson: () => runAgentJson
62247
62247
  });
62248
- import { join as join21 } from "path";
62248
+ import { join as join22 } from "path";
62249
62249
  import { mkdir as mkdir6 } from "fs/promises";
62250
62250
  import { homedir as homedir4 } from "os";
62251
62251
  function cleanOutputLine2(raw) {
@@ -62270,7 +62270,7 @@ async function runAgentJson({
62270
62270
  statesDir,
62271
62271
  tasksDir
62272
62272
  }) {
62273
- await mkdir6(join21(homedir4(), ".ralph"), { recursive: true }).catch(() => {
62273
+ await mkdir6(join22(homedir4(), ".ralph"), { recursive: true }).catch(() => {
62274
62274
  return;
62275
62275
  });
62276
62276
  const cfgPath = await ensureRalphyConfig(projectRoot);
@@ -62409,7 +62409,7 @@ var init_json_runner = __esm(() => {
62409
62409
  });
62410
62410
 
62411
62411
  // apps/cli/src/index.ts
62412
- import { resolve as resolve2, join as join22, dirname as dirname6 } from "path";
62412
+ import { resolve as resolve2, join as join23 } from "path";
62413
62413
  import { exists as exists2, mkdir as mkdir7, rm } from "fs/promises";
62414
62414
 
62415
62415
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
@@ -67532,7 +67532,7 @@ function createDefaultContext() {
67532
67532
 
67533
67533
  // apps/cli/src/components/App.tsx
67534
67534
  var import_react58 = __toESM(require_react(), 1);
67535
- import { join as join19 } from "path";
67535
+ import { join as join20 } from "path";
67536
67536
 
67537
67537
  // packages/core/src/state.ts
67538
67538
  init_types2();
@@ -74236,16 +74236,79 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
74236
74236
  }
74237
74237
 
74238
74238
  // packages/openspec/src/openspec-change-store.ts
74239
- import { join as join18, dirname as dirname5 } from "path";
74239
+ import { dirname as dirname6, join as join19 } from "path";
74240
74240
  import { readdir, mkdir as mkdir5 } from "fs/promises";
74241
- function resolveOpenspecBin() {
74242
- const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
74243
- return join18(dirname5(pkgJsonPath), "bin", "openspec.js");
74241
+
74242
+ // packages/openspec/src/openspec-bin.ts
74243
+ import { dirname as dirname5, join as join18 } from "path";
74244
+ var bunInstallRunner = {
74245
+ spawnSync: (cmd, cwd2) => {
74246
+ const proc = Bun.spawnSync({
74247
+ cmd,
74248
+ cwd: cwd2,
74249
+ stdio: ["ignore", "inherit", "inherit"]
74250
+ });
74251
+ return { exitCode: proc.exitCode };
74252
+ },
74253
+ resolveSync: (specifier, fromDir) => Bun.resolveSync(specifier, fromDir),
74254
+ log: (text) => {
74255
+ process.stderr.write(text);
74256
+ }
74257
+ };
74258
+ function findPackageRoot(startDir) {
74259
+ let dir = startDir;
74260
+ for (let i = 0;i < 8; i++) {
74261
+ if (Bun.file(join18(dir, "package.json")).size >= 0) {
74262
+ try {
74263
+ if (Bun.file(join18(dir, "package.json")).size > 0)
74264
+ return dir;
74265
+ } catch {}
74266
+ }
74267
+ const parent = dirname5(dir);
74268
+ if (parent === dir)
74269
+ break;
74270
+ dir = parent;
74271
+ }
74272
+ return startDir;
74244
74273
  }
74274
+ function ensureOpenspecInstalled(fromDir, runner) {
74275
+ const installDir = findPackageRoot(fromDir);
74276
+ runner.log(`[ralphy] @fission-ai/openspec not found in ${installDir} \u2014 installing automatically...
74277
+ `);
74278
+ const candidates = [
74279
+ ["npm", "install", "--no-save", "--no-audit", "--no-fund", "@fission-ai/openspec@latest"],
74280
+ ["bun", "add", "@fission-ai/openspec@latest"]
74281
+ ];
74282
+ for (const cmd of candidates) {
74283
+ try {
74284
+ const result2 = runner.spawnSync(cmd, installDir);
74285
+ if (result2.exitCode === 0) {
74286
+ runner.log(`[ralphy] installed @fission-ai/openspec via ${cmd[0]}.
74287
+ `);
74288
+ return;
74289
+ }
74290
+ } catch {}
74291
+ }
74292
+ const err = new Error("openspec auto-install failed");
74293
+ err.installDir = installDir;
74294
+ throw err;
74295
+ }
74296
+ function resolveOpenspecBin(fromDir, runner = bunInstallRunner) {
74297
+ try {
74298
+ const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
74299
+ return join18(dirname5(pkgJsonPath), "bin", "openspec.js");
74300
+ } catch {
74301
+ ensureOpenspecInstalled(fromDir, runner);
74302
+ const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
74303
+ return join18(dirname5(pkgJsonPath), "bin", "openspec.js");
74304
+ }
74305
+ }
74306
+
74307
+ // packages/openspec/src/openspec-change-store.ts
74245
74308
  function runOpenspec(args, options = {}) {
74246
74309
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
74247
74310
  const proc = Bun.spawnSync({
74248
- cmd: [process.execPath, resolveOpenspecBin(), ...args],
74311
+ cmd: [process.execPath, resolveOpenspecBin(import.meta.dir), ...args],
74249
74312
  stdio
74250
74313
  });
74251
74314
  const decoder = new TextDecoder;
@@ -74266,7 +74329,7 @@ class OpenSpecChangeStore {
74266
74329
  }
74267
74330
  }
74268
74331
  getChangeDirectory(name) {
74269
- return join18("openspec", "changes", name);
74332
+ return join19("openspec", "changes", name);
74270
74333
  }
74271
74334
  async listChanges() {
74272
74335
  const result2 = runOpenspec(["list", "--json"]);
@@ -74280,7 +74343,7 @@ class OpenSpecChangeStore {
74280
74343
  }
74281
74344
  } catch {}
74282
74345
  }
74283
- const changesDir = join18("openspec", "changes");
74346
+ const changesDir = join19("openspec", "changes");
74284
74347
  if (!await Bun.file(changesDir).exists())
74285
74348
  return [];
74286
74349
  try {
@@ -74291,29 +74354,29 @@ class OpenSpecChangeStore {
74291
74354
  }
74292
74355
  }
74293
74356
  async readTaskList(name) {
74294
- const file = Bun.file(join18("openspec", "changes", name, "tasks.md"));
74357
+ const file = Bun.file(join19("openspec", "changes", name, "tasks.md"));
74295
74358
  if (!await file.exists())
74296
74359
  return "";
74297
74360
  return await file.text();
74298
74361
  }
74299
74362
  async writeTaskList(name, content) {
74300
- const path = join18("openspec", "changes", name, "tasks.md");
74301
- await mkdir5(dirname5(path), { recursive: true });
74363
+ const path = join19("openspec", "changes", name, "tasks.md");
74364
+ await mkdir5(dirname6(path), { recursive: true });
74302
74365
  await Bun.write(path, content);
74303
74366
  }
74304
74367
  async appendSteering(name, message) {
74305
- const path = join18("openspec", "changes", name, "steering.md");
74368
+ const path = join19("openspec", "changes", name, "steering.md");
74306
74369
  const file = Bun.file(path);
74307
74370
  const existing = await file.exists() ? await file.text() : null;
74308
74371
  const updated = existing ? `${message}
74309
74372
 
74310
74373
  ${existing.trimStart()}` : `${message}
74311
74374
  `;
74312
- await mkdir5(dirname5(path), { recursive: true });
74375
+ await mkdir5(dirname6(path), { recursive: true });
74313
74376
  await Bun.write(path, updated);
74314
74377
  }
74315
74378
  async readSection(name, artifact, heading) {
74316
- const file = Bun.file(join18("openspec", "changes", name, artifact));
74379
+ const file = Bun.file(join19("openspec", "changes", name, artifact));
74317
74380
  if (!await file.exists())
74318
74381
  return "";
74319
74382
  const content = await file.text();
@@ -74432,8 +74495,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
74432
74495
  message: "Error: --name is required for status mode"
74433
74496
  }, undefined, false, undefined, this);
74434
74497
  }
74435
- const stateDir = join19(statesDir, args.name);
74436
- if (getStorage().read(join19(stateDir, ".ralph-state.json")) === null) {
74498
+ const stateDir = join20(statesDir, args.name);
74499
+ if (getStorage().read(join20(stateDir, ".ralph-state.json")) === null) {
74437
74500
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
74438
74501
  message: `Error: change '${args.name}' not found`
74439
74502
  }, undefined, false, undefined, this);
@@ -74480,7 +74543,7 @@ init_worktree();
74480
74543
 
74481
74544
  // apps/cli/src/debug.ts
74482
74545
  init_log();
74483
- import { join as join20 } from "path";
74546
+ import { join as join21 } from "path";
74484
74547
  function fmtTs(d) {
74485
74548
  return d.toISOString().replace("T", " ").slice(0, 23);
74486
74549
  }
@@ -74593,7 +74656,7 @@ function detectStuck(lines) {
74593
74656
  };
74594
74657
  }
74595
74658
  async function inspectBinary(projectRoot) {
74596
- const binPath = join20(projectRoot, ".ralph", "bin", "cli.js");
74659
+ const binPath = join21(projectRoot, ".ralph", "bin", "cli.js");
74597
74660
  const file = Bun.file(binPath);
74598
74661
  if (!await file.exists())
74599
74662
  return null;
@@ -74619,7 +74682,7 @@ var SPAWN_RE = /\u25B6 (\S+) \u2192 (\S+)/;
74619
74682
  async function resolveDebugTarget(projectRoot, opts) {
74620
74683
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
74621
74684
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
74622
- const jsonlLogFile = Bun.file(join20(projectRoot, ".ralph", "agent.log"));
74685
+ const jsonlLogFile = Bun.file(join21(projectRoot, ".ralph", "agent.log"));
74623
74686
  const jsonlLines = await jsonlLogFile.exists() ? parseJsonlLog(await jsonlLogFile.text()) : [];
74624
74687
  const allLines = [...textLines, ...jsonlLines];
74625
74688
  if (opts.name && !opts.issue) {
@@ -74724,7 +74787,7 @@ async function runDebug(opts) {
74724
74787
  `);
74725
74788
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
74726
74789
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
74727
- const jsonlLogPath = join20(projectRoot, ".ralph", "agent.log");
74790
+ const jsonlLogPath = join21(projectRoot, ".ralph", "agent.log");
74728
74791
  const jsonlLogFile = Bun.file(jsonlLogPath);
74729
74792
  const hasJsonlLog = await jsonlLogFile.exists();
74730
74793
  let { changeName, identifier: issueIdentifier } = await resolveDebugTarget(projectRoot, {
@@ -74738,7 +74801,7 @@ async function runDebug(opts) {
74738
74801
  }
74739
74802
  const jsonlLines = hasJsonlLog ? parseJsonlLog(await jsonlLogFile.text(), changeName) : [];
74740
74803
  const relevantText = textLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
74741
- const workerLogFile = Bun.file(join20(projectRoot, ".ralph", "logs", `${changeName}.log`));
74804
+ const workerLogFile = Bun.file(join21(projectRoot, ".ralph", "logs", `${changeName}.log`));
74742
74805
  const workerLines = await workerLogFile.exists() ? parseTextLog(await workerLogFile.text()) : [];
74743
74806
  const merged = [...relevantText, ...jsonlLines, ...workerLines].sort((a, b) => +a.ts - +b.ts);
74744
74807
  const seen = new Set;
@@ -74895,8 +74958,8 @@ async function runDebug(opts) {
74895
74958
  out(" \u26A0 PR currently has merge conflicts");
74896
74959
  if (pr?.checks.some((c) => c.conclusion === "FAILURE"))
74897
74960
  out(" \u26A0 PR has failing CI checks");
74898
- const worktreePath = join20(projectRoot, ".ralph", "worktrees", changeName);
74899
- if (await Bun.file(join20(worktreePath, ".git")).exists()) {
74961
+ const worktreePath = join21(projectRoot, ".ralph", "worktrees", changeName);
74962
+ if (await Bun.file(join21(worktreePath, ".git")).exists()) {
74900
74963
  out(` Worktree : ${worktreePath}`);
74901
74964
  }
74902
74965
  if (!timeline.length)
@@ -74914,7 +74977,7 @@ if (typeof globalThis.Bun === "undefined") {
74914
74977
  async function findProjectRoot() {
74915
74978
  let dir = process.cwd();
74916
74979
  while (dir !== "/") {
74917
- if (await exists2(join22(dir, "openspec")))
74980
+ if (await exists2(join23(dir, "openspec")))
74918
74981
  return dir;
74919
74982
  dir = resolve2(dir, "..");
74920
74983
  }
@@ -74955,7 +75018,7 @@ try {
74955
75018
  const tasksDir = layout.tasksDir;
74956
75019
  if (args.mode === "init") {
74957
75020
  await mkdir7(statesDir, { recursive: true });
74958
- const openspecBin = join22(dirname6(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
75021
+ const openspecBin = resolveOpenspecBin(import.meta.dir);
74959
75022
  Bun.spawnSync({
74960
75023
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
74961
75024
  stdio: ["inherit", "inherit", "inherit"],
@@ -74978,9 +75041,9 @@ try {
74978
75041
  `);
74979
75042
  process.exit(1);
74980
75043
  }
74981
- const worktreeDir = join22(worktreesDir(projectRoot), args.name);
74982
- const changeDir = join22(tasksDir, args.name);
74983
- const stateDir = join22(statesDir, args.name);
75044
+ const worktreeDir = join23(worktreesDir(projectRoot), args.name);
75045
+ const changeDir = join23(tasksDir, args.name);
75046
+ const stateDir = join23(statesDir, args.name);
74984
75047
  const branch = `ralph/${args.name}`;
74985
75048
  const removed = [];
74986
75049
  if (await exists2(worktreeDir)) {
@@ -75032,13 +75095,13 @@ try {
75032
75095
  process.exit(0);
75033
75096
  }
75034
75097
  if (args.mode === "task" && args.name) {
75035
- await mkdir7(join22(statesDir, args.name), { recursive: true });
75036
- await mkdir7(join22(tasksDir, args.name), { recursive: true });
75098
+ await mkdir7(join23(statesDir, args.name), { recursive: true });
75099
+ await mkdir7(join23(tasksDir, args.name), { recursive: true });
75037
75100
  }
75038
75101
  if (args.mode === "agent") {
75039
75102
  await mkdir7(statesDir, { recursive: true });
75040
75103
  await mkdir7(tasksDir, { recursive: true });
75041
- await mkdir7(join22(projectRoot, ".ralph"), { recursive: true });
75104
+ await mkdir7(join23(projectRoot, ".ralph"), { recursive: true });
75042
75105
  }
75043
75106
  if (args.mode === "agent" && args.jsonOutput) {
75044
75107
  const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
package/dist/mcp/index.js CHANGED
@@ -6605,7 +6605,7 @@ var require_dist = __commonJS((exports, module) => {
6605
6605
  });
6606
6606
 
6607
6607
  // apps/mcp/src/index.ts
6608
- import { resolve, join as join4 } from "path";
6608
+ import { resolve, join as join5 } from "path";
6609
6609
  import { exists } from "fs/promises";
6610
6610
 
6611
6611
  // packages/context/src/context.ts
@@ -24900,16 +24900,79 @@ function error2(msg) {
24900
24900
  }
24901
24901
 
24902
24902
  // packages/openspec/src/openspec-change-store.ts
24903
- import { join as join3, dirname as dirname2 } from "path";
24903
+ import { dirname as dirname3, join as join4 } from "path";
24904
24904
  import { readdir, mkdir } from "fs/promises";
24905
- function resolveOpenspecBin() {
24906
- const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
24907
- return join3(dirname2(pkgJsonPath), "bin", "openspec.js");
24905
+
24906
+ // packages/openspec/src/openspec-bin.ts
24907
+ import { dirname as dirname2, join as join3 } from "path";
24908
+ var bunInstallRunner = {
24909
+ spawnSync: (cmd, cwd) => {
24910
+ const proc = Bun.spawnSync({
24911
+ cmd,
24912
+ cwd,
24913
+ stdio: ["ignore", "inherit", "inherit"]
24914
+ });
24915
+ return { exitCode: proc.exitCode };
24916
+ },
24917
+ resolveSync: (specifier, fromDir) => Bun.resolveSync(specifier, fromDir),
24918
+ log: (text) => {
24919
+ process.stderr.write(text);
24920
+ }
24921
+ };
24922
+ function findPackageRoot(startDir) {
24923
+ let dir = startDir;
24924
+ for (let i = 0;i < 8; i++) {
24925
+ if (Bun.file(join3(dir, "package.json")).size >= 0) {
24926
+ try {
24927
+ if (Bun.file(join3(dir, "package.json")).size > 0)
24928
+ return dir;
24929
+ } catch {}
24930
+ }
24931
+ const parent = dirname2(dir);
24932
+ if (parent === dir)
24933
+ break;
24934
+ dir = parent;
24935
+ }
24936
+ return startDir;
24937
+ }
24938
+ function ensureOpenspecInstalled(fromDir, runner) {
24939
+ const installDir = findPackageRoot(fromDir);
24940
+ runner.log(`[ralphy] @fission-ai/openspec not found in ${installDir} \u2014 installing automatically...
24941
+ `);
24942
+ const candidates = [
24943
+ ["npm", "install", "--no-save", "--no-audit", "--no-fund", "@fission-ai/openspec@latest"],
24944
+ ["bun", "add", "@fission-ai/openspec@latest"]
24945
+ ];
24946
+ for (const cmd of candidates) {
24947
+ try {
24948
+ const result = runner.spawnSync(cmd, installDir);
24949
+ if (result.exitCode === 0) {
24950
+ runner.log(`[ralphy] installed @fission-ai/openspec via ${cmd[0]}.
24951
+ `);
24952
+ return;
24953
+ }
24954
+ } catch {}
24955
+ }
24956
+ const err = new Error("openspec auto-install failed");
24957
+ err.installDir = installDir;
24958
+ throw err;
24908
24959
  }
24960
+ function resolveOpenspecBin(fromDir, runner = bunInstallRunner) {
24961
+ try {
24962
+ const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
24963
+ return join3(dirname2(pkgJsonPath), "bin", "openspec.js");
24964
+ } catch {
24965
+ ensureOpenspecInstalled(fromDir, runner);
24966
+ const pkgJsonPath = runner.resolveSync("@fission-ai/openspec/package.json", fromDir);
24967
+ return join3(dirname2(pkgJsonPath), "bin", "openspec.js");
24968
+ }
24969
+ }
24970
+
24971
+ // packages/openspec/src/openspec-change-store.ts
24909
24972
  function runOpenspec(args, options = {}) {
24910
24973
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
24911
24974
  const proc = Bun.spawnSync({
24912
- cmd: [process.execPath, resolveOpenspecBin(), ...args],
24975
+ cmd: [process.execPath, resolveOpenspecBin(import.meta.dir), ...args],
24913
24976
  stdio
24914
24977
  });
24915
24978
  const decoder = new TextDecoder;
@@ -24930,7 +24993,7 @@ class OpenSpecChangeStore {
24930
24993
  }
24931
24994
  }
24932
24995
  getChangeDirectory(name) {
24933
- return join3("openspec", "changes", name);
24996
+ return join4("openspec", "changes", name);
24934
24997
  }
24935
24998
  async listChanges() {
24936
24999
  const result = runOpenspec(["list", "--json"]);
@@ -24944,7 +25007,7 @@ class OpenSpecChangeStore {
24944
25007
  }
24945
25008
  } catch {}
24946
25009
  }
24947
- const changesDir = join3("openspec", "changes");
25010
+ const changesDir = join4("openspec", "changes");
24948
25011
  if (!await Bun.file(changesDir).exists())
24949
25012
  return [];
24950
25013
  try {
@@ -24955,29 +25018,29 @@ class OpenSpecChangeStore {
24955
25018
  }
24956
25019
  }
24957
25020
  async readTaskList(name) {
24958
- const file = Bun.file(join3("openspec", "changes", name, "tasks.md"));
25021
+ const file = Bun.file(join4("openspec", "changes", name, "tasks.md"));
24959
25022
  if (!await file.exists())
24960
25023
  return "";
24961
25024
  return await file.text();
24962
25025
  }
24963
25026
  async writeTaskList(name, content) {
24964
- const path = join3("openspec", "changes", name, "tasks.md");
24965
- await mkdir(dirname2(path), { recursive: true });
25027
+ const path = join4("openspec", "changes", name, "tasks.md");
25028
+ await mkdir(dirname3(path), { recursive: true });
24966
25029
  await Bun.write(path, content);
24967
25030
  }
24968
25031
  async appendSteering(name, message) {
24969
- const path = join3("openspec", "changes", name, "steering.md");
25032
+ const path = join4("openspec", "changes", name, "steering.md");
24970
25033
  const file = Bun.file(path);
24971
25034
  const existing = await file.exists() ? await file.text() : null;
24972
25035
  const updated = existing ? `${message}
24973
25036
 
24974
25037
  ${existing.trimStart()}` : `${message}
24975
25038
  `;
24976
- await mkdir(dirname2(path), { recursive: true });
25039
+ await mkdir(dirname3(path), { recursive: true });
24977
25040
  await Bun.write(path, updated);
24978
25041
  }
24979
25042
  async readSection(name, artifact, heading) {
24980
- const file = Bun.file(join3("openspec", "changes", name, artifact));
25043
+ const file = Bun.file(join4("openspec", "changes", name, artifact));
24981
25044
  if (!await file.exists())
24982
25045
  return "";
24983
25046
  const content = await file.text();
@@ -25021,7 +25084,7 @@ ${existing.trimStart()}` : `${message}
25021
25084
  async function findProjectRoot(startDir) {
25022
25085
  let dir = startDir;
25023
25086
  while (dir !== "/") {
25024
- if (await exists(join4(dir, "openspec")))
25087
+ if (await exists(join5(dir, "openspec")))
25025
25088
  return dir;
25026
25089
  dir = resolve(dir, "..");
25027
25090
  }
@@ -25035,8 +25098,8 @@ async function main() {
25035
25098
  startDir = resolve(args[dirIdx + 1]);
25036
25099
  }
25037
25100
  const projectRoot = await findProjectRoot(startDir);
25038
- const changesDir = join4(projectRoot, ".ralph", "tasks");
25039
- const taskFilesDir = join4(projectRoot, "openspec", "changes");
25101
+ const changesDir = join5(projectRoot, ".ralph", "tasks");
25102
+ const taskFilesDir = join5(projectRoot, "openspec", "changes");
25040
25103
  const changeStore = new OpenSpecChangeStore;
25041
25104
  const server = new McpServer({
25042
25105
  name: "ralph",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.21.0",
3
+ "version": "2.21.2",
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",
@@ -56,10 +56,12 @@
56
56
  "copy-assets": "bun scripts/copy-assets.ts",
57
57
  "prepublishOnly": "bun run build:publish"
58
58
  },
59
+ "dependencies": {
60
+ "@fission-ai/openspec": "latest"
61
+ },
59
62
  "devDependencies": {
60
63
  "@commitlint/cli": "^20.5.3",
61
64
  "@commitlint/config-conventional": "^20.5.3",
62
- "@fission-ai/openspec": "latest",
63
65
  "@modelcontextprotocol/sdk": "^1.29.0",
64
66
  "@nx/devkit": "^22.7.1",
65
67
  "@nx/js": "^22.7.1",