@neriros/ralphy 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/shell/index.js +329 -46
  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.4.0")
18932
- return "3.4.0";
18931
+ if ("3.5.0")
18932
+ return "3.5.0";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -92668,6 +92668,7 @@ var init_schema = __esm(() => {
92668
92668
  codeReviewTrigger: exports_external2.boolean().default(true),
92669
92669
  codeReviewStaleHours: exports_external2.number().nonnegative().default(24),
92670
92670
  syncTasksToComment: exports_external2.boolean().default(true),
92671
+ syncSpecsAsAttachments: exports_external2.boolean().default(true),
92671
92672
  indicators: IndicatorsSchema.default({})
92672
92673
  }).strict().default({
92673
92674
  postComments: true,
@@ -92677,6 +92678,7 @@ var init_schema = __esm(() => {
92677
92678
  codeReviewTrigger: true,
92678
92679
  codeReviewStaleHours: 24,
92679
92680
  syncTasksToComment: true,
92681
+ syncSpecsAsAttachments: true,
92680
92682
  indicators: {}
92681
92683
  }),
92682
92684
  github: exports_external2.object({
@@ -92831,6 +92833,11 @@ linear:
92831
92833
  # cadence as updateEveryIterations, and on done-transition.
92832
92834
  syncTasksToComment: true
92833
92835
 
92836
+ # Upload openspec proposal.md and design.md as Linear attachments on the
92837
+ # parent issue. Refreshed when file contents change, no-op otherwise.
92838
+ # Requires syncTasksToComment.
92839
+ syncSpecsAsAttachments: true
92840
+
92834
92841
  # Indicators map Ralph lifecycle events to Linear labels/statuses.
92835
92842
  #
92836
92843
  # Filter semantics (per indicator's \`filter:\` list):
@@ -93818,10 +93825,45 @@ function buildIssueFilter(spec) {
93818
93825
  }
93819
93826
  return where;
93820
93827
  }
93828
+ function clauseFromMarkers(markers) {
93829
+ if (markers.length === 0)
93830
+ return null;
93831
+ const { statuses, labels, attachmentSubtitles, projects } = partition2(markers);
93832
+ const parts = {};
93833
+ if (statuses.length > 0)
93834
+ parts.state = { name: { in: statuses } };
93835
+ if (labels.length > 0)
93836
+ parts.labels = { some: { name: { in: labels } } };
93837
+ if (attachmentSubtitles.length > 0) {
93838
+ parts.attachments = {
93839
+ some: {
93840
+ title: { eq: RALPHY_ATTACHMENT_TITLE_FILTER },
93841
+ subtitle: { in: attachmentSubtitles }
93842
+ }
93843
+ };
93844
+ }
93845
+ if (projects.length > 0)
93846
+ parts.project = { name: { in: projects } };
93847
+ return Object.keys(parts).length > 0 ? parts : null;
93848
+ }
93821
93849
  async function fetchMentionScanIssues(apiKey, spec) {
93822
- const where = {
93823
- state: { type: { in: ["unstarted", "started", "backlog", "triage", "completed"] } }
93824
- };
93850
+ const branches = [];
93851
+ const { getTodo, getInProgress, setDone } = spec.indicators;
93852
+ for (const ind of [getTodo, getInProgress]) {
93853
+ if (!ind)
93854
+ continue;
93855
+ const c = clauseFromMarkers(ind.filter);
93856
+ if (c)
93857
+ branches.push(c);
93858
+ }
93859
+ if (setDone) {
93860
+ const c = clauseFromMarkers(markersOf(setDone));
93861
+ if (c)
93862
+ branches.push(c);
93863
+ }
93864
+ if (branches.length === 0)
93865
+ return [];
93866
+ const where = branches.length === 1 ? { ...branches[0] } : { or: branches };
93825
93867
  if (spec.team)
93826
93868
  where.team = { key: { eq: spec.team } };
93827
93869
  if (spec.assignee) {
@@ -94004,6 +94046,66 @@ async function linearRequest(apiKey, query, variables) {
94004
94046
  }
94005
94047
  throw lastHttpError ?? new Error("Linear API request failed");
94006
94048
  }
94049
+ async function uploadFileToLinear(apiKey, input) {
94050
+ const mutation = `mutation FileUpload($filename: String!, $contentType: String!, $size: Int!) {
94051
+ fileUpload(filename: $filename, contentType: $contentType, size: $size) {
94052
+ uploadFile { uploadUrl assetUrl headers { key value } }
94053
+ }
94054
+ }`;
94055
+ const data = await linearRequest(apiKey, mutation, {
94056
+ filename: input.filename,
94057
+ contentType: input.contentType,
94058
+ size: input.bytes.byteLength
94059
+ });
94060
+ const up = data.uploadFile;
94061
+ if (!up)
94062
+ throw new Error("fileUpload returned no uploadFile payload");
94063
+ const headers = { "Content-Type": input.contentType };
94064
+ for (const h of up.headers)
94065
+ headers[h.key] = h.value;
94066
+ const res = await fetch(up.uploadUrl, {
94067
+ method: "PUT",
94068
+ headers,
94069
+ body: input.bytes
94070
+ });
94071
+ if (!res.ok) {
94072
+ const body = await res.text().catch(() => "");
94073
+ const err = new Error("Linear file upload PUT failed");
94074
+ err.status = res.status;
94075
+ err.body = body;
94076
+ throw err;
94077
+ }
94078
+ return { assetUrl: up.assetUrl };
94079
+ }
94080
+ async function createAttachmentForUrl(apiKey, input) {
94081
+ const mutation = `mutation CreateAttachment(
94082
+ $issueId: String!, $url: String!, $title: String!, $subtitle: String
94083
+ ) {
94084
+ attachmentCreate(input: { issueId: $issueId, url: $url, title: $title, subtitle: $subtitle }) {
94085
+ success
94086
+ attachment { id }
94087
+ }
94088
+ }`;
94089
+ const data = await linearRequest(apiKey, mutation, {
94090
+ issueId: input.issueId,
94091
+ url: input.url,
94092
+ title: input.title,
94093
+ subtitle: input.subtitle ?? null
94094
+ });
94095
+ const id = data.attachmentCreate.attachment?.id;
94096
+ if (!id)
94097
+ throw new Error("attachmentCreate returned no attachment id");
94098
+ return id;
94099
+ }
94100
+ async function updateAttachmentUrl(apiKey, attachmentId, url2, subtitle) {
94101
+ const mutation = subtitle === undefined ? `mutation UpdateAttachmentUrl($id: String!, $url: String!) {
94102
+ attachmentUpdate(id: $id, input: { url: $url }) { success }
94103
+ }` : `mutation UpdateAttachmentUrl($id: String!, $url: String!, $subtitle: String!) {
94104
+ attachmentUpdate(id: $id, input: { url: $url, subtitle: $subtitle }) { success }
94105
+ }`;
94106
+ const variables = subtitle === undefined ? { id: attachmentId, url: url2 } : { id: attachmentId, url: url2, subtitle };
94107
+ await linearRequest(apiKey, mutation, variables);
94108
+ }
94007
94109
  async function addReactionToComment(apiKey, commentId, emoji3) {
94008
94110
  const mutation = `mutation Reaction($commentId: String!, $emoji: String!) {
94009
94111
  reactionCreate(input: { commentId: $commentId, emoji: $emoji }) { success }
@@ -94332,6 +94434,7 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
94332
94434
  }
94333
94435
  var LINEAR_API = "https://api.linear.app/graphql", RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", linearRequestInternals, MAX_LINEAR_ATTEMPTS = 3, MAX_RETRY_AFTER_MS = 2000, RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
94334
94436
  var init_linear = __esm(() => {
94437
+ init_types2();
94335
94438
  linearRequestInternals = {
94336
94439
  sleep: (ms) => Bun.sleep(ms)
94337
94440
  };
@@ -94526,6 +94629,7 @@ class AgentCoordinator {
94526
94629
  this.spawnNext();
94527
94630
  const prStatus = await this.scanDoneForConflicts();
94528
94631
  await this.reportProgress();
94632
+ await this.syncWorkerTasks();
94529
94633
  const buckets = {
94530
94634
  todo: todo.length,
94531
94635
  inProgress: inProgress.length,
@@ -94580,12 +94684,26 @@ class AgentCoordinator {
94580
94684
  } catch (err) {
94581
94685
  this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
94582
94686
  }
94583
- if (this.deps.syncTasks) {
94584
- try {
94585
- await this.deps.syncTasks(w, count);
94586
- } catch (err) {
94587
- this.deps.onLog(`! sync-tasks (progress) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
94588
- }
94687
+ }
94688
+ }
94689
+ async syncWorkerTasks() {
94690
+ if (!this.deps.syncTasks || !this.deps.getIterationCount)
94691
+ return;
94692
+ for (const w of this.workers) {
94693
+ let count;
94694
+ try {
94695
+ count = await this.deps.getIterationCount(w.changeName);
94696
+ } catch (err) {
94697
+ this.deps.onLog(`! iteration count read failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
94698
+ continue;
94699
+ }
94700
+ if (count === w.lastSyncedIteration)
94701
+ continue;
94702
+ try {
94703
+ await this.deps.syncTasks(w, count);
94704
+ w.lastSyncedIteration = count;
94705
+ } catch (err) {
94706
+ this.deps.onLog(`! sync-tasks (poll) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
94589
94707
  }
94590
94708
  }
94591
94709
  }
@@ -94745,6 +94863,7 @@ class AgentCoordinator {
94745
94863
  mode,
94746
94864
  kill: handle.kill,
94747
94865
  lastReportedIteration: 0,
94866
+ lastSyncedIteration: 0,
94748
94867
  restarting: false
94749
94868
  };
94750
94869
  this.workers.push(worker);
@@ -94828,6 +94947,7 @@ class AgentCoordinator {
94828
94947
  mode,
94829
94948
  kill: () => {},
94830
94949
  lastReportedIteration: 0,
94950
+ lastSyncedIteration: 0,
94831
94951
  restarting: false
94832
94952
  };
94833
94953
  try {
@@ -94935,7 +95055,7 @@ var init_coordinator = __esm(() => {
94935
95055
  import { join as join18 } from "path";
94936
95056
  import { mkdir as mkdir5 } from "fs/promises";
94937
95057
  function changeNameForIssue(issue2) {
94938
- const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
95058
+ const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
94939
95059
  return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
94940
95060
  }
94941
95061
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "") {
@@ -96544,9 +96664,139 @@ var init_comment_sync = __esm(() => {
96544
96664
  init_linear_sync();
96545
96665
  });
96546
96666
 
96547
- // apps/agent/src/agent/wire.ts
96548
- import { join as join22 } from "path";
96667
+ // apps/agent/src/agent/linear-sync/spec-attachments.ts
96668
+ import { dirname as dirname8, join as join22 } from "path";
96549
96669
  import { mkdir as mkdir7 } from "fs/promises";
96670
+ async function readStateJson2(statePath) {
96671
+ const file2 = Bun.file(statePath);
96672
+ if (!await file2.exists())
96673
+ return null;
96674
+ try {
96675
+ return await file2.json();
96676
+ } catch {
96677
+ return null;
96678
+ }
96679
+ }
96680
+ async function writeStateJson2(statePath, state) {
96681
+ await mkdir7(dirname8(statePath), { recursive: true });
96682
+ await Bun.write(statePath, JSON.stringify(state, null, 2) + `
96683
+ `);
96684
+ }
96685
+ function readSpecState(state) {
96686
+ const raw = state?.specAttachments ?? {};
96687
+ return {
96688
+ proposal: {
96689
+ attachmentId: raw?.proposal?.attachmentId ?? null,
96690
+ sha256: raw?.proposal?.sha256 ?? null
96691
+ },
96692
+ design: {
96693
+ attachmentId: raw?.design?.attachmentId ?? null,
96694
+ sha256: raw?.design?.sha256 ?? null
96695
+ }
96696
+ };
96697
+ }
96698
+ async function patchSpecState(statePath, patch) {
96699
+ const existing = await readStateJson2(statePath) ?? {};
96700
+ const current = readSpecState(existing);
96701
+ const next = { ...current, [patch.slot]: patch.value };
96702
+ await writeStateJson2(statePath, { ...existing, specAttachments: next });
96703
+ }
96704
+ function sha256Hex(bytes) {
96705
+ const hasher = new Bun.CryptoHasher("sha256");
96706
+ hasher.update(bytes);
96707
+ return hasher.digest("hex");
96708
+ }
96709
+ async function syncSlot(deps, slot) {
96710
+ const filename = SLOT_FILES[slot];
96711
+ const path = join22(deps.changeDir, filename);
96712
+ const file2 = Bun.file(path);
96713
+ if (!await file2.exists()) {
96714
+ deps.log(` spec-attachments: ${filename} missing, skipping`, "gray");
96715
+ return;
96716
+ }
96717
+ let bytes;
96718
+ try {
96719
+ bytes = await file2.bytes();
96720
+ } catch (err) {
96721
+ deps.log(`! spec-attachments: read ${filename} failed: ${err.message}`, "yellow");
96722
+ return;
96723
+ }
96724
+ const hash2 = sha256Hex(bytes);
96725
+ const state = await readStateJson2(deps.statePath);
96726
+ const current = readSpecState(state)[slot] ?? EMPTY_SLOT;
96727
+ if (current.attachmentId && current.sha256 === hash2) {
96728
+ deps.log(` spec-attachments: ${filename} unchanged, skipping`, "gray");
96729
+ return;
96730
+ }
96731
+ const subtitle = `iteration ${deps.iteration}`;
96732
+ let assetUrl;
96733
+ try {
96734
+ const uploaded = await deps.mutations.uploadFileToLinear(deps.apiKey, {
96735
+ filename,
96736
+ contentType: "text/markdown",
96737
+ bytes
96738
+ });
96739
+ assetUrl = uploaded.assetUrl;
96740
+ } catch (err) {
96741
+ deps.log(`! spec-attachments: upload ${filename} failed: ${err.message}`, "yellow");
96742
+ return;
96743
+ }
96744
+ if (current.attachmentId) {
96745
+ try {
96746
+ await deps.mutations.updateAttachmentUrl(deps.apiKey, current.attachmentId, assetUrl, subtitle);
96747
+ await patchSpecState(deps.statePath, {
96748
+ slot,
96749
+ value: { attachmentId: current.attachmentId, sha256: hash2 }
96750
+ });
96751
+ deps.log(` spec-attachments: refreshed ${filename}`, "gray");
96752
+ return;
96753
+ } catch (err) {
96754
+ if (!isCommentNotFoundError(err)) {
96755
+ deps.log(`! spec-attachments: updateAttachmentUrl ${filename} failed: ${err.message}`, "yellow");
96756
+ return;
96757
+ }
96758
+ deps.log(` spec-attachments: attachment ${current.attachmentId} not found \u2014 recreating`, "gray");
96759
+ }
96760
+ }
96761
+ let newId;
96762
+ try {
96763
+ newId = await deps.mutations.createAttachmentForUrl(deps.apiKey, {
96764
+ issueId: deps.issueId,
96765
+ url: assetUrl,
96766
+ title: ATTACHMENT_TITLES[slot],
96767
+ subtitle
96768
+ });
96769
+ } catch (err) {
96770
+ deps.log(`! spec-attachments: createAttachmentForUrl ${filename} failed: ${err.message}`, "yellow");
96771
+ return;
96772
+ }
96773
+ await patchSpecState(deps.statePath, {
96774
+ slot,
96775
+ value: { attachmentId: newId, sha256: hash2 }
96776
+ });
96777
+ deps.log(` spec-attachments: created ${filename} attachment`, "gray");
96778
+ }
96779
+ async function syncSpecAttachments(deps) {
96780
+ await syncSlot(deps, "proposal");
96781
+ await syncSlot(deps, "design");
96782
+ }
96783
+ var ATTACHMENT_TITLES, SLOT_FILES, EMPTY_SLOT;
96784
+ var init_spec_attachments = __esm(() => {
96785
+ init_comment_sync();
96786
+ ATTACHMENT_TITLES = {
96787
+ proposal: "Ralph proposal",
96788
+ design: "Ralph design"
96789
+ };
96790
+ SLOT_FILES = {
96791
+ proposal: "proposal.md",
96792
+ design: "design.md"
96793
+ };
96794
+ EMPTY_SLOT = { attachmentId: null, sha256: null };
96795
+ });
96796
+
96797
+ // apps/agent/src/agent/wire.ts
96798
+ import { join as join23 } from "path";
96799
+ import { mkdir as mkdir8 } from "fs/promises";
96550
96800
  async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog) {
96551
96801
  const candidates = urls.filter((url2) => GITHUB_PR_URL_RE.test(url2));
96552
96802
  let sawNonOpenPr = false;
@@ -96750,7 +97000,7 @@ function buildAgentCoordinator(input) {
96750
97000
  onWorkerOutput,
96751
97001
  onWorkerCmd
96752
97002
  } = input;
96753
- const logsDir = join22(projectRoot, ".ralph", "logs");
97003
+ const logsDir = join23(projectRoot, ".ralph", "logs");
96754
97004
  const concurrency = args.concurrency || cfg.concurrency;
96755
97005
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
96756
97006
  const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
@@ -96942,7 +97192,15 @@ function buildAgentCoordinator(input) {
96942
97192
  async function prepare(issue2, mode, trigger) {
96943
97193
  const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue2);
96944
97194
  let changeName;
96945
- const isFresh = mode === "fresh";
97195
+ const wtLayoutPre = projectLayout(workerCwd);
97196
+ const derivedName = changeNameForIssue(issue2);
97197
+ const tasksMdPath = join23(wtLayoutPre.changeDir(derivedName), "tasks.md");
97198
+ const tasksMdExists = await Bun.file(tasksMdPath).exists();
97199
+ const needsScaffold = !tasksMdExists;
97200
+ if (mode !== "fresh" && needsScaffold) {
97201
+ onLog(` ${issue2.identifier}: tasks.md missing at ${tasksMdPath} \u2014 rescaffolding`, "yellow");
97202
+ }
97203
+ const isFresh = mode === "fresh" || needsScaffold;
96946
97204
  if (isFresh) {
96947
97205
  let comments = [];
96948
97206
  try {
@@ -96972,10 +97230,9 @@ function buildAgentCoordinator(input) {
96972
97230
  `);
96973
97231
  changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue2, comments, appendPrompt);
96974
97232
  } else {
96975
- changeName = changeNameForIssue(issue2);
96976
- const wtLayout = projectLayout(workerCwd);
96977
- await mkdir7(wtLayout.changeDir(changeName), { recursive: true });
96978
- await mkdir7(wtLayout.taskStateDir(changeName), { recursive: true });
97233
+ changeName = derivedName;
97234
+ await mkdir8(wtLayoutPre.changeDir(changeName), { recursive: true });
97235
+ await mkdir8(wtLayoutPre.taskStateDir(changeName), { recursive: true });
96979
97236
  }
96980
97237
  cwdByChange.set(changeName, workerCwd);
96981
97238
  statesDirByChange.set(changeName, scaffoldStatesDir);
@@ -96984,7 +97241,7 @@ function buildAgentCoordinator(input) {
96984
97241
  branchByChange.set(changeName, branch);
96985
97242
  if (mode === "review") {
96986
97243
  const wtLayout = projectLayout(workerCwd);
96987
- const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
97244
+ const tasksFile = join23(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
96988
97245
  let body;
96989
97246
  let heading;
96990
97247
  if (trigger) {
@@ -97009,7 +97266,7 @@ function buildAgentCoordinator(input) {
97009
97266
  await reactivateState2(wtLayout.stateFile(changeName), changeName);
97010
97267
  } else if (mode === "conflict-fix") {
97011
97268
  const wtLayout = projectLayout(workerCwd);
97012
- const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
97269
+ const tasksFile = join23(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
97013
97270
  const prUrl = prByChange.get(changeName);
97014
97271
  const body = [
97015
97272
  `The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
@@ -97089,7 +97346,7 @@ PR: ${prUrl}` : ""
97089
97346
  return c;
97090
97347
  }
97091
97348
  function defaultSpawn(changeName, cmd, cwd2, note) {
97092
- const logFilePath = join22(logsDir, `${changeName}.log`);
97349
+ const logFilePath = join23(logsDir, `${changeName}.log`);
97093
97350
  const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
97094
97351
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
97095
97352
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -97148,7 +97405,7 @@ PR: ${prUrl}` : ""
97148
97405
  function spawnWorker(changeName) {
97149
97406
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
97150
97407
  const injected = input.runners?.spawnWorker;
97151
- const missionTasksPath = join22(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
97408
+ const missionTasksPath = join23(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
97152
97409
  const prevTasksPromise = (async () => {
97153
97410
  const f2 = Bun.file(missionTasksPath);
97154
97411
  return await f2.exists() ? await f2.text() : "";
@@ -97156,7 +97413,7 @@ PR: ${prUrl}` : ""
97156
97413
  let logFilePath;
97157
97414
  let handle;
97158
97415
  if (injected) {
97159
- logFilePath = join22(logsDir, `${changeName}.log`);
97416
+ logFilePath = join23(logsDir, `${changeName}.log`);
97160
97417
  handle = injected(buildTaskCmdFor(changeName), cwd2);
97161
97418
  } else {
97162
97419
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
@@ -97372,7 +97629,15 @@ PR: ${prUrl}` : ""
97372
97629
  const handle = cfg.linear.mentionHandle;
97373
97630
  let candidates = [];
97374
97631
  try {
97375
- candidates = await fetchMentionScanIssues(apiKey, { team, assignee });
97632
+ candidates = await fetchMentionScanIssues(apiKey, {
97633
+ team,
97634
+ assignee,
97635
+ indicators: {
97636
+ ...indicators.getTodo !== undefined ? { getTodo: indicators.getTodo } : {},
97637
+ ...indicators.getInProgress !== undefined ? { getInProgress: indicators.getInProgress } : {},
97638
+ ...indicators.setDone !== undefined ? { setDone: indicators.setDone } : {}
97639
+ }
97640
+ });
97376
97641
  } catch (err) {
97377
97642
  if (isRateLimitedError(err)) {
97378
97643
  onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
@@ -97663,6 +97928,12 @@ PR: ${prUrl}` : ""
97663
97928
  updateIssueComment,
97664
97929
  deleteIssueComment
97665
97930
  };
97931
+ const specAttachmentsEnabled = Boolean(commentSyncEnabled && cfg.linear.syncSpecsAsAttachments);
97932
+ const specAttachmentMutations = {
97933
+ uploadFileToLinear,
97934
+ createAttachmentForUrl,
97935
+ updateAttachmentUrl
97936
+ };
97666
97937
  const coord = new AgentCoordinator({
97667
97938
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
97668
97939
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
@@ -97716,6 +97987,17 @@ PR: ${prUrl}` : ""
97716
97987
  log: onLog,
97717
97988
  mutations: commentMutations
97718
97989
  });
97990
+ if (specAttachmentsEnabled) {
97991
+ await syncSpecAttachments({
97992
+ apiKey,
97993
+ issueId: worker.issueId,
97994
+ statePath,
97995
+ changeDir,
97996
+ iteration,
97997
+ log: onLog,
97998
+ mutations: specAttachmentMutations
97999
+ });
98000
+ }
97719
98001
  },
97720
98002
  onSteeringAppended: async (changeName, message) => {
97721
98003
  const root = cwdByChange.get(changeName) ?? projectRoot;
@@ -97853,6 +98135,7 @@ var init_wire = __esm(() => {
97853
98135
  init_gate();
97854
98136
  init_workflow();
97855
98137
  init_comment_sync();
98138
+ init_spec_attachments();
97856
98139
  GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
97857
98140
  bunGitRunner = {
97858
98141
  run: async (args, cwd2) => {
@@ -98180,7 +98463,7 @@ var init_SteeringField = __esm(async () => {
98180
98463
  });
98181
98464
 
98182
98465
  // apps/agent/src/components/AgentMode.tsx
98183
- import { join as join23 } from "path";
98466
+ import { join as join24 } from "path";
98184
98467
  async function appendSteeringImpl(changeDir, message) {
98185
98468
  await runWithContext(createDefaultContext(), async () => {
98186
98469
  appendSteeringMessage(changeDir, message);
@@ -98635,7 +98918,7 @@ function AgentMode({
98635
98918
  (async () => {
98636
98919
  for (const [changeName, meta3] of workerMetaRef.current) {
98637
98920
  try {
98638
- const file2 = Bun.file(join23(meta3.statesDir, changeName, ".ralph-state.json"));
98921
+ const file2 = Bun.file(join24(meta3.statesDir, changeName, ".ralph-state.json"));
98639
98922
  if (await file2.exists()) {
98640
98923
  const json2 = await file2.json();
98641
98924
  meta3.iter = json2.iteration ?? meta3.iter;
@@ -98645,9 +98928,9 @@ function AgentMode({
98645
98928
  }
98646
98929
  if (meta3.changeDir) {
98647
98930
  try {
98648
- const tasksFile = Bun.file(join23(meta3.changeDir, "tasks.md"));
98649
- const proposalFile = Bun.file(join23(meta3.changeDir, "proposal.md"));
98650
- const designFile = Bun.file(join23(meta3.changeDir, "design.md"));
98931
+ const tasksFile = Bun.file(join24(meta3.changeDir, "tasks.md"));
98932
+ const proposalFile = Bun.file(join24(meta3.changeDir, "proposal.md"));
98933
+ const designFile = Bun.file(join24(meta3.changeDir, "design.md"));
98651
98934
  const [tasksText, proposalText, designText] = await Promise.all([
98652
98935
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
98653
98936
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -98695,7 +98978,7 @@ function AgentMode({
98695
98978
  use_input_default((input, key) => {
98696
98979
  if (steeringFocusedRef.current)
98697
98980
  return;
98698
- if (key.ctrl && key.meta && (input === "t" || input === "T")) {
98981
+ if (key.ctrl && (input === "l" || input === "L")) {
98699
98982
  if (activeCount > 0)
98700
98983
  setShowAllSubtasks((v) => !v);
98701
98984
  return;
@@ -99456,7 +99739,7 @@ function AgentMode({
99456
99739
  }),
99457
99740
  !showAllSubtasks && subtasks.length > MAX_PENDING_DISPLAY && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
99458
99741
  dimColor: true,
99459
- children: ` \u2026 +${subtasks.length - MAX_PENDING_DISPLAY} more (CTRL+ALT+T to expand)`
99742
+ children: ` \u2026 +${subtasks.length - MAX_PENDING_DISPLAY} more (CTRL+L to expand)`
99460
99743
  }, undefined, false, undefined, this)
99461
99744
  ]
99462
99745
  }, undefined, true, undefined, this),
@@ -99478,7 +99761,7 @@ function AgentMode({
99478
99761
  },
99479
99762
  onSubmit: async (message) => {
99480
99763
  try {
99481
- await appendSteering(join23(tasksDir, w.changeName), message);
99764
+ await appendSteering(join24(tasksDir, w.changeName), message);
99482
99765
  } catch (err) {
99483
99766
  appendLog(`! steering append failed for ${w.changeName}: ${err.message}`, "red");
99484
99767
  throw err;
@@ -99688,7 +99971,7 @@ var exports_list = {};
99688
99971
  __export(exports_list, {
99689
99972
  runList: () => runList
99690
99973
  });
99691
- import { join as join24 } from "path";
99974
+ import { join as join25 } from "path";
99692
99975
  function countTaskItems(content) {
99693
99976
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
99694
99977
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -99701,13 +99984,13 @@ function buildLocalRows(statesDir, projectRoot) {
99701
99984
  const sources = [{ dir: statesDir, label: "main" }];
99702
99985
  const worktreesRoot = worktreesDir2(projectRoot);
99703
99986
  for (const wt of storage.list(worktreesRoot)) {
99704
- sources.push({ dir: join24(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
99987
+ sources.push({ dir: join25(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
99705
99988
  }
99706
99989
  for (const { dir, label } of sources) {
99707
99990
  for (const entry of storage.list(dir)) {
99708
99991
  if (seen.has(entry))
99709
99992
  continue;
99710
- const raw = storage.read(join24(dir, entry, ".ralph-state.json"));
99993
+ const raw = storage.read(join25(dir, entry, ".ralph-state.json"));
99711
99994
  if (raw === null)
99712
99995
  continue;
99713
99996
  let state;
@@ -99722,7 +100005,7 @@ function buildLocalRows(statesDir, projectRoot) {
99722
100005
  const firstLine = promptRaw.split(`
99723
100006
  `).find((l) => l.trim() !== "") ?? "";
99724
100007
  let progress = "\u2014";
99725
- const tasksContent = storage.read(join24(dir, entry, "tasks.md"));
100008
+ const tasksContent = storage.read(join25(dir, entry, "tasks.md"));
99726
100009
  if (tasksContent !== null) {
99727
100010
  const { checked, unchecked } = countTaskItems(tasksContent);
99728
100011
  const total = checked + unchecked;
@@ -100149,8 +100432,8 @@ var exports_json_runner = {};
100149
100432
  __export(exports_json_runner, {
100150
100433
  runAgentJson: () => runAgentJson
100151
100434
  });
100152
- import { join as join25 } from "path";
100153
- import { mkdir as mkdir8 } from "fs/promises";
100435
+ import { join as join26 } from "path";
100436
+ import { mkdir as mkdir9 } from "fs/promises";
100154
100437
  import { homedir as homedir5 } from "os";
100155
100438
  function cleanOutputLine2(raw) {
100156
100439
  const clean = raw.replace(ANSI_STRIP_RE2, "").trim();
@@ -100174,7 +100457,7 @@ async function runAgentJson({
100174
100457
  statesDir,
100175
100458
  tasksDir
100176
100459
  }) {
100177
- await mkdir8(join25(homedir5(), ".ralph"), { recursive: true }).catch(() => {
100460
+ await mkdir9(join26(homedir5(), ".ralph"), { recursive: true }).catch(() => {
100178
100461
  return;
100179
100462
  });
100180
100463
  const cfgPath = await ensureRalphyConfig(projectRoot);
@@ -100328,8 +100611,8 @@ var exports_src2 = {};
100328
100611
  __export(exports_src2, {
100329
100612
  main: () => main2
100330
100613
  });
100331
- import { mkdir as mkdir9 } from "fs/promises";
100332
- import { join as join26 } from "path";
100614
+ import { mkdir as mkdir10 } from "fs/promises";
100615
+ import { join as join27 } from "path";
100333
100616
  async function main2(argv) {
100334
100617
  if (argv.includes("--help") || argv.includes("-h")) {
100335
100618
  printHelp2();
@@ -100363,9 +100646,9 @@ async function main2(argv) {
100363
100646
  });
100364
100647
  return typeof process.exitCode === "number" ? process.exitCode : 0;
100365
100648
  }
100366
- await mkdir9(statesDir, { recursive: true });
100367
- await mkdir9(tasksDir, { recursive: true });
100368
- await mkdir9(join26(projectRoot, ".ralph"), { recursive: true });
100649
+ await mkdir10(statesDir, { recursive: true });
100650
+ await mkdir10(tasksDir, { recursive: true });
100651
+ await mkdir10(join27(projectRoot, ".ralph"), { recursive: true });
100369
100652
  if (args.jsonOutput) {
100370
100653
  const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
100371
100654
  await runAgentJson2({ args, projectRoot, statesDir, tasksDir });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
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",