@neriros/ralphy 3.3.0 → 3.3.1
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/dist/shell/index.js +327 -98
- package/package.json +1 -1
package/dist/shell/index.js
CHANGED
|
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
|
|
|
18928
18928
|
import { resolve } from "path";
|
|
18929
18929
|
function getVersion() {
|
|
18930
18930
|
try {
|
|
18931
|
-
if ("3.3.
|
|
18932
|
-
return "3.3.
|
|
18931
|
+
if ("3.3.1")
|
|
18932
|
+
return "3.3.1";
|
|
18933
18933
|
} catch {}
|
|
18934
18934
|
const dirsToTry = [];
|
|
18935
18935
|
try {
|
|
@@ -63946,6 +63946,21 @@ function readState(changeDir) {
|
|
|
63946
63946
|
throw new Error(".ralph-state.json not found");
|
|
63947
63947
|
return StateSchema.parse(JSON.parse(raw));
|
|
63948
63948
|
}
|
|
63949
|
+
function tryReadStateRaw(changeDir) {
|
|
63950
|
+
const filePath = join7(changeDir, STATE_FILE2);
|
|
63951
|
+
const text = getStorage().read(filePath);
|
|
63952
|
+
if (text === null)
|
|
63953
|
+
return { state: null, raw: null };
|
|
63954
|
+
let parsed;
|
|
63955
|
+
try {
|
|
63956
|
+
parsed = JSON.parse(text);
|
|
63957
|
+
} catch {
|
|
63958
|
+
return { state: null, raw: null };
|
|
63959
|
+
}
|
|
63960
|
+
const raw = parsed && typeof parsed === "object" ? parsed : {};
|
|
63961
|
+
const result2 = StateSchema.safeParse(parsed);
|
|
63962
|
+
return { state: result2.success ? result2.data : null, raw };
|
|
63963
|
+
}
|
|
63949
63964
|
function writeState(changeDir, state) {
|
|
63950
63965
|
const filePath = join7(changeDir, STATE_FILE2);
|
|
63951
63966
|
getStorage().write(filePath, JSON.stringify(state, null, 2) + `
|
|
@@ -70613,9 +70628,9 @@ function useLoop(opts) {
|
|
|
70613
70628
|
const tasksDir = join11(opts.tasksDir, opts.name);
|
|
70614
70629
|
const storage = getStorage();
|
|
70615
70630
|
let currentState;
|
|
70616
|
-
const
|
|
70617
|
-
if (
|
|
70618
|
-
currentState =
|
|
70631
|
+
const { state: parsedState, raw: rawState } = tryReadStateRaw(stateDir);
|
|
70632
|
+
if (parsedState !== null) {
|
|
70633
|
+
currentState = parsedState;
|
|
70619
70634
|
if (currentState.engine !== opts.engine || currentState.model !== opts.model) {
|
|
70620
70635
|
currentState = {
|
|
70621
70636
|
...currentState,
|
|
@@ -70625,6 +70640,9 @@ function useLoop(opts) {
|
|
|
70625
70640
|
writeState(stateDir, currentState);
|
|
70626
70641
|
}
|
|
70627
70642
|
} else {
|
|
70643
|
+
if (rawState !== null) {
|
|
70644
|
+
addInfo(`.ralph-state.json was malformed \u2014 reinitialising. External fields (linearComments) preserved.`);
|
|
70645
|
+
}
|
|
70628
70646
|
currentState = buildInitialState({
|
|
70629
70647
|
name: opts.name,
|
|
70630
70648
|
prompt: opts.prompt,
|
|
@@ -70633,6 +70651,9 @@ function useLoop(opts) {
|
|
|
70633
70651
|
manualTest: opts.manualTest,
|
|
70634
70652
|
createPr: opts.createPr ?? false
|
|
70635
70653
|
});
|
|
70654
|
+
if (rawState !== null && rawState.linearComments) {
|
|
70655
|
+
currentState.linearComments = rawState.linearComments;
|
|
70656
|
+
}
|
|
70636
70657
|
writeState(stateDir, currentState);
|
|
70637
70658
|
}
|
|
70638
70659
|
const isResume2 = currentState.iteration > 0;
|
|
@@ -70661,6 +70682,27 @@ function useLoop(opts) {
|
|
|
70661
70682
|
}
|
|
70662
70683
|
const tasksContent = storage.read(join11(tasksDir, MISSION_TASKS_FILENAME));
|
|
70663
70684
|
const agentTasksContent = storage.read(join11(tasksDir, AGENT_TASKS_FILENAME));
|
|
70685
|
+
if (tasksContent === null && currentState.iteration > 0 && typeof opts.changeStore.listChanges === "function") {
|
|
70686
|
+
let stillActive = true;
|
|
70687
|
+
try {
|
|
70688
|
+
const active = await opts.changeStore.listChanges();
|
|
70689
|
+
stillActive = active.includes(opts.name);
|
|
70690
|
+
} catch {
|
|
70691
|
+
stillActive = true;
|
|
70692
|
+
}
|
|
70693
|
+
if (!stillActive) {
|
|
70694
|
+
addInfo(`tasks.md not found and change "${opts.name}" is no longer active \u2014 it was archived externally. Exiting.`);
|
|
70695
|
+
currentState = {
|
|
70696
|
+
...currentState,
|
|
70697
|
+
status: "completed",
|
|
70698
|
+
lastModified: new Date().toISOString()
|
|
70699
|
+
};
|
|
70700
|
+
writeState(stateDir, currentState);
|
|
70701
|
+
setState(currentState);
|
|
70702
|
+
finalStopReason = "completed";
|
|
70703
|
+
break;
|
|
70704
|
+
}
|
|
70705
|
+
}
|
|
70664
70706
|
if (tasksContent !== null) {
|
|
70665
70707
|
const remaining = countUnchecked(tasksContent);
|
|
70666
70708
|
const agentRemaining = agentTasksContent !== null ? countUnchecked(agentTasksContent) : 0;
|
|
@@ -92471,7 +92513,7 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Proj
|
|
|
92471
92513
|
var init_schema = __esm(() => {
|
|
92472
92514
|
init_zod2();
|
|
92473
92515
|
MarkerSchema = exports_external2.object({
|
|
92474
|
-
type: exports_external2.enum(["label", "status", "attachment"]),
|
|
92516
|
+
type: exports_external2.enum(["label", "status", "attachment", "project"]),
|
|
92475
92517
|
value: exports_external2.string().min(1)
|
|
92476
92518
|
});
|
|
92477
92519
|
GetIndicatorSchema = exports_external2.object({
|
|
@@ -92796,6 +92838,17 @@ linear:
|
|
|
92796
92838
|
# setError:
|
|
92797
92839
|
# type: label
|
|
92798
92840
|
# value: "ralph:error"
|
|
92841
|
+
#
|
|
92842
|
+
# # Project-based filter / assignment
|
|
92843
|
+
# # getTodo can filter by Linear project name, and setInProgress can
|
|
92844
|
+
# # reassign the issue into a different project.
|
|
92845
|
+
# getTodo:
|
|
92846
|
+
# filter:
|
|
92847
|
+
# - type: project
|
|
92848
|
+
# value: "Ralph Queue"
|
|
92849
|
+
# setInProgress:
|
|
92850
|
+
# type: project
|
|
92851
|
+
# value: "Ralph In Progress"
|
|
92799
92852
|
---
|
|
92800
92853
|
You are working on {{ issue.identifier }}: {{ issue.title }}.
|
|
92801
92854
|
|
|
@@ -93603,15 +93656,18 @@ function partition2(markers) {
|
|
|
93603
93656
|
const statuses = [];
|
|
93604
93657
|
const labels = [];
|
|
93605
93658
|
const attachmentSubtitles = [];
|
|
93659
|
+
const projects = [];
|
|
93606
93660
|
for (const m of markers) {
|
|
93607
93661
|
if (m.type === "status")
|
|
93608
93662
|
statuses.push(m.value);
|
|
93609
93663
|
else if (m.type === "label")
|
|
93610
93664
|
labels.push(m.value);
|
|
93611
|
-
else
|
|
93665
|
+
else if (m.type === "attachment")
|
|
93612
93666
|
attachmentSubtitles.push(m.value);
|
|
93667
|
+
else if (m.type === "project")
|
|
93668
|
+
projects.push(m.value);
|
|
93613
93669
|
}
|
|
93614
|
-
return { statuses, labels, attachmentSubtitles };
|
|
93670
|
+
return { statuses, labels, attachmentSubtitles, projects };
|
|
93615
93671
|
}
|
|
93616
93672
|
function buildIssueFilter(spec) {
|
|
93617
93673
|
const where = {};
|
|
@@ -93627,7 +93683,7 @@ function buildIssueFilter(spec) {
|
|
|
93627
93683
|
}
|
|
93628
93684
|
const inc = spec.include ?? [];
|
|
93629
93685
|
if (inc.length > 0) {
|
|
93630
|
-
const { statuses, labels, attachmentSubtitles } = partition2(inc);
|
|
93686
|
+
const { statuses, labels, attachmentSubtitles, projects } = partition2(inc);
|
|
93631
93687
|
const branches = [];
|
|
93632
93688
|
if (statuses.length > 0)
|
|
93633
93689
|
branches.push({ state: { name: { in: statuses } } });
|
|
@@ -93643,6 +93699,8 @@ function buildIssueFilter(spec) {
|
|
|
93643
93699
|
}
|
|
93644
93700
|
});
|
|
93645
93701
|
}
|
|
93702
|
+
if (projects.length > 0)
|
|
93703
|
+
branches.push({ project: { name: { in: projects } } });
|
|
93646
93704
|
for (const b of branches)
|
|
93647
93705
|
Object.assign(where, b);
|
|
93648
93706
|
} else {
|
|
@@ -93650,7 +93708,23 @@ function buildIssueFilter(spec) {
|
|
|
93650
93708
|
}
|
|
93651
93709
|
const exc = spec.exclude ?? [];
|
|
93652
93710
|
if (exc.length > 0) {
|
|
93653
|
-
const {
|
|
93711
|
+
const {
|
|
93712
|
+
statuses,
|
|
93713
|
+
labels,
|
|
93714
|
+
attachmentSubtitles: excludedSubtitles,
|
|
93715
|
+
projects: excludedProjects
|
|
93716
|
+
} = partition2(exc);
|
|
93717
|
+
if (excludedProjects.length > 0) {
|
|
93718
|
+
const current = where.project;
|
|
93719
|
+
const noProject = { project: { name: { nin: excludedProjects } } };
|
|
93720
|
+
if (current === undefined)
|
|
93721
|
+
Object.assign(where, noProject);
|
|
93722
|
+
else {
|
|
93723
|
+
const existingAnd = where.and ?? [];
|
|
93724
|
+
where.and = [...existingAnd, { project: current }, noProject];
|
|
93725
|
+
delete where.project;
|
|
93726
|
+
}
|
|
93727
|
+
}
|
|
93654
93728
|
if (excludedSubtitles.length > 0) {
|
|
93655
93729
|
const existingAnd = where.and ?? [];
|
|
93656
93730
|
where.and = [
|
|
@@ -93709,10 +93783,14 @@ async function fetchMentionScanIssues(apiKey, spec) {
|
|
|
93709
93783
|
id identifier title description url priority createdAt
|
|
93710
93784
|
state { name type }
|
|
93711
93785
|
assignee { id email name }
|
|
93786
|
+
project { id name }
|
|
93712
93787
|
labels { nodes { name } }
|
|
93713
93788
|
relations(first: 50) {
|
|
93714
93789
|
nodes { type relatedIssue { id state { type } } }
|
|
93715
93790
|
}
|
|
93791
|
+
comments(first: 50) {
|
|
93792
|
+
nodes { id body createdAt user { name email } }
|
|
93793
|
+
}
|
|
93716
93794
|
}
|
|
93717
93795
|
}
|
|
93718
93796
|
}`;
|
|
@@ -93728,10 +93806,12 @@ async function fetchMentionScanIssues(apiKey, spec) {
|
|
|
93728
93806
|
url: n.url,
|
|
93729
93807
|
state: n.state,
|
|
93730
93808
|
assignee: n.assignee,
|
|
93809
|
+
project: n.project ?? null,
|
|
93731
93810
|
labels: n.labels.nodes.map((l) => l.name),
|
|
93732
93811
|
priority: n.priority,
|
|
93733
93812
|
createdAt: n.createdAt ?? "",
|
|
93734
|
-
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
|
|
93813
|
+
blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id),
|
|
93814
|
+
comments: n.comments?.nodes ?? []
|
|
93735
93815
|
}));
|
|
93736
93816
|
}
|
|
93737
93817
|
async function fetchOpenIssues(apiKey, spec) {
|
|
@@ -93742,6 +93822,7 @@ async function fetchOpenIssues(apiKey, spec) {
|
|
|
93742
93822
|
id identifier title description url priority createdAt
|
|
93743
93823
|
state { name type }
|
|
93744
93824
|
assignee { id email name }
|
|
93825
|
+
project { id name }
|
|
93745
93826
|
labels { nodes { name } }
|
|
93746
93827
|
relations(first: 50) {
|
|
93747
93828
|
nodes {
|
|
@@ -93764,6 +93845,7 @@ async function fetchOpenIssues(apiKey, spec) {
|
|
|
93764
93845
|
url: n.url,
|
|
93765
93846
|
state: n.state,
|
|
93766
93847
|
assignee: n.assignee,
|
|
93848
|
+
project: n.project ?? null,
|
|
93767
93849
|
labels: n.labels.nodes.map((l) => l.name),
|
|
93768
93850
|
priority: n.priority,
|
|
93769
93851
|
createdAt: n.createdAt ?? "",
|
|
@@ -93956,25 +94038,55 @@ async function updateAttachmentSubtitle(apiKey, attachmentId, subtitle) {
|
|
|
93956
94038
|
});
|
|
93957
94039
|
}
|
|
93958
94040
|
async function upsertRalphyAttachment(apiKey, issueId, issueUrl, subtitle) {
|
|
93959
|
-
const attachments = await fetchIssueAttachments(apiKey, issueId
|
|
93960
|
-
|
|
94041
|
+
const attachments = await fetchIssueAttachments(apiKey, issueId, {
|
|
94042
|
+
titleFilter: RALPHY_ATTACHMENT_TITLE
|
|
94043
|
+
});
|
|
94044
|
+
const existing = attachments[0];
|
|
93961
94045
|
if (existing) {
|
|
93962
94046
|
await updateAttachmentSubtitle(apiKey, existing.id, subtitle);
|
|
93963
94047
|
} else {
|
|
93964
94048
|
await createRalphyAttachment(apiKey, issueId, issueUrl, subtitle);
|
|
93965
94049
|
}
|
|
93966
94050
|
}
|
|
93967
|
-
async function fetchIssueAttachments(apiKey, issueId) {
|
|
93968
|
-
const
|
|
94051
|
+
async function fetchIssueAttachments(apiKey, issueId, options) {
|
|
94052
|
+
const titleFilter = options?.titleFilter;
|
|
94053
|
+
const query = titleFilter !== undefined ? `query IssueAttachments($id: String!, $titleFilter: String!) {
|
|
94054
|
+
issue(id: $id) {
|
|
94055
|
+
attachments(filter: { title: { eq: $titleFilter } }, first: 25) {
|
|
94056
|
+
nodes { id url sourceType title }
|
|
94057
|
+
}
|
|
94058
|
+
}
|
|
94059
|
+
}` : `query IssueAttachments($id: String!) {
|
|
93969
94060
|
issue(id: $id) {
|
|
93970
94061
|
attachments(first: 25) {
|
|
93971
94062
|
nodes { id url sourceType title }
|
|
93972
94063
|
}
|
|
93973
94064
|
}
|
|
93974
94065
|
}`;
|
|
93975
|
-
const
|
|
94066
|
+
const variables = titleFilter !== undefined ? { id: issueId, titleFilter } : { id: issueId };
|
|
94067
|
+
const data = await linearRequest(apiKey, query, variables);
|
|
93976
94068
|
return data.issue?.attachments?.nodes ?? [];
|
|
93977
94069
|
}
|
|
94070
|
+
async function fetchAttachmentsForIssues(apiKey, issueIds) {
|
|
94071
|
+
const out = new Map;
|
|
94072
|
+
if (issueIds.length === 0)
|
|
94073
|
+
return out;
|
|
94074
|
+
const query = `query IssuesAttachments($ids: [ID!]!) {
|
|
94075
|
+
issues(filter: { id: { in: $ids } }, first: 250) {
|
|
94076
|
+
nodes {
|
|
94077
|
+
id
|
|
94078
|
+
attachments(first: 25) {
|
|
94079
|
+
nodes { id url sourceType title }
|
|
94080
|
+
}
|
|
94081
|
+
}
|
|
94082
|
+
}
|
|
94083
|
+
}`;
|
|
94084
|
+
const data = await linearRequest(apiKey, query, { ids: issueIds });
|
|
94085
|
+
for (const node2 of data.issues.nodes) {
|
|
94086
|
+
out.set(node2.id, node2.attachments?.nodes ?? []);
|
|
94087
|
+
}
|
|
94088
|
+
return out;
|
|
94089
|
+
}
|
|
93978
94090
|
async function fetchWorkflowStates(apiKey, teamKey) {
|
|
93979
94091
|
const query = `query States($team: String!) {
|
|
93980
94092
|
workflowStates(filter: { team: { key: { eq: $team } } }, first: 50) {
|
|
@@ -94073,14 +94185,40 @@ function issueMatchesGetIndicator(issue2, indicator) {
|
|
|
94073
94185
|
return false;
|
|
94074
94186
|
const labels = new Set(issue2.labels.map((l) => l.toLowerCase()));
|
|
94075
94187
|
const stateName = issue2.state.name.toLowerCase();
|
|
94188
|
+
const projectName = issue2.project?.name.toLowerCase() ?? null;
|
|
94076
94189
|
return indicator.filter.some((m) => {
|
|
94077
94190
|
if (m.type === "label")
|
|
94078
94191
|
return labels.has(m.value.toLowerCase());
|
|
94079
94192
|
if (m.type === "status")
|
|
94080
94193
|
return stateName === m.value.toLowerCase();
|
|
94194
|
+
if (m.type === "project") {
|
|
94195
|
+
if (projectName === null)
|
|
94196
|
+
return false;
|
|
94197
|
+
return projectName === m.value.toLowerCase();
|
|
94198
|
+
}
|
|
94081
94199
|
return false;
|
|
94082
94200
|
});
|
|
94083
94201
|
}
|
|
94202
|
+
async function fetchProjectIdByName(apiKey, name) {
|
|
94203
|
+
const query = `query ProjectId($name: String!) {
|
|
94204
|
+
projects(filter: { name: { eq: $name } }, first: 1) {
|
|
94205
|
+
nodes { id }
|
|
94206
|
+
}
|
|
94207
|
+
}`;
|
|
94208
|
+
const data = await linearRequest(apiKey, query, {
|
|
94209
|
+
name
|
|
94210
|
+
});
|
|
94211
|
+
return data.projects.nodes[0]?.id ?? null;
|
|
94212
|
+
}
|
|
94213
|
+
async function setIssueProject(apiKey, issueId, projectId) {
|
|
94214
|
+
const mutation = `mutation SetProject($id: String!, $projectId: String!) {
|
|
94215
|
+
issueUpdate(id: $id, input: { projectId: $projectId }) { success }
|
|
94216
|
+
}`;
|
|
94217
|
+
await linearRequest(apiKey, mutation, {
|
|
94218
|
+
id: issueId,
|
|
94219
|
+
projectId
|
|
94220
|
+
});
|
|
94221
|
+
}
|
|
94084
94222
|
async function createIssue(apiKey, input) {
|
|
94085
94223
|
const mutation = `mutation CreateIssue($input: IssueCreateInput!) {
|
|
94086
94224
|
issueCreate(input: $input) {
|
|
@@ -94917,6 +95055,64 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
|
94917
95055
|
}
|
|
94918
95056
|
var init_worktree = () => {};
|
|
94919
95057
|
|
|
95058
|
+
// apps/agent/src/agent/pr-url/index.ts
|
|
95059
|
+
function escapeRegex2(s) {
|
|
95060
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95061
|
+
}
|
|
95062
|
+
async function discoverPrUrlFromGitHub(identifier, runner, cwd2, onLog) {
|
|
95063
|
+
if (!identifier)
|
|
95064
|
+
return null;
|
|
95065
|
+
const slug = identifier.toLowerCase();
|
|
95066
|
+
let rows;
|
|
95067
|
+
try {
|
|
95068
|
+
const res = await runner.run([
|
|
95069
|
+
"gh",
|
|
95070
|
+
"pr",
|
|
95071
|
+
"list",
|
|
95072
|
+
"--search",
|
|
95073
|
+
`${identifier} in:title`,
|
|
95074
|
+
"--state",
|
|
95075
|
+
"all",
|
|
95076
|
+
"--json",
|
|
95077
|
+
"url,state,headRefName,title,updatedAt"
|
|
95078
|
+
], cwd2);
|
|
95079
|
+
const text = res.stdout.trim();
|
|
95080
|
+
rows = text ? JSON.parse(text) : [];
|
|
95081
|
+
} catch (err) {
|
|
95082
|
+
onLog?.(`! gh pr list (${identifier}) failed: ${err.message}`, "yellow");
|
|
95083
|
+
return null;
|
|
95084
|
+
}
|
|
95085
|
+
const idRe = new RegExp(`\\b${escapeRegex2(identifier)}\\b`, "i");
|
|
95086
|
+
const matches2 = rows.filter((r) => Boolean(r.url) && (idRe.test(r.title ?? "") || (r.headRefName ?? "").toLowerCase().includes(slug)));
|
|
95087
|
+
if (matches2.length === 0)
|
|
95088
|
+
return null;
|
|
95089
|
+
const open = matches2.filter((r) => r.state === "OPEN");
|
|
95090
|
+
const pool = open.length > 0 ? open : matches2;
|
|
95091
|
+
pool.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
95092
|
+
return pool[0]?.url ?? null;
|
|
95093
|
+
}
|
|
95094
|
+
function createPrUrlCache(ttlMs = 5 * 60 * 1000, now2 = Date.now) {
|
|
95095
|
+
const map3 = new Map;
|
|
95096
|
+
return {
|
|
95097
|
+
get(issueId) {
|
|
95098
|
+
const e = map3.get(issueId);
|
|
95099
|
+
if (!e)
|
|
95100
|
+
return;
|
|
95101
|
+
if (now2() - e.fetchedAt >= ttlMs) {
|
|
95102
|
+
map3.delete(issueId);
|
|
95103
|
+
return;
|
|
95104
|
+
}
|
|
95105
|
+
return e.url;
|
|
95106
|
+
},
|
|
95107
|
+
set(issueId, url2) {
|
|
95108
|
+
map3.set(issueId, { url: url2, fetchedAt: now2() });
|
|
95109
|
+
},
|
|
95110
|
+
invalidate(issueId) {
|
|
95111
|
+
map3.delete(issueId);
|
|
95112
|
+
}
|
|
95113
|
+
};
|
|
95114
|
+
}
|
|
95115
|
+
|
|
94920
95116
|
// apps/agent/src/agent/ci.ts
|
|
94921
95117
|
async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
94922
95118
|
let lastErr;
|
|
@@ -95128,6 +95324,33 @@ async function classifyDiffAgainstMeta(runner, cwd2, base2, metaOnlyFiles) {
|
|
|
95128
95324
|
const onlyMeta = files.every((f2) => metaSet.has(f2.replace(/\\/g, "/")));
|
|
95129
95325
|
return { files, onlyMeta };
|
|
95130
95326
|
}
|
|
95327
|
+
async function branchAlreadyMerged(runner, cwd2, branch, base2) {
|
|
95328
|
+
try {
|
|
95329
|
+
const r = await runner.run([
|
|
95330
|
+
"gh",
|
|
95331
|
+
"pr",
|
|
95332
|
+
"list",
|
|
95333
|
+
"--head",
|
|
95334
|
+
branch,
|
|
95335
|
+
"--state",
|
|
95336
|
+
"merged",
|
|
95337
|
+
"--json",
|
|
95338
|
+
"number",
|
|
95339
|
+
"--jq",
|
|
95340
|
+
".[0].number // empty"
|
|
95341
|
+
], cwd2);
|
|
95342
|
+
if (r.stdout.trim() !== "")
|
|
95343
|
+
return true;
|
|
95344
|
+
} catch {}
|
|
95345
|
+
try {
|
|
95346
|
+
const r = await runner.run(["git", "cherry", base2, "HEAD"], cwd2);
|
|
95347
|
+
const lines = r.stdout.split(`
|
|
95348
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
95349
|
+
if (lines.length > 0 && lines.every((l) => l.startsWith("-")))
|
|
95350
|
+
return true;
|
|
95351
|
+
} catch {}
|
|
95352
|
+
return false;
|
|
95353
|
+
}
|
|
95131
95354
|
async function createPullRequest(input, runner) {
|
|
95132
95355
|
const base2 = input.base ?? "main";
|
|
95133
95356
|
const log2 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
|
|
@@ -95137,6 +95360,9 @@ async function createPullRequest(input, runner) {
|
|
|
95137
95360
|
if (metaOnlyFiles.length > 0) {
|
|
95138
95361
|
const classification = await classifyDiffAgainstMeta(runner, input.cwd, base2, metaOnlyFiles);
|
|
95139
95362
|
if (classification.onlyMeta && classification.files.length > 0) {
|
|
95363
|
+
if (await branchAlreadyMerged(runner, input.cwd, input.branch, base2)) {
|
|
95364
|
+
return null;
|
|
95365
|
+
}
|
|
95140
95366
|
return {
|
|
95141
95367
|
url: null,
|
|
95142
95368
|
created: false,
|
|
@@ -96259,6 +96485,46 @@ async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog)
|
|
|
96259
96485
|
}
|
|
96260
96486
|
return { url: null, sawNonOpenPr };
|
|
96261
96487
|
}
|
|
96488
|
+
async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps) {
|
|
96489
|
+
const blockerIds = issue2.blockedByIds;
|
|
96490
|
+
if (blockerIds.length === 0)
|
|
96491
|
+
return null;
|
|
96492
|
+
let attachmentsByBlocker;
|
|
96493
|
+
try {
|
|
96494
|
+
attachmentsByBlocker = await fetchAttachmentsForIssues(deps.apiKey, blockerIds);
|
|
96495
|
+
} catch (err) {
|
|
96496
|
+
deps.onLog(`! could not fetch attachments for blockers of ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96497
|
+
return null;
|
|
96498
|
+
}
|
|
96499
|
+
const candidates = [];
|
|
96500
|
+
for (const blockerId of blockerIds) {
|
|
96501
|
+
const attachments = attachmentsByBlocker.get(blockerId) ?? [];
|
|
96502
|
+
const prUrls = attachments.map((a) => a.url).filter((url2) => /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(url2));
|
|
96503
|
+
const openHeads = [];
|
|
96504
|
+
for (const url2 of prUrls) {
|
|
96505
|
+
try {
|
|
96506
|
+
const res = await runner.run(["gh", "pr", "view", url2, "--json", "state,headRefName", "--jq", "."], runnerCwd);
|
|
96507
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
96508
|
+
if (parsed.state === "OPEN" && parsed.headRefName) {
|
|
96509
|
+
openHeads.push(parsed.headRefName);
|
|
96510
|
+
}
|
|
96511
|
+
} catch (err) {
|
|
96512
|
+
deps.onLog(`! gh pr view failed for ${url2} (blocker of ${issue2.identifier}): ${err.message}`, "yellow");
|
|
96513
|
+
}
|
|
96514
|
+
}
|
|
96515
|
+
if (openHeads.length === 1) {
|
|
96516
|
+
candidates.push(openHeads[0]);
|
|
96517
|
+
} else if (openHeads.length > 1) {
|
|
96518
|
+
deps.onLog(` ${issue2.identifier}: blocker ${blockerId} has ${openHeads.length} open PRs \u2014 skipping dependency base resolution`, "gray");
|
|
96519
|
+
}
|
|
96520
|
+
}
|
|
96521
|
+
if (candidates.length === 1)
|
|
96522
|
+
return candidates[0];
|
|
96523
|
+
if (candidates.length > 1) {
|
|
96524
|
+
deps.onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs \u2014 falling back to default base`, "gray");
|
|
96525
|
+
}
|
|
96526
|
+
return null;
|
|
96527
|
+
}
|
|
96262
96528
|
function githubReactionSlug(emoji3) {
|
|
96263
96529
|
switch (emoji3) {
|
|
96264
96530
|
case "\uD83D\uDC40":
|
|
@@ -96333,7 +96599,7 @@ ${c.body.trim()}`;
|
|
|
96333
96599
|
].join(`
|
|
96334
96600
|
`);
|
|
96335
96601
|
}
|
|
96336
|
-
function
|
|
96602
|
+
function escapeRegex3(s) {
|
|
96337
96603
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
96338
96604
|
}
|
|
96339
96605
|
function buildMentionTaskBody(trigger, issueUrl) {
|
|
@@ -96477,6 +96743,16 @@ function buildAgentCoordinator(input) {
|
|
|
96477
96743
|
} else if (m.type === "attachment") {
|
|
96478
96744
|
await upsertRalphyAttachment(apiKey, issue2.id, issue2.url, m.value);
|
|
96479
96745
|
onLog(` \u2192 ${issue2.identifier} attachment='${m.value}'`, "gray");
|
|
96746
|
+
} else if (m.type === "project") {
|
|
96747
|
+
const projectId = await fetchProjectIdByName(apiKey, m.value);
|
|
96748
|
+
if (!projectId) {
|
|
96749
|
+
const err = new Error("Linear project not found");
|
|
96750
|
+
err.project = m.value;
|
|
96751
|
+
err.issue = issue2.identifier;
|
|
96752
|
+
throw err;
|
|
96753
|
+
}
|
|
96754
|
+
await setIssueProject(apiKey, issue2.id, projectId);
|
|
96755
|
+
onLog(` \u2192 ${issue2.identifier} project='${m.value}'`, "gray");
|
|
96480
96756
|
} else {
|
|
96481
96757
|
const id = await resolveLabelId(issue2, m.value);
|
|
96482
96758
|
if (!id) {
|
|
@@ -96527,6 +96803,7 @@ function buildAgentCoordinator(input) {
|
|
|
96527
96803
|
const prByChange = new Map;
|
|
96528
96804
|
const prUnavailable = new Map;
|
|
96529
96805
|
const PR_UNAVAILABLE_TTL_MS = 10 * 60 * 1000;
|
|
96806
|
+
const prUrlByIssue = createPrUrlCache(5 * 60 * 1000);
|
|
96530
96807
|
const stalePingedAt = new Map;
|
|
96531
96808
|
const lastHandledReviewActivity = new Map;
|
|
96532
96809
|
const useWorktree = args.worktree || cfg.useWorktree;
|
|
@@ -96872,6 +97149,9 @@ PR: ${prUrl}` : ""
|
|
|
96872
97149
|
registerPr: (cn, url2) => {
|
|
96873
97150
|
prByChange.set(cn, url2);
|
|
96874
97151
|
prUnavailable.delete(cn);
|
|
97152
|
+
const issue2 = issueByChange.get(cn);
|
|
97153
|
+
if (issue2)
|
|
97154
|
+
prUrlByIssue.invalidate(issue2.id);
|
|
96875
97155
|
input.onWorkerPr?.(cn, url2);
|
|
96876
97156
|
},
|
|
96877
97157
|
...onWorkerPhase && {
|
|
@@ -96929,6 +97209,7 @@ PR: ${prUrl}` : ""
|
|
|
96929
97209
|
}
|
|
96930
97210
|
if (state && state !== "OPEN") {
|
|
96931
97211
|
markPrUnavailable(changeName);
|
|
97212
|
+
prUrlByIssue.invalidate(issue2.id);
|
|
96932
97213
|
return null;
|
|
96933
97214
|
}
|
|
96934
97215
|
if (m && m !== "UNKNOWN") {
|
|
@@ -96966,32 +97247,9 @@ PR: ${prUrl}` : ""
|
|
|
96966
97247
|
prUnavailable.set(changeName, Date.now() + PR_UNAVAILABLE_TTL_MS);
|
|
96967
97248
|
}
|
|
96968
97249
|
async function discoverPrUrl(issue2, changeName) {
|
|
96969
|
-
const
|
|
96970
|
-
|
|
96971
|
-
|
|
96972
|
-
const res = await cmdRunner.run(args2, projectRoot);
|
|
96973
|
-
const found = res.stdout.trim();
|
|
96974
|
-
return found || null;
|
|
96975
|
-
} catch (err) {
|
|
96976
|
-
onLog(`! gh ${args2[1] ?? ""} failed for ${issue2.identifier}: ${err.message}`, "yellow");
|
|
96977
|
-
return null;
|
|
96978
|
-
}
|
|
96979
|
-
};
|
|
96980
|
-
const byBranch = await tryGh([
|
|
96981
|
-
"gh",
|
|
96982
|
-
"pr",
|
|
96983
|
-
"list",
|
|
96984
|
-
"--head",
|
|
96985
|
-
branch,
|
|
96986
|
-
"--state",
|
|
96987
|
-
"open",
|
|
96988
|
-
"--json",
|
|
96989
|
-
"url",
|
|
96990
|
-
"--jq",
|
|
96991
|
-
".[0].url // empty"
|
|
96992
|
-
]);
|
|
96993
|
-
if (byBranch)
|
|
96994
|
-
return byBranch;
|
|
97250
|
+
const fromGitHub = await discoverPrUrlFromGitHub(issue2.identifier, cmdRunner, projectRoot, onLog);
|
|
97251
|
+
if (fromGitHub)
|
|
97252
|
+
return fromGitHub;
|
|
96995
97253
|
const fromLinear = await discoverPrUrlFromLinear(issue2);
|
|
96996
97254
|
if (fromLinear.url) {
|
|
96997
97255
|
onLog(` ${issue2.identifier}: PR discovered via Linear attachment (${fromLinear.url})`, "gray");
|
|
@@ -97001,48 +97259,12 @@ PR: ${prUrl}` : ""
|
|
|
97001
97259
|
markPrUnavailable(changeName);
|
|
97002
97260
|
return null;
|
|
97003
97261
|
}
|
|
97004
|
-
onLog(` ${issue2.identifier}: no
|
|
97262
|
+
onLog(` ${issue2.identifier}: no PR found via GitHub search or Linear attachments; conflict scan skipped for ${PR_UNAVAILABLE_TTL_MS / 60000}m`, "gray");
|
|
97005
97263
|
markPrUnavailable(changeName);
|
|
97006
97264
|
return null;
|
|
97007
97265
|
}
|
|
97008
97266
|
async function resolveDependencyBaseBranch(issue2, runner, runnerCwd) {
|
|
97009
|
-
|
|
97010
|
-
if (blockerIds.length === 0)
|
|
97011
|
-
return null;
|
|
97012
|
-
const candidates = [];
|
|
97013
|
-
for (const blockerId of blockerIds) {
|
|
97014
|
-
let attachments;
|
|
97015
|
-
try {
|
|
97016
|
-
attachments = await fetchIssueAttachments(apiKey, blockerId);
|
|
97017
|
-
} catch (err) {
|
|
97018
|
-
onLog(`! could not fetch attachments for blocker ${blockerId} of ${issue2.identifier}: ${err.message}`, "yellow");
|
|
97019
|
-
continue;
|
|
97020
|
-
}
|
|
97021
|
-
const prUrls = attachments.map((a) => a.url).filter((url2) => /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(url2));
|
|
97022
|
-
const openHeads = [];
|
|
97023
|
-
for (const url2 of prUrls) {
|
|
97024
|
-
try {
|
|
97025
|
-
const res = await runner.run(["gh", "pr", "view", url2, "--json", "state,headRefName", "--jq", "."], runnerCwd);
|
|
97026
|
-
const parsed = JSON.parse(res.stdout.trim());
|
|
97027
|
-
if (parsed.state === "OPEN" && parsed.headRefName) {
|
|
97028
|
-
openHeads.push(parsed.headRefName);
|
|
97029
|
-
}
|
|
97030
|
-
} catch (err) {
|
|
97031
|
-
onLog(`! gh pr view failed for ${url2} (blocker of ${issue2.identifier}): ${err.message}`, "yellow");
|
|
97032
|
-
}
|
|
97033
|
-
}
|
|
97034
|
-
if (openHeads.length === 1) {
|
|
97035
|
-
candidates.push(openHeads[0]);
|
|
97036
|
-
} else if (openHeads.length > 1) {
|
|
97037
|
-
onLog(` ${issue2.identifier}: blocker ${blockerId} has ${openHeads.length} open PRs \u2014 skipping dependency base resolution`, "gray");
|
|
97038
|
-
}
|
|
97039
|
-
}
|
|
97040
|
-
if (candidates.length === 1)
|
|
97041
|
-
return candidates[0];
|
|
97042
|
-
if (candidates.length > 1) {
|
|
97043
|
-
onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs \u2014 falling back to default base`, "gray");
|
|
97044
|
-
}
|
|
97045
|
-
return null;
|
|
97267
|
+
return resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, { apiKey, onLog });
|
|
97046
97268
|
}
|
|
97047
97269
|
async function discoverPrUrlFromLinear(issue2) {
|
|
97048
97270
|
let attachments;
|
|
@@ -97090,17 +97312,7 @@ PR: ${prUrl}` : ""
|
|
|
97090
97312
|
onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
|
|
97091
97313
|
};
|
|
97092
97314
|
for (const issue2 of candidates) {
|
|
97093
|
-
|
|
97094
|
-
try {
|
|
97095
|
-
comments = await fetchIssueComments(apiKey, issue2.id);
|
|
97096
|
-
} catch (err) {
|
|
97097
|
-
if (isRateLimitedError(err)) {
|
|
97098
|
-
logRateLimited();
|
|
97099
|
-
break;
|
|
97100
|
-
}
|
|
97101
|
-
onLog(`! mention scan: Linear comments failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
|
|
97102
|
-
continue;
|
|
97103
|
-
}
|
|
97315
|
+
const comments = issue2.comments ?? [];
|
|
97104
97316
|
const lastRalphPickup = findLastRalphPickupISO(comments);
|
|
97105
97317
|
if (wantMention) {
|
|
97106
97318
|
for (const c of comments) {
|
|
@@ -97322,17 +97534,21 @@ PR: ${prUrl}` : ""
|
|
|
97322
97534
|
return latest;
|
|
97323
97535
|
}
|
|
97324
97536
|
function containsHandle(body, handle) {
|
|
97325
|
-
const re = new RegExp(`(^|\\s|[^A-Za-z0-9_])${
|
|
97537
|
+
const re = new RegExp(`(^|\\s|[^A-Za-z0-9_])${escapeRegex3(handle)}\\b`, "i");
|
|
97326
97538
|
return re.test(body);
|
|
97327
97539
|
}
|
|
97328
97540
|
async function resolvePrUrlForIssue(issue2) {
|
|
97329
97541
|
const changeName = changeNameForIssue(issue2);
|
|
97330
97542
|
if (isPrUnavailable(changeName))
|
|
97331
97543
|
return null;
|
|
97332
|
-
const
|
|
97333
|
-
if (
|
|
97544
|
+
const inflight = prByChange.get(changeName);
|
|
97545
|
+
if (inflight)
|
|
97546
|
+
return inflight;
|
|
97547
|
+
const cached2 = prUrlByIssue.get(issue2.id);
|
|
97548
|
+
if (cached2 !== undefined)
|
|
97334
97549
|
return cached2;
|
|
97335
97550
|
const found = await discoverPrUrl(issue2, changeName);
|
|
97551
|
+
prUrlByIssue.set(issue2.id, found);
|
|
97336
97552
|
if (found)
|
|
97337
97553
|
prByChange.set(changeName, found);
|
|
97338
97554
|
return found;
|
|
@@ -99309,7 +99525,7 @@ function bucketChecks(rollup, prState) {
|
|
|
99309
99525
|
return "fail";
|
|
99310
99526
|
return "pass";
|
|
99311
99527
|
}
|
|
99312
|
-
async function fetchPrStatus(url2, runner, cwd2) {
|
|
99528
|
+
async function fetchPrStatus(url2, runner, cwd2, transition) {
|
|
99313
99529
|
let stdout;
|
|
99314
99530
|
try {
|
|
99315
99531
|
const out = await runner.run(["gh", "pr", "view", url2, "--json", PR_VIEW_FIELDS], cwd2);
|
|
@@ -99330,6 +99546,9 @@ async function fetchPrStatus(url2, runner, cwd2) {
|
|
|
99330
99546
|
const state = stateUpper === "OPEN" || stateUpper === "CLOSED" || stateUpper === "MERGED" ? stateUpper : "OPEN";
|
|
99331
99547
|
const mergeableUpper = (raw.mergeable ?? "UNKNOWN").toUpperCase();
|
|
99332
99548
|
const mergeable = mergeableUpper === "MERGEABLE" || mergeableUpper === "CONFLICTING" ? mergeableUpper : "UNKNOWN";
|
|
99549
|
+
if (transition && transition.priorState !== state) {
|
|
99550
|
+
transition.onTransition(state);
|
|
99551
|
+
}
|
|
99333
99552
|
return {
|
|
99334
99553
|
kind: "ok",
|
|
99335
99554
|
state,
|
|
@@ -99583,10 +99802,20 @@ ${bucket.label}: error fetching from Linear \u2014 ${error48}
|
|
|
99583
99802
|
}
|
|
99584
99803
|
}
|
|
99585
99804
|
const rows = [...seen.values()];
|
|
99805
|
+
try {
|
|
99806
|
+
const attachmentsByIssue = await fetchAttachmentsForIssues(apiKey, rows.map((r) => r.issueId));
|
|
99807
|
+
for (const row of rows) {
|
|
99808
|
+
const attachments = attachmentsByIssue.get(row.issueId) ?? [];
|
|
99809
|
+
row.prUrl = findPullRequestUrl(attachments);
|
|
99810
|
+
}
|
|
99811
|
+
} catch {}
|
|
99586
99812
|
await Promise.all(rows.map(async (row) => {
|
|
99813
|
+
if (row.prUrl)
|
|
99814
|
+
return;
|
|
99587
99815
|
try {
|
|
99588
|
-
const
|
|
99589
|
-
|
|
99816
|
+
const fromGitHub = await discoverPrUrlFromGitHub(row.identifier, runner, cwd2);
|
|
99817
|
+
if (fromGitHub)
|
|
99818
|
+
row.prUrl = fromGitHub;
|
|
99590
99819
|
} catch {}
|
|
99591
99820
|
}));
|
|
99592
99821
|
await Promise.all(rows.map(async (row) => {
|