@neriros/ralphy 2.7.0 → 2.7.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/cli/index.js +169 -31
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -56149,7 +56149,7 @@ var HELP_TEXT = [
|
|
|
56149
56149
|
" --linear-team <key> Linear team key (e.g. ENG)",
|
|
56150
56150
|
" --linear-assignee <id> Filter by assignee (user id, email, or 'me')",
|
|
56151
56151
|
" --linear-status <name> Filter by status name (repeatable, e.g. Todo, In Progress)",
|
|
56152
|
-
" --linear-label <name> Filter by label name",
|
|
56152
|
+
" --linear-label <name> Filter by label name (repeatable, any-of)",
|
|
56153
56153
|
" --poll-interval <s> Seconds between Linear polls (default: 60)",
|
|
56154
56154
|
" --concurrency <n> Max concurrent task loops (default: 1)",
|
|
56155
56155
|
"",
|
|
@@ -56185,7 +56185,7 @@ async function parseArgs(argv) {
|
|
|
56185
56185
|
linearTeam: "",
|
|
56186
56186
|
linearAssignee: "",
|
|
56187
56187
|
linearStatus: [],
|
|
56188
|
-
linearLabel:
|
|
56188
|
+
linearLabel: [],
|
|
56189
56189
|
pollInterval: 60,
|
|
56190
56190
|
concurrency: 1
|
|
56191
56191
|
};
|
|
@@ -56288,7 +56288,7 @@ async function parseArgs(argv) {
|
|
|
56288
56288
|
continue;
|
|
56289
56289
|
}
|
|
56290
56290
|
if (expectLinearLabel) {
|
|
56291
|
-
result2.linearLabel
|
|
56291
|
+
result2.linearLabel.push(arg);
|
|
56292
56292
|
expectLinearLabel = false;
|
|
56293
56293
|
continue;
|
|
56294
56294
|
}
|
|
@@ -69595,8 +69595,9 @@ async function fetchOpenIssues(apiKey, filter2) {
|
|
|
69595
69595
|
} else {
|
|
69596
69596
|
where.state = { type: { in: [...OPEN_STATE_TYPES] } };
|
|
69597
69597
|
}
|
|
69598
|
-
if (filter2.
|
|
69599
|
-
where.labels = { some: { name: {
|
|
69598
|
+
if (filter2.labels && filter2.labels.length > 0) {
|
|
69599
|
+
where.labels = { some: { name: { in: filter2.labels } } };
|
|
69600
|
+
}
|
|
69600
69601
|
const query = `query Issues($filter: IssueFilter) {
|
|
69601
69602
|
issues(filter: $filter, first: 50) {
|
|
69602
69603
|
nodes {
|
|
@@ -69607,13 +69608,25 @@ async function fetchOpenIssues(apiKey, filter2) {
|
|
|
69607
69608
|
}
|
|
69608
69609
|
}
|
|
69609
69610
|
}`;
|
|
69611
|
+
const data = await linearRequest(apiKey, query, {
|
|
69612
|
+
filter: where
|
|
69613
|
+
});
|
|
69614
|
+
return data.issues.nodes.map((n) => ({
|
|
69615
|
+
id: n.id,
|
|
69616
|
+
identifier: n.identifier,
|
|
69617
|
+
title: n.title,
|
|
69618
|
+
description: n.description,
|
|
69619
|
+
url: n.url,
|
|
69620
|
+
state: n.state,
|
|
69621
|
+
assignee: n.assignee,
|
|
69622
|
+
labels: n.labels.nodes.map((l) => l.name)
|
|
69623
|
+
}));
|
|
69624
|
+
}
|
|
69625
|
+
async function linearRequest(apiKey, query, variables) {
|
|
69610
69626
|
const res = await fetch(LINEAR_API, {
|
|
69611
69627
|
method: "POST",
|
|
69612
|
-
headers: {
|
|
69613
|
-
|
|
69614
|
-
Authorization: apiKey
|
|
69615
|
-
},
|
|
69616
|
-
body: JSON.stringify({ query, variables: { filter: where } })
|
|
69628
|
+
headers: { "Content-Type": "application/json", Authorization: apiKey },
|
|
69629
|
+
body: JSON.stringify({ query, variables })
|
|
69617
69630
|
});
|
|
69618
69631
|
if (!res.ok) {
|
|
69619
69632
|
const err = new Error("Linear API request failed");
|
|
@@ -69627,18 +69640,50 @@ async function fetchOpenIssues(apiKey, filter2) {
|
|
|
69627
69640
|
err.messages = json.errors.map((e) => e.message);
|
|
69628
69641
|
throw err;
|
|
69629
69642
|
}
|
|
69630
|
-
if (!json.data)
|
|
69631
|
-
|
|
69632
|
-
|
|
69633
|
-
|
|
69634
|
-
|
|
69635
|
-
|
|
69636
|
-
|
|
69637
|
-
|
|
69638
|
-
|
|
69639
|
-
|
|
69640
|
-
|
|
69641
|
-
|
|
69643
|
+
if (!json.data) {
|
|
69644
|
+
throw new Error("Linear API returned no data");
|
|
69645
|
+
}
|
|
69646
|
+
return json.data;
|
|
69647
|
+
}
|
|
69648
|
+
async function addIssueComment(apiKey, issueId, body) {
|
|
69649
|
+
const mutation = `mutation Comment($issueId: String!, $body: String!) {
|
|
69650
|
+
commentCreate(input: { issueId: $issueId, body: $body }) { success }
|
|
69651
|
+
}`;
|
|
69652
|
+
await linearRequest(apiKey, mutation, {
|
|
69653
|
+
issueId,
|
|
69654
|
+
body
|
|
69655
|
+
});
|
|
69656
|
+
}
|
|
69657
|
+
async function fetchIssueComments(apiKey, issueId) {
|
|
69658
|
+
const query = `query Comments($id: String!) {
|
|
69659
|
+
issue(id: $id) {
|
|
69660
|
+
comments(first: 50) {
|
|
69661
|
+
nodes { id body createdAt user { name email } }
|
|
69662
|
+
}
|
|
69663
|
+
}
|
|
69664
|
+
}`;
|
|
69665
|
+
const data = await linearRequest(apiKey, query, { id: issueId });
|
|
69666
|
+
return data.issue?.comments.nodes ?? [];
|
|
69667
|
+
}
|
|
69668
|
+
async function fetchWorkflowStates(apiKey, teamKey) {
|
|
69669
|
+
const query = `query States($team: String!) {
|
|
69670
|
+
workflowStates(filter: { team: { key: { eq: $team } } }, first: 50) {
|
|
69671
|
+
nodes { id name type }
|
|
69672
|
+
}
|
|
69673
|
+
}`;
|
|
69674
|
+
const data = await linearRequest(apiKey, query, {
|
|
69675
|
+
team: teamKey
|
|
69676
|
+
});
|
|
69677
|
+
return data.workflowStates.nodes;
|
|
69678
|
+
}
|
|
69679
|
+
async function updateIssueState(apiKey, issueId, stateId) {
|
|
69680
|
+
const mutation = `mutation Update($id: String!, $stateId: String!) {
|
|
69681
|
+
issueUpdate(id: $id, input: { stateId: $stateId }) { success }
|
|
69682
|
+
}`;
|
|
69683
|
+
await linearRequest(apiKey, mutation, {
|
|
69684
|
+
id: issueId,
|
|
69685
|
+
stateId
|
|
69686
|
+
});
|
|
69642
69687
|
}
|
|
69643
69688
|
|
|
69644
69689
|
// apps/cli/src/agent/state.ts
|
|
@@ -69669,13 +69714,24 @@ function changeNameForIssue(issue) {
|
|
|
69669
69714
|
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
69670
69715
|
return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
|
|
69671
69716
|
}
|
|
69672
|
-
async function scaffoldChangeForIssue(tasksDir, statesDir, issue) {
|
|
69717
|
+
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = []) {
|
|
69673
69718
|
const name = changeNameForIssue(issue);
|
|
69674
69719
|
const changeDir = join11(tasksDir, name);
|
|
69675
69720
|
const stateDir = join11(statesDir, name);
|
|
69676
69721
|
await mkdir(changeDir, { recursive: true });
|
|
69677
69722
|
await mkdir(join11(changeDir, "specs"), { recursive: true });
|
|
69678
69723
|
await mkdir(stateDir, { recursive: true });
|
|
69724
|
+
const commentsBlock = comments.length > 0 ? [
|
|
69725
|
+
"",
|
|
69726
|
+
"## Linear comments",
|
|
69727
|
+
"",
|
|
69728
|
+
...comments.flatMap((c) => [
|
|
69729
|
+
`**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
|
|
69730
|
+
"",
|
|
69731
|
+
c.body.trim(),
|
|
69732
|
+
""
|
|
69733
|
+
])
|
|
69734
|
+
] : [];
|
|
69679
69735
|
const proposal = [
|
|
69680
69736
|
`# ${issue.identifier}: ${issue.title}`,
|
|
69681
69737
|
"",
|
|
@@ -69687,6 +69743,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue) {
|
|
|
69687
69743
|
"## Description",
|
|
69688
69744
|
"",
|
|
69689
69745
|
issue.description?.trim() || "_No description provided in Linear._",
|
|
69746
|
+
...commentsBlock,
|
|
69690
69747
|
"",
|
|
69691
69748
|
"## Steering",
|
|
69692
69749
|
"",
|
|
@@ -69730,8 +69787,11 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
69730
69787
|
team: exports_external.string().optional(),
|
|
69731
69788
|
assignee: exports_external.string().optional(),
|
|
69732
69789
|
statuses: exports_external.array(exports_external.string()).default([]),
|
|
69733
|
-
|
|
69734
|
-
|
|
69790
|
+
labels: exports_external.union([exports_external.array(exports_external.string()), exports_external.string()]).transform((v) => typeof v === "string" ? [v] : v).default([]),
|
|
69791
|
+
inProgressStatus: exports_external.string().optional(),
|
|
69792
|
+
doneStatus: exports_external.string().optional(),
|
|
69793
|
+
postComments: exports_external.boolean().default(true)
|
|
69794
|
+
}).default({ statuses: [], labels: [], postComments: true })
|
|
69735
69795
|
}).default({
|
|
69736
69796
|
concurrency: 1,
|
|
69737
69797
|
pollIntervalSeconds: 60,
|
|
@@ -69739,7 +69799,7 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
69739
69799
|
maxCostUsdPerTask: 0,
|
|
69740
69800
|
engine: "claude",
|
|
69741
69801
|
model: "opus",
|
|
69742
|
-
linear: { statuses: [] }
|
|
69802
|
+
linear: { statuses: [], labels: [], postComments: true }
|
|
69743
69803
|
});
|
|
69744
69804
|
async function loadRalphyConfig(projectRoot) {
|
|
69745
69805
|
const path = join12(projectRoot, "ralphy.config.json");
|
|
@@ -69852,6 +69912,7 @@ class AgentCoordinator {
|
|
|
69852
69912
|
this.workers.push(worker);
|
|
69853
69913
|
this.pendingIds.delete(issue.id);
|
|
69854
69914
|
this.deps.onWorkersChanged();
|
|
69915
|
+
this.notifyStarted(issue, changeName);
|
|
69855
69916
|
handle.exited.then((code) => {
|
|
69856
69917
|
const idx = this.workers.indexOf(worker);
|
|
69857
69918
|
if (idx >= 0)
|
|
@@ -69862,10 +69923,57 @@ class AgentCoordinator {
|
|
|
69862
69923
|
this.state.processedIssueIds.push(issue.id);
|
|
69863
69924
|
this.deps.saveState(this.state);
|
|
69864
69925
|
}
|
|
69926
|
+
this.notifyExited(issue, changeName, code);
|
|
69865
69927
|
this.deps.onWorkersChanged();
|
|
69866
69928
|
this.spawnNext();
|
|
69867
69929
|
});
|
|
69868
69930
|
}
|
|
69931
|
+
async notifyStarted(issue, changeName) {
|
|
69932
|
+
const updater = this.deps.updater;
|
|
69933
|
+
if (!updater)
|
|
69934
|
+
return;
|
|
69935
|
+
if (this.opts.postComments !== false) {
|
|
69936
|
+
try {
|
|
69937
|
+
await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
|
|
69938
|
+
} catch (err) {
|
|
69939
|
+
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
69940
|
+
}
|
|
69941
|
+
}
|
|
69942
|
+
if (this.opts.inProgressStatus) {
|
|
69943
|
+
await this.moveIssue(issue, this.opts.inProgressStatus);
|
|
69944
|
+
}
|
|
69945
|
+
}
|
|
69946
|
+
async notifyExited(issue, changeName, code) {
|
|
69947
|
+
const updater = this.deps.updater;
|
|
69948
|
+
if (!updater)
|
|
69949
|
+
return;
|
|
69950
|
+
const ok = code === 0;
|
|
69951
|
+
if (this.opts.postComments !== false) {
|
|
69952
|
+
const body = ok ? `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\``;
|
|
69953
|
+
try {
|
|
69954
|
+
await updater.postComment(issue, body);
|
|
69955
|
+
} catch (err) {
|
|
69956
|
+
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
69957
|
+
}
|
|
69958
|
+
}
|
|
69959
|
+
if (ok && this.opts.doneStatus) {
|
|
69960
|
+
await this.moveIssue(issue, this.opts.doneStatus);
|
|
69961
|
+
}
|
|
69962
|
+
}
|
|
69963
|
+
async moveIssue(issue, stateName) {
|
|
69964
|
+
const updater = this.deps.updater;
|
|
69965
|
+
try {
|
|
69966
|
+
const stateId = await updater.resolveStateId(issue, stateName);
|
|
69967
|
+
if (!stateId) {
|
|
69968
|
+
this.deps.onLog(`! Linear state '${stateName}' not found for ${issue.identifier}`, "yellow");
|
|
69969
|
+
return;
|
|
69970
|
+
}
|
|
69971
|
+
await updater.setState(issue, stateId);
|
|
69972
|
+
this.deps.onLog(` \u2192 ${issue.identifier} moved to '${stateName}'`, "gray");
|
|
69973
|
+
} catch (err) {
|
|
69974
|
+
this.deps.onLog(`! Linear state move failed for ${issue.identifier}: ${err.message}`, "red");
|
|
69975
|
+
}
|
|
69976
|
+
}
|
|
69869
69977
|
stop() {
|
|
69870
69978
|
this.stopped = true;
|
|
69871
69979
|
for (const w of this.workers) {
|
|
@@ -69911,11 +70019,21 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
69911
70019
|
team: args.linearTeam || cfg.linear.team,
|
|
69912
70020
|
assignee: args.linearAssignee || cfg.linear.assignee,
|
|
69913
70021
|
statuses: args.linearStatus.length ? args.linearStatus : cfg.linear.statuses,
|
|
69914
|
-
|
|
70022
|
+
labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
|
|
69915
70023
|
};
|
|
70024
|
+
const stateCache = new Map;
|
|
70025
|
+
const teamKeyOf = (issue) => issue.identifier.split("-")[0];
|
|
69916
70026
|
const coord2 = new AgentCoordinator({
|
|
69917
70027
|
fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
|
|
69918
|
-
scaffold: (issue) =>
|
|
70028
|
+
scaffold: async (issue) => {
|
|
70029
|
+
let comments = [];
|
|
70030
|
+
try {
|
|
70031
|
+
comments = await fetchIssueComments(apiKey, issue.id);
|
|
70032
|
+
} catch (err) {
|
|
70033
|
+
appendLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
70034
|
+
}
|
|
70035
|
+
return scaffoldChangeForIssue(tasksDir, statesDir, issue, comments);
|
|
70036
|
+
},
|
|
69919
70037
|
spawnWorker: (changeName) => {
|
|
69920
70038
|
const cmd = [
|
|
69921
70039
|
process.execPath,
|
|
@@ -69944,14 +70062,34 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
69944
70062
|
loadState: () => readAgentState(projectRoot),
|
|
69945
70063
|
saveState: (s) => writeAgentState(projectRoot, s),
|
|
69946
70064
|
onLog: appendLog,
|
|
69947
|
-
onWorkersChanged: () => setTick((t) => t + 1)
|
|
69948
|
-
|
|
70065
|
+
onWorkersChanged: () => setTick((t) => t + 1),
|
|
70066
|
+
updater: {
|
|
70067
|
+
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
70068
|
+
setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
|
|
70069
|
+
resolveStateId: async (issue, stateName) => {
|
|
70070
|
+
const team = teamKeyOf(issue);
|
|
70071
|
+
let map2 = stateCache.get(team);
|
|
70072
|
+
if (!map2) {
|
|
70073
|
+
const states = await fetchWorkflowStates(apiKey, team);
|
|
70074
|
+
map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
|
|
70075
|
+
stateCache.set(team, map2);
|
|
70076
|
+
}
|
|
70077
|
+
return map2.get(stateName.toLowerCase()) ?? null;
|
|
70078
|
+
}
|
|
70079
|
+
}
|
|
70080
|
+
}, {
|
|
70081
|
+
concurrency,
|
|
70082
|
+
filter: filter2,
|
|
70083
|
+
inProgressStatus: cfg.linear.inProgressStatus,
|
|
70084
|
+
doneStatus: cfg.linear.doneStatus,
|
|
70085
|
+
postComments: cfg.linear.postComments
|
|
70086
|
+
});
|
|
69949
70087
|
coordRef.current = coord2;
|
|
69950
70088
|
await coord2.init();
|
|
69951
70089
|
const tick = async () => {
|
|
69952
70090
|
if (cancelled)
|
|
69953
70091
|
return;
|
|
69954
|
-
const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.
|
|
70092
|
+
const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
|
|
69955
70093
|
appendLog(`\u2026 polling Linear (${filterDesc})`);
|
|
69956
70094
|
const { found, added } = await coord2.pollOnce();
|
|
69957
70095
|
appendLog(` found ${found} open, ${added} new (queue=${coord2.queuedCount})`);
|