@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.
Files changed (2) hide show
  1. package/dist/shell/index.js +327 -98
  2. package/package.json +1 -1
@@ -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.0")
18932
- return "3.3.0";
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 existingStateRaw = storage.read(join11(stateDir, ".ralph-state.json"));
70617
- if (existingStateRaw !== null) {
70618
- currentState = readState(stateDir);
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 { statuses, labels, attachmentSubtitles: excludedSubtitles } = partition2(exc);
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
- const existing = attachments.find((a) => a.title === RALPHY_ATTACHMENT_TITLE);
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 query = `query IssueAttachments($id: String!) {
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 data = await linearRequest(apiKey, query, { id: issueId });
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 escapeRegex2(s) {
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 branch = branchForChange(changeName);
96970
- const tryGh = async (args2) => {
96971
- try {
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 open PR found on head=${branch} or Linear attachments; conflict scan skipped for ${PR_UNAVAILABLE_TTL_MS / 60000}m`, "gray");
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
- const blockerIds = issue2.blockedByIds;
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
- let comments = [];
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_])${escapeRegex2(handle)}\\b`, "i");
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 cached2 = prByChange.get(changeName);
97333
- if (cached2)
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 attachments = await fetchIssueAttachments(apiKey, row.issueId);
99589
- row.prUrl = findPullRequestUrl(attachments);
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "3.3.0",
3
+ "version": "3.3.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",