@neriros/ralphy 2.9.1 → 2.9.3

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
@@ -1,5 +1,12 @@
1
1
  # Ralphy
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@neriros/ralphy.svg)](https://www.npmjs.com/package/@neriros/ralphy)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@neriros/ralphy.svg)](https://www.npmjs.com/package/@neriros/ralphy)
5
+ [![license](https://img.shields.io/npm/l/@neriros/ralphy.svg)](https://github.com/NeriRos/ralphy/blob/main/LICENSE)
6
+ [![GitHub stars](https://img.shields.io/github/stars/NeriRos/ralphy.svg?style=social)](https://github.com/NeriRos/ralphy)
7
+ [![GitHub issues](https://img.shields.io/github/issues/NeriRos/ralphy.svg)](https://github.com/NeriRos/ralphy/issues)
8
+ [![Bun](https://img.shields.io/badge/runtime-Bun-fbf0df.svg)](https://bun.sh)
9
+
3
10
  An iterative AI task execution framework. Ralphy orchestrates multi-phase autonomous work using Claude or Codex engines, with built-in state management, progress tracking, and cost safeguards.
4
11
 
5
12
  ## How It Works
@@ -19,9 +26,9 @@ Each iteration reads the `## Steering` section of `proposal.md`, picks the first
19
26
  ### npm (global)
20
27
 
21
28
  ```bash
22
- npm install -g ralphy
29
+ npm install -g @neriros/ralphy
23
30
  # or
24
- bunx ralphy
31
+ bunx @neriros/ralphy
25
32
  ```
26
33
 
27
34
  Requires [Bun](https://bun.sh) as the runtime.
package/dist/cli/index.js CHANGED
@@ -56407,7 +56407,7 @@ function log(msg) {
56407
56407
  // package.json
56408
56408
  var package_default = {
56409
56409
  name: "@neriros/ralphy",
56410
- version: "2.9.1",
56410
+ version: "2.9.3",
56411
56411
  description: "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
56412
56412
  keywords: [
56413
56413
  "agent",
@@ -70606,6 +70606,42 @@ async function createWorktree(projectRoot, changeName, runner) {
70606
70606
  async function removeWorktree(projectRoot, cwd2, runner) {
70607
70607
  await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
70608
70608
  }
70609
+ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
70610
+ const status = await runner.run(["status", "--porcelain"], cwd2);
70611
+ const dirty = status.stdout.trim();
70612
+ let unpushedCommits = "";
70613
+ try {
70614
+ const log2 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
70615
+ unpushedCommits = log2.stdout.trim();
70616
+ } catch {
70617
+ unpushedCommits = "<unknown: failed to compare against base>";
70618
+ }
70619
+ if (dirty && unpushedCommits) {
70620
+ return {
70621
+ safe: false,
70622
+ reason: "uncommitted changes AND unpushed commits present",
70623
+ dirty,
70624
+ unpushedCommits
70625
+ };
70626
+ }
70627
+ if (dirty) {
70628
+ return {
70629
+ safe: false,
70630
+ reason: "uncommitted or untracked files present",
70631
+ dirty,
70632
+ unpushedCommits
70633
+ };
70634
+ }
70635
+ if (unpushedCommits) {
70636
+ return {
70637
+ safe: false,
70638
+ reason: `commits ahead of ${base2} were not pushed/PR'd`,
70639
+ dirty,
70640
+ unpushedCommits
70641
+ };
70642
+ }
70643
+ return { safe: true, dirty, unpushedCommits };
70644
+ }
70609
70645
 
70610
70646
  // apps/cli/src/agent/pr.ts
70611
70647
  function defaultTitle(issue) {
@@ -70770,6 +70806,23 @@ function nextId() {
70770
70806
  return `${Date.now()}-${lineCounter}`;
70771
70807
  }
70772
70808
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
70809
+ async function appendSteering(proposalPath, steering) {
70810
+ const file = Bun.file(proposalPath);
70811
+ const prev = await file.exists() ? await file.text() : "";
70812
+ const stamped = `
70813
+
70814
+ ### Steering feedback (${new Date().toISOString()})
70815
+
70816
+ ${steering}
70817
+ `;
70818
+ const next = prev.includes("## Steering") ? prev.replace(/## Steering\n+([\s\S]*?)$/, (_m, rest2) => `## Steering
70819
+ ${stamped}
70820
+ ${rest2}`) : prev + `
70821
+ ## Steering
70822
+ ${stamped}
70823
+ `;
70824
+ await Bun.write(proposalPath, next);
70825
+ }
70773
70826
  function fmtElapsed(ms) {
70774
70827
  const s = Math.floor(ms / 1000);
70775
70828
  if (s < 60)
@@ -70952,78 +71005,178 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70952
71005
  appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
70953
71006
  effectiveCode = PR_FAILED_EXIT;
70954
71007
  } else {
70955
- try {
70956
- const pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
70957
- if (!pr) {
70958
- appendLog(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
70959
- } else {
70960
- appendLog(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
70961
- const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
70962
- if (wantFixCi) {
70963
- appendLog(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
70964
- const proposalPath = join14(statesDirByChange.get(changeName) ?? statesDir, "..", "..", "openspec", "changes", changeName, "proposal.md");
70965
- const result2 = await fixCiUntilGreen({
70966
- getStatus: () => getPrChecksStatus(pr.url, bunCmdRunner, cwd2),
70967
- getFailedLogs: (ids) => fetchFailedRunLogs(ids, bunCmdRunner, cwd2),
70968
- runTaskWithSteering: async (steering) => {
70969
- try {
70970
- const file = Bun.file(proposalPath);
70971
- const prev = await file.exists() ? await file.text() : "";
70972
- const stamped = `
70973
-
70974
- ### CI feedback (${new Date().toISOString()})
71008
+ const proposalPath = join14(statesDirByChange.get(changeName) ?? statesDir, "..", "..", "openspec", "changes", changeName, "proposal.md");
71009
+ const maxHookFixAttempts = cfg.maxCiFixAttempts;
71010
+ const runWorkerWithSteering = async (steering) => {
71011
+ try {
71012
+ await appendSteering(proposalPath, steering);
71013
+ } catch (steerErr) {
71014
+ appendLog(`! could not append steering: ${steerErr.message}`, "red");
71015
+ return 1;
71016
+ }
71017
+ const rp = Bun.spawn({
71018
+ cmd: buildTaskCmd(),
71019
+ cwd: cwd2,
71020
+ stdout: "ignore",
71021
+ stderr: "ignore",
71022
+ stdin: "ignore"
71023
+ });
71024
+ return rp.exited;
71025
+ };
71026
+ let commitFixAttempt = 0;
71027
+ let commitGaveUp = false;
71028
+ while (true) {
71029
+ let dirty = "";
71030
+ try {
71031
+ const status = await bunCmdRunner.run(["git", "status", "--porcelain"], cwd2);
71032
+ dirty = status.stdout.trim();
71033
+ } catch (err) {
71034
+ appendLog(`! git status failed for ${changeName}: ${err.message}`, "yellow");
71035
+ break;
71036
+ }
71037
+ if (!dirty)
71038
+ break;
71039
+ try {
71040
+ await bunCmdRunner.run(["git", "add", "-A"], cwd2);
71041
+ await bunCmdRunner.run(["git", "commit", "-m", `ralph: residual changes for ${changeName}`], cwd2);
71042
+ appendLog(` committed residual changes for ${changeName}`, "gray");
71043
+ break;
71044
+ } catch (err) {
71045
+ const e = err;
71046
+ const detail = e.stderr?.trim() || e.message;
71047
+ const combined = `${e.stdout ?? ""}
71048
+ ${e.stderr ?? ""}`;
71049
+ if (/nothing to commit/i.test(combined))
71050
+ break;
71051
+ if (commitFixAttempt >= maxHookFixAttempts) {
71052
+ appendLog(`! commit rejected for ${changeName} after ${commitFixAttempt} fix attempts (host pre-commit hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
71053
+ appendLog(` detail: ${detail}`, "red");
71054
+ effectiveCode = PR_FAILED_EXIT;
71055
+ commitGaveUp = true;
71056
+ break;
71057
+ }
71058
+ commitFixAttempt += 1;
71059
+ appendLog(`! commit rejected for ${changeName} \u2014 feeding error back to worker (attempt ${commitFixAttempt}/${maxHookFixAttempts})`, "yellow");
71060
+ appendLog(` detail: ${detail}`, "yellow");
71061
+ const retryCode = await runWorkerWithSteering(`Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem reported below, then the commit will be retried.
70975
71062
 
70976
- ${steering}
70977
- `;
70978
- const next = prev.includes("## Steering") ? prev.replace(/## Steering\n+([\s\S]*?)$/, (_m, rest2) => `## Steering
70979
- ${stamped}
70980
- ${rest2}`) : prev + `
70981
- ## Steering
70982
- ${stamped}
70983
- `;
70984
- await Bun.write(proposalPath, next);
70985
- } catch (err) {
70986
- appendLog(`! could not append steering: ${err.message}`, "red");
70987
- }
70988
- const p = Bun.spawn({
70989
- cmd: buildTaskCmd(),
70990
- cwd: cwd2,
70991
- stdout: "ignore",
70992
- stderr: "ignore",
70993
- stdin: "ignore"
70994
- });
70995
- return p.exited;
70996
- },
70997
- pushBranch: async () => {
70998
- await bunCmdRunner.run(["git", "push", "origin", branch], cwd2);
70999
- },
71000
- log: appendLog,
71001
- sleep: (ms) => new Promise((r) => setTimeout(r, ms))
71002
- }, {
71003
- maxAttempts: cfg.maxCiFixAttempts,
71004
- pollIntervalSeconds: cfg.ciPollIntervalSeconds
71005
- });
71006
- if (!result2.success) {
71007
- appendLog(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
71008
- effectiveCode = CI_FAILED_EXIT;
71063
+ ` + "```\n" + combined.trim() + "\n```");
71064
+ if (retryCode !== 0) {
71065
+ appendLog(`! worker re-run after commit rejection exited code ${retryCode} \u2014 giving up`, "red");
71066
+ effectiveCode = PR_FAILED_EXIT;
71067
+ commitGaveUp = true;
71068
+ break;
71069
+ }
71070
+ }
71071
+ }
71072
+ let pushFixAttempt = 0;
71073
+ let pr = null;
71074
+ let prGaveUp = commitGaveUp;
71075
+ while (!prGaveUp) {
71076
+ try {
71077
+ pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
71078
+ break;
71079
+ } catch (err) {
71080
+ const e = err;
71081
+ const detail = e.stderr?.trim() || e.message;
71082
+ const combined = `${e.stdout ?? ""}
71083
+ ${e.stderr ?? ""}`;
71084
+ const pushRejected = /failed to push some refs|pre-push hook|hook declined/i.test(combined);
71085
+ if (!pushRejected || pushFixAttempt >= maxHookFixAttempts) {
71086
+ if (pushRejected) {
71087
+ appendLog(`! push rejected for ${changeName} after ${pushFixAttempt} fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
71088
+ appendLog(` detail: ${detail}`, "red");
71089
+ } else {
71090
+ appendLog(`! PR create failed for ${changeName}: ${detail}`, "red");
71009
71091
  }
71092
+ effectiveCode = PR_FAILED_EXIT;
71093
+ prGaveUp = true;
71094
+ break;
71095
+ }
71096
+ pushFixAttempt += 1;
71097
+ appendLog(`! push rejected for ${changeName} \u2014 feeding error back to worker (attempt ${pushFixAttempt}/${maxHookFixAttempts})`, "yellow");
71098
+ appendLog(` detail: ${detail}`, "yellow");
71099
+ const retryCode = await runWorkerWithSteering(`Push to origin/${branch} was rejected by the host repo's pre-push hook. ` + `Fix the underlying problem reported below, then the push will be retried.
71100
+
71101
+ ` + "```\n" + combined.trim() + "\n```");
71102
+ if (retryCode !== 0) {
71103
+ appendLog(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
71104
+ effectiveCode = PR_FAILED_EXIT;
71105
+ prGaveUp = true;
71106
+ break;
71107
+ }
71108
+ }
71109
+ }
71110
+ if (prGaveUp) {} else if (!pr) {
71111
+ appendLog(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
71112
+ } else {
71113
+ appendLog(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
71114
+ const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
71115
+ if (wantFixCi) {
71116
+ appendLog(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
71117
+ const result2 = await fixCiUntilGreen({
71118
+ getStatus: () => getPrChecksStatus(pr.url, bunCmdRunner, cwd2),
71119
+ getFailedLogs: (ids) => fetchFailedRunLogs(ids, bunCmdRunner, cwd2),
71120
+ runTaskWithSteering: async (steering) => {
71121
+ try {
71122
+ await appendSteering(proposalPath, `CI feedback:
71123
+
71124
+ ${steering}`);
71125
+ } catch (err) {
71126
+ appendLog(`! could not append steering: ${err.message}`, "red");
71127
+ }
71128
+ const p = Bun.spawn({
71129
+ cmd: buildTaskCmd(),
71130
+ cwd: cwd2,
71131
+ stdout: "ignore",
71132
+ stderr: "ignore",
71133
+ stdin: "ignore"
71134
+ });
71135
+ return p.exited;
71136
+ },
71137
+ pushBranch: async () => {
71138
+ await bunCmdRunner.run(["git", "push", "origin", branch], cwd2);
71139
+ },
71140
+ log: appendLog,
71141
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms))
71142
+ }, {
71143
+ maxAttempts: cfg.maxCiFixAttempts,
71144
+ pollIntervalSeconds: cfg.ciPollIntervalSeconds
71145
+ });
71146
+ if (!result2.success) {
71147
+ appendLog(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
71148
+ effectiveCode = CI_FAILED_EXIT;
71010
71149
  }
71011
71150
  }
71012
- } catch (err) {
71013
- const e = err;
71014
- const detail = e.stderr?.trim() || e.message;
71015
- appendLog(`! PR create failed for ${changeName}: ${detail}`, "red");
71016
- effectiveCode = PR_FAILED_EXIT;
71017
71151
  }
71018
71152
  }
71019
71153
  }
71020
71154
  if (useWorktree && cwd2 !== projectRoot) {
71021
71155
  if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
71022
- try {
71023
- await removeWorktree(projectRoot, cwd2, bunGitRunner);
71024
- appendLog(` removed worktree ${cwd2}`, "gray");
71025
- } catch (err) {
71026
- appendLog(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
71156
+ const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, bunGitRunner).catch((err) => ({
71157
+ safe: false,
71158
+ reason: `safety check failed: ${err.message}`,
71159
+ dirty: "",
71160
+ unpushedCommits: ""
71161
+ }));
71162
+ if (!check.safe) {
71163
+ appendLog(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
71164
+ if (check.dirty) {
71165
+ appendLog(` uncommitted:
71166
+ ${check.dirty}`, "yellow");
71167
+ }
71168
+ if (check.unpushedCommits) {
71169
+ appendLog(` commits:
71170
+ ${check.unpushedCommits}`, "yellow");
71171
+ }
71172
+ appendLog(` path: ${cwd2}`, "yellow");
71173
+ } else {
71174
+ try {
71175
+ await removeWorktree(projectRoot, cwd2, bunGitRunner);
71176
+ appendLog(` removed worktree ${cwd2}`, "gray");
71177
+ } catch (err) {
71178
+ appendLog(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
71179
+ }
71027
71180
  }
71028
71181
  }
71029
71182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
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",