@neriros/ralphy 3.3.2 → 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.
package/dist/mcp/index.js CHANGED
@@ -25106,22 +25106,6 @@ ${existing.trimStart()}` : `${message}
25106
25106
  await mkdir(dirname3(tasksPath), { recursive: true });
25107
25107
  await Bun.write(tasksPath, next);
25108
25108
  }
25109
- async readSection(name, artifact, heading) {
25110
- const file = Bun.file(join4("openspec", "changes", name, artifact));
25111
- if (!await file.exists())
25112
- return "";
25113
- const content = await file.text();
25114
- const headingIndex = content.indexOf(heading);
25115
- if (headingIndex === -1)
25116
- return "";
25117
- const afterHeading = content.slice(headingIndex + heading.length);
25118
- const levelMatch = heading.match(/^(#+)/);
25119
- const level = levelMatch ? levelMatch[1].length : 2;
25120
- const nextHeadingPattern = new RegExp(`\\n#{1,${level}} `);
25121
- const nextMatch = afterHeading.match(nextHeadingPattern);
25122
- const sectionContent = nextMatch ? afterHeading.slice(0, nextMatch.index) : afterHeading;
25123
- return sectionContent.trim();
25124
- }
25125
25109
  async validateChange(name) {
25126
25110
  const result = runOpenspec(["validate", name, "--json", "--no-interactive"]);
25127
25111
  if (result.stdout) {
@@ -25140,8 +25124,71 @@ ${existing.trimStart()}` : `${message}
25140
25124
  errors: result.stderr ? [result.stderr] : []
25141
25125
  };
25142
25126
  }
25127
+ async getStatus(name) {
25128
+ const result = runOpenspec(["status", "--change", name, "--json"]);
25129
+ if (result.stdout) {
25130
+ try {
25131
+ const parsed = JSON.parse(result.stdout);
25132
+ const status = {
25133
+ changeName: parsed.changeName ?? name,
25134
+ isComplete: parsed.isComplete ?? false,
25135
+ applyRequires: parsed.applyRequires ?? [],
25136
+ artifacts: parsed.artifacts ?? []
25137
+ };
25138
+ if (parsed.schemaName !== undefined)
25139
+ status.schemaName = parsed.schemaName;
25140
+ return status;
25141
+ } catch {}
25142
+ }
25143
+ return {
25144
+ changeName: name,
25145
+ isComplete: false,
25146
+ applyRequires: [],
25147
+ artifacts: []
25148
+ };
25149
+ }
25150
+ async getInstructions(name, artifact) {
25151
+ const result = runOpenspec(["instructions", artifact, "--change", name, "--json"]);
25152
+ if (result.stdout) {
25153
+ try {
25154
+ const parsed = JSON.parse(result.stdout);
25155
+ const out = {
25156
+ changeName: parsed.changeName ?? name,
25157
+ artifactId: parsed.artifactId ?? artifact,
25158
+ instruction: parsed.instruction ?? ""
25159
+ };
25160
+ if (parsed.outputPath !== undefined)
25161
+ out.outputPath = parsed.outputPath;
25162
+ if (parsed.description !== undefined)
25163
+ out.description = parsed.description;
25164
+ if (parsed.template !== undefined)
25165
+ out.template = parsed.template;
25166
+ if (parsed.dependencies !== undefined)
25167
+ out.dependencies = parsed.dependencies;
25168
+ return out;
25169
+ } catch {}
25170
+ }
25171
+ return { changeName: name, artifactId: artifact, instruction: "" };
25172
+ }
25173
+ async showChange(name) {
25174
+ const result = runOpenspec(["show", name, "--json", "--type", "change"]);
25175
+ if (result.stdout) {
25176
+ try {
25177
+ const parsed = JSON.parse(result.stdout);
25178
+ const out = {
25179
+ id: parsed.id ?? name,
25180
+ deltaCount: parsed.deltaCount ?? 0,
25181
+ deltas: parsed.deltas ?? []
25182
+ };
25183
+ if (parsed.title !== undefined)
25184
+ out.title = parsed.title;
25185
+ return out;
25186
+ } catch {}
25187
+ }
25188
+ return { id: name, deltaCount: 0, deltas: [] };
25189
+ }
25143
25190
  async archiveChange(name) {
25144
- const result = runOpenspec(["archive", name, "-y", "--skip-specs"], { inherit: true });
25191
+ const result = runOpenspec(["archive", name, "-y"], { inherit: true });
25145
25192
  if (result.status !== 0) {
25146
25193
  throw new Error("openspec archive failed");
25147
25194
  }
@@ -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.2")
18932
- return "3.3.2";
18931
+ if ("3.5.0")
18932
+ return "3.5.0";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -59558,22 +59558,6 @@ ${existing.trimStart()}` : `${message}
59558
59558
  await mkdir(dirname4(tasksPath), { recursive: true });
59559
59559
  await Bun.write(tasksPath, next);
59560
59560
  }
59561
- async readSection(name, artifact, heading) {
59562
- const file = Bun.file(join6("openspec", "changes", name, artifact));
59563
- if (!await file.exists())
59564
- return "";
59565
- const content = await file.text();
59566
- const headingIndex = content.indexOf(heading);
59567
- if (headingIndex === -1)
59568
- return "";
59569
- const afterHeading = content.slice(headingIndex + heading.length);
59570
- const levelMatch = heading.match(/^(#+)/);
59571
- const level = levelMatch ? levelMatch[1].length : 2;
59572
- const nextHeadingPattern = new RegExp(`\\n#{1,${level}} `);
59573
- const nextMatch = afterHeading.match(nextHeadingPattern);
59574
- const sectionContent = nextMatch ? afterHeading.slice(0, nextMatch.index) : afterHeading;
59575
- return sectionContent.trim();
59576
- }
59577
59561
  async validateChange(name) {
59578
59562
  const result2 = runOpenspec(["validate", name, "--json", "--no-interactive"]);
59579
59563
  if (result2.stdout) {
@@ -59592,8 +59576,71 @@ ${existing.trimStart()}` : `${message}
59592
59576
  errors: result2.stderr ? [result2.stderr] : []
59593
59577
  };
59594
59578
  }
59579
+ async getStatus(name) {
59580
+ const result2 = runOpenspec(["status", "--change", name, "--json"]);
59581
+ if (result2.stdout) {
59582
+ try {
59583
+ const parsed = JSON.parse(result2.stdout);
59584
+ const status = {
59585
+ changeName: parsed.changeName ?? name,
59586
+ isComplete: parsed.isComplete ?? false,
59587
+ applyRequires: parsed.applyRequires ?? [],
59588
+ artifacts: parsed.artifacts ?? []
59589
+ };
59590
+ if (parsed.schemaName !== undefined)
59591
+ status.schemaName = parsed.schemaName;
59592
+ return status;
59593
+ } catch {}
59594
+ }
59595
+ return {
59596
+ changeName: name,
59597
+ isComplete: false,
59598
+ applyRequires: [],
59599
+ artifacts: []
59600
+ };
59601
+ }
59602
+ async getInstructions(name, artifact) {
59603
+ const result2 = runOpenspec(["instructions", artifact, "--change", name, "--json"]);
59604
+ if (result2.stdout) {
59605
+ try {
59606
+ const parsed = JSON.parse(result2.stdout);
59607
+ const out = {
59608
+ changeName: parsed.changeName ?? name,
59609
+ artifactId: parsed.artifactId ?? artifact,
59610
+ instruction: parsed.instruction ?? ""
59611
+ };
59612
+ if (parsed.outputPath !== undefined)
59613
+ out.outputPath = parsed.outputPath;
59614
+ if (parsed.description !== undefined)
59615
+ out.description = parsed.description;
59616
+ if (parsed.template !== undefined)
59617
+ out.template = parsed.template;
59618
+ if (parsed.dependencies !== undefined)
59619
+ out.dependencies = parsed.dependencies;
59620
+ return out;
59621
+ } catch {}
59622
+ }
59623
+ return { changeName: name, artifactId: artifact, instruction: "" };
59624
+ }
59625
+ async showChange(name) {
59626
+ const result2 = runOpenspec(["show", name, "--json", "--type", "change"]);
59627
+ if (result2.stdout) {
59628
+ try {
59629
+ const parsed = JSON.parse(result2.stdout);
59630
+ const out = {
59631
+ id: parsed.id ?? name,
59632
+ deltaCount: parsed.deltaCount ?? 0,
59633
+ deltas: parsed.deltas ?? []
59634
+ };
59635
+ if (parsed.title !== undefined)
59636
+ out.title = parsed.title;
59637
+ return out;
59638
+ } catch {}
59639
+ }
59640
+ return { id: name, deltaCount: 0, deltas: [] };
59641
+ }
59595
59642
  async archiveChange(name) {
59596
- const result2 = runOpenspec(["archive", name, "-y", "--skip-specs"], { inherit: true });
59643
+ const result2 = runOpenspec(["archive", name, "-y"], { inherit: true });
59597
59644
  if (result2.status !== 0) {
59598
59645
  throw new Error("openspec archive failed");
59599
59646
  }
@@ -70726,6 +70773,14 @@ function useLoop(opts) {
70726
70773
  writeState(stateDir, currentState);
70727
70774
  setState(currentState);
70728
70775
  try {
70776
+ if (typeof opts.changeStore.getStatus === "function") {
70777
+ const status = await opts.changeStore.getStatus(opts.name);
70778
+ if (!status.isComplete) {
70779
+ const blocked = status.artifacts.filter((a) => a.status !== "done").map((a) => `${a.id}=${a.status}`).join(", ");
70780
+ addInfo(`Archive skipped: openspec status reports change incomplete (${blocked || "no artifacts"}).`);
70781
+ throw new Error("openspec status: change not complete");
70782
+ }
70783
+ }
70729
70784
  await opts.changeStore.archiveChange(opts.name);
70730
70785
  addInfo("Change archived.");
70731
70786
  } catch (err) {
@@ -92613,6 +92668,7 @@ var init_schema = __esm(() => {
92613
92668
  codeReviewTrigger: exports_external2.boolean().default(true),
92614
92669
  codeReviewStaleHours: exports_external2.number().nonnegative().default(24),
92615
92670
  syncTasksToComment: exports_external2.boolean().default(true),
92671
+ syncSpecsAsAttachments: exports_external2.boolean().default(true),
92616
92672
  indicators: IndicatorsSchema.default({})
92617
92673
  }).strict().default({
92618
92674
  postComments: true,
@@ -92622,6 +92678,7 @@ var init_schema = __esm(() => {
92622
92678
  codeReviewTrigger: true,
92623
92679
  codeReviewStaleHours: 24,
92624
92680
  syncTasksToComment: true,
92681
+ syncSpecsAsAttachments: true,
92625
92682
  indicators: {}
92626
92683
  }),
92627
92684
  github: exports_external2.object({
@@ -92776,6 +92833,11 @@ linear:
92776
92833
  # cadence as updateEveryIterations, and on done-transition.
92777
92834
  syncTasksToComment: true
92778
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
+
92779
92841
  # Indicators map Ralph lifecycle events to Linear labels/statuses.
92780
92842
  #
92781
92843
  # Filter semantics (per indicator's \`filter:\` list):
@@ -93763,10 +93825,45 @@ function buildIssueFilter(spec) {
93763
93825
  }
93764
93826
  return where;
93765
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
+ }
93766
93849
  async function fetchMentionScanIssues(apiKey, spec) {
93767
- const where = {
93768
- state: { type: { in: ["unstarted", "started", "backlog", "triage", "completed"] } }
93769
- };
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 };
93770
93867
  if (spec.team)
93771
93868
  where.team = { key: { eq: spec.team } };
93772
93869
  if (spec.assignee) {
@@ -93949,6 +94046,66 @@ async function linearRequest(apiKey, query, variables) {
93949
94046
  }
93950
94047
  throw lastHttpError ?? new Error("Linear API request failed");
93951
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
+ }
93952
94109
  async function addReactionToComment(apiKey, commentId, emoji3) {
93953
94110
  const mutation = `mutation Reaction($commentId: String!, $emoji: String!) {
93954
94111
  reactionCreate(input: { commentId: $commentId, emoji: $emoji }) { success }
@@ -94277,6 +94434,7 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
94277
94434
  }
94278
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:";
94279
94436
  var init_linear = __esm(() => {
94437
+ init_types2();
94280
94438
  linearRequestInternals = {
94281
94439
  sleep: (ms) => Bun.sleep(ms)
94282
94440
  };
@@ -94471,6 +94629,7 @@ class AgentCoordinator {
94471
94629
  this.spawnNext();
94472
94630
  const prStatus = await this.scanDoneForConflicts();
94473
94631
  await this.reportProgress();
94632
+ await this.syncWorkerTasks();
94474
94633
  const buckets = {
94475
94634
  todo: todo.length,
94476
94635
  inProgress: inProgress.length,
@@ -94525,12 +94684,26 @@ class AgentCoordinator {
94525
94684
  } catch (err) {
94526
94685
  this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
94527
94686
  }
94528
- if (this.deps.syncTasks) {
94529
- try {
94530
- await this.deps.syncTasks(w, count);
94531
- } catch (err) {
94532
- this.deps.onLog(`! sync-tasks (progress) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
94533
- }
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");
94534
94707
  }
94535
94708
  }
94536
94709
  }
@@ -94690,6 +94863,7 @@ class AgentCoordinator {
94690
94863
  mode,
94691
94864
  kill: handle.kill,
94692
94865
  lastReportedIteration: 0,
94866
+ lastSyncedIteration: 0,
94693
94867
  restarting: false
94694
94868
  };
94695
94869
  this.workers.push(worker);
@@ -94773,6 +94947,7 @@ class AgentCoordinator {
94773
94947
  mode,
94774
94948
  kill: () => {},
94775
94949
  lastReportedIteration: 0,
94950
+ lastSyncedIteration: 0,
94776
94951
  restarting: false
94777
94952
  };
94778
94953
  try {
@@ -94880,7 +95055,7 @@ var init_coordinator = __esm(() => {
94880
95055
  import { join as join18 } from "path";
94881
95056
  import { mkdir as mkdir5 } from "fs/promises";
94882
95057
  function changeNameForIssue(issue2) {
94883
- 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, "");
94884
95059
  return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
94885
95060
  }
94886
95061
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "") {
@@ -96235,24 +96410,28 @@ function truncate4(s, max2) {
96235
96410
  \u2026(truncated)`;
96236
96411
  }
96237
96412
  function renderTasksBlock(tasksMd, meta3) {
96238
- const sections = parseTasksMd(tasksMd);
96413
+ const sections = parseTasksMd(tasksMd).filter((s) => s.heading.trim().toLowerCase() !== "planning");
96239
96414
  const out = [];
96240
96415
  out.push(RALPHY_TASKS_START);
96241
96416
  out.push("### Ralph progress");
96242
96417
  out.push("");
96243
- for (const section of sections) {
96244
- if (section.items.length === 0)
96245
- continue;
96246
- out.push(`**${section.heading}**`);
96418
+ const renderable = sections.filter((s) => s.items.length > 0);
96419
+ if (renderable.length === 0) {
96420
+ out.push("_No mission tasks yet \u2014 planning in progress._");
96247
96421
  out.push("");
96248
- for (const item of section.items) {
96249
- out.push(item.bullet);
96250
- if (item.code !== undefined) {
96251
- const inner = truncate4(item.code, MAX_CODE_BLOCK_BYTES);
96252
- out.push(` <details><summary>output</summary><pre>${inner}</pre></details>`);
96422
+ } else {
96423
+ for (const section of renderable) {
96424
+ out.push(`**${section.heading}**`);
96425
+ out.push("");
96426
+ for (const item of section.items) {
96427
+ out.push(item.bullet);
96428
+ if (item.code !== undefined) {
96429
+ const inner = truncate4(item.code, MAX_CODE_BLOCK_BYTES);
96430
+ out.push(` <details><summary>output</summary><pre>${inner}</pre></details>`);
96431
+ }
96253
96432
  }
96433
+ out.push("");
96254
96434
  }
96255
- out.push("");
96256
96435
  }
96257
96436
  out.push(`<sub>\`${meta3.changeName}\` \xB7 iteration ${meta3.iteration}</sub>`);
96258
96437
  out.push(RALPHY_TASKS_END);
@@ -96485,9 +96664,139 @@ var init_comment_sync = __esm(() => {
96485
96664
  init_linear_sync();
96486
96665
  });
96487
96666
 
96488
- // apps/agent/src/agent/wire.ts
96489
- 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";
96490
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";
96491
96800
  async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog) {
96492
96801
  const candidates = urls.filter((url2) => GITHUB_PR_URL_RE.test(url2));
96493
96802
  let sawNonOpenPr = false;
@@ -96691,7 +97000,7 @@ function buildAgentCoordinator(input) {
96691
97000
  onWorkerOutput,
96692
97001
  onWorkerCmd
96693
97002
  } = input;
96694
- const logsDir = join22(projectRoot, ".ralph", "logs");
97003
+ const logsDir = join23(projectRoot, ".ralph", "logs");
96695
97004
  const concurrency = args.concurrency || cfg.concurrency;
96696
97005
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
96697
97006
  const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
@@ -96883,7 +97192,15 @@ function buildAgentCoordinator(input) {
96883
97192
  async function prepare(issue2, mode, trigger) {
96884
97193
  const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue2);
96885
97194
  let changeName;
96886
- 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;
96887
97204
  if (isFresh) {
96888
97205
  let comments = [];
96889
97206
  try {
@@ -96913,10 +97230,9 @@ function buildAgentCoordinator(input) {
96913
97230
  `);
96914
97231
  changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue2, comments, appendPrompt);
96915
97232
  } else {
96916
- changeName = changeNameForIssue(issue2);
96917
- const wtLayout = projectLayout(workerCwd);
96918
- await mkdir7(wtLayout.changeDir(changeName), { recursive: true });
96919
- 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 });
96920
97236
  }
96921
97237
  cwdByChange.set(changeName, workerCwd);
96922
97238
  statesDirByChange.set(changeName, scaffoldStatesDir);
@@ -96925,7 +97241,7 @@ function buildAgentCoordinator(input) {
96925
97241
  branchByChange.set(changeName, branch);
96926
97242
  if (mode === "review") {
96927
97243
  const wtLayout = projectLayout(workerCwd);
96928
- const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
97244
+ const tasksFile = join23(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
96929
97245
  let body;
96930
97246
  let heading;
96931
97247
  if (trigger) {
@@ -96950,7 +97266,7 @@ function buildAgentCoordinator(input) {
96950
97266
  await reactivateState2(wtLayout.stateFile(changeName), changeName);
96951
97267
  } else if (mode === "conflict-fix") {
96952
97268
  const wtLayout = projectLayout(workerCwd);
96953
- const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
97269
+ const tasksFile = join23(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
96954
97270
  const prUrl = prByChange.get(changeName);
96955
97271
  const body = [
96956
97272
  `The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
@@ -97030,7 +97346,7 @@ PR: ${prUrl}` : ""
97030
97346
  return c;
97031
97347
  }
97032
97348
  function defaultSpawn(changeName, cmd, cwd2, note) {
97033
- const logFilePath = join22(logsDir, `${changeName}.log`);
97349
+ const logFilePath = join23(logsDir, `${changeName}.log`);
97034
97350
  const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
97035
97351
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
97036
97352
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -97089,7 +97405,7 @@ PR: ${prUrl}` : ""
97089
97405
  function spawnWorker(changeName) {
97090
97406
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
97091
97407
  const injected = input.runners?.spawnWorker;
97092
- const missionTasksPath = join22(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
97408
+ const missionTasksPath = join23(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
97093
97409
  const prevTasksPromise = (async () => {
97094
97410
  const f2 = Bun.file(missionTasksPath);
97095
97411
  return await f2.exists() ? await f2.text() : "";
@@ -97097,7 +97413,7 @@ PR: ${prUrl}` : ""
97097
97413
  let logFilePath;
97098
97414
  let handle;
97099
97415
  if (injected) {
97100
- logFilePath = join22(logsDir, `${changeName}.log`);
97416
+ logFilePath = join23(logsDir, `${changeName}.log`);
97101
97417
  handle = injected(buildTaskCmdFor(changeName), cwd2);
97102
97418
  } else {
97103
97419
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
@@ -97313,7 +97629,15 @@ PR: ${prUrl}` : ""
97313
97629
  const handle = cfg.linear.mentionHandle;
97314
97630
  let candidates = [];
97315
97631
  try {
97316
- 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
+ });
97317
97641
  } catch (err) {
97318
97642
  if (isRateLimitedError(err)) {
97319
97643
  onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
@@ -97604,6 +97928,12 @@ PR: ${prUrl}` : ""
97604
97928
  updateIssueComment,
97605
97929
  deleteIssueComment
97606
97930
  };
97931
+ const specAttachmentsEnabled = Boolean(commentSyncEnabled && cfg.linear.syncSpecsAsAttachments);
97932
+ const specAttachmentMutations = {
97933
+ uploadFileToLinear,
97934
+ createAttachmentForUrl,
97935
+ updateAttachmentUrl
97936
+ };
97607
97937
  const coord = new AgentCoordinator({
97608
97938
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
97609
97939
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
@@ -97657,6 +97987,17 @@ PR: ${prUrl}` : ""
97657
97987
  log: onLog,
97658
97988
  mutations: commentMutations
97659
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
+ }
97660
98001
  },
97661
98002
  onSteeringAppended: async (changeName, message) => {
97662
98003
  const root = cwdByChange.get(changeName) ?? projectRoot;
@@ -97794,6 +98135,7 @@ var init_wire = __esm(() => {
97794
98135
  init_gate();
97795
98136
  init_workflow();
97796
98137
  init_comment_sync();
98138
+ init_spec_attachments();
97797
98139
  GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
97798
98140
  bunGitRunner = {
97799
98141
  run: async (args, cwd2) => {
@@ -98121,7 +98463,7 @@ var init_SteeringField = __esm(async () => {
98121
98463
  });
98122
98464
 
98123
98465
  // apps/agent/src/components/AgentMode.tsx
98124
- import { join as join23 } from "path";
98466
+ import { join as join24 } from "path";
98125
98467
  async function appendSteeringImpl(changeDir, message) {
98126
98468
  await runWithContext(createDefaultContext(), async () => {
98127
98469
  appendSteeringMessage(changeDir, message);
@@ -98576,7 +98918,7 @@ function AgentMode({
98576
98918
  (async () => {
98577
98919
  for (const [changeName, meta3] of workerMetaRef.current) {
98578
98920
  try {
98579
- const file2 = Bun.file(join23(meta3.statesDir, changeName, ".ralph-state.json"));
98921
+ const file2 = Bun.file(join24(meta3.statesDir, changeName, ".ralph-state.json"));
98580
98922
  if (await file2.exists()) {
98581
98923
  const json2 = await file2.json();
98582
98924
  meta3.iter = json2.iteration ?? meta3.iter;
@@ -98586,9 +98928,9 @@ function AgentMode({
98586
98928
  }
98587
98929
  if (meta3.changeDir) {
98588
98930
  try {
98589
- const tasksFile = Bun.file(join23(meta3.changeDir, "tasks.md"));
98590
- const proposalFile = Bun.file(join23(meta3.changeDir, "proposal.md"));
98591
- 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"));
98592
98934
  const [tasksText, proposalText, designText] = await Promise.all([
98593
98935
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
98594
98936
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -98636,7 +98978,7 @@ function AgentMode({
98636
98978
  use_input_default((input, key) => {
98637
98979
  if (steeringFocusedRef.current)
98638
98980
  return;
98639
- if (key.ctrl && key.meta && (input === "t" || input === "T")) {
98981
+ if (key.ctrl && (input === "l" || input === "L")) {
98640
98982
  if (activeCount > 0)
98641
98983
  setShowAllSubtasks((v) => !v);
98642
98984
  return;
@@ -99397,7 +99739,7 @@ function AgentMode({
99397
99739
  }),
99398
99740
  !showAllSubtasks && subtasks.length > MAX_PENDING_DISPLAY && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
99399
99741
  dimColor: true,
99400
- 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)`
99401
99743
  }, undefined, false, undefined, this)
99402
99744
  ]
99403
99745
  }, undefined, true, undefined, this),
@@ -99419,7 +99761,7 @@ function AgentMode({
99419
99761
  },
99420
99762
  onSubmit: async (message) => {
99421
99763
  try {
99422
- await appendSteering(join23(tasksDir, w.changeName), message);
99764
+ await appendSteering(join24(tasksDir, w.changeName), message);
99423
99765
  } catch (err) {
99424
99766
  appendLog(`! steering append failed for ${w.changeName}: ${err.message}`, "red");
99425
99767
  throw err;
@@ -99629,7 +99971,7 @@ var exports_list = {};
99629
99971
  __export(exports_list, {
99630
99972
  runList: () => runList
99631
99973
  });
99632
- import { join as join24 } from "path";
99974
+ import { join as join25 } from "path";
99633
99975
  function countTaskItems(content) {
99634
99976
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
99635
99977
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -99642,13 +99984,13 @@ function buildLocalRows(statesDir, projectRoot) {
99642
99984
  const sources = [{ dir: statesDir, label: "main" }];
99643
99985
  const worktreesRoot = worktreesDir2(projectRoot);
99644
99986
  for (const wt of storage.list(worktreesRoot)) {
99645
- sources.push({ dir: join24(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
99987
+ sources.push({ dir: join25(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
99646
99988
  }
99647
99989
  for (const { dir, label } of sources) {
99648
99990
  for (const entry of storage.list(dir)) {
99649
99991
  if (seen.has(entry))
99650
99992
  continue;
99651
- const raw = storage.read(join24(dir, entry, ".ralph-state.json"));
99993
+ const raw = storage.read(join25(dir, entry, ".ralph-state.json"));
99652
99994
  if (raw === null)
99653
99995
  continue;
99654
99996
  let state;
@@ -99663,7 +100005,7 @@ function buildLocalRows(statesDir, projectRoot) {
99663
100005
  const firstLine = promptRaw.split(`
99664
100006
  `).find((l) => l.trim() !== "") ?? "";
99665
100007
  let progress = "\u2014";
99666
- const tasksContent = storage.read(join24(dir, entry, "tasks.md"));
100008
+ const tasksContent = storage.read(join25(dir, entry, "tasks.md"));
99667
100009
  if (tasksContent !== null) {
99668
100010
  const { checked, unchecked } = countTaskItems(tasksContent);
99669
100011
  const total = checked + unchecked;
@@ -100090,8 +100432,8 @@ var exports_json_runner = {};
100090
100432
  __export(exports_json_runner, {
100091
100433
  runAgentJson: () => runAgentJson
100092
100434
  });
100093
- import { join as join25 } from "path";
100094
- import { mkdir as mkdir8 } from "fs/promises";
100435
+ import { join as join26 } from "path";
100436
+ import { mkdir as mkdir9 } from "fs/promises";
100095
100437
  import { homedir as homedir5 } from "os";
100096
100438
  function cleanOutputLine2(raw) {
100097
100439
  const clean = raw.replace(ANSI_STRIP_RE2, "").trim();
@@ -100115,7 +100457,7 @@ async function runAgentJson({
100115
100457
  statesDir,
100116
100458
  tasksDir
100117
100459
  }) {
100118
- await mkdir8(join25(homedir5(), ".ralph"), { recursive: true }).catch(() => {
100460
+ await mkdir9(join26(homedir5(), ".ralph"), { recursive: true }).catch(() => {
100119
100461
  return;
100120
100462
  });
100121
100463
  const cfgPath = await ensureRalphyConfig(projectRoot);
@@ -100269,8 +100611,8 @@ var exports_src2 = {};
100269
100611
  __export(exports_src2, {
100270
100612
  main: () => main2
100271
100613
  });
100272
- import { mkdir as mkdir9 } from "fs/promises";
100273
- import { join as join26 } from "path";
100614
+ import { mkdir as mkdir10 } from "fs/promises";
100615
+ import { join as join27 } from "path";
100274
100616
  async function main2(argv) {
100275
100617
  if (argv.includes("--help") || argv.includes("-h")) {
100276
100618
  printHelp2();
@@ -100304,9 +100646,9 @@ async function main2(argv) {
100304
100646
  });
100305
100647
  return typeof process.exitCode === "number" ? process.exitCode : 0;
100306
100648
  }
100307
- await mkdir9(statesDir, { recursive: true });
100308
- await mkdir9(tasksDir, { recursive: true });
100309
- 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 });
100310
100652
  if (args.jsonOutput) {
100311
100653
  const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
100312
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.3.2",
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",