@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 +4 -0
- package/dist/cli/index.js +216 -22
- package/package.json +1 -1
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.
|
|
35033
|
-
return "2.
|
|
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({
|
|
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: {
|
|
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
|
|
61349
|
-
|
|
61350
|
-
|
|
61351
|
-
|
|
61352
|
-
|
|
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,
|
|
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,
|