@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 +10 -2
- package/dist/cli/index.js +328 -34
- package/package.json +1 -1
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
|
|
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.
|
|
35033
|
-
return "2.
|
|
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,
|
|
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
|
|
59671
|
-
|
|
59672
|
-
|
|
59673
|
-
|
|
59674
|
-
|
|
59675
|
-
|
|
59676
|
-
|
|
59677
|
-
|
|
59678
|
-
|
|
59679
|
-
|
|
59680
|
-
|
|
59681
|
-
|
|
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({
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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;
|