@neriros/ralphy 3.1.0 → 3.2.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/README.md CHANGED
@@ -187,6 +187,7 @@ Example `ralphy.config.json`:
187
187
  "mentionHandle": "@ralphy",
188
188
  "codeReviewTrigger": true,
189
189
  "codeReviewStaleHours": 24,
190
+ "syncTasksToDescription": false,
190
191
  "indicators": {
191
192
  "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
192
193
  "getInProgress": {
@@ -219,7 +220,7 @@ When a Linear issue is in a done state and a reviewer adds the `getReview` marke
219
220
 
220
221
  #### `@ralphy` mention trigger
221
222
 
222
- Set `linear.mentionTrigger: true` to scan done-issue comments on Linear _and_ on the linked GitHub PR for a configurable handle (`linear.mentionHandle`, default `@ralphy`). Each unprocessed mention queues the issue as a review run, with the mention text used **verbatim** as the prepended task. Idempotency: a mention is processed when its `createdAt` is older than Ralph's latest `🔁 picked up` Linear comment, so the same comment never re-fires. Requires `gh` for the GitHub side.
223
+ Set `linear.mentionTrigger: true` to scan Linear issue comments on every non-cancelled issue (Todo, In Progress, Backlog, Triage, Done) _and_ on the linked GitHub PR for a configurable handle (`linear.mentionHandle`, default `@ralphy`). Each unprocessed mention queues the issue as a review run, with the mention text used **verbatim** as the prepended task. Idempotency: a mention is processed when its `createdAt` is older than Ralph's latest `🔁 picked up` Linear comment, so the same comment never re-fires. Requires `gh` for the GitHub side.
223
224
 
224
225
  #### Code-review iteration
225
226
 
@@ -230,6 +231,10 @@ Set `linear.codeReviewTrigger: true` (or pass `--code-review`) to watch open, un
230
231
 
231
232
  The loop exits; the next poll re-checks the PR. The cycle continues until the PR is **approved** or **merged**. If the reviewer is silent for more than `linear.codeReviewStaleHours` (default `24`, `0` disables) while Ralph is the last actor, one `@`-mention ping comment is posted on the GitHub PR.
232
233
 
234
+ #### Sync tasks into Linear description
235
+
236
+ Set `linear.syncTasksToDescription: true` to mirror the active change's `tasks.md` into the linked Linear issue description. Ralph writes a checklist between sentinel HTML comments (`<!-- ralphy:tasks:start -->` / `<!-- ralphy:tasks:end -->`); any content outside the markers is preserved verbatim. The block is refreshed when the worker launches, on the same cadence as `updateEveryIterations`, and on done-transition. Sync failures are logged but never abort the loop.
237
+
233
238
  #### Conflict re-fix
234
239
 
235
240
  Done issues whose PR `gh pr view --json mergeable` reports as `CONFLICTING` get `setConflicted` applied and a conflict-fix task prepended. The scanner is resilient to:
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
18928
18928
  import { resolve } from "path";
18929
18929
  function getVersion() {
18930
18930
  try {
18931
- if ("3.1.0")
18932
- return "3.1.0";
18931
+ if ("3.2.0")
18932
+ return "3.2.0";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -68864,7 +68864,7 @@ function StatusBar({
68864
68864
  return () => clearInterval(id);
68865
68865
  }, [isRunning, startedAt]);
68866
68866
  const { columns } = useTerminalSize();
68867
- const barWidth = Math.max(8, Math.min(52, columns));
68867
+ const barWidth = Math.max(8, columns);
68868
68868
  const bar = "\u2500".repeat(barWidth);
68869
68869
  return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
68870
68870
  flexDirection: "column",
@@ -70223,6 +70223,37 @@ ${failureOutput.trim()}
70223
70223
  ${fence}`;
70224
70224
  await Bun.write(tasksPath, prependSection(existing, stamped, body));
70225
70225
  }
70226
+ function normalizeNewlyAppendedSectionWithReport(previous, current) {
70227
+ const prevHeadings = new Set;
70228
+ for (const line of previous.split(`
70229
+ `)) {
70230
+ if (line.startsWith("## "))
70231
+ prevHeadings.add(line);
70232
+ }
70233
+ const sections = current.split(/(?=^## )/m);
70234
+ const headings = [];
70235
+ let count = 0;
70236
+ const out = sections.map((section) => {
70237
+ const nlIdx = section.indexOf(`
70238
+ `);
70239
+ const headingLine = nlIdx === -1 ? section.replace(/\n$/, "") : section.slice(0, nlIdx);
70240
+ if (!headingLine.startsWith("## "))
70241
+ return section;
70242
+ if (prevHeadings.has(headingLine))
70243
+ return section;
70244
+ let localCount = 0;
70245
+ const rewritten = section.replace(/^(\s*)- \[[xX]\] (.+)$/gm, (_m, indent, rest2) => {
70246
+ localCount += 1;
70247
+ return `${indent}- [ ] ${rest2}`;
70248
+ });
70249
+ if (localCount > 0) {
70250
+ headings.push(headingLine.slice(3));
70251
+ count += localCount;
70252
+ }
70253
+ return rewritten;
70254
+ });
70255
+ return { text: count > 0 ? out.join("") : current, headings, count };
70256
+ }
70226
70257
  var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", FLOW_TASK_HEADING_PREFIXES;
70227
70258
  var init_tasks_md = __esm(() => {
70228
70259
  FLOW_TASK_HEADING_PREFIXES = [
@@ -92474,6 +92505,7 @@ var init_schema = __esm(() => {
92474
92505
  mentionHandle: exports_external2.string().default("@ralphy"),
92475
92506
  codeReviewTrigger: exports_external2.boolean().default(false),
92476
92507
  codeReviewStaleHours: exports_external2.number().nonnegative().default(24),
92508
+ syncTasksToDescription: exports_external2.boolean().default(false),
92477
92509
  indicators: IndicatorsSchema.default({})
92478
92510
  }).strict().default({
92479
92511
  postComments: true,
@@ -92482,6 +92514,7 @@ var init_schema = __esm(() => {
92482
92514
  mentionHandle: "@ralphy",
92483
92515
  codeReviewTrigger: false,
92484
92516
  codeReviewStaleHours: 24,
92517
+ syncTasksToDescription: false,
92485
92518
  indicators: {}
92486
92519
  }),
92487
92520
  github: exports_external2.object({
@@ -92630,6 +92663,11 @@ linear:
92630
92663
  codeReviewTrigger: false
92631
92664
  codeReviewStaleHours: 24
92632
92665
 
92666
+ # Mirror the loop's tasks.md into the Linear issue description as a
92667
+ # checklist between sentinel markers. Updates on worker launch, on the
92668
+ # same cadence as updateEveryIterations, and on done-transition.
92669
+ syncTasksToDescription: false
92670
+
92633
92671
  # Indicators map Ralph lifecycle events to Linear labels/statuses.
92634
92672
  # Grouped by lifecycle: each get* is followed by the set*/clear* that
92635
92673
  # mutates the same state, so the lifecycle reads top-to-bottom.
@@ -93574,6 +93612,51 @@ function buildIssueFilter(spec) {
93574
93612
  }
93575
93613
  return where;
93576
93614
  }
93615
+ async function fetchMentionScanIssues(apiKey, spec) {
93616
+ const where = {
93617
+ state: { type: { in: ["unstarted", "started", "backlog", "triage", "completed"] } }
93618
+ };
93619
+ if (spec.team)
93620
+ where.team = { key: { eq: spec.team } };
93621
+ if (spec.assignee) {
93622
+ if (spec.assignee === "me")
93623
+ where.assignee = { isMe: { eq: true } };
93624
+ else if (spec.assignee.includes("@"))
93625
+ where.assignee = { email: { eq: spec.assignee } };
93626
+ else
93627
+ where.assignee = { id: { eq: spec.assignee } };
93628
+ }
93629
+ const query = `query MentionScanIssues($filter: IssueFilter) {
93630
+ issues(filter: $filter, first: 50) {
93631
+ nodes {
93632
+ id identifier title description url priority createdAt
93633
+ state { name type }
93634
+ assignee { id email name }
93635
+ labels { nodes { name } }
93636
+ relations(first: 50) {
93637
+ nodes { type relatedIssue { id state { type } } }
93638
+ }
93639
+ }
93640
+ }
93641
+ }`;
93642
+ const data = await linearRequest(apiKey, query, {
93643
+ filter: where
93644
+ });
93645
+ const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
93646
+ return data.issues.nodes.map((n) => ({
93647
+ id: n.id,
93648
+ identifier: n.identifier,
93649
+ title: n.title,
93650
+ description: n.description,
93651
+ url: n.url,
93652
+ state: n.state,
93653
+ assignee: n.assignee,
93654
+ labels: n.labels.nodes.map((l) => l.name),
93655
+ priority: n.priority,
93656
+ createdAt: n.createdAt ?? "",
93657
+ blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
93658
+ }));
93659
+ }
93577
93660
  async function fetchOpenIssues(apiKey, spec) {
93578
93661
  const where = buildIssueFilter(spec);
93579
93662
  const query = `query Issues($filter: IssueFilter) {
@@ -94117,6 +94200,13 @@ class AgentCoordinator {
94117
94200
  } catch (err) {
94118
94201
  this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
94119
94202
  }
94203
+ if (this.deps.syncTasks) {
94204
+ try {
94205
+ await this.deps.syncTasks(w, count);
94206
+ } catch (err) {
94207
+ this.deps.onLog(`! sync-tasks (progress) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
94208
+ }
94209
+ }
94120
94210
  }
94121
94211
  }
94122
94212
  async scanDoneForConflicts() {
@@ -94289,6 +94379,13 @@ class AgentCoordinator {
94289
94379
  issue_identifier: issue2.identifier
94290
94380
  });
94291
94381
  this.deps.onWorkersChanged();
94382
+ if (this.deps.syncTasks) {
94383
+ try {
94384
+ await this.deps.syncTasks(worker, 0);
94385
+ } catch (err) {
94386
+ this.deps.onLog(`! sync-tasks (launch) failed for ${issue2.identifier}: ${err.message}`, "yellow");
94387
+ }
94388
+ }
94292
94389
  handle.exited.then(async (code) => {
94293
94390
  const idx = this.workers.indexOf(worker);
94294
94391
  if (idx >= 0)
@@ -94333,6 +94430,31 @@ class AgentCoordinator {
94333
94430
  }
94334
94431
  async notifyExited(issue2, changeName, code, mode) {
94335
94432
  const ok = code === 0;
94433
+ if (this.deps.syncTasks && ok) {
94434
+ const synthetic = {
94435
+ changeName,
94436
+ issueId: issue2.id,
94437
+ issueIdentifier: issue2.identifier,
94438
+ issue: issue2,
94439
+ mode,
94440
+ kill: () => {},
94441
+ lastReportedIteration: 0,
94442
+ restarting: false
94443
+ };
94444
+ try {
94445
+ let iteration = 0;
94446
+ if (this.deps.getIterationCount) {
94447
+ try {
94448
+ iteration = await this.deps.getIterationCount(changeName);
94449
+ } catch {
94450
+ iteration = 0;
94451
+ }
94452
+ }
94453
+ await this.deps.syncTasks(synthetic, iteration);
94454
+ } catch (err) {
94455
+ this.deps.onLog(`! sync-tasks (done) failed for ${issue2.identifier}: ${err.message}`, "yellow");
94456
+ }
94457
+ }
94336
94458
  if (this.opts.postComments !== false) {
94337
94459
  const body = ok ? mode === "conflict-fix" ? `\u2705 Ralph resolved merge conflicts on this issue. Change: \`${changeName}\`` : `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
94338
94460
 
@@ -94483,7 +94605,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = []
94483
94605
  `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
94484
94606
  `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
94485
94607
  `- [ ] Fill in design.md with the technical design (files to touch, data flow, edge cases)`,
94486
- `- [ ] Append an \`## Implementation\` section below with concrete mission-specific tasks derived from the plan (one \`- [ ] task\` per discrete unit of work, including tests and \`bun run lint\` / \`bun run test\`)`,
94608
+ `- [ ] Append an \`## Implementation\` section below with concrete mission-specific tasks derived from the plan, including tests and \`bun run lint\` / \`bun run test\`. Every item in the new section MUST start as \`- [ ]\` (unchecked) \u2014 do not pre-check items even if you already did the work during planning. The loop ticks them off in later iterations after each one is verified.`,
94487
94609
  ""
94488
94610
  ].join(`
94489
94611
  `);
@@ -95517,6 +95639,143 @@ var init_gate = __esm(() => {
95517
95639
  FINGERPRINT_MARKER_RE = /<!--\s*ralphy:baseline:([a-f0-9]+)\s*-->/i;
95518
95640
  });
95519
95641
 
95642
+ // apps/agent/src/agent/linear-sync/index.ts
95643
+ function parseTasksMd(md) {
95644
+ const lines = md.split(/\r?\n/);
95645
+ const sections = [];
95646
+ let current = null;
95647
+ let i = 0;
95648
+ while (i < lines.length) {
95649
+ const line = lines[i];
95650
+ const headingMatch = /^##\s+(.+?)\s*$/.exec(line);
95651
+ if (headingMatch) {
95652
+ current = { heading: headingMatch[1], items: [] };
95653
+ sections.push(current);
95654
+ i += 1;
95655
+ continue;
95656
+ }
95657
+ const bulletMatch = /^(\s*)-\s+\[( |x|X)\]\s+(.+?)\s*$/.exec(line);
95658
+ if (bulletMatch && current) {
95659
+ const indent = bulletMatch[1] ?? "";
95660
+ const checked = bulletMatch[2]?.toLowerCase() === "x";
95661
+ const text = bulletMatch[3] ?? "";
95662
+ const bullet = `${indent}- [${checked ? "x" : " "}] ${text}`;
95663
+ i += 1;
95664
+ let j = i;
95665
+ while (j < lines.length && lines[j].trim() === "")
95666
+ j += 1;
95667
+ let code;
95668
+ if (j < lines.length && /^\s*```/.test(lines[j])) {
95669
+ const fenceOpen = lines[j];
95670
+ const fenceMatch = /^(\s*)```/.exec(fenceOpen);
95671
+ const fenceIndent = fenceMatch?.[1] ?? "";
95672
+ const buf = [];
95673
+ j += 1;
95674
+ while (j < lines.length) {
95675
+ if (new RegExp(`^${fenceIndent}\`\`\`\\s*$`).test(lines[j])) {
95676
+ j += 1;
95677
+ break;
95678
+ }
95679
+ buf.push(lines[j]);
95680
+ j += 1;
95681
+ }
95682
+ code = buf.join(`
95683
+ `);
95684
+ i = j;
95685
+ }
95686
+ current.items.push(code !== undefined ? { bullet, code } : { bullet });
95687
+ continue;
95688
+ }
95689
+ i += 1;
95690
+ }
95691
+ return sections;
95692
+ }
95693
+ function truncate4(s, max2) {
95694
+ if (s.length <= max2)
95695
+ return s;
95696
+ return `${s.slice(0, max2)}
95697
+ \u2026(truncated)`;
95698
+ }
95699
+ function renderTasksBlock(tasksMd, meta3) {
95700
+ const sections = parseTasksMd(tasksMd);
95701
+ const out = [];
95702
+ out.push(RALPHY_TASKS_START);
95703
+ out.push("### Ralph progress");
95704
+ out.push("");
95705
+ for (const section of sections) {
95706
+ if (section.items.length === 0)
95707
+ continue;
95708
+ out.push(`**${section.heading}**`);
95709
+ out.push("");
95710
+ for (const item of section.items) {
95711
+ out.push(item.bullet);
95712
+ if (item.code !== undefined) {
95713
+ const inner = truncate4(item.code, MAX_CODE_BLOCK_BYTES);
95714
+ out.push(` <details><summary>output</summary><pre>${inner}</pre></details>`);
95715
+ }
95716
+ }
95717
+ out.push("");
95718
+ }
95719
+ out.push(`<sub>\`${meta3.changeName}\` \xB7 iteration ${meta3.iteration}</sub>`);
95720
+ out.push(RALPHY_TASKS_END);
95721
+ return out.join(`
95722
+ `);
95723
+ }
95724
+ function applyTasksBlock(existingDescription, block) {
95725
+ const existing = existingDescription ?? "";
95726
+ const startIdx = existing.indexOf(RALPHY_TASKS_START);
95727
+ const endIdx = startIdx >= 0 ? existing.indexOf(RALPHY_TASKS_END, startIdx + RALPHY_TASKS_START.length) : -1;
95728
+ if (startIdx >= 0 && endIdx >= 0) {
95729
+ const before2 = existing.slice(0, startIdx);
95730
+ const after2 = existing.slice(endIdx + RALPHY_TASKS_END.length);
95731
+ return `${before2}${block}${after2}`;
95732
+ }
95733
+ if (existing.length === 0)
95734
+ return block;
95735
+ const trimmed = existing.replace(/\s+$/, "");
95736
+ return `${trimmed}
95737
+
95738
+ ${block}`;
95739
+ }
95740
+ async function syncTasksToLinearDescription(deps) {
95741
+ const file2 = Bun.file(deps.tasksPath);
95742
+ if (!await file2.exists()) {
95743
+ deps.log(` sync-tasks: tasks.md missing at ${deps.tasksPath}, skipping`, "gray");
95744
+ return null;
95745
+ }
95746
+ let tasksMd;
95747
+ try {
95748
+ tasksMd = await file2.text();
95749
+ } catch (err) {
95750
+ deps.log(`! sync-tasks: read failed for ${deps.tasksPath}: ${err.message}`, "yellow");
95751
+ return null;
95752
+ }
95753
+ const block = renderTasksBlock(tasksMd, {
95754
+ changeName: deps.changeName,
95755
+ iteration: deps.iteration
95756
+ });
95757
+ if (block.length > MAX_BLOCK_BYTES) {
95758
+ deps.log(`! sync-tasks: rendered block exceeds ${MAX_BLOCK_BYTES} bytes (${block.length}), skipping update`, "yellow");
95759
+ return null;
95760
+ }
95761
+ const next = applyTasksBlock(deps.currentDescription, block);
95762
+ if (next === (deps.currentDescription ?? ""))
95763
+ return null;
95764
+ try {
95765
+ await deps.updateIssueDescription(deps.apiKey, deps.issueId, next);
95766
+ deps.log(` sync-tasks: updated Linear description for ${deps.changeName}`, "gray");
95767
+ return next;
95768
+ } catch (err) {
95769
+ deps.log(`! sync-tasks: updateIssueDescription failed: ${err.message}`, "yellow");
95770
+ return null;
95771
+ }
95772
+ }
95773
+ var RALPHY_TASKS_START = "<!-- ralphy:tasks:start -->", RALPHY_TASKS_END = "<!-- ralphy:tasks:end -->", MAX_BLOCK_BYTES, MAX_CODE_BLOCK_BYTES;
95774
+ var init_linear_sync = __esm(() => {
95775
+ MAX_BLOCK_BYTES = 60 * 1024;
95776
+ MAX_CODE_BLOCK_BYTES = 2 * 1024;
95777
+ });
95778
+
95520
95779
  // apps/agent/src/agent/wire.ts
95521
95780
  import { join as join21 } from "path";
95522
95781
  import { mkdir as mkdir6 } from "fs/promises";
@@ -96069,6 +96328,11 @@ PR: ${prUrl}` : ""
96069
96328
  function spawnWorker(changeName) {
96070
96329
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
96071
96330
  const injected = input.runners?.spawnWorker;
96331
+ const missionTasksPath = join21(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
96332
+ const prevTasksPromise = (async () => {
96333
+ const f2 = Bun.file(missionTasksPath);
96334
+ return await f2.exists() ? await f2.text() : "";
96335
+ })();
96072
96336
  let logFilePath;
96073
96337
  let handle;
96074
96338
  if (injected) {
@@ -96094,6 +96358,21 @@ PR: ${prUrl}` : ""
96094
96358
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
96095
96359
  const wrapped = handle.exited.then(async (code) => {
96096
96360
  const workerLayout = projectLayout(cwd2);
96361
+ try {
96362
+ const prevTasks = await prevTasksPromise;
96363
+ const nextFile = Bun.file(missionTasksPath);
96364
+ if (await nextFile.exists()) {
96365
+ const nextTasks = await nextFile.text();
96366
+ const report = normalizeNewlyAppendedSectionWithReport(prevTasks, nextTasks);
96367
+ if (report.text !== nextTasks) {
96368
+ await Bun.write(missionTasksPath, report.text);
96369
+ const sections = report.headings.map((h) => `## ${h}`).join(", ");
96370
+ onLog(`! normalized ${report.count} pre-checked item(s) in newly added section(s) ${sections}`, "yellow");
96371
+ }
96372
+ }
96373
+ } catch (err) {
96374
+ onLog(`! tasks.md normalization failed: ${err.message}`, "yellow");
96375
+ }
96097
96376
  const effectiveCode = await runPostTask({
96098
96377
  changeName,
96099
96378
  cwd: cwd2,
@@ -96327,9 +96606,9 @@ PR: ${prUrl}` : ""
96327
96606
  const handle = cfg.linear.mentionHandle;
96328
96607
  let candidates = [];
96329
96608
  try {
96330
- candidates = await fetchDoneCandidates();
96609
+ candidates = await fetchMentionScanIssues(apiKey, { team, assignee });
96331
96610
  } catch (err) {
96332
- onLog(`! mention scan: fetchDoneCandidates failed: ${err.message}`, "yellow");
96611
+ onLog(`! mention scan: fetchMentionScanIssues failed: ${err.message}`, "yellow");
96333
96612
  return [];
96334
96613
  }
96335
96614
  const out = [];
@@ -96620,7 +96899,29 @@ PR: ${prUrl}` : ""
96620
96899
  return 0;
96621
96900
  const json2 = await file2.json();
96622
96901
  return json2.iteration ?? 0;
96623
- }
96902
+ },
96903
+ ...cfg.linear.syncTasksToDescription && apiKey ? {
96904
+ syncTasks: async (worker, iteration) => {
96905
+ const root = cwdByChange.get(worker.changeName) ?? projectRoot;
96906
+ const tasksPath = join21(projectLayout(root).changeDir(worker.changeName), "tasks.md");
96907
+ const cachedIssue = issueByChange.get(worker.changeName) ?? worker.issue;
96908
+ const next = await syncTasksToLinearDescription({
96909
+ apiKey,
96910
+ issueId: worker.issueId,
96911
+ currentDescription: cachedIssue.description,
96912
+ tasksPath,
96913
+ changeName: worker.changeName,
96914
+ iteration,
96915
+ log: onLog,
96916
+ updateIssueDescription
96917
+ });
96918
+ if (next !== null) {
96919
+ const updated = { ...cachedIssue, description: next };
96920
+ issueByChange.set(worker.changeName, updated);
96921
+ worker.issue = updated;
96922
+ }
96923
+ }
96924
+ } : {}
96624
96925
  }, {
96625
96926
  concurrency,
96626
96927
  ...indicators.setInProgress !== undefined ? { setInProgress: indicators.setInProgress } : {},
@@ -96687,6 +96988,7 @@ PR: ${prUrl}` : ""
96687
96988
  concurrency,
96688
96989
  pollInterval,
96689
96990
  getWorkerCwd: (changeName) => cwdByChange.get(changeName),
96991
+ syncTasksEnabled: Boolean(cfg.linear.syncTasksToDescription && apiKey),
96690
96992
  runBaselineGate: runBaselineGateOnce
96691
96993
  };
96692
96994
  }
@@ -96722,6 +97024,7 @@ var init_wire = __esm(() => {
96722
97024
  init_post_task();
96723
97025
  init_gate();
96724
97026
  init_workflow();
97027
+ init_linear_sync();
96725
97028
  GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
96726
97029
  bunGitRunner = {
96727
97030
  run: async (args, cwd2) => {
@@ -97313,6 +97616,7 @@ function AgentMode({
97313
97616
  const workerMetaRef = import_react61.useRef(new Map);
97314
97617
  const nextPollAtRef = import_react61.useRef(0);
97315
97618
  const cfgRef = import_react61.useRef(null);
97619
+ const [effective, setEffective] = import_react61.useState(null);
97316
97620
  const [pollStatus, setPollStatus] = import_react61.useState({
97317
97621
  state: "idle",
97318
97622
  lastFound: null,
@@ -97411,6 +97715,7 @@ function AgentMode({
97411
97715
  m.prUrl = prUrl;
97412
97716
  }
97413
97717
  });
97718
+ setEffective({ concurrency, pollInterval });
97414
97719
  coordRef.current = coord2;
97415
97720
  await coord2.init();
97416
97721
  const tick = async () => {
@@ -97670,14 +97975,14 @@ function AgentMode({
97670
97975
  dimColor: true,
97671
97976
  children: [
97672
97977
  " \u2502 \xD7",
97673
- cfg.concurrency
97978
+ effective?.concurrency ?? cfg.concurrency
97674
97979
  ]
97675
97980
  }, undefined, true, undefined, this),
97676
97981
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97677
97982
  dimColor: true,
97678
97983
  children: [
97679
97984
  " \u2502 poll ",
97680
- cfg.pollIntervalSeconds,
97985
+ effective?.pollInterval ?? cfg.pollIntervalSeconds,
97681
97986
  "s"
97682
97987
  ]
97683
97988
  }, undefined, true, undefined, this),
@@ -97794,18 +98099,6 @@ function AgentMode({
97794
98099
  dimColor: true,
97795
98100
  children: "\xB7"
97796
98101
  }, undefined, false, undefined, this),
97797
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97798
- dimColor: true,
97799
- children: "conflict"
97800
- }, undefined, false, undefined, this),
97801
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97802
- color: pollStatus.lastBuckets.conflicted > 0 ? "red" : "white",
97803
- children: pollStatus.lastBuckets.conflicted
97804
- }, undefined, false, undefined, this),
97805
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97806
- dimColor: true,
97807
- children: "\xB7"
97808
- }, undefined, false, undefined, this),
97809
98102
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97810
98103
  dimColor: true,
97811
98104
  children: "review"
@@ -97836,6 +98129,7 @@ function AgentMode({
97836
98129
  children: [
97837
98130
  secsToNextPoll !== null ? /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
97838
98131
  gap: 1,
98132
+ width: 7,
97839
98133
  children: [
97840
98134
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97841
98135
  dimColor: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "3.1.0",
3
+ "version": "3.2.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",