@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.
Files changed (2) hide show
  1. package/dist/cli/index.js +169 -31
  2. 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 = arg;
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.label)
69599
- where.labels = { some: { name: { eq: filter2.label } } };
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
- "Content-Type": "application/json",
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
- return [];
69632
- return json.data.issues.nodes.map((n) => ({
69633
- id: n.id,
69634
- identifier: n.identifier,
69635
- title: n.title,
69636
- description: n.description,
69637
- url: n.url,
69638
- state: n.state,
69639
- assignee: n.assignee,
69640
- labels: n.labels.nodes.map((l) => l.name)
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
- label: exports_external.string().optional()
69734
- }).default({ statuses: [] })
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
- label: args.linearLabel || cfg.linear.label
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) => scaffoldChangeForIssue(tasksDir, statesDir, 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
- }, { concurrency, filter: filter2 });
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.label ? `, label=${filter2.label}` : ""}`;
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})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "description": "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
5
5
  "keywords": [
6
6
  "agent",