@neriros/ralphy 2.17.3 → 2.19.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
@@ -169,10 +169,13 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
169
169
  "assignee": "me",
170
170
  "postComments": true,
171
171
  "updateEveryIterations": 10,
172
+ "mentionTrigger": true,
173
+ "mentionHandle": "@ralphy",
172
174
  "indicators": {
173
175
  "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
174
176
  "getInProgress": { "filter": [{ "type": "status", "value": "In Progress" }] },
175
177
  "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflicted" }] },
178
+ "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
176
179
  "setInProgress": { "type": "status", "value": "In Progress" },
177
180
  "setDone": {
178
181
  "apply": [
@@ -183,6 +186,7 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
183
186
  "setError": { "type": "label", "value": "ralph:error" },
184
187
  "setConflicted": { "type": "label", "value": "ralph:conflicted" },
185
188
  "clearConflicted": { "type": "label", "value": "ralph:conflicted" },
189
+ "clearReview": { "type": "label", "value": "ralph:review" },
186
190
  },
187
191
  },
188
192
  "useWorktree": true,
@@ -205,9 +209,13 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
205
209
 
206
210
  Linear is the source of truth for which issues Ralph has touched. Each `linear.indicators` key names a lifecycle event:
207
211
 
208
- - `getTodo` / `getInProgress` / `getConflicted` — `{ filter: [...] }` selectors used to find issues to pick up, resume, or repair.
212
+ - `getTodo` / `getInProgress` / `getConflicted` / `getReview` — `{ filter: [...] }` selectors used to find issues to pick up, resume, repair, or follow up on after review.
209
213
  - `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).
214
+ - `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).
215
+
216
+ **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.
217
+
218
+ **`@ralphy` mention trigger.** Set `linear.mentionTrigger: true` (default `false`) to scan done-issue comments on both Linear and their linked GitHub PR for `@ralphy` mentions (configurable via `linear.mentionHandle`). New mentions enqueue the issue as a review run with the mention text used verbatim as the prepended task. Idempotency: a mention is considered processed when its `createdAt` is older than the most recent Ralph `🔁 picked up` comment on the Linear issue, so the same comment never re-fires across polls. Requires the `gh` CLI authenticated for the GitHub side.
211
219
 
212
220
  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
221
 
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.19.0")
35033
+ return "2.19.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)",
@@ -59613,6 +59621,12 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59613
59621
  // Post a progress update every N iterations. 0 disables. Requires postComments.
59614
59622
  "updateEveryIterations": 10,
59615
59623
 
59624
+ // Watch done-issue Linear comments AND their linked GitHub PR comments
59625
+ // for "@ralphy" mentions. New mentions enqueue the issue as a review run
59626
+ // with the mention text as the prepended task.
59627
+ "mentionTrigger": false,
59628
+ // "mentionHandle": "@ralphy",
59629
+
59616
59630
  // Indicators map Ralph lifecycle events to Linear labels/statuses.
59617
59631
  // WARNING: activating indicators will query AND mutate your Linear workspace.
59618
59632
  // Uncomment each entry after confirming the label/status names match your workspace.
@@ -59626,6 +59640,10 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59626
59640
  // Issues whose PR has a merge conflict (Ralph will attempt a re-fix run).
59627
59641
  // "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflict" }] },
59628
59642
 
59643
+ // Done issues with new review comments to address (Ralph will re-open
59644
+ // and prepend a task that ingests the non-Ralph comments).
59645
+ // "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
59646
+
59629
59647
  // Applied when Ralph picks up an issue.
59630
59648
  // "setInProgress": { "type": "label", "value": "ralph:in-progress" },
59631
59649
 
@@ -59639,7 +59657,10 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59639
59657
  // "setConflicted": { "type": "label", "value": "ralph:conflict" },
59640
59658
 
59641
59659
  // Label removed once the conflict is fixed (status removal is not supported here).
59642
- // "clearConflicted": { "type": "label", "value": "ralph:conflict" }
59660
+ // "clearConflicted": { "type": "label", "value": "ralph:conflict" },
59661
+
59662
+ // Label removed when Ralph picks up a review-mode issue (status removal not supported).
59663
+ // "clearReview": { "type": "label", "value": "ralph:review" }
59643
59664
  }
59644
59665
  }
59645
59666
  }
@@ -59661,24 +59682,28 @@ var init_config = __esm(() => {
59661
59682
  getTodo: GetIndicatorSchema.optional(),
59662
59683
  getInProgress: GetIndicatorSchema.optional(),
59663
59684
  getConflicted: GetIndicatorSchema.optional(),
59685
+ getReview: GetIndicatorSchema.optional(),
59664
59686
  setInProgress: SetIndicatorSchema.optional(),
59665
59687
  setDone: SetIndicatorSchema.optional(),
59666
59688
  setError: SetIndicatorSchema.optional(),
59667
59689
  setConflicted: SetIndicatorSchema.optional(),
59668
- clearConflicted: SetIndicatorSchema.optional()
59690
+ clearConflicted: SetIndicatorSchema.optional(),
59691
+ clearReview: SetIndicatorSchema.optional()
59669
59692
  }).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;
59693
+ for (const key of ["clearConflicted", "clearReview"]) {
59694
+ const clear = value[key];
59695
+ if (!clear)
59696
+ continue;
59697
+ const markers = "apply" in clear ? clear.apply : [clear];
59698
+ for (const m of markers) {
59699
+ if (m.type !== "label") {
59700
+ ctx.addIssue({
59701
+ code: exports_external.ZodIssueCode.custom,
59702
+ path: [key],
59703
+ message: `${key} markers must be label-typed (status removal is not supported)`
59704
+ });
59705
+ break;
59706
+ }
59682
59707
  }
59683
59708
  }
59684
59709
  });
@@ -59711,8 +59736,16 @@ var init_config = __esm(() => {
59711
59736
  assignee: exports_external.string().optional(),
59712
59737
  postComments: exports_external.boolean().default(true),
59713
59738
  updateEveryIterations: exports_external.number().int().nonnegative().default(10),
59739
+ mentionTrigger: exports_external.boolean().default(false),
59740
+ mentionHandle: exports_external.string().default("@ralphy"),
59714
59741
  indicators: IndicatorsSchema.default({})
59715
- }).strict().default({ postComments: true, updateEveryIterations: 10, indicators: {} })
59742
+ }).strict().default({
59743
+ postComments: true,
59744
+ updateEveryIterations: 10,
59745
+ mentionTrigger: false,
59746
+ mentionHandle: "@ralphy",
59747
+ indicators: {}
59748
+ })
59716
59749
  }).default({
59717
59750
  concurrency: 1,
59718
59751
  pollIntervalSeconds: 60,
@@ -59721,7 +59754,13 @@ var init_config = __esm(() => {
59721
59754
  enableManualTest: false,
59722
59755
  engine: "claude",
59723
59756
  model: "opus",
59724
- linear: { postComments: true, updateEveryIterations: 10, indicators: {} }
59757
+ linear: {
59758
+ postComments: true,
59759
+ updateEveryIterations: 10,
59760
+ mentionTrigger: false,
59761
+ mentionHandle: "@ralphy",
59762
+ indicators: {}
59763
+ }
59725
59764
  });
59726
59765
  });
59727
59766
 
@@ -60050,19 +60089,23 @@ class AgentCoordinator {
60050
60089
  let todo = [];
60051
60090
  let inProgress = [];
60052
60091
  let conflicted = [];
60092
+ let review = [];
60093
+ let mentions = [];
60053
60094
  try {
60054
- [todo, inProgress, conflicted] = await Promise.all([
60095
+ [todo, inProgress, conflicted, review, mentions] = await Promise.all([
60055
60096
  this.deps.fetchTodo(),
60056
60097
  this.deps.fetchInProgress(),
60057
- this.deps.fetchConflicted()
60098
+ this.deps.fetchConflicted(),
60099
+ this.deps.fetchReview(),
60100
+ this.deps.fetchMentions()
60058
60101
  ]);
60059
60102
  } catch (err) {
60060
60103
  this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
60061
60104
  capture("agent_linear_poll_failed", { error: err.message });
60062
60105
  return { found: 0, added: 0 };
60063
60106
  }
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");
60107
+ if (todo.length + inProgress.length + conflicted.length + review.length + mentions.length > 0) {
60108
+ this.deps.onLog(` poll: ${todo.length} todo, ${inProgress.length} in-progress, ${conflicted.length} conflicted, ${review.length} review, ${mentions.length} mention`, "gray");
60066
60109
  }
60067
60110
  const queuedIds = new Set(this.queue.map((q) => q.issue.id));
60068
60111
  const activeIds = new Set(this.workers.map((w) => w.issueId));
@@ -60097,6 +60140,26 @@ class AgentCoordinator {
60097
60140
  added += 1;
60098
60141
  this.deps.onLog(` \u21B3 ${issue.identifier} queued (conflict-fix)`, "gray");
60099
60142
  }
60143
+ for (const issue of review) {
60144
+ if (atTicketLimit())
60145
+ break;
60146
+ if (!eligible(issue.id))
60147
+ continue;
60148
+ this.queue.push({ issue, mode: "review" });
60149
+ queuedIds.add(issue.id);
60150
+ added += 1;
60151
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (review)`, "gray");
60152
+ }
60153
+ for (const { issue, trigger } of mentions) {
60154
+ if (atTicketLimit())
60155
+ break;
60156
+ if (!eligible(issue.id))
60157
+ continue;
60158
+ this.queue.push({ issue, mode: "review", trigger });
60159
+ queuedIds.add(issue.id);
60160
+ added += 1;
60161
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (review via ${trigger.source} mention)`, "gray");
60162
+ }
60100
60163
  for (const issue of todo) {
60101
60164
  if (atTicketLimit())
60102
60165
  break;
@@ -60113,7 +60176,8 @@ class AgentCoordinator {
60113
60176
  const modeRank = {
60114
60177
  resume: 0,
60115
60178
  "conflict-fix": 1,
60116
- fresh: 2
60179
+ review: 2,
60180
+ fresh: 3
60117
60181
  };
60118
60182
  this.queue.sort((a, b) => {
60119
60183
  const pa = a.issue.priority === 0 ? Infinity : a.issue.priority;
@@ -60126,7 +60190,7 @@ class AgentCoordinator {
60126
60190
  this.spawnNext();
60127
60191
  await this.scanDoneForConflicts();
60128
60192
  await this.reportProgress();
60129
- const found = todo.length + inProgress.length + conflicted.length;
60193
+ const found = todo.length + inProgress.length + conflicted.length + review.length + mentions.length;
60130
60194
  return { found, added };
60131
60195
  }
60132
60196
  dependenciesResolved(issue) {
@@ -60235,13 +60299,13 @@ class AgentCoordinator {
60235
60299
  while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
60236
60300
  const next = this.queue.shift();
60237
60301
  this.pendingIds.add(next.issue.id);
60238
- this.launchWorker(next.issue, next.mode);
60302
+ this.launchWorker(next.issue, next.mode, next.trigger);
60239
60303
  }
60240
60304
  }
60241
- async launchWorker(issue, mode) {
60305
+ async launchWorker(issue, mode, trigger) {
60242
60306
  let prep;
60243
60307
  try {
60244
- prep = await this.deps.prepare(issue, mode);
60308
+ prep = await this.deps.prepare(issue, mode, trigger);
60245
60309
  } catch (err) {
60246
60310
  this.pendingIds.delete(issue.id);
60247
60311
  this.deps.onLog(`! prepare(${mode}) failed for ${issue.identifier}: ${err.message}`, "red");
@@ -60270,6 +60334,27 @@ class AgentCoordinator {
60270
60334
  });
60271
60335
  }
60272
60336
  }
60337
+ if (mode === "review" && this.opts.clearReview) {
60338
+ try {
60339
+ await this.deps.removeIndicator(issue, this.opts.clearReview);
60340
+ this.deps.onLog(` ${issue.identifier}: clearReview applied`, "gray");
60341
+ } catch (err) {
60342
+ this.deps.onLog(`! Linear clearReview failed for ${issue.identifier}: ${err.message}`, "yellow");
60343
+ capture("agent_indicator_failed", {
60344
+ indicator: "clearReview",
60345
+ issue_identifier: issue.identifier,
60346
+ error: err.message
60347
+ });
60348
+ }
60349
+ }
60350
+ if (mode === "review" && this.opts.postComments !== false) {
60351
+ const sourceTag = trigger ? trigger.source === "github" ? " (GitHub @mention)" : " (Linear @mention)" : "";
60352
+ try {
60353
+ await this.deps.postComment(issue, `\uD83D\uDD01 Ralph picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``);
60354
+ } catch (err) {
60355
+ this.deps.onLog(`! Linear review comment failed for ${issue.identifier}: ${err.message}`, "yellow");
60356
+ }
60357
+ }
60273
60358
  if (mode === "fresh" && this.opts.postComments !== false) {
60274
60359
  let alreadyPosted = false;
60275
60360
  try {
@@ -61032,6 +61117,49 @@ function mergeIndicators(cfg, cli) {
61032
61117
  }
61033
61118
  return out;
61034
61119
  }
61120
+ function isRalphComment(body) {
61121
+ const trimmed = body.trimStart();
61122
+ return /^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u26A0|\uD83D\uDD01)\s*Ralph\b/.test(trimmed);
61123
+ }
61124
+ function buildReviewTaskBody(comments, url) {
61125
+ if (comments.length === 0) {
61126
+ return `No non-Ralph reviewer comments were found on ${url}. Recheck the issue manually before continuing.`;
61127
+ }
61128
+ const blocks = comments.map((c) => {
61129
+ const author = c.user?.name ?? "unknown";
61130
+ return `**${author}** \u2014 ${c.createdAt}
61131
+
61132
+ ${c.body.trim()}`;
61133
+ });
61134
+ return [
61135
+ `Reviewer comments left on the Linear issue (${url}):`,
61136
+ "",
61137
+ ...blocks,
61138
+ "",
61139
+ "Address every concrete request above. If a comment is ambiguous, note",
61140
+ "your interpretation in proposal.md `## Steering` before acting."
61141
+ ].join(`
61142
+ `);
61143
+ }
61144
+ function escapeRegex(s) {
61145
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61146
+ }
61147
+ function buildMentionTaskBody(trigger, issueUrl) {
61148
+ const sourceLabel = trigger.source === "github" ? "GitHub PR" : "Linear issue";
61149
+ const permalink = trigger.url ?? issueUrl;
61150
+ const header = `${trigger.author ?? "unknown"} \u2014 ${trigger.createdAt} (${sourceLabel})`;
61151
+ return [
61152
+ `An @ralphy mention was left on ${sourceLabel} (${permalink}):`,
61153
+ "",
61154
+ `**${header}**`,
61155
+ "",
61156
+ trigger.body.trim(),
61157
+ "",
61158
+ "Treat this comment as the next concrete request. If it's ambiguous,",
61159
+ "note your interpretation in proposal.md `## Steering` before acting."
61160
+ ].join(`
61161
+ `);
61162
+ }
61035
61163
  function unionMarkers(...sets) {
61036
61164
  const out = [];
61037
61165
  const seen = new Set;
@@ -61071,6 +61199,7 @@ function buildAgentCoordinator(input) {
61071
61199
  const team = args.linearTeam || cfg.linear.team;
61072
61200
  const assignee = args.linearAssignee || cfg.linear.assignee;
61073
61201
  const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError, indicators.setConflicted);
61202
+ const excludeFromReview = unionMarkers(indicators.setInProgress, indicators.setError, indicators.setConflicted);
61074
61203
  const gitRunner = input.runners?.git ?? bunGitRunner;
61075
61204
  const cmdRunner = input.runners?.cmd ?? bunCmdRunner;
61076
61205
  const stateCache = new Map;
@@ -61241,10 +61370,11 @@ function buildAgentCoordinator(input) {
61241
61370
  }
61242
61371
  return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
61243
61372
  }
61244
- async function prepare(issue, mode) {
61373
+ async function prepare(issue, mode, trigger) {
61245
61374
  const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue);
61246
61375
  let changeName;
61247
- if (mode === "fresh") {
61376
+ const isFresh = mode === "fresh";
61377
+ if (isFresh) {
61248
61378
  let comments = [];
61249
61379
  try {
61250
61380
  comments = await fetchIssueComments(apiKey, issue.id);
@@ -61264,7 +61394,32 @@ function buildAgentCoordinator(input) {
61264
61394
  issueByChange.set(changeName, issue);
61265
61395
  if (branch)
61266
61396
  branchByChange.set(changeName, branch);
61267
- if (mode === "conflict-fix") {
61397
+ if (mode === "review") {
61398
+ const wtLayout = projectLayout(workerCwd);
61399
+ const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61400
+ let body;
61401
+ let heading;
61402
+ if (trigger) {
61403
+ heading = trigger.source === "github" ? "Address GitHub @ralphy mention" : "Address Linear @ralphy mention";
61404
+ body = buildMentionTaskBody(trigger, issue.url);
61405
+ } else {
61406
+ heading = "Address reviewer comments";
61407
+ let comments = [];
61408
+ try {
61409
+ comments = await fetchIssueComments(apiKey, issue.id);
61410
+ } catch (err) {
61411
+ onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
61412
+ }
61413
+ const reviewerComments = comments.filter((c) => !isRalphComment(c.body));
61414
+ body = buildReviewTaskBody(reviewerComments, issue.url);
61415
+ }
61416
+ try {
61417
+ await prependFixTask(tasksFile, heading, body);
61418
+ } catch (err) {
61419
+ onLog(`! could not prepend review task: ${err.message}`, "red");
61420
+ }
61421
+ await reactivateState2(wtLayout.stateFile(changeName), changeName);
61422
+ } else if (mode === "conflict-fix") {
61268
61423
  const wtLayout = projectLayout(workerCwd);
61269
61424
  const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61270
61425
  const prUrl = prByChange.get(changeName);
@@ -61535,10 +61690,145 @@ PR: ${prUrl}` : ""
61535
61690
  return [];
61536
61691
  return fetchOpenIssues(apiKey, { team, assignee, include, exclude });
61537
61692
  }
61693
+ async function fetchMentions() {
61694
+ if (!cfg.linear.mentionTrigger)
61695
+ return [];
61696
+ const handle = cfg.linear.mentionHandle;
61697
+ let candidates = [];
61698
+ try {
61699
+ candidates = await fetchDoneCandidates();
61700
+ } catch (err) {
61701
+ onLog(`! mention scan: fetchDoneCandidates failed: ${err.message}`, "yellow");
61702
+ return [];
61703
+ }
61704
+ const out = [];
61705
+ for (const issue of candidates) {
61706
+ let comments = [];
61707
+ try {
61708
+ comments = await fetchIssueComments(apiKey, issue.id);
61709
+ } catch (err) {
61710
+ onLog(`! mention scan: Linear comments failed for ${issue.identifier}: ${err.message}`, "yellow");
61711
+ continue;
61712
+ }
61713
+ const lastRalphPickup = findLastRalphPickupISO(comments);
61714
+ for (const c of comments) {
61715
+ if (isRalphComment(c.body))
61716
+ continue;
61717
+ if (!containsHandle(c.body, handle))
61718
+ continue;
61719
+ if (lastRalphPickup && c.createdAt <= lastRalphPickup)
61720
+ continue;
61721
+ out.push({
61722
+ issue,
61723
+ trigger: {
61724
+ source: "linear",
61725
+ body: c.body,
61726
+ createdAt: c.createdAt,
61727
+ ...c.user?.name ? { author: c.user.name } : {},
61728
+ url: issue.url
61729
+ }
61730
+ });
61731
+ break;
61732
+ }
61733
+ if (out.length > 0 && out[out.length - 1].issue.id === issue.id)
61734
+ continue;
61735
+ const prUrl = await resolvePrUrlForIssue(issue);
61736
+ if (!prUrl)
61737
+ continue;
61738
+ const ghComments = await fetchPrIssueComments(prUrl);
61739
+ for (const c of ghComments) {
61740
+ if (!containsHandle(c.body, handle))
61741
+ continue;
61742
+ if (lastRalphPickup && c.createdAt <= lastRalphPickup)
61743
+ continue;
61744
+ out.push({
61745
+ issue,
61746
+ trigger: {
61747
+ source: "github",
61748
+ body: c.body,
61749
+ createdAt: c.createdAt,
61750
+ ...c.author ? { author: c.author } : {},
61751
+ url: c.url
61752
+ }
61753
+ });
61754
+ break;
61755
+ }
61756
+ }
61757
+ return out;
61758
+ }
61759
+ function findLastRalphPickupISO(comments) {
61760
+ let latest = null;
61761
+ for (const c of comments) {
61762
+ if (!/^\uD83D\uDD01\s*Ralph picked up/.test(c.body.trimStart()))
61763
+ continue;
61764
+ if (latest === null || c.createdAt > latest)
61765
+ latest = c.createdAt;
61766
+ }
61767
+ return latest;
61768
+ }
61769
+ function containsHandle(body, handle) {
61770
+ const re = new RegExp(`(^|\\s|[^A-Za-z0-9_])${escapeRegex(handle)}\\b`, "i");
61771
+ return re.test(body);
61772
+ }
61773
+ async function resolvePrUrlForIssue(issue) {
61774
+ const changeName = changeNameForIssue(issue);
61775
+ if (prUnavailable.has(changeName))
61776
+ return null;
61777
+ const cached = prByChange.get(changeName);
61778
+ if (cached)
61779
+ return cached;
61780
+ const branch = branchForChange(changeName);
61781
+ try {
61782
+ const res = await cmdRunner.run([
61783
+ "gh",
61784
+ "pr",
61785
+ "list",
61786
+ "--head",
61787
+ branch,
61788
+ "--state",
61789
+ "all",
61790
+ "--json",
61791
+ "url",
61792
+ "--jq",
61793
+ ".[0].url // empty"
61794
+ ], projectRoot);
61795
+ const found = res.stdout.trim();
61796
+ if (!found) {
61797
+ prUnavailable.add(changeName);
61798
+ return null;
61799
+ }
61800
+ prByChange.set(changeName, found);
61801
+ return found;
61802
+ } catch {
61803
+ return null;
61804
+ }
61805
+ }
61806
+ async function fetchPrIssueComments(prUrl) {
61807
+ const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/.exec(prUrl);
61808
+ if (!m)
61809
+ return [];
61810
+ const [, owner, repo, num] = m;
61811
+ try {
61812
+ const res = await cmdRunner.run([
61813
+ "gh",
61814
+ "api",
61815
+ `repos/${owner}/${repo}/issues/${num}/comments`,
61816
+ "--jq",
61817
+ "[.[] | {body: .body, createdAt: .created_at, author: .user.login, url: .html_url}]"
61818
+ ], projectRoot);
61819
+ const parsed = JSON.parse(res.stdout || "[]");
61820
+ return parsed;
61821
+ } catch (err) {
61822
+ onLog(`! mention scan: gh comments failed for ${prUrl}: ${err.message}`, "yellow");
61823
+ return [];
61824
+ }
61825
+ }
61538
61826
  const coord = new AgentCoordinator({
61539
61827
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
61540
61828
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
61541
61829
  fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
61830
+ fetchReview: () => fetchByGet(indicators.getReview, excludeFromReview),
61831
+ fetchMentions,
61542
61832
  fetchDoneCandidates,
61543
61833
  prepare,
61544
61834
  spawnWorker,
@@ -61567,6 +61857,7 @@ PR: ${prUrl}` : ""
61567
61857
  ...indicators.setError !== undefined ? { setError: indicators.setError } : {},
61568
61858
  ...indicators.setConflicted !== undefined ? { setConflicted: indicators.setConflicted } : {},
61569
61859
  ...indicators.clearConflicted !== undefined ? { clearConflicted: indicators.clearConflicted } : {},
61860
+ ...indicators.clearReview !== undefined ? { clearReview: indicators.clearReview } : {},
61570
61861
  postComments: cfg.linear.postComments,
61571
61862
  commentEveryIterations: cfg.linear.updateEveryIterations,
61572
61863
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {}
@@ -61593,6 +61884,9 @@ function describeIndicators(indicators, team, assignee) {
61593
61884
  if (indicators.getConflicted) {
61594
61885
  parts.push(`conflicted=[${indicators.getConflicted.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
61595
61886
  }
61887
+ if (indicators.getReview) {
61888
+ parts.push(`review=[${indicators.getReview.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
61889
+ }
61596
61890
  return parts.join(", ");
61597
61891
  }
61598
61892
  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.19.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",