@neriros/ralphy 2.17.3 → 2.18.0

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
@@ -173,6 +173,7 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
173
173
  "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
174
174
  "getInProgress": { "filter": [{ "type": "status", "value": "In Progress" }] },
175
175
  "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflicted" }] },
176
+ "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
176
177
  "setInProgress": { "type": "status", "value": "In Progress" },
177
178
  "setDone": {
178
179
  "apply": [
@@ -183,6 +184,7 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
183
184
  "setError": { "type": "label", "value": "ralph:error" },
184
185
  "setConflicted": { "type": "label", "value": "ralph:conflicted" },
185
186
  "clearConflicted": { "type": "label", "value": "ralph:conflicted" },
187
+ "clearReview": { "type": "label", "value": "ralph:review" },
186
188
  },
187
189
  },
188
190
  "useWorktree": true,
@@ -205,9 +207,11 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
205
207
 
206
208
  Linear is the source of truth for which issues Ralph has touched. Each `linear.indicators` key names a lifecycle event:
207
209
 
208
- - `getTodo` / `getInProgress` / `getConflicted` — `{ filter: [...] }` selectors used to find issues to pick up, resume, or repair.
210
+ - `getTodo` / `getInProgress` / `getConflicted` / `getReview` — `{ filter: [...] }` selectors used to find issues to pick up, resume, repair, or follow up on after review.
209
211
  - `setInProgress` / `setDone` / `setError` / `setConflicted` — single marker `{ type, value }` or `{ apply: [...] }` for multi-marker.
210
- - `clearConflicted` — labels to remove once a conflicted PR is fixed (status removal is not supported).
212
+ - `clearConflicted` / `clearReview` — labels to remove once a conflicted PR is fixed or a review-mode issue is picked back up (status removal is not supported).
213
+
214
+ **Review follow-ups.** When a Linear issue is in a "done" state and a reviewer adds the `getReview` marker (typically a label like `ralph:review` after leaving comments), Ralph picks it up, applies `setInProgress`, removes the `clearReview` label so the same trigger doesn't re-fire, fetches the comment thread, filters out Ralph's own comments, and prepends those reviewer comments as a new task at the top of `tasks.md`. The worker addresses them in the same change branch and `setDone` is re-applied on success.
211
215
 
212
216
  Marker types are `"label"` or `"status"`. Combine markers under `apply` when one event needs to set multiple — e.g. `setDone` flipping a status _and_ adding a "shipped" label.
213
217
 
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.17.3")
35033
- return "2.17.3";
35032
+ if ("2.18.0")
35033
+ return "2.18.0";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -35356,13 +35356,20 @@ var init_cli = __esm(() => {
35356
35356
  "getTodo",
35357
35357
  "getInProgress",
35358
35358
  "getConflicted",
35359
+ "getReview",
35359
35360
  "setInProgress",
35360
35361
  "setDone",
35361
35362
  "setError",
35362
35363
  "setConflicted",
35363
- "clearConflicted"
35364
+ "clearConflicted",
35365
+ "clearReview"
35366
+ ]);
35367
+ GET_KEYS = new Set([
35368
+ "getTodo",
35369
+ "getInProgress",
35370
+ "getConflicted",
35371
+ "getReview"
35364
35372
  ]);
35365
- GET_KEYS = new Set(["getTodo", "getInProgress", "getConflicted"]);
35366
35373
  HELP_TEXT = [
35367
35374
  `ralph v${VERSION}`,
35368
35375
  "",
@@ -35404,8 +35411,9 @@ var init_cli = __esm(() => {
35404
35411
  " --indicator getTodo:status:Todo",
35405
35412
  " --indicator setDone:label:shipped",
35406
35413
  " --indicator setDone:status:Done (combined with above \u2192 multi-marker)",
35407
- " Keys: getTodo, getInProgress, getConflicted,",
35408
- " setInProgress, setDone, setError, setConflicted, clearConflicted",
35414
+ " Keys: getTodo, getInProgress, getConflicted, getReview,",
35415
+ " setInProgress, setDone, setError, setConflicted,",
35416
+ " clearConflicted, clearReview",
35409
35417
  " Types: label, status",
35410
35418
  " --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
35411
35419
  " --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
@@ -59626,6 +59634,10 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59626
59634
  // Issues whose PR has a merge conflict (Ralph will attempt a re-fix run).
59627
59635
  // "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflict" }] },
59628
59636
 
59637
+ // Done issues with new review comments to address (Ralph will re-open
59638
+ // and prepend a task that ingests the non-Ralph comments).
59639
+ // "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
59640
+
59629
59641
  // Applied when Ralph picks up an issue.
59630
59642
  // "setInProgress": { "type": "label", "value": "ralph:in-progress" },
59631
59643
 
@@ -59639,7 +59651,10 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59639
59651
  // "setConflicted": { "type": "label", "value": "ralph:conflict" },
59640
59652
 
59641
59653
  // Label removed once the conflict is fixed (status removal is not supported here).
59642
- // "clearConflicted": { "type": "label", "value": "ralph:conflict" }
59654
+ // "clearConflicted": { "type": "label", "value": "ralph:conflict" },
59655
+
59656
+ // Label removed when Ralph picks up a review-mode issue (status removal not supported).
59657
+ // "clearReview": { "type": "label", "value": "ralph:review" }
59643
59658
  }
59644
59659
  }
59645
59660
  }
@@ -59661,24 +59676,28 @@ var init_config = __esm(() => {
59661
59676
  getTodo: GetIndicatorSchema.optional(),
59662
59677
  getInProgress: GetIndicatorSchema.optional(),
59663
59678
  getConflicted: GetIndicatorSchema.optional(),
59679
+ getReview: GetIndicatorSchema.optional(),
59664
59680
  setInProgress: SetIndicatorSchema.optional(),
59665
59681
  setDone: SetIndicatorSchema.optional(),
59666
59682
  setError: SetIndicatorSchema.optional(),
59667
59683
  setConflicted: SetIndicatorSchema.optional(),
59668
- clearConflicted: SetIndicatorSchema.optional()
59684
+ clearConflicted: SetIndicatorSchema.optional(),
59685
+ clearReview: SetIndicatorSchema.optional()
59669
59686
  }).superRefine((value, ctx) => {
59670
- const clear = value.clearConflicted;
59671
- if (!clear)
59672
- return;
59673
- const markers = "apply" in clear ? clear.apply : [clear];
59674
- for (const m of markers) {
59675
- if (m.type !== "label") {
59676
- ctx.addIssue({
59677
- code: exports_external.ZodIssueCode.custom,
59678
- path: ["clearConflicted"],
59679
- message: "clearConflicted markers must be label-typed (status removal is not supported)"
59680
- });
59681
- return;
59687
+ for (const key of ["clearConflicted", "clearReview"]) {
59688
+ const clear = value[key];
59689
+ if (!clear)
59690
+ continue;
59691
+ const markers = "apply" in clear ? clear.apply : [clear];
59692
+ for (const m of markers) {
59693
+ if (m.type !== "label") {
59694
+ ctx.addIssue({
59695
+ code: exports_external.ZodIssueCode.custom,
59696
+ path: [key],
59697
+ message: `${key} markers must be label-typed (status removal is not supported)`
59698
+ });
59699
+ break;
59700
+ }
59682
59701
  }
59683
59702
  }
59684
59703
  });
@@ -60050,19 +60069,21 @@ class AgentCoordinator {
60050
60069
  let todo = [];
60051
60070
  let inProgress = [];
60052
60071
  let conflicted = [];
60072
+ let review = [];
60053
60073
  try {
60054
- [todo, inProgress, conflicted] = await Promise.all([
60074
+ [todo, inProgress, conflicted, review] = await Promise.all([
60055
60075
  this.deps.fetchTodo(),
60056
60076
  this.deps.fetchInProgress(),
60057
- this.deps.fetchConflicted()
60077
+ this.deps.fetchConflicted(),
60078
+ this.deps.fetchReview()
60058
60079
  ]);
60059
60080
  } catch (err) {
60060
60081
  this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
60061
60082
  capture("agent_linear_poll_failed", { error: err.message });
60062
60083
  return { found: 0, added: 0 };
60063
60084
  }
60064
- if (todo.length + inProgress.length + conflicted.length > 0) {
60065
- this.deps.onLog(` poll: ${todo.length} todo, ${inProgress.length} in-progress, ${conflicted.length} conflicted`, "gray");
60085
+ if (todo.length + inProgress.length + conflicted.length + review.length > 0) {
60086
+ this.deps.onLog(` poll: ${todo.length} todo, ${inProgress.length} in-progress, ${conflicted.length} conflicted, ${review.length} review`, "gray");
60066
60087
  }
60067
60088
  const queuedIds = new Set(this.queue.map((q) => q.issue.id));
60068
60089
  const activeIds = new Set(this.workers.map((w) => w.issueId));
@@ -60097,6 +60118,16 @@ class AgentCoordinator {
60097
60118
  added += 1;
60098
60119
  this.deps.onLog(` \u21B3 ${issue.identifier} queued (conflict-fix)`, "gray");
60099
60120
  }
60121
+ for (const issue of review) {
60122
+ if (atTicketLimit())
60123
+ break;
60124
+ if (!eligible(issue.id))
60125
+ continue;
60126
+ this.queue.push({ issue, mode: "review" });
60127
+ queuedIds.add(issue.id);
60128
+ added += 1;
60129
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (review)`, "gray");
60130
+ }
60100
60131
  for (const issue of todo) {
60101
60132
  if (atTicketLimit())
60102
60133
  break;
@@ -60113,7 +60144,8 @@ class AgentCoordinator {
60113
60144
  const modeRank = {
60114
60145
  resume: 0,
60115
60146
  "conflict-fix": 1,
60116
- fresh: 2
60147
+ review: 2,
60148
+ fresh: 3
60117
60149
  };
60118
60150
  this.queue.sort((a, b) => {
60119
60151
  const pa = a.issue.priority === 0 ? Infinity : a.issue.priority;
@@ -60126,7 +60158,7 @@ class AgentCoordinator {
60126
60158
  this.spawnNext();
60127
60159
  await this.scanDoneForConflicts();
60128
60160
  await this.reportProgress();
60129
- const found = todo.length + inProgress.length + conflicted.length;
60161
+ const found = todo.length + inProgress.length + conflicted.length + review.length;
60130
60162
  return { found, added };
60131
60163
  }
60132
60164
  dependenciesResolved(issue) {
@@ -60270,6 +60302,26 @@ class AgentCoordinator {
60270
60302
  });
60271
60303
  }
60272
60304
  }
60305
+ if (mode === "review" && this.opts.clearReview) {
60306
+ try {
60307
+ await this.deps.removeIndicator(issue, this.opts.clearReview);
60308
+ this.deps.onLog(` ${issue.identifier}: clearReview applied`, "gray");
60309
+ } catch (err) {
60310
+ this.deps.onLog(`! Linear clearReview failed for ${issue.identifier}: ${err.message}`, "yellow");
60311
+ capture("agent_indicator_failed", {
60312
+ indicator: "clearReview",
60313
+ issue_identifier: issue.identifier,
60314
+ error: err.message
60315
+ });
60316
+ }
60317
+ }
60318
+ if (mode === "review" && this.opts.postComments !== false) {
60319
+ try {
60320
+ await this.deps.postComment(issue, `\uD83D\uDD01 Ralph picked up new review comments. Tracking change: \`${prep.changeName}\``);
60321
+ } catch (err) {
60322
+ this.deps.onLog(`! Linear review comment failed for ${issue.identifier}: ${err.message}`, "yellow");
60323
+ }
60324
+ }
60273
60325
  if (mode === "fresh" && this.opts.postComments !== false) {
60274
60326
  let alreadyPosted = false;
60275
60327
  try {
@@ -61032,6 +61084,30 @@ function mergeIndicators(cfg, cli) {
61032
61084
  }
61033
61085
  return out;
61034
61086
  }
61087
+ function isRalphComment(body) {
61088
+ const trimmed = body.trimStart();
61089
+ return /^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u26A0|\uD83D\uDD01)\s*Ralph\b/.test(trimmed);
61090
+ }
61091
+ function buildReviewTaskBody(comments, url) {
61092
+ if (comments.length === 0) {
61093
+ return `No non-Ralph reviewer comments were found on ${url}. Recheck the issue manually before continuing.`;
61094
+ }
61095
+ const blocks = comments.map((c) => {
61096
+ const author = c.user?.name ?? "unknown";
61097
+ return `**${author}** \u2014 ${c.createdAt}
61098
+
61099
+ ${c.body.trim()}`;
61100
+ });
61101
+ return [
61102
+ `Reviewer comments left on the Linear issue (${url}):`,
61103
+ "",
61104
+ ...blocks,
61105
+ "",
61106
+ "Address every concrete request above. If a comment is ambiguous, note",
61107
+ "your interpretation in proposal.md `## Steering` before acting."
61108
+ ].join(`
61109
+ `);
61110
+ }
61035
61111
  function unionMarkers(...sets) {
61036
61112
  const out = [];
61037
61113
  const seen = new Set;
@@ -61071,6 +61147,7 @@ function buildAgentCoordinator(input) {
61071
61147
  const team = args.linearTeam || cfg.linear.team;
61072
61148
  const assignee = args.linearAssignee || cfg.linear.assignee;
61073
61149
  const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError, indicators.setConflicted);
61150
+ const excludeFromReview = unionMarkers(indicators.setInProgress, indicators.setError, indicators.setConflicted);
61074
61151
  const gitRunner = input.runners?.git ?? bunGitRunner;
61075
61152
  const cmdRunner = input.runners?.cmd ?? bunCmdRunner;
61076
61153
  const stateCache = new Map;
@@ -61244,7 +61321,8 @@ function buildAgentCoordinator(input) {
61244
61321
  async function prepare(issue, mode) {
61245
61322
  const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue);
61246
61323
  let changeName;
61247
- if (mode === "fresh") {
61324
+ const isFresh = mode === "fresh";
61325
+ if (isFresh) {
61248
61326
  let comments = [];
61249
61327
  try {
61250
61328
  comments = await fetchIssueComments(apiKey, issue.id);
@@ -61264,7 +61342,24 @@ function buildAgentCoordinator(input) {
61264
61342
  issueByChange.set(changeName, issue);
61265
61343
  if (branch)
61266
61344
  branchByChange.set(changeName, branch);
61267
- if (mode === "conflict-fix") {
61345
+ if (mode === "review") {
61346
+ const wtLayout = projectLayout(workerCwd);
61347
+ const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61348
+ let comments = [];
61349
+ try {
61350
+ comments = await fetchIssueComments(apiKey, issue.id);
61351
+ } catch (err) {
61352
+ onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
61353
+ }
61354
+ const reviewerComments = comments.filter((c) => !isRalphComment(c.body));
61355
+ const body = buildReviewTaskBody(reviewerComments, issue.url);
61356
+ try {
61357
+ await prependFixTask(tasksFile, "Address reviewer comments", body);
61358
+ } catch (err) {
61359
+ onLog(`! could not prepend review task: ${err.message}`, "red");
61360
+ }
61361
+ await reactivateState2(wtLayout.stateFile(changeName), changeName);
61362
+ } else if (mode === "conflict-fix") {
61268
61363
  const wtLayout = projectLayout(workerCwd);
61269
61364
  const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61270
61365
  const prUrl = prByChange.get(changeName);
@@ -61539,6 +61634,7 @@ PR: ${prUrl}` : ""
61539
61634
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
61540
61635
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
61541
61636
  fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
61637
+ fetchReview: () => fetchByGet(indicators.getReview, excludeFromReview),
61542
61638
  fetchDoneCandidates,
61543
61639
  prepare,
61544
61640
  spawnWorker,
@@ -61567,6 +61663,7 @@ PR: ${prUrl}` : ""
61567
61663
  ...indicators.setError !== undefined ? { setError: indicators.setError } : {},
61568
61664
  ...indicators.setConflicted !== undefined ? { setConflicted: indicators.setConflicted } : {},
61569
61665
  ...indicators.clearConflicted !== undefined ? { clearConflicted: indicators.clearConflicted } : {},
61666
+ ...indicators.clearReview !== undefined ? { clearReview: indicators.clearReview } : {},
61570
61667
  postComments: cfg.linear.postComments,
61571
61668
  commentEveryIterations: cfg.linear.updateEveryIterations,
61572
61669
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {}
@@ -61593,6 +61690,9 @@ function describeIndicators(indicators, team, assignee) {
61593
61690
  if (indicators.getConflicted) {
61594
61691
  parts.push(`conflicted=[${indicators.getConflicted.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
61595
61692
  }
61693
+ if (indicators.getReview) {
61694
+ parts.push(`review=[${indicators.getReview.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
61695
+ }
61596
61696
  return parts.join(", ");
61597
61697
  }
61598
61698
  var bunGitRunner, bunCmdRunner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.17.3",
3
+ "version": "2.18.0",
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",