@neriros/ralphy 2.18.0 → 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,6 +169,8 @@ 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" }] },
@@ -213,6 +215,8 @@ Linear is the source of truth for which issues Ralph has touched. Each `linear.i
213
215
 
214
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.
215
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.
219
+
216
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.
217
221
 
218
222
  #### Per-task git worktrees
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.18.0")
35033
- return "2.18.0";
35032
+ if ("2.19.0")
35033
+ return "2.19.0";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -59621,6 +59621,12 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59621
59621
  // Post a progress update every N iterations. 0 disables. Requires postComments.
59622
59622
  "updateEveryIterations": 10,
59623
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
+
59624
59630
  // Indicators map Ralph lifecycle events to Linear labels/statuses.
59625
59631
  // WARNING: activating indicators will query AND mutate your Linear workspace.
59626
59632
  // Uncomment each entry after confirming the label/status names match your workspace.
@@ -59730,8 +59736,16 @@ var init_config = __esm(() => {
59730
59736
  assignee: exports_external.string().optional(),
59731
59737
  postComments: exports_external.boolean().default(true),
59732
59738
  updateEveryIterations: exports_external.number().int().nonnegative().default(10),
59739
+ mentionTrigger: exports_external.boolean().default(false),
59740
+ mentionHandle: exports_external.string().default("@ralphy"),
59733
59741
  indicators: IndicatorsSchema.default({})
59734
- }).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
+ })
59735
59749
  }).default({
59736
59750
  concurrency: 1,
59737
59751
  pollIntervalSeconds: 60,
@@ -59740,7 +59754,13 @@ var init_config = __esm(() => {
59740
59754
  enableManualTest: false,
59741
59755
  engine: "claude",
59742
59756
  model: "opus",
59743
- linear: { postComments: true, updateEveryIterations: 10, indicators: {} }
59757
+ linear: {
59758
+ postComments: true,
59759
+ updateEveryIterations: 10,
59760
+ mentionTrigger: false,
59761
+ mentionHandle: "@ralphy",
59762
+ indicators: {}
59763
+ }
59744
59764
  });
59745
59765
  });
59746
59766
 
@@ -60070,20 +60090,22 @@ class AgentCoordinator {
60070
60090
  let inProgress = [];
60071
60091
  let conflicted = [];
60072
60092
  let review = [];
60093
+ let mentions = [];
60073
60094
  try {
60074
- [todo, inProgress, conflicted, review] = await Promise.all([
60095
+ [todo, inProgress, conflicted, review, mentions] = await Promise.all([
60075
60096
  this.deps.fetchTodo(),
60076
60097
  this.deps.fetchInProgress(),
60077
60098
  this.deps.fetchConflicted(),
60078
- this.deps.fetchReview()
60099
+ this.deps.fetchReview(),
60100
+ this.deps.fetchMentions()
60079
60101
  ]);
60080
60102
  } catch (err) {
60081
60103
  this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
60082
60104
  capture("agent_linear_poll_failed", { error: err.message });
60083
60105
  return { found: 0, added: 0 };
60084
60106
  }
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");
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");
60087
60109
  }
60088
60110
  const queuedIds = new Set(this.queue.map((q) => q.issue.id));
60089
60111
  const activeIds = new Set(this.workers.map((w) => w.issueId));
@@ -60128,6 +60150,16 @@ class AgentCoordinator {
60128
60150
  added += 1;
60129
60151
  this.deps.onLog(` \u21B3 ${issue.identifier} queued (review)`, "gray");
60130
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
+ }
60131
60163
  for (const issue of todo) {
60132
60164
  if (atTicketLimit())
60133
60165
  break;
@@ -60158,7 +60190,7 @@ class AgentCoordinator {
60158
60190
  this.spawnNext();
60159
60191
  await this.scanDoneForConflicts();
60160
60192
  await this.reportProgress();
60161
- const found = todo.length + inProgress.length + conflicted.length + review.length;
60193
+ const found = todo.length + inProgress.length + conflicted.length + review.length + mentions.length;
60162
60194
  return { found, added };
60163
60195
  }
60164
60196
  dependenciesResolved(issue) {
@@ -60267,13 +60299,13 @@ class AgentCoordinator {
60267
60299
  while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
60268
60300
  const next = this.queue.shift();
60269
60301
  this.pendingIds.add(next.issue.id);
60270
- this.launchWorker(next.issue, next.mode);
60302
+ this.launchWorker(next.issue, next.mode, next.trigger);
60271
60303
  }
60272
60304
  }
60273
- async launchWorker(issue, mode) {
60305
+ async launchWorker(issue, mode, trigger) {
60274
60306
  let prep;
60275
60307
  try {
60276
- prep = await this.deps.prepare(issue, mode);
60308
+ prep = await this.deps.prepare(issue, mode, trigger);
60277
60309
  } catch (err) {
60278
60310
  this.pendingIds.delete(issue.id);
60279
60311
  this.deps.onLog(`! prepare(${mode}) failed for ${issue.identifier}: ${err.message}`, "red");
@@ -60316,8 +60348,9 @@ class AgentCoordinator {
60316
60348
  }
60317
60349
  }
60318
60350
  if (mode === "review" && this.opts.postComments !== false) {
60351
+ const sourceTag = trigger ? trigger.source === "github" ? " (GitHub @mention)" : " (Linear @mention)" : "";
60319
60352
  try {
60320
- await this.deps.postComment(issue, `\uD83D\uDD01 Ralph picked up new review comments. Tracking change: \`${prep.changeName}\``);
60353
+ await this.deps.postComment(issue, `\uD83D\uDD01 Ralph picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``);
60321
60354
  } catch (err) {
60322
60355
  this.deps.onLog(`! Linear review comment failed for ${issue.identifier}: ${err.message}`, "yellow");
60323
60356
  }
@@ -61108,6 +61141,25 @@ ${c.body.trim()}`;
61108
61141
  ].join(`
61109
61142
  `);
61110
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
+ }
61111
61163
  function unionMarkers(...sets) {
61112
61164
  const out = [];
61113
61165
  const seen = new Set;
@@ -61318,7 +61370,7 @@ function buildAgentCoordinator(input) {
61318
61370
  }
61319
61371
  return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
61320
61372
  }
61321
- async function prepare(issue, mode) {
61373
+ async function prepare(issue, mode, trigger) {
61322
61374
  const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue);
61323
61375
  let changeName;
61324
61376
  const isFresh = mode === "fresh";
@@ -61345,16 +61397,24 @@ function buildAgentCoordinator(input) {
61345
61397
  if (mode === "review") {
61346
61398
  const wtLayout = projectLayout(workerCwd);
61347
61399
  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");
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);
61353
61415
  }
61354
- const reviewerComments = comments.filter((c) => !isRalphComment(c.body));
61355
- const body = buildReviewTaskBody(reviewerComments, issue.url);
61356
61416
  try {
61357
- await prependFixTask(tasksFile, "Address reviewer comments", body);
61417
+ await prependFixTask(tasksFile, heading, body);
61358
61418
  } catch (err) {
61359
61419
  onLog(`! could not prepend review task: ${err.message}`, "red");
61360
61420
  }
@@ -61630,11 +61690,145 @@ PR: ${prUrl}` : ""
61630
61690
  return [];
61631
61691
  return fetchOpenIssues(apiKey, { team, assignee, include, exclude });
61632
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
+ }
61633
61826
  const coord = new AgentCoordinator({
61634
61827
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
61635
61828
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
61636
61829
  fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
61637
61830
  fetchReview: () => fetchByGet(indicators.getReview, excludeFromReview),
61831
+ fetchMentions,
61638
61832
  fetchDoneCandidates,
61639
61833
  prepare,
61640
61834
  spawnWorker,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.18.0",
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",