@neriros/ralphy 3.1.0 → 3.3.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.
@@ -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.3.0")
18932
+ return "3.3.0";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -59219,11 +59219,10 @@ var init_use_app = __esm(() => {
59219
59219
  });
59220
59220
 
59221
59221
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/hooks/use-stdout.js
59222
- var import_react18, useStdout = () => import_react18.useContext(StdoutContext_default), use_stdout_default;
59222
+ var import_react18;
59223
59223
  var init_use_stdout = __esm(() => {
59224
59224
  init_StdoutContext();
59225
59225
  import_react18 = __toESM(require_react(), 1);
59226
- use_stdout_default = useStdout;
59227
59226
  });
59228
59227
 
59229
59228
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/hooks/use-stderr.js
@@ -59448,6 +59447,50 @@ function runOpenspec(args, options = {}) {
59448
59447
  stderr: proc.stderr ? decoder.decode(proc.stderr) : ""
59449
59448
  };
59450
59449
  }
59450
+ function appendSteeringTaskToTasksMd(existing, taskLine) {
59451
+ const SECTION = "## Steering";
59452
+ const trimmed = existing.replace(/\s+$/, "");
59453
+ if (trimmed.length === 0) {
59454
+ return `${SECTION}
59455
+
59456
+ ${taskLine}
59457
+ `;
59458
+ }
59459
+ const lines = trimmed.split(/\r?\n/);
59460
+ let sectionStart = -1;
59461
+ for (let i = 0;i < lines.length; i += 1) {
59462
+ if (/^##\s+Steering\s*$/i.test(lines[i])) {
59463
+ sectionStart = i;
59464
+ break;
59465
+ }
59466
+ }
59467
+ if (sectionStart === -1) {
59468
+ return `${trimmed}
59469
+
59470
+ ${SECTION}
59471
+
59472
+ ${taskLine}
59473
+ `;
59474
+ }
59475
+ let sectionEnd = lines.length;
59476
+ for (let i = sectionStart + 1;i < lines.length; i += 1) {
59477
+ if (/^##\s+/.test(lines[i])) {
59478
+ sectionEnd = i;
59479
+ break;
59480
+ }
59481
+ }
59482
+ let insertAt = sectionEnd;
59483
+ while (insertAt - 1 > sectionStart && (lines[insertAt - 1] ?? "").trim() === "") {
59484
+ insertAt -= 1;
59485
+ }
59486
+ const before2 = lines.slice(0, insertAt);
59487
+ const after2 = lines.slice(insertAt);
59488
+ const out = [...before2, taskLine, ...after2.length ? [""] : [], ...after2].join(`
59489
+ `);
59490
+ return out.endsWith(`
59491
+ `) ? out : `${out}
59492
+ `;
59493
+ }
59451
59494
 
59452
59495
  class OpenSpecChangeStore {
59453
59496
  async createChange(name, description) {
@@ -59504,6 +59547,16 @@ ${existing.trimStart()}` : `${message}
59504
59547
  `;
59505
59548
  await mkdir(dirname4(path), { recursive: true });
59506
59549
  await Bun.write(path, updated);
59550
+ const firstLine = message.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? message.trim();
59551
+ if (firstLine.length === 0)
59552
+ return;
59553
+ const tasksPath = join6("openspec", "changes", name, "tasks.md");
59554
+ const tasksFile = Bun.file(tasksPath);
59555
+ const existingTasks = await tasksFile.exists() ? await tasksFile.text() : "";
59556
+ const taskLine = `- [ ] Address steering: ${firstLine}`;
59557
+ const next = appendSteeringTaskToTasksMd(existingTasks, taskLine);
59558
+ await mkdir(dirname4(tasksPath), { recursive: true });
59559
+ await Bun.write(tasksPath, next);
59507
59560
  }
59508
59561
  async readSection(name, artifact, heading) {
59509
59562
  const file = Bun.file(join6("openspec", "changes", name, artifact));
@@ -63850,7 +63903,12 @@ var init_types2 = __esm(() => {
63850
63903
  createPr: exports_external.boolean().default(false),
63851
63904
  usage: UsageSchema.default({}),
63852
63905
  history: exports_external.array(HistoryEntrySchema).default([]),
63853
- metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
63906
+ metadata: exports_external.object({ branch: exports_external.string().optional() }).default({}),
63907
+ linearComments: exports_external.object({
63908
+ planCommentId: exports_external.string().nullable().default(null),
63909
+ tasksCommentId: exports_external.string().nullable().default(null),
63910
+ planPostedAt: exports_external.string().nullable().default(null)
63911
+ }).default({ planCommentId: null, tasksCommentId: null, planPostedAt: null })
63854
63912
  });
63855
63913
  PhaseFrontmatterSchema = exports_external.object({
63856
63914
  name: exports_external.string(),
@@ -68802,21 +68860,26 @@ function readSize() {
68802
68860
  rows: process.stdout.rows ?? 24
68803
68861
  };
68804
68862
  }
68863
+ function clearScreenAndScrollback() {
68864
+ if (process.stdout.isTTY)
68865
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
68866
+ }
68805
68867
  function useTerminalSize() {
68806
- const [size2, setSize] = import_react53.useState(() => ({
68807
- ...readSize(),
68808
- resizeKey: 0
68809
- }));
68868
+ const initial2 = import_react53.useRef({ ...readSize(), resizeKey: 0 });
68869
+ const [size2, setSize] = import_react53.useState(initial2.current);
68870
+ const sizeRef = import_react53.useRef(initial2.current);
68810
68871
  import_react53.useEffect(() => {
68811
68872
  if (!process.stdout.isTTY)
68812
68873
  return;
68813
68874
  const onResize = () => {
68814
68875
  const { columns, rows } = readSize();
68815
- setSize((prev) => {
68816
- if (prev.columns === columns && prev.rows === rows)
68817
- return prev;
68818
- return { columns, rows, resizeKey: prev.resizeKey + 1 };
68819
- });
68876
+ const prev = sizeRef.current;
68877
+ if (prev.columns === columns && prev.rows === rows)
68878
+ return;
68879
+ clearScreenAndScrollback();
68880
+ const next = { columns, rows, resizeKey: prev.resizeKey + 1 };
68881
+ sizeRef.current = next;
68882
+ setSize(next);
68820
68883
  };
68821
68884
  process.stdout.on("resize", onResize);
68822
68885
  return () => {
@@ -68864,7 +68927,7 @@ function StatusBar({
68864
68927
  return () => clearInterval(id);
68865
68928
  }, [isRunning, startedAt]);
68866
68929
  const { columns } = useTerminalSize();
68867
- const barWidth = Math.max(8, Math.min(52, columns));
68930
+ const barWidth = Math.max(8, columns);
68868
68931
  const bar = "\u2500".repeat(barWidth);
68869
68932
  return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
68870
68933
  flexDirection: "column",
@@ -70223,6 +70286,37 @@ ${failureOutput.trim()}
70223
70286
  ${fence}`;
70224
70287
  await Bun.write(tasksPath, prependSection(existing, stamped, body));
70225
70288
  }
70289
+ function normalizeNewlyAppendedSectionWithReport(previous, current) {
70290
+ const prevHeadings = new Set;
70291
+ for (const line of previous.split(`
70292
+ `)) {
70293
+ if (line.startsWith("## "))
70294
+ prevHeadings.add(line);
70295
+ }
70296
+ const sections = current.split(/(?=^## )/m);
70297
+ const headings = [];
70298
+ let count = 0;
70299
+ const out = sections.map((section) => {
70300
+ const nlIdx = section.indexOf(`
70301
+ `);
70302
+ const headingLine = nlIdx === -1 ? section.replace(/\n$/, "") : section.slice(0, nlIdx);
70303
+ if (!headingLine.startsWith("## "))
70304
+ return section;
70305
+ if (prevHeadings.has(headingLine))
70306
+ return section;
70307
+ let localCount = 0;
70308
+ const rewritten = section.replace(/^(\s*)- \[[xX]\] (.+)$/gm, (_m, indent, rest2) => {
70309
+ localCount += 1;
70310
+ return `${indent}- [ ] ${rest2}`;
70311
+ });
70312
+ if (localCount > 0) {
70313
+ headings.push(headingLine.slice(3));
70314
+ count += localCount;
70315
+ }
70316
+ return rewritten;
70317
+ });
70318
+ return { text: count > 0 ? out.join("") : current, headings, count };
70319
+ }
70226
70320
  var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", FLOW_TASK_HEADING_PREFIXES;
70227
70321
  var init_tasks_md = __esm(() => {
70228
70322
  FLOW_TASK_HEADING_PREFIXES = [
@@ -70832,14 +70926,8 @@ function TaskLoop({ opts }) {
70832
70926
  const { exit } = use_app_default();
70833
70927
  const loop = useLoop(opts);
70834
70928
  const { isRawModeSupported } = use_stdin_default();
70835
- const { stdout } = use_stdout_default();
70836
70929
  const { resizeKey } = useTerminalSize();
70837
70930
  const bannerItem = import_react56.useRef({ id: "__banner__", kind: "banner" });
70838
- import_react56.useEffect(() => {
70839
- if (resizeKey === 0)
70840
- return;
70841
- stdout.write("\x1B[2J\x1B[3J\x1B[H");
70842
- }, [resizeKey, stdout]);
70843
70931
  const feedItems = import_react56.useMemo(() => [
70844
70932
  bannerItem.current,
70845
70933
  ...loop.logLines.map((e) => ({ id: e.id, kind: "entry", entry: e }))
@@ -92379,7 +92467,7 @@ var init_zod2 = __esm(() => {
92379
92467
  });
92380
92468
 
92381
92469
  // packages/workflow/src/schema.ts
92382
- var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, BoundariesSchema, WorkflowConfigSchema;
92470
+ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
92383
92471
  var init_schema = __esm(() => {
92384
92472
  init_zod2();
92385
92473
  MarkerSchema = exports_external2.object({
@@ -92390,7 +92478,7 @@ var init_schema = __esm(() => {
92390
92478
  filter: exports_external2.array(MarkerSchema).default([])
92391
92479
  });
92392
92480
  SetIndicatorSchema = exports_external2.union([exports_external2.array(MarkerSchema).min(1), MarkerSchema]);
92393
- IndicatorsSchema = exports_external2.object({
92481
+ IndicatorsSchema = exports_external2.preprocess((v) => v == null ? {} : v, exports_external2.object({
92394
92482
  getTodo: GetIndicatorSchema.optional(),
92395
92483
  getInProgress: GetIndicatorSchema.optional(),
92396
92484
  getConflicted: GetIndicatorSchema.optional(),
@@ -92419,7 +92507,7 @@ var init_schema = __esm(() => {
92419
92507
  }
92420
92508
  }
92421
92509
  }
92422
- });
92510
+ }));
92423
92511
  ProjectSchema = exports_external2.object({
92424
92512
  name: exports_external2.string().optional(),
92425
92513
  language: exports_external2.string().optional(),
@@ -92431,9 +92519,17 @@ var init_schema = __esm(() => {
92431
92519
  build: exports_external2.string().optional(),
92432
92520
  typecheck: exports_external2.string().optional()
92433
92521
  }).catchall(exports_external2.string()).default({});
92522
+ DEFAULT_META_ONLY_FILES = [
92523
+ "openspec/**",
92524
+ ".ralph/**",
92525
+ "**/agent-tasks.md",
92526
+ "**/tasks.md",
92527
+ "**/MANUAL_TESTING*.md"
92528
+ ];
92434
92529
  BoundariesSchema = exports_external2.object({
92435
- never_touch: exports_external2.array(exports_external2.string()).default([])
92436
- }).strict().default({ never_touch: [] });
92530
+ never_touch: exports_external2.array(exports_external2.string()).default([]),
92531
+ meta_only_files: exports_external2.array(exports_external2.string()).default(DEFAULT_META_ONLY_FILES)
92532
+ }).strict().default({ never_touch: [], meta_only_files: DEFAULT_META_ONLY_FILES });
92437
92533
  WorkflowConfigSchema = exports_external2.object({
92438
92534
  project: ProjectSchema,
92439
92535
  commands: CommandsSchema,
@@ -92470,18 +92566,20 @@ var init_schema = __esm(() => {
92470
92566
  assignee: exports_external2.string().optional(),
92471
92567
  postComments: exports_external2.boolean().default(true),
92472
92568
  updateEveryIterations: exports_external2.number().int().nonnegative().default(10),
92473
- mentionTrigger: exports_external2.boolean().default(false),
92569
+ mentionTrigger: exports_external2.boolean().default(true),
92474
92570
  mentionHandle: exports_external2.string().default("@ralphy"),
92475
- codeReviewTrigger: exports_external2.boolean().default(false),
92571
+ codeReviewTrigger: exports_external2.boolean().default(true),
92476
92572
  codeReviewStaleHours: exports_external2.number().nonnegative().default(24),
92573
+ syncTasksToComment: exports_external2.boolean().default(true),
92477
92574
  indicators: IndicatorsSchema.default({})
92478
92575
  }).strict().default({
92479
92576
  postComments: true,
92480
92577
  updateEveryIterations: 10,
92481
- mentionTrigger: false,
92578
+ mentionTrigger: true,
92482
92579
  mentionHandle: "@ralphy",
92483
- codeReviewTrigger: false,
92580
+ codeReviewTrigger: true,
92484
92581
  codeReviewStaleHours: 24,
92582
+ syncTasksToComment: true,
92485
92583
  indicators: {}
92486
92584
  }),
92487
92585
  github: exports_external2.object({
@@ -92540,70 +92638,71 @@ boundaries:
92540
92638
  never_touch:
92541
92639
  - "dist/**"
92542
92640
  - ".claude/worktrees/**"
92543
-
92641
+ # Files that count as "meta only" for the pre-PR substantive-diff guard.
92642
+ # If every changed file matches one of these globs, the loop refuses to
92643
+ # open the PR and respawns the worker \u2014 the actual implementation was
92644
+ # lost (either deleted mid-loop or absorbed by a merge from base).
92645
+ meta_only_files:
92646
+ - "openspec/**"
92647
+ - ".ralph/**"
92648
+ - "**/agent-tasks.md"
92649
+ - "**/tasks.md"
92650
+ - "**/MANUAL_TESTING*.md"
92651
+
92652
+ # \u2500\u2500\u2500 Scheduling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92544
92653
  # How many tasks to run in parallel.
92545
92654
  concurrency: 1
92546
-
92547
92655
  # Seconds between polls for new Linear issues (agent mode).
92548
92656
  pollIntervalSeconds: 60
92657
+ # Seconds to wait between loop iterations (throttle).
92658
+ iterationDelaySeconds: 0
92549
92659
 
92550
- # Maximum iterations per task. 0 = unlimited.
92660
+ # \u2500\u2500\u2500 Per-task limits (0 = unlimited) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92551
92661
  maxIterationsPerTask: 0
92552
-
92553
- # Maximum cost in USD per task. 0 = unlimited.
92554
92662
  maxCostUsdPerTask: 0
92555
-
92556
- # Maximum wall-clock minutes per task. 0 = unlimited.
92557
92663
  maxRuntimeMinutesPerTask: 0
92558
-
92559
92664
  # Stop a task after this many consecutive identical failures.
92560
92665
  maxConsecutiveFailuresPerTask: 5
92561
92666
 
92562
- # Seconds to wait between loop iterations (throttle).
92563
- iterationDelaySeconds: 0
92564
-
92667
+ # \u2500\u2500\u2500 Engine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92668
+ # Underlying engine: "claude" or "codex".
92669
+ engine: claude
92670
+ # Model tier: "haiku", "sonnet", or "opus".
92671
+ model: opus
92565
92672
  # Log the raw engine stream to stdout.
92566
92673
  logRawStream: false
92567
-
92568
92674
  # Pass --verbose to the ralph task sub-process.
92569
92675
  taskVerbose: false
92570
92676
 
92677
+ # \u2500\u2500\u2500 Worktree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92571
92678
  # Run each task in an isolated git worktree.
92572
92679
  useWorktree: false
92573
-
92574
92680
  # Delete the worktree after a successful task.
92575
92681
  cleanupWorktreeOnSuccess: false
92576
92682
 
92683
+ # \u2500\u2500\u2500 Pull requests \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92577
92684
  # Open a pull request after a task succeeds.
92578
92685
  createPrOnSuccess: false
92579
-
92580
92686
  # Base branch for pull requests.
92581
92687
  prBaseBranch: main
92582
-
92583
92688
  # When true, stack dependent issues' PRs onto their blocker's open PR.
92584
92689
  stackPrsOnDependencies: false
92585
-
92586
92690
  # Strategy used when GitHub auto-merge is enabled.
92587
92691
  autoMergeStrategy: squash
92588
92692
 
92693
+ # \u2500\u2500\u2500 CI auto-fix \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92589
92694
  # Let the agent attempt to fix CI failures after a PR is created.
92590
92695
  fixCiOnFailure: false
92591
-
92592
92696
  # Maximum number of CI-fix attempts per task.
92593
92697
  maxCiFixAttempts: 5
92594
-
92595
92698
  # Seconds between CI status polls.
92596
92699
  ciPollIntervalSeconds: 30
92597
92700
 
92598
- # Underlying engine: "claude" or "codex".
92599
- engine: claude
92600
-
92601
- # Model tier: "haiku", "sonnet", or "opus".
92602
- model: opus
92603
-
92604
- # Pre-existing error check: gate the agent when the base branch is already broken.
92605
- # When enabled, the agent runs these commands against the base branch HEAD before
92606
- # scheduling new work; failures open a Linear ticket and pause new pickups.
92701
+ # \u2500\u2500\u2500 Base-branch health gate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92702
+ # Pre-existing error check: gate the agent when the base branch is already
92703
+ # broken. When enabled, the agent runs these commands against the base
92704
+ # branch HEAD before scheduling new work; failures open a Linear ticket
92705
+ # and pause new pickups.
92607
92706
  preExistingErrorCheck:
92608
92707
  enabled: false
92609
92708
  # Commands to run against the base branch. When empty, falls back to commands.lint / commands.test.
@@ -92612,33 +92711,49 @@ preExistingErrorCheck:
92612
92711
  label: "ralph:pre-existing-error"
92613
92712
  outputCharLimit: 4000
92614
92713
 
92714
+ # \u2500\u2500\u2500 Linear integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92615
92715
  linear:
92616
92716
  # Linear team key (e.g. "ENG"). Omit to match all teams.
92617
92717
  # team: ENG
92618
92718
 
92619
92719
  # Post progress comments on the Linear issue while a task is running.
92620
92720
  postComments: true
92621
-
92622
92721
  # Post a progress update every N iterations. 0 disables.
92623
92722
  updateEveryIterations: 10
92624
92723
 
92625
92724
  # Watch done-issue comments + linked GitHub PR comments for @ralphy mentions.
92626
- mentionTrigger: false
92725
+ mentionTrigger: true
92627
92726
  mentionHandle: "@ralphy"
92628
92727
 
92629
92728
  # Watch open tracked PRs for unresolved review-thread comments.
92630
- codeReviewTrigger: false
92729
+ codeReviewTrigger: true
92631
92730
  codeReviewStaleHours: 24
92632
92731
 
92732
+ # Mirror the loop's tasks.md into a sticky Linear comment (always the
92733
+ # last comment on the issue). Updates on worker launch, on the same
92734
+ # cadence as updateEveryIterations, and on done-transition.
92735
+ syncTasksToComment: true
92736
+
92633
92737
  # Indicators map Ralph lifecycle events to Linear labels/statuses.
92634
- # Grouped by lifecycle: each get* is followed by the set*/clear* that
92635
- # mutates the same state, so the lifecycle reads top-to-bottom.
92636
- indicators: {}
92637
- # Todo -> In Progress
92738
+ #
92739
+ # Filter semantics (per indicator's \`filter:\` list):
92740
+ # \u2022 Entries of the SAME type (e.g. two \`status\` entries) are ORed
92741
+ # \u2014 the issue matches if any value matches.
92742
+ # \u2022 Entries of DIFFERENT types (one \`status\` + one \`label\`) are
92743
+ # ANDed \u2014 the issue must satisfy every type.
92744
+ # Example: a filter with two statuses + one label matches issues
92745
+ # where status \u2208 {A, B} AND label = L.
92746
+ #
92747
+ # Sections below group one state at a time; its get/set/clear sit
92748
+ # adjacent so the lifecycle reads top-to-bottom.
92749
+ indicators:
92750
+ # \u2500\u2500 Todo (pickup trigger) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92638
92751
  # getTodo:
92639
92752
  # filter:
92640
92753
  # - type: status
92641
92754
  # value: Todo
92755
+ #
92756
+ # \u2500\u2500 In Progress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92642
92757
  # getInProgress:
92643
92758
  # filter:
92644
92759
  # - type: status
@@ -92647,7 +92762,7 @@ linear:
92647
92762
  # type: status
92648
92763
  # value: In Progress
92649
92764
  #
92650
- # # Done / review hand-off
92765
+ # \u2500\u2500 Done \u2192 Review hand-off \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92651
92766
  # setDone:
92652
92767
  # type: status
92653
92768
  # value: In Review
@@ -92659,7 +92774,7 @@ linear:
92659
92774
  # type: label
92660
92775
  # value: "ralph:review"
92661
92776
  #
92662
- # # Conflict lifecycle
92777
+ # \u2500\u2500 Conflicted \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92663
92778
  # getConflicted:
92664
92779
  # filter:
92665
92780
  # - type: label
@@ -92671,13 +92786,13 @@ linear:
92671
92786
  # type: label
92672
92787
  # value: "ralph:conflict"
92673
92788
  #
92674
- # # Auto-merge opt-in
92789
+ # \u2500\u2500 Auto-merge (opt-in) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92675
92790
  # getAutoMerge:
92676
92791
  # filter:
92677
92792
  # - type: label
92678
92793
  # value: "ralph:auto-merge"
92679
92794
  #
92680
- # # Error quarantine
92795
+ # \u2500\u2500 Error quarantine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
92681
92796
  # setError:
92682
92797
  # type: label
92683
92798
  # value: "ralph:error"
@@ -93574,6 +93689,51 @@ function buildIssueFilter(spec) {
93574
93689
  }
93575
93690
  return where;
93576
93691
  }
93692
+ async function fetchMentionScanIssues(apiKey, spec) {
93693
+ const where = {
93694
+ state: { type: { in: ["unstarted", "started", "backlog", "triage", "completed"] } }
93695
+ };
93696
+ if (spec.team)
93697
+ where.team = { key: { eq: spec.team } };
93698
+ if (spec.assignee) {
93699
+ if (spec.assignee === "me")
93700
+ where.assignee = { isMe: { eq: true } };
93701
+ else if (spec.assignee.includes("@"))
93702
+ where.assignee = { email: { eq: spec.assignee } };
93703
+ else
93704
+ where.assignee = { id: { eq: spec.assignee } };
93705
+ }
93706
+ const query = `query MentionScanIssues($filter: IssueFilter) {
93707
+ issues(filter: $filter, first: 50) {
93708
+ nodes {
93709
+ id identifier title description url priority createdAt
93710
+ state { name type }
93711
+ assignee { id email name }
93712
+ labels { nodes { name } }
93713
+ relations(first: 50) {
93714
+ nodes { type relatedIssue { id state { type } } }
93715
+ }
93716
+ }
93717
+ }
93718
+ }`;
93719
+ const data = await linearRequest(apiKey, query, {
93720
+ filter: where
93721
+ });
93722
+ const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
93723
+ return data.issues.nodes.map((n) => ({
93724
+ id: n.id,
93725
+ identifier: n.identifier,
93726
+ title: n.title,
93727
+ description: n.description,
93728
+ url: n.url,
93729
+ state: n.state,
93730
+ assignee: n.assignee,
93731
+ labels: n.labels.nodes.map((l) => l.name),
93732
+ priority: n.priority,
93733
+ createdAt: n.createdAt ?? "",
93734
+ blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
93735
+ }));
93736
+ }
93577
93737
  async function fetchOpenIssues(apiKey, spec) {
93578
93738
  const where = buildIssueFilter(spec);
93579
93739
  const query = `query Issues($filter: IssueFilter) {
@@ -93610,28 +93770,102 @@ async function fetchOpenIssues(apiKey, spec) {
93610
93770
  blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id)
93611
93771
  }));
93612
93772
  }
93773
+ function isRetryableStatus(status) {
93774
+ return status >= 500 && status <= 599;
93775
+ }
93776
+ function parseRetryAfter(header) {
93777
+ if (!header)
93778
+ return;
93779
+ const trimmed = header.trim();
93780
+ if (!trimmed)
93781
+ return;
93782
+ const asNum = Number(trimmed);
93783
+ if (Number.isFinite(asNum))
93784
+ return Math.max(0, asNum * 1000);
93785
+ const asDate = Date.parse(trimmed);
93786
+ if (Number.isFinite(asDate))
93787
+ return Math.max(0, asDate - Date.now());
93788
+ return;
93789
+ }
93790
+ function backoffMs(attempt2) {
93791
+ const base2 = 250 * 2 ** (attempt2 - 1);
93792
+ const jitter = Math.floor(Math.random() * 100);
93793
+ return base2 + jitter;
93794
+ }
93795
+ function isRateLimitedBody(body) {
93796
+ if (typeof body !== "string" || body.length === 0)
93797
+ return false;
93798
+ return body.toLowerCase().includes("rate limit exceeded");
93799
+ }
93800
+ function isRateLimitedError(err) {
93801
+ if (err === null || typeof err !== "object")
93802
+ return false;
93803
+ return err.rateLimited === true;
93804
+ }
93805
+ function formatLinearError(err) {
93806
+ if (err === null || err === undefined)
93807
+ return String(err);
93808
+ if (typeof err !== "object")
93809
+ return String(err);
93810
+ const e = err;
93811
+ const parts = [];
93812
+ if (e.rateLimited)
93813
+ parts.push("rate limited");
93814
+ if (typeof e.status === "number")
93815
+ parts.push(`HTTP ${e.status}`);
93816
+ if (Array.isArray(e.messages) && e.messages.length > 0) {
93817
+ parts.push(`graphql: ${e.messages.join("; ")}`);
93818
+ }
93819
+ if (typeof e.body === "string" && e.body.length > 0 && !e.rateLimited) {
93820
+ const truncated = e.body.length > 200 ? `${e.body.slice(0, 200)}\u2026` : e.body;
93821
+ parts.push(`body: ${truncated}`);
93822
+ }
93823
+ if (parts.length === 0) {
93824
+ if (typeof e.message === "string" && e.message)
93825
+ return e.message;
93826
+ return String(err);
93827
+ }
93828
+ if (typeof e.message === "string" && e.message && !e.rateLimited)
93829
+ parts.unshift(e.message);
93830
+ return parts.join(" \u2014 ");
93831
+ }
93613
93832
  async function linearRequest(apiKey, query, variables) {
93614
- const res = await fetch(LINEAR_API, {
93615
- method: "POST",
93616
- headers: { "Content-Type": "application/json", Authorization: apiKey },
93617
- body: JSON.stringify({ query, variables })
93618
- });
93619
- if (!res.ok) {
93620
- const err = new Error("Linear API request failed");
93621
- err.status = res.status;
93622
- err.body = await res.text();
93623
- throw err;
93624
- }
93625
- const json2 = await res.json();
93626
- if (json2.errors?.length) {
93627
- const err = new Error("Linear API returned errors");
93628
- err.messages = json2.errors.map((e) => e.message);
93629
- throw err;
93630
- }
93631
- if (!json2.data) {
93632
- throw new Error("Linear API returned no data");
93833
+ let lastHttpError;
93834
+ for (let attempt2 = 1;attempt2 <= MAX_LINEAR_ATTEMPTS; attempt2++) {
93835
+ const res = await fetch(LINEAR_API, {
93836
+ method: "POST",
93837
+ headers: { "Content-Type": "application/json", Authorization: apiKey },
93838
+ body: JSON.stringify({ query, variables })
93839
+ });
93840
+ if (!res.ok) {
93841
+ const err = new Error("Linear API request failed");
93842
+ err.status = res.status;
93843
+ err.body = await res.text();
93844
+ if (res.status === 429 || isRateLimitedBody(err.body)) {
93845
+ err.rateLimited = true;
93846
+ throw err;
93847
+ }
93848
+ lastHttpError = err;
93849
+ if (isRetryableStatus(res.status) && attempt2 < MAX_LINEAR_ATTEMPTS) {
93850
+ const ra = parseRetryAfter(res.headers.get("Retry-After"));
93851
+ const waitMs = Math.min(ra ?? backoffMs(attempt2), MAX_RETRY_AFTER_MS);
93852
+ await linearRequestInternals.sleep(waitMs);
93853
+ continue;
93854
+ }
93855
+ throw err;
93856
+ }
93857
+ const json2 = await res.json();
93858
+ if (json2.errors?.length) {
93859
+ const err = new Error("Linear API returned errors");
93860
+ err.messages = json2.errors.map((e) => e.message);
93861
+ throw err;
93862
+ }
93863
+ if (!json2.data) {
93864
+ throw new Error("Linear API returned no data");
93865
+ }
93866
+ return json2.data;
93633
93867
  }
93634
- return json2.data;
93868
+ throw lastHttpError ?? new Error("Linear API request failed");
93635
93869
  }
93636
93870
  async function addReactionToComment(apiKey, commentId, emoji3) {
93637
93871
  const mutation = `mutation Reaction($commentId: String!, $emoji: String!) {
@@ -93651,6 +93885,36 @@ async function addIssueComment(apiKey, issueId, body) {
93651
93885
  body
93652
93886
  });
93653
93887
  }
93888
+ async function createIssueComment(apiKey, issueId, body) {
93889
+ const mutation = `mutation Comment($issueId: String!, $body: String!) {
93890
+ commentCreate(input: { issueId: $issueId, body: $body }) {
93891
+ success
93892
+ comment { id }
93893
+ }
93894
+ }`;
93895
+ const data = await linearRequest(apiKey, mutation, { issueId, body });
93896
+ const id = data.commentCreate.comment?.id;
93897
+ if (!id)
93898
+ throw new Error("commentCreate returned no comment id");
93899
+ return id;
93900
+ }
93901
+ async function updateIssueComment(apiKey, commentId, body) {
93902
+ const mutation = `mutation UpdateComment($id: String!, $body: String!) {
93903
+ commentUpdate(id: $id, input: { body: $body }) { success }
93904
+ }`;
93905
+ await linearRequest(apiKey, mutation, {
93906
+ id: commentId,
93907
+ body
93908
+ });
93909
+ }
93910
+ async function deleteIssueComment(apiKey, commentId) {
93911
+ const mutation = `mutation DeleteComment($id: String!) {
93912
+ commentDelete(id: $id) { success }
93913
+ }`;
93914
+ await linearRequest(apiKey, mutation, {
93915
+ id: commentId
93916
+ });
93917
+ }
93654
93918
  async function fetchIssueComments(apiKey, issueId) {
93655
93919
  const query = `query Comments($id: String!) {
93656
93920
  issue(id: $id) {
@@ -93873,7 +94137,12 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
93873
94137
  labelId
93874
94138
  });
93875
94139
  }
93876
- var LINEAR_API = "https://api.linear.app/graphql", RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
94140
+ 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:";
94141
+ var init_linear = __esm(() => {
94142
+ linearRequestInternals = {
94143
+ sleep: (ms) => Bun.sleep(ms)
94144
+ };
94145
+ });
93877
94146
 
93878
94147
  // apps/agent/src/sort/compare.ts
93879
94148
  function chain(...comparators) {
@@ -93904,6 +94173,7 @@ function compareQueueEntries(getAutoMerge) {
93904
94173
  }
93905
94174
  var MODE_RANK;
93906
94175
  var init_queue_order = __esm(() => {
94176
+ init_linear();
93907
94177
  MODE_RANK = {
93908
94178
  resume: 0,
93909
94179
  "conflict-fix": 1,
@@ -94117,6 +94387,13 @@ class AgentCoordinator {
94117
94387
  } catch (err) {
94118
94388
  this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
94119
94389
  }
94390
+ if (this.deps.syncTasks) {
94391
+ try {
94392
+ await this.deps.syncTasks(w, count);
94393
+ } catch (err) {
94394
+ this.deps.onLog(`! sync-tasks (progress) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
94395
+ }
94396
+ }
94120
94397
  }
94121
94398
  }
94122
94399
  async scanDoneForConflicts() {
@@ -94289,6 +94566,13 @@ class AgentCoordinator {
94289
94566
  issue_identifier: issue2.identifier
94290
94567
  });
94291
94568
  this.deps.onWorkersChanged();
94569
+ if (this.deps.syncTasks) {
94570
+ try {
94571
+ await this.deps.syncTasks(worker, 0);
94572
+ } catch (err) {
94573
+ this.deps.onLog(`! sync-tasks (launch) failed for ${issue2.identifier}: ${err.message}`, "yellow");
94574
+ }
94575
+ }
94292
94576
  handle.exited.then(async (code) => {
94293
94577
  const idx = this.workers.indexOf(worker);
94294
94578
  if (idx >= 0)
@@ -94331,8 +94615,42 @@ class AgentCoordinator {
94331
94615
  } catch {}
94332
94616
  return true;
94333
94617
  }
94618
+ async notifySteeringAppended(changeName, message) {
94619
+ if (!this.deps.onSteeringAppended)
94620
+ return;
94621
+ try {
94622
+ await this.deps.onSteeringAppended(changeName, message);
94623
+ } catch (err) {
94624
+ this.deps.onLog(`! onSteeringAppended failed for ${changeName}: ${err.message}`, "yellow");
94625
+ }
94626
+ }
94334
94627
  async notifyExited(issue2, changeName, code, mode) {
94335
94628
  const ok = code === 0;
94629
+ if (this.deps.syncTasks && ok) {
94630
+ const synthetic = {
94631
+ changeName,
94632
+ issueId: issue2.id,
94633
+ issueIdentifier: issue2.identifier,
94634
+ issue: issue2,
94635
+ mode,
94636
+ kill: () => {},
94637
+ lastReportedIteration: 0,
94638
+ restarting: false
94639
+ };
94640
+ try {
94641
+ let iteration = 0;
94642
+ if (this.deps.getIterationCount) {
94643
+ try {
94644
+ iteration = await this.deps.getIterationCount(changeName);
94645
+ } catch {
94646
+ iteration = 0;
94647
+ }
94648
+ }
94649
+ await this.deps.syncTasks(synthetic, iteration);
94650
+ } catch (err) {
94651
+ this.deps.onLog(`! sync-tasks (done) failed for ${issue2.identifier}: ${err.message}`, "yellow");
94652
+ }
94653
+ }
94336
94654
  if (this.opts.postComments !== false) {
94337
94655
  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
94656
 
@@ -94415,6 +94733,7 @@ var emptyPrStatus = () => ({ mergeable: 0, conflicted: 0, ciFailed: 0 }), emptyP
94415
94733
  prStatus: emptyPrStatus()
94416
94734
  });
94417
94735
  var init_coordinator = __esm(() => {
94736
+ init_linear();
94418
94737
  init_queue_order();
94419
94738
  init_src();
94420
94739
  });
@@ -94483,7 +94802,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = []
94483
94802
  `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
94484
94803
  `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
94485
94804
  `- [ ] 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\`)`,
94805
+ `- [ ] 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
94806
  ""
94488
94807
  ].join(`
94489
94808
  `);
@@ -94783,11 +95102,49 @@ ${issue2.description.trim()}` : ""
94783
95102
  ].filter(Boolean).join(`
94784
95103
  `);
94785
95104
  }
95105
+ async function diffFilesAgainstBase(runner, cwd2, base2) {
95106
+ let raw = "";
95107
+ try {
95108
+ const r = await runner.run(["git", "diff", "--name-only", `origin/${base2}...HEAD`], cwd2);
95109
+ raw = r.stdout;
95110
+ } catch {
95111
+ try {
95112
+ const r = await runner.run(["git", "diff", "--name-only", `${base2}...HEAD`], cwd2);
95113
+ raw = r.stdout;
95114
+ } catch {
95115
+ return [];
95116
+ }
95117
+ }
95118
+ return raw.split(`
95119
+ `).map((s) => s.trim()).filter(Boolean);
95120
+ }
95121
+ async function classifyDiffAgainstMeta(runner, cwd2, base2, metaOnlyFiles) {
95122
+ const files = await diffFilesAgainstBase(runner, cwd2, base2);
95123
+ if (files.length === 0 || metaOnlyFiles.length === 0) {
95124
+ return { files, onlyMeta: false };
95125
+ }
95126
+ const violations = findBoundaryViolations(files, metaOnlyFiles);
95127
+ const metaSet = new Set(violations.map((v) => v.file));
95128
+ const onlyMeta = files.every((f2) => metaSet.has(f2.replace(/\\/g, "/")));
95129
+ return { files, onlyMeta };
95130
+ }
94786
95131
  async function createPullRequest(input, runner) {
94787
95132
  const base2 = input.base ?? "main";
94788
95133
  const log2 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
94789
95134
  if (log2.stdout.trim() === "")
94790
95135
  return null;
95136
+ const metaOnlyFiles = input.metaOnlyFiles ?? [];
95137
+ if (metaOnlyFiles.length > 0) {
95138
+ const classification = await classifyDiffAgainstMeta(runner, input.cwd, base2, metaOnlyFiles);
95139
+ if (classification.onlyMeta && classification.files.length > 0) {
95140
+ return {
95141
+ url: null,
95142
+ created: false,
95143
+ blocked: "only-meta",
95144
+ blockedFiles: classification.files
95145
+ };
95146
+ }
95147
+ }
94791
95148
  await runner.run(["git", "push", "-u", "origin", input.branch], input.cwd);
94792
95149
  const existing = await runner.run([
94793
95150
  "gh",
@@ -94812,6 +95169,7 @@ async function createPullRequest(input, runner) {
94812
95169
  `).pop() ?? "";
94813
95170
  return { url: url2, created: true };
94814
95171
  }
95172
+ var init_pr = () => {};
94815
95173
 
94816
95174
  // apps/agent/src/agent/post-task.ts
94817
95175
  import { join as join20 } from "path";
@@ -94924,7 +95282,13 @@ async function createPrWithRetry(ctx, issue2) {
94924
95282
  while (true) {
94925
95283
  try {
94926
95284
  ctx.emit("pr-create", "git push + gh pr create");
94927
- pr = await createPullRequest({ cwd: ctx.cwd, branch: ctx.branch, issue: issue2, base: base2 }, ctx.cmd);
95285
+ pr = await createPullRequest({
95286
+ cwd: ctx.cwd,
95287
+ branch: ctx.branch,
95288
+ issue: issue2,
95289
+ base: base2,
95290
+ metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? []
95291
+ }, ctx.cmd);
94928
95292
  return { pr, gaveUp: false };
94929
95293
  } catch (err) {
94930
95294
  const e = err;
@@ -95190,49 +95554,97 @@ ${indented}${suffix}`, "yellow");
95190
95554
  }
95191
95555
  return PR_FAILED_EXIT;
95192
95556
  }
95193
- const { pr, gaveUp: prGaveUp } = await createPrWithRetry(ctx, issue2);
95194
- if (prGaveUp)
95195
- return PR_FAILED_EXIT;
95557
+ const maxOuterAttempts = cfg.maxCiFixAttempts;
95558
+ let onlyMetaAttempts = 0;
95559
+ let pr = null;
95560
+ while (true) {
95561
+ const attempt2 = await createPrWithRetry(ctx, issue2);
95562
+ if (attempt2.gaveUp)
95563
+ return PR_FAILED_EXIT;
95564
+ if (attempt2.pr?.blocked === "only-meta") {
95565
+ onlyMetaAttempts += 1;
95566
+ const files = attempt2.pr.blockedFiles ?? [];
95567
+ emit("pr-only-meta", `${files.length} meta file(s)`);
95568
+ log2(`! ${changeName}: branch diff against ${base2} contains only meta files \u2014 implementation appears lost. Refusing to open PR.`, "red");
95569
+ for (const f2 of files)
95570
+ log2(` ${f2}`, "red");
95571
+ if (onlyMetaAttempts > maxOuterAttempts) {
95572
+ log2(`! exceeded ${maxOuterAttempts} only-meta recovery attempts for ${changeName} \u2014 giving up`, "red");
95573
+ return PR_FAILED_EXIT;
95574
+ }
95575
+ const fileList = files.length > 0 ? files.map((f2) => `- ${f2}`).join(`
95576
+ `) : "(empty diff)";
95577
+ const retryCode = await runWorkerWithFixTask(ctx, "Reapply lost implementation files", [
95578
+ `The diff against \`${base2}\` contains only meta files`,
95579
+ `(openspec/tasks.md and similar). The substantive implementation`,
95580
+ `is missing from the branch \u2014 likely deleted by an earlier commit`,
95581
+ `or absorbed by a merge from origin/${base2}.`,
95582
+ "",
95583
+ `Files currently in the diff:`,
95584
+ fileList,
95585
+ "",
95586
+ `Re-apply the actual implementation work the change is supposed`,
95587
+ `to ship. Inspect git history (\`git log ${base2}..HEAD\`) to see`,
95588
+ `what was created earlier and lost, then restore those files`,
95589
+ `(or reproduce the work). Commit the restored files so the next`,
95590
+ `iteration's diff against \`${base2}\` contains real code, not`,
95591
+ `just meta files.`
95592
+ ].join(`
95593
+ `));
95594
+ if (retryCode !== 0) {
95595
+ log2(`! worker re-run after only-meta block exited code ${retryCode} \u2014 giving up`, "red");
95596
+ return PR_FAILED_EXIT;
95597
+ }
95598
+ continue;
95599
+ }
95600
+ pr = attempt2.pr;
95601
+ break;
95602
+ }
95196
95603
  if (!pr) {
95197
95604
  log2(` no commits ahead of ${base2} \u2014 skipping PR`, "gray");
95198
95605
  return 0;
95199
95606
  }
95200
- log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
95201
- registerPr?.(changeName, pr.url);
95607
+ const prUrl = pr.url;
95608
+ if (!prUrl) {
95609
+ log2(`! PR creation returned a null URL for ${changeName} \u2014 giving up`, "red");
95610
+ return PR_FAILED_EXIT;
95611
+ }
95612
+ log2(` ${pr.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
95613
+ registerPr?.(changeName, prUrl);
95202
95614
  let manualMergePending = false;
95203
95615
  if (wantAutoMerge) {
95204
95616
  const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
95205
- const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(pr.url, cmd, cwd2, log2);
95617
+ const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log2);
95206
95618
  if (repoAllowsAutoMerge === false && fallbackEnabled) {
95207
- log2(` repo has auto-merge disabled \u2014 will poll ${pr.url} and merge via gh pr merge once checks pass`, "yellow");
95619
+ log2(` repo has auto-merge disabled \u2014 will poll ${prUrl} and merge via gh pr merge once checks pass`, "yellow");
95208
95620
  manualMergePending = true;
95209
95621
  } else {
95210
95622
  try {
95211
- await cmd.run(["gh", "pr", "merge", pr.url, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
95212
- log2(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${pr.url}`, "green");
95623
+ await cmd.run(["gh", "pr", "merge", prUrl, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
95624
+ log2(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${prUrl}`, "green");
95213
95625
  emit("auto-merge-enabled", cfg.autoMergeStrategy);
95214
95626
  } catch (err) {
95215
95627
  const e = err;
95216
95628
  const detail = e.stderr?.trim() || e.message;
95217
- log2(`! failed to enable auto-merge on ${pr.url}: ${detail}`, "yellow");
95629
+ log2(`! failed to enable auto-merge on ${prUrl}: ${detail}`, "yellow");
95218
95630
  if (fallbackEnabled && /auto[- ]merge/i.test(detail)) {
95219
- log2(` falling back to manual merge after CI passes for ${pr.url}`, "yellow");
95631
+ log2(` falling back to manual merge after CI passes for ${prUrl}`, "yellow");
95220
95632
  manualMergePending = true;
95221
95633
  }
95222
95634
  }
95223
95635
  }
95224
95636
  }
95225
- const ciResult = await fixConflictsAndCiLoop(ctx, pr.url, wantFixCi, checkPrConflict);
95637
+ const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
95226
95638
  if (ciResult !== 0)
95227
95639
  return ciResult;
95228
95640
  if (manualMergePending) {
95229
95641
  try {
95230
- await cmd.run(["gh", "pr", "merge", pr.url, `--${cfg.autoMergeStrategy}`], cwd2);
95231
- log2(` manually merged (${cfg.autoMergeStrategy}) ${pr.url}`, "green");
95642
+ await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
95643
+ log2(` manually merged (${cfg.autoMergeStrategy}) ${prUrl}`, "green");
95232
95644
  emit("auto-merge-enabled", `manual:${cfg.autoMergeStrategy}`);
95233
95645
  } catch (err) {
95234
95646
  const e = err;
95235
- log2(`! manual merge failed for ${pr.url}: ${e.stderr?.trim() || e.message}`, "yellow");
95647
+ log2(`! manual merge failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
95236
95648
  }
95237
95649
  }
95238
95650
  return 0;
@@ -95333,6 +95745,8 @@ async function runPostTask(input, deps) {
95333
95745
  var CI_FAILED_EXIT = 70, PR_FAILED_EXIT = 71, repoAutoMergeCache;
95334
95746
  var init_post_task = __esm(() => {
95335
95747
  init_tasks_md();
95748
+ init_linear();
95749
+ init_pr();
95336
95750
  init_ci();
95337
95751
  init_worktree();
95338
95752
  repoAutoMergeCache = new Map;
@@ -95517,9 +95931,317 @@ var init_gate = __esm(() => {
95517
95931
  FINGERPRINT_MARKER_RE = /<!--\s*ralphy:baseline:([a-f0-9]+)\s*-->/i;
95518
95932
  });
95519
95933
 
95520
- // apps/agent/src/agent/wire.ts
95521
- import { join as join21 } from "path";
95934
+ // apps/agent/src/agent/linear-sync/index.ts
95935
+ function parseTasksMd(md) {
95936
+ const lines = md.split(/\r?\n/);
95937
+ const sections = [];
95938
+ let current = null;
95939
+ let i = 0;
95940
+ while (i < lines.length) {
95941
+ const line = lines[i];
95942
+ const headingMatch = /^##\s+(.+?)\s*$/.exec(line);
95943
+ if (headingMatch) {
95944
+ current = { heading: headingMatch[1], items: [] };
95945
+ sections.push(current);
95946
+ i += 1;
95947
+ continue;
95948
+ }
95949
+ const bulletMatch = /^(\s*)-\s+\[( |x|X)\]\s+(.+?)\s*$/.exec(line);
95950
+ if (bulletMatch && current) {
95951
+ const indent = bulletMatch[1] ?? "";
95952
+ const checked = bulletMatch[2]?.toLowerCase() === "x";
95953
+ const text = bulletMatch[3] ?? "";
95954
+ const bullet = `${indent}- [${checked ? "x" : " "}] ${text}`;
95955
+ i += 1;
95956
+ let j = i;
95957
+ while (j < lines.length && lines[j].trim() === "")
95958
+ j += 1;
95959
+ let code;
95960
+ if (j < lines.length && /^\s*```/.test(lines[j])) {
95961
+ const fenceOpen = lines[j];
95962
+ const fenceMatch = /^(\s*)```/.exec(fenceOpen);
95963
+ const fenceIndent = fenceMatch?.[1] ?? "";
95964
+ const buf = [];
95965
+ j += 1;
95966
+ while (j < lines.length) {
95967
+ if (new RegExp(`^${fenceIndent}\`\`\`\\s*$`).test(lines[j])) {
95968
+ j += 1;
95969
+ break;
95970
+ }
95971
+ buf.push(lines[j]);
95972
+ j += 1;
95973
+ }
95974
+ code = buf.join(`
95975
+ `);
95976
+ i = j;
95977
+ }
95978
+ current.items.push(code !== undefined ? { bullet, code } : { bullet });
95979
+ continue;
95980
+ }
95981
+ i += 1;
95982
+ }
95983
+ return sections;
95984
+ }
95985
+ function truncate4(s, max2) {
95986
+ if (s.length <= max2)
95987
+ return s;
95988
+ return `${s.slice(0, max2)}
95989
+ \u2026(truncated)`;
95990
+ }
95991
+ function renderTasksBlock(tasksMd, meta3) {
95992
+ const sections = parseTasksMd(tasksMd);
95993
+ const out = [];
95994
+ out.push(RALPHY_TASKS_START);
95995
+ out.push("### Ralph progress");
95996
+ out.push("");
95997
+ for (const section of sections) {
95998
+ if (section.items.length === 0)
95999
+ continue;
96000
+ out.push(`**${section.heading}**`);
96001
+ out.push("");
96002
+ for (const item of section.items) {
96003
+ out.push(item.bullet);
96004
+ if (item.code !== undefined) {
96005
+ const inner = truncate4(item.code, MAX_CODE_BLOCK_BYTES);
96006
+ out.push(` <details><summary>output</summary><pre>${inner}</pre></details>`);
96007
+ }
96008
+ }
96009
+ out.push("");
96010
+ }
96011
+ out.push(`<sub>\`${meta3.changeName}\` \xB7 iteration ${meta3.iteration}</sub>`);
96012
+ out.push(RALPHY_TASKS_END);
96013
+ return out.join(`
96014
+ `);
96015
+ }
96016
+ var RALPHY_TASKS_START = "<!-- ralphy:tasks:start -->", RALPHY_TASKS_END = "<!-- ralphy:tasks:end -->", MAX_CODE_BLOCK_BYTES;
96017
+ var init_linear_sync = __esm(() => {
96018
+ MAX_CODE_BLOCK_BYTES = 2 * 1024;
96019
+ });
96020
+
96021
+ // apps/agent/src/agent/linear-sync/comment-sync.ts
96022
+ import { dirname as dirname7, join as join21 } from "path";
95522
96023
  import { mkdir as mkdir6 } from "fs/promises";
96024
+ async function readStateJson(statePath) {
96025
+ const file2 = Bun.file(statePath);
96026
+ if (!await file2.exists())
96027
+ return null;
96028
+ try {
96029
+ return await file2.json();
96030
+ } catch {
96031
+ return null;
96032
+ }
96033
+ }
96034
+ async function writeStateJson(statePath, state) {
96035
+ await mkdir6(dirname7(statePath), { recursive: true });
96036
+ await Bun.write(statePath, JSON.stringify(state, null, 2) + `
96037
+ `);
96038
+ }
96039
+ function readComments(state) {
96040
+ const raw = state?.linearComments ?? {};
96041
+ return {
96042
+ planCommentId: raw?.planCommentId ?? null,
96043
+ tasksCommentId: raw?.tasksCommentId ?? null,
96044
+ planPostedAt: raw?.planPostedAt ?? null
96045
+ };
96046
+ }
96047
+ async function patchComments(statePath, patch) {
96048
+ const existing = await readStateJson(statePath) ?? {};
96049
+ const current = readComments(existing);
96050
+ const next = { ...current, ...patch };
96051
+ await writeStateJson(statePath, { ...existing, linearComments: next });
96052
+ }
96053
+ function isCommentNotFoundError(err) {
96054
+ if (!err)
96055
+ return false;
96056
+ const candidates = [];
96057
+ const e = err;
96058
+ if (Array.isArray(e.messages))
96059
+ candidates.push(...e.messages);
96060
+ if (typeof e.message === "string")
96061
+ candidates.push(e.message);
96062
+ const text = candidates.join(" ").toLowerCase();
96063
+ return text.includes("not found") || text.includes("could not find") || text.includes("entity not found");
96064
+ }
96065
+ async function readTasksMd(changeDir, log2) {
96066
+ const file2 = Bun.file(join21(changeDir, "tasks.md"));
96067
+ if (!await file2.exists()) {
96068
+ log2(` comment-sync: tasks.md missing in ${changeDir}, skipping`, "gray");
96069
+ return null;
96070
+ }
96071
+ try {
96072
+ return await file2.text();
96073
+ } catch (err) {
96074
+ log2(`! comment-sync: read tasks.md failed: ${err.message}`, "yellow");
96075
+ return null;
96076
+ }
96077
+ }
96078
+ function renderTasksCommentBody(tasksMd, changeName, iteration) {
96079
+ return renderTasksBlock(tasksMd, { changeName, iteration });
96080
+ }
96081
+ async function postOrUpdateTasksComment(deps) {
96082
+ const tasksMd = await readTasksMd(deps.changeDir, deps.log);
96083
+ if (!tasksMd)
96084
+ return null;
96085
+ const body = renderTasksCommentBody(tasksMd, deps.changeName, deps.iteration);
96086
+ const state = await readStateJson(deps.statePath);
96087
+ const comments = readComments(state);
96088
+ if (comments.tasksCommentId) {
96089
+ try {
96090
+ await deps.mutations.updateIssueComment(deps.apiKey, comments.tasksCommentId, body);
96091
+ deps.log(` comment-sync: updated tasks comment for ${deps.changeName}`, "gray");
96092
+ return comments.tasksCommentId;
96093
+ } catch (err) {
96094
+ if (!isCommentNotFoundError(err)) {
96095
+ deps.log(`! comment-sync: updateIssueComment failed: ${err.message}`, "yellow");
96096
+ return null;
96097
+ }
96098
+ deps.log(` comment-sync: tasks comment ${comments.tasksCommentId} not found \u2014 recreating`, "gray");
96099
+ }
96100
+ }
96101
+ let newId;
96102
+ try {
96103
+ newId = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
96104
+ } catch (err) {
96105
+ deps.log(`! comment-sync: createIssueComment failed: ${err.message}`, "yellow");
96106
+ return null;
96107
+ }
96108
+ await patchComments(deps.statePath, { tasksCommentId: newId });
96109
+ deps.log(` comment-sync: created tasks comment for ${deps.changeName}`, "gray");
96110
+ return newId;
96111
+ }
96112
+ function planningComplete(tasksMd) {
96113
+ const lines = tasksMd.split(/\r?\n/);
96114
+ let inPlanning = false;
96115
+ let total = 0;
96116
+ let unchecked = 0;
96117
+ for (const line of lines) {
96118
+ const h = /^##\s+(.+?)\s*$/.exec(line);
96119
+ if (h) {
96120
+ inPlanning = h[1].trim().toLowerCase() === "planning";
96121
+ continue;
96122
+ }
96123
+ if (!inPlanning)
96124
+ continue;
96125
+ const m = /^\s*-\s+\[( |x|X)\]/.exec(line);
96126
+ if (!m)
96127
+ continue;
96128
+ total += 1;
96129
+ if (m[1] === " ")
96130
+ unchecked += 1;
96131
+ }
96132
+ return { allChecked: total > 0 && unchecked === 0, total };
96133
+ }
96134
+ async function readFirstParagraph(path) {
96135
+ const file2 = Bun.file(path);
96136
+ if (!await file2.exists())
96137
+ return null;
96138
+ const text = await file2.text();
96139
+ const blocks = text.split(/\r?\n\s*\r?\n/).map((b) => b.trim()).filter((b) => b.length > 0 && !/^#\s/.test(b));
96140
+ return blocks[0] ?? null;
96141
+ }
96142
+ async function readSection(path, heading) {
96143
+ const file2 = Bun.file(path);
96144
+ if (!await file2.exists())
96145
+ return null;
96146
+ const text = await file2.text();
96147
+ const headingRe = new RegExp(`(^|\\n)##\\s+${heading}\\s*\\n`);
96148
+ const m = headingRe.exec(text);
96149
+ if (!m)
96150
+ return null;
96151
+ const start = m.index + m[0].length;
96152
+ const rest2 = text.slice(start);
96153
+ const next = /\n##\s+/.exec(rest2);
96154
+ const body = next ? rest2.slice(0, next.index) : rest2;
96155
+ return body.trim() || null;
96156
+ }
96157
+ async function postPlanCommentOnce(deps) {
96158
+ const state = await readStateJson(deps.statePath);
96159
+ const comments = readComments(state);
96160
+ if (comments.planCommentId)
96161
+ return null;
96162
+ const tasksMd = await readTasksMd(deps.changeDir, deps.log);
96163
+ if (!tasksMd)
96164
+ return null;
96165
+ const check2 = planningComplete(tasksMd);
96166
+ if (!check2.allChecked)
96167
+ return null;
96168
+ const proposalPath = join21(deps.changeDir, "proposal.md");
96169
+ const why = await readSection(proposalPath, "Why");
96170
+ const whatChanges = await readSection(proposalPath, "What Changes");
96171
+ if (!why && !whatChanges) {
96172
+ deps.log(` comment-sync: proposal.md has no Why/What Changes, skipping plan comment`, "gray");
96173
+ return null;
96174
+ }
96175
+ const designSummary = await readFirstParagraph(join21(deps.changeDir, "design.md"));
96176
+ const parts = [`### ${PLAN_COMMENT_TITLE} \u2014 \`${deps.changeName}\``];
96177
+ if (why) {
96178
+ parts.push("", "**Why**", "", why);
96179
+ }
96180
+ if (whatChanges) {
96181
+ parts.push("", "**What Changes**", "", whatChanges);
96182
+ }
96183
+ if (designSummary) {
96184
+ parts.push("", "**Design**", "", designSummary);
96185
+ }
96186
+ const body = parts.join(`
96187
+ `);
96188
+ let id;
96189
+ try {
96190
+ id = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
96191
+ } catch (err) {
96192
+ deps.log(`! comment-sync: plan comment create failed: ${err.message}`, "yellow");
96193
+ return null;
96194
+ }
96195
+ await patchComments(deps.statePath, {
96196
+ planCommentId: id,
96197
+ planPostedAt: new Date().toISOString()
96198
+ });
96199
+ deps.log(` comment-sync: posted plan comment for ${deps.changeName}`, "gray");
96200
+ return id;
96201
+ }
96202
+ async function postSteeringAndRefreshTasks(deps) {
96203
+ const firstLine = deps.message.split(/\r?\n/, 1)[0].trim() || deps.message.trim();
96204
+ const steeringBody = `### ${STEERING_COMMENT_TITLE}
96205
+
96206
+ ${deps.message.trim()}`;
96207
+ try {
96208
+ await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, steeringBody);
96209
+ deps.log(` comment-sync: posted steering comment (${firstLine})`, "gray");
96210
+ } catch (err) {
96211
+ deps.log(`! comment-sync: steering comment create failed: ${err.message}`, "yellow");
96212
+ }
96213
+ const state = await readStateJson(deps.statePath);
96214
+ const comments = readComments(state);
96215
+ if (comments.tasksCommentId) {
96216
+ try {
96217
+ await deps.mutations.deleteIssueComment(deps.apiKey, comments.tasksCommentId);
96218
+ deps.log(` comment-sync: deleted old tasks comment`, "gray");
96219
+ } catch (err) {
96220
+ if (!isCommentNotFoundError(err)) {
96221
+ deps.log(`! comment-sync: deleteIssueComment failed: ${err.message}`, "yellow");
96222
+ }
96223
+ }
96224
+ await patchComments(deps.statePath, { tasksCommentId: null });
96225
+ }
96226
+ await postOrUpdateTasksComment({
96227
+ apiKey: deps.apiKey,
96228
+ issueId: deps.issueId,
96229
+ statePath: deps.statePath,
96230
+ changeDir: deps.changeDir,
96231
+ changeName: deps.changeName,
96232
+ log: deps.log,
96233
+ mutations: deps.mutations,
96234
+ iteration: deps.iteration
96235
+ });
96236
+ }
96237
+ var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering";
96238
+ var init_comment_sync = __esm(() => {
96239
+ init_linear_sync();
96240
+ });
96241
+
96242
+ // apps/agent/src/agent/wire.ts
96243
+ import { join as join22 } from "path";
96244
+ import { mkdir as mkdir7 } from "fs/promises";
95523
96245
  async function pickOpenPrUrlFromAttachments(urls, issueIdent, cmd, cwd2, onLog) {
95524
96246
  const candidates = urls.filter((url2) => GITHUB_PR_URL_RE.test(url2));
95525
96247
  let sawNonOpenPr = false;
@@ -95683,7 +96405,7 @@ function buildAgentCoordinator(input) {
95683
96405
  onWorkerOutput,
95684
96406
  onWorkerCmd
95685
96407
  } = input;
95686
- const logsDir = join21(projectRoot, ".ralph", "logs");
96408
+ const logsDir = join22(projectRoot, ".ralph", "logs");
95687
96409
  const concurrency = args.concurrency || cfg.concurrency;
95688
96410
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
95689
96411
  const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
@@ -95806,6 +96528,7 @@ function buildAgentCoordinator(input) {
95806
96528
  const prUnavailable = new Map;
95807
96529
  const PR_UNAVAILABLE_TTL_MS = 10 * 60 * 1000;
95808
96530
  const stalePingedAt = new Map;
96531
+ const lastHandledReviewActivity = new Map;
95809
96532
  const useWorktree = args.worktree || cfg.useWorktree;
95810
96533
  const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
95811
96534
  const proc = Bun.spawn({
@@ -95895,8 +96618,8 @@ function buildAgentCoordinator(input) {
95895
96618
  } else {
95896
96619
  changeName = changeNameForIssue(issue2);
95897
96620
  const wtLayout = projectLayout(workerCwd);
95898
- await mkdir6(wtLayout.changeDir(changeName), { recursive: true });
95899
- await mkdir6(wtLayout.taskStateDir(changeName), { recursive: true });
96621
+ await mkdir7(wtLayout.changeDir(changeName), { recursive: true });
96622
+ await mkdir7(wtLayout.taskStateDir(changeName), { recursive: true });
95900
96623
  }
95901
96624
  cwdByChange.set(changeName, workerCwd);
95902
96625
  statesDirByChange.set(changeName, scaffoldStatesDir);
@@ -95905,7 +96628,7 @@ function buildAgentCoordinator(input) {
95905
96628
  branchByChange.set(changeName, branch);
95906
96629
  if (mode === "review") {
95907
96630
  const wtLayout = projectLayout(workerCwd);
95908
- const tasksFile = join21(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
96631
+ const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
95909
96632
  let body;
95910
96633
  let heading;
95911
96634
  if (trigger) {
@@ -95930,7 +96653,7 @@ function buildAgentCoordinator(input) {
95930
96653
  await reactivateState2(wtLayout.stateFile(changeName), changeName);
95931
96654
  } else if (mode === "conflict-fix") {
95932
96655
  const wtLayout = projectLayout(workerCwd);
95933
- const tasksFile = join21(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
96656
+ const tasksFile = join22(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
95934
96657
  const prUrl = prByChange.get(changeName);
95935
96658
  const body = [
95936
96659
  `The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
@@ -96010,7 +96733,7 @@ PR: ${prUrl}` : ""
96010
96733
  return c;
96011
96734
  }
96012
96735
  function defaultSpawn(changeName, cmd, cwd2, note) {
96013
- const logFilePath = join21(logsDir, `${changeName}.log`);
96736
+ const logFilePath = join22(logsDir, `${changeName}.log`);
96014
96737
  const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
96015
96738
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
96016
96739
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -96069,10 +96792,15 @@ PR: ${prUrl}` : ""
96069
96792
  function spawnWorker(changeName) {
96070
96793
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
96071
96794
  const injected = input.runners?.spawnWorker;
96795
+ const missionTasksPath = join22(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
96796
+ const prevTasksPromise = (async () => {
96797
+ const f2 = Bun.file(missionTasksPath);
96798
+ return await f2.exists() ? await f2.text() : "";
96799
+ })();
96072
96800
  let logFilePath;
96073
96801
  let handle;
96074
96802
  if (injected) {
96075
- logFilePath = join21(logsDir, `${changeName}.log`);
96803
+ logFilePath = join22(logsDir, `${changeName}.log`);
96076
96804
  handle = injected(buildTaskCmdFor(changeName), cwd2);
96077
96805
  } else {
96078
96806
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
@@ -96094,6 +96822,21 @@ PR: ${prUrl}` : ""
96094
96822
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
96095
96823
  const wrapped = handle.exited.then(async (code) => {
96096
96824
  const workerLayout = projectLayout(cwd2);
96825
+ try {
96826
+ const prevTasks = await prevTasksPromise;
96827
+ const nextFile = Bun.file(missionTasksPath);
96828
+ if (await nextFile.exists()) {
96829
+ const nextTasks = await nextFile.text();
96830
+ const report = normalizeNewlyAppendedSectionWithReport(prevTasks, nextTasks);
96831
+ if (report.text !== nextTasks) {
96832
+ await Bun.write(missionTasksPath, report.text);
96833
+ const sections = report.headings.map((h) => `## ${h}`).join(", ");
96834
+ onLog(`! normalized ${report.count} pre-checked item(s) in newly added section(s) ${sections}`, "yellow");
96835
+ }
96836
+ }
96837
+ } catch (err) {
96838
+ onLog(`! tasks.md normalization failed: ${err.message}`, "yellow");
96839
+ }
96097
96840
  const effectiveCode = await runPostTask({
96098
96841
  changeName,
96099
96842
  cwd: cwd2,
@@ -96117,6 +96860,7 @@ PR: ${prUrl}` : ""
96117
96860
  ignoreCiChecks: cfg.ignoreCiChecks,
96118
96861
  stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
96119
96862
  neverTouch: cfg.boundaries.never_touch,
96863
+ metaOnlyFiles: cfg.boundaries.meta_only_files,
96120
96864
  manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled
96121
96865
  },
96122
96866
  respawnWorker: respawn
@@ -96327,19 +97071,34 @@ PR: ${prUrl}` : ""
96327
97071
  const handle = cfg.linear.mentionHandle;
96328
97072
  let candidates = [];
96329
97073
  try {
96330
- candidates = await fetchDoneCandidates();
97074
+ candidates = await fetchMentionScanIssues(apiKey, { team, assignee });
96331
97075
  } catch (err) {
96332
- onLog(`! mention scan: fetchDoneCandidates failed: ${err.message}`, "yellow");
97076
+ if (isRateLimitedError(err)) {
97077
+ onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
97078
+ return [];
97079
+ }
97080
+ onLog(`! mention scan: fetchMentionScanIssues failed: ${formatLinearError(err)}`, "yellow");
96333
97081
  return [];
96334
97082
  }
96335
97083
  const out = [];
96336
97084
  const queued = new Set;
97085
+ let rateLimitedLogged = false;
97086
+ const logRateLimited = () => {
97087
+ if (rateLimitedLogged)
97088
+ return;
97089
+ rateLimitedLogged = true;
97090
+ onLog(`! mention scan: rate limited, deferring rest of scan to next poll`, "yellow");
97091
+ };
96337
97092
  for (const issue2 of candidates) {
96338
97093
  let comments = [];
96339
97094
  try {
96340
97095
  comments = await fetchIssueComments(apiKey, issue2.id);
96341
97096
  } catch (err) {
96342
- onLog(`! mention scan: Linear comments failed for ${issue2.identifier}: ${err.message}`, "yellow");
97097
+ if (isRateLimitedError(err)) {
97098
+ logRateLimited();
97099
+ break;
97100
+ }
97101
+ onLog(`! mention scan: Linear comments failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
96343
97102
  continue;
96344
97103
  }
96345
97104
  const lastRalphPickup = findLastRalphPickupISO(comments);
@@ -96364,11 +97123,18 @@ PR: ${prUrl}` : ""
96364
97123
  try {
96365
97124
  await addReactionToComment(apiKey, c.id, "\uD83D\uDC40");
96366
97125
  } catch (err) {
96367
- onLog(`! mention scan: Linear reaction failed for ${issue2.identifier}: ${err.message}`, "yellow");
97126
+ if (isRateLimitedError(err)) {
97127
+ logRateLimited();
97128
+ queued.add(issue2.id);
97129
+ break;
97130
+ }
97131
+ onLog(`! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
96368
97132
  }
96369
97133
  queued.add(issue2.id);
96370
97134
  break;
96371
97135
  }
97136
+ if (rateLimitedLogged)
97137
+ break;
96372
97138
  if (queued.has(issue2.id))
96373
97139
  continue;
96374
97140
  }
@@ -96398,7 +97164,7 @@ PR: ${prUrl}` : ""
96398
97164
  try {
96399
97165
  await addGithubReactionToComment({ owner, repo, kind: "issue" }, c.id, "\uD83D\uDC40");
96400
97166
  } catch (err) {
96401
- onLog(`! mention scan: GitHub reaction failed for ${prUrl}: ${err.message}`, "yellow");
97167
+ onLog(`! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
96402
97168
  }
96403
97169
  }
96404
97170
  queued.add(issue2.id);
@@ -96428,7 +97194,9 @@ PR: ${prUrl}` : ""
96428
97194
  const last2 = t.comments[t.comments.length - 1].createdAt;
96429
97195
  return last2 > acc ? last2 : acc;
96430
97196
  }, "");
96431
- if (!lastRalphPickup || newestReviewerActivity > lastRalphPickup) {
97197
+ const lastHandled = lastHandledReviewActivity.get(prUrl) ?? null;
97198
+ const effectiveLastHandled = lastRalphPickup && lastHandled ? lastRalphPickup > lastHandled ? lastRalphPickup : lastHandled : lastRalphPickup ?? lastHandled;
97199
+ if (!effectiveLastHandled || newestReviewerActivity > effectiveLastHandled) {
96432
97200
  const body = unresolved.map((t) => {
96433
97201
  const head3 = t.path ? `_${t.path}${t.line ? `:${t.line}` : ""}_` : "_(general)_";
96434
97202
  const lines = t.comments.map((c) => `> **${c.author ?? "reviewer"}** (${c.createdAt})
@@ -96442,6 +97210,7 @@ PR: ${prUrl}` : ""
96442
97210
  ---
96443
97211
 
96444
97212
  `);
97213
+ lastHandledReviewActivity.set(prUrl, newestReviewerActivity);
96445
97214
  return {
96446
97215
  source: "github-review",
96447
97216
  body,
@@ -96589,10 +97358,16 @@ PR: ${prUrl}` : ""
96589
97358
  const parsed = JSON.parse(res.stdout || "[]");
96590
97359
  return parsed;
96591
97360
  } catch (err) {
96592
- onLog(`! mention scan: gh comments failed for ${prUrl}: ${err.message}`, "yellow");
97361
+ onLog(`! mention scan: gh comments failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
96593
97362
  return [];
96594
97363
  }
96595
97364
  }
97365
+ const commentSyncEnabled = Boolean(cfg.linear.syncTasksToComment && apiKey);
97366
+ const commentMutations = {
97367
+ createIssueComment,
97368
+ updateIssueComment,
97369
+ deleteIssueComment
97370
+ };
96596
97371
  const coord = new AgentCoordinator({
96597
97372
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
96598
97373
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
@@ -96620,7 +97395,65 @@ PR: ${prUrl}` : ""
96620
97395
  return 0;
96621
97396
  const json2 = await file2.json();
96622
97397
  return json2.iteration ?? 0;
96623
- }
97398
+ },
97399
+ ...commentSyncEnabled ? {
97400
+ syncTasks: async (worker, iteration) => {
97401
+ const root = cwdByChange.get(worker.changeName) ?? projectRoot;
97402
+ const layout = projectLayout(root);
97403
+ const changeDir = layout.changeDir(worker.changeName);
97404
+ const statePath = layout.stateFile(worker.changeName);
97405
+ await postPlanCommentOnce({
97406
+ apiKey,
97407
+ issueId: worker.issueId,
97408
+ statePath,
97409
+ changeDir,
97410
+ changeName: worker.changeName,
97411
+ log: onLog,
97412
+ mutations: commentMutations
97413
+ });
97414
+ await postOrUpdateTasksComment({
97415
+ apiKey,
97416
+ issueId: worker.issueId,
97417
+ statePath,
97418
+ changeDir,
97419
+ changeName: worker.changeName,
97420
+ iteration,
97421
+ log: onLog,
97422
+ mutations: commentMutations
97423
+ });
97424
+ },
97425
+ onSteeringAppended: async (changeName, message) => {
97426
+ const root = cwdByChange.get(changeName) ?? projectRoot;
97427
+ const layout = projectLayout(root);
97428
+ const changeDir = layout.changeDir(changeName);
97429
+ const statePath = layout.stateFile(changeName);
97430
+ const issue2 = issueByChange.get(changeName) ?? null;
97431
+ const issueId = issue2?.id ?? null;
97432
+ if (!issueId) {
97433
+ onLog(` comment-sync: no Linear issue cached for ${changeName}; skipping steering refresh`, "gray");
97434
+ return;
97435
+ }
97436
+ let iteration = 0;
97437
+ try {
97438
+ const f2 = Bun.file(statePath);
97439
+ if (await f2.exists()) {
97440
+ const json2 = await f2.json();
97441
+ iteration = json2.iteration ?? 0;
97442
+ }
97443
+ } catch {}
97444
+ await postSteeringAndRefreshTasks({
97445
+ apiKey,
97446
+ issueId,
97447
+ statePath,
97448
+ changeDir,
97449
+ changeName,
97450
+ iteration,
97451
+ message,
97452
+ log: onLog,
97453
+ mutations: commentMutations
97454
+ });
97455
+ }
97456
+ } : {}
96624
97457
  }, {
96625
97458
  concurrency,
96626
97459
  ...indicators.setInProgress !== undefined ? { setInProgress: indicators.setInProgress } : {},
@@ -96687,6 +97520,7 @@ PR: ${prUrl}` : ""
96687
97520
  concurrency,
96688
97521
  pollInterval,
96689
97522
  getWorkerCwd: (changeName) => cwdByChange.get(changeName),
97523
+ syncTasksEnabled: commentSyncEnabled,
96690
97524
  runBaselineGate: runBaselineGateOnce
96691
97525
  };
96692
97526
  }
@@ -96715,6 +97549,7 @@ var init_wire = __esm(() => {
96715
97549
  init_tasks_md();
96716
97550
  init_workflow();
96717
97551
  init_types2();
97552
+ init_linear();
96718
97553
  init_coordinator();
96719
97554
  init_scaffold();
96720
97555
  init_worktree();
@@ -96722,6 +97557,7 @@ var init_wire = __esm(() => {
96722
97557
  init_post_task();
96723
97558
  init_gate();
96724
97559
  init_workflow();
97560
+ init_comment_sync();
96725
97561
  GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
96726
97562
  bunGitRunner = {
96727
97563
  run: async (args, cwd2) => {
@@ -96840,21 +97676,26 @@ function readSize2() {
96840
97676
  rows: process.stdout.rows ?? 24
96841
97677
  };
96842
97678
  }
97679
+ function clearScreenAndScrollback2() {
97680
+ if (process.stdout.isTTY)
97681
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
97682
+ }
96843
97683
  function useTerminalSize2() {
96844
- const [size2, setSize] = import_react59.useState(() => ({
96845
- ...readSize2(),
96846
- resizeKey: 0
96847
- }));
97684
+ const initial2 = import_react59.useRef({ ...readSize2(), resizeKey: 0 });
97685
+ const [size2, setSize] = import_react59.useState(initial2.current);
97686
+ const sizeRef = import_react59.useRef(initial2.current);
96848
97687
  import_react59.useEffect(() => {
96849
97688
  if (!process.stdout.isTTY)
96850
97689
  return;
96851
97690
  const onResize = () => {
96852
97691
  const { columns, rows } = readSize2();
96853
- setSize((prev) => {
96854
- if (prev.columns === columns && prev.rows === rows)
96855
- return prev;
96856
- return { columns, rows, resizeKey: prev.resizeKey + 1 };
96857
- });
97692
+ const prev = sizeRef.current;
97693
+ if (prev.columns === columns && prev.rows === rows)
97694
+ return;
97695
+ clearScreenAndScrollback2();
97696
+ const next = { columns, rows, resizeKey: prev.resizeKey + 1 };
97697
+ sizeRef.current = next;
97698
+ setSize(next);
96858
97699
  };
96859
97700
  process.stdout.on("resize", onResize);
96860
97701
  return () => {
@@ -97044,7 +97885,7 @@ var init_SteeringField = __esm(async () => {
97044
97885
  });
97045
97886
 
97046
97887
  // apps/agent/src/components/AgentMode.tsx
97047
- import { join as join22 } from "path";
97888
+ import { join as join23 } from "path";
97048
97889
  async function appendSteeringImpl(changeDir, message) {
97049
97890
  await runWithContext(createDefaultContext(), async () => {
97050
97891
  appendSteeringMessage(changeDir, message);
@@ -97295,24 +98136,19 @@ function AgentMode({
97295
98136
  loadConfig = loadRalphyConfig
97296
98137
  }) {
97297
98138
  const { exit } = use_app_default();
97298
- const { stdout } = use_stdout_default();
97299
98139
  const { isRawModeSupported } = use_stdin_default();
97300
98140
  const { columns, rows, resizeKey } = useTerminalSize2();
97301
- import_react61.useEffect(() => {
97302
- if (resizeKey === 0)
97303
- return;
97304
- stdout.write("\x1B[2J\x1B[3J\x1B[H");
97305
- }, [resizeKey, stdout]);
97306
98141
  const [logs, setLogs] = import_react61.useState([]);
97307
98142
  const [, setTick] = import_react61.useState(0);
97308
98143
  const [clock, setClock] = import_react61.useState(0);
97309
98144
  const [focusedIdx, setFocusedIdx] = import_react61.useState(0);
97310
- const [showPendingTasks, setShowPendingTasks] = import_react61.useState(true);
98145
+ const [showPendingTasks, setShowPendingTasks] = import_react61.useState(false);
97311
98146
  const [showAllSubtasks, setShowAllSubtasks] = import_react61.useState(false);
97312
98147
  const coordRef = import_react61.useRef(null);
97313
98148
  const workerMetaRef = import_react61.useRef(new Map);
97314
98149
  const nextPollAtRef = import_react61.useRef(0);
97315
98150
  const cfgRef = import_react61.useRef(null);
98151
+ const [effective, setEffective] = import_react61.useState(null);
97316
98152
  const [pollStatus, setPollStatus] = import_react61.useState({
97317
98153
  state: "idle",
97318
98154
  lastFound: null,
@@ -97411,6 +98247,7 @@ function AgentMode({
97411
98247
  m.prUrl = prUrl;
97412
98248
  }
97413
98249
  });
98250
+ setEffective({ concurrency, pollInterval });
97414
98251
  coordRef.current = coord2;
97415
98252
  await coord2.init();
97416
98253
  const tick = async () => {
@@ -97503,7 +98340,7 @@ function AgentMode({
97503
98340
  (async () => {
97504
98341
  for (const [changeName, meta3] of workerMetaRef.current) {
97505
98342
  try {
97506
- const file2 = Bun.file(join22(meta3.statesDir, changeName, ".ralph-state.json"));
98343
+ const file2 = Bun.file(join23(meta3.statesDir, changeName, ".ralph-state.json"));
97507
98344
  if (await file2.exists()) {
97508
98345
  const json2 = await file2.json();
97509
98346
  meta3.iter = json2.iteration ?? meta3.iter;
@@ -97513,9 +98350,9 @@ function AgentMode({
97513
98350
  }
97514
98351
  if (meta3.changeDir) {
97515
98352
  try {
97516
- const tasksFile = Bun.file(join22(meta3.changeDir, "tasks.md"));
97517
- const proposalFile = Bun.file(join22(meta3.changeDir, "proposal.md"));
97518
- const designFile = Bun.file(join22(meta3.changeDir, "design.md"));
98353
+ const tasksFile = Bun.file(join23(meta3.changeDir, "tasks.md"));
98354
+ const proposalFile = Bun.file(join23(meta3.changeDir, "proposal.md"));
98355
+ const designFile = Bun.file(join23(meta3.changeDir, "design.md"));
97519
98356
  const [tasksText, proposalText, designText] = await Promise.all([
97520
98357
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
97521
98358
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -97670,14 +98507,14 @@ function AgentMode({
97670
98507
  dimColor: true,
97671
98508
  children: [
97672
98509
  " \u2502 \xD7",
97673
- cfg.concurrency
98510
+ effective?.concurrency ?? cfg.concurrency
97674
98511
  ]
97675
98512
  }, undefined, true, undefined, this),
97676
98513
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97677
98514
  dimColor: true,
97678
98515
  children: [
97679
98516
  " \u2502 poll ",
97680
- cfg.pollIntervalSeconds,
98517
+ effective?.pollInterval ?? cfg.pollIntervalSeconds,
97681
98518
  "s"
97682
98519
  ]
97683
98520
  }, undefined, true, undefined, this),
@@ -97794,18 +98631,6 @@ function AgentMode({
97794
98631
  dimColor: true,
97795
98632
  children: "\xB7"
97796
98633
  }, 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
98634
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97810
98635
  dimColor: true,
97811
98636
  children: "review"
@@ -97836,6 +98661,7 @@ function AgentMode({
97836
98661
  children: [
97837
98662
  secsToNextPoll !== null ? /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Box_default, {
97838
98663
  gap: 1,
98664
+ width: 7,
97839
98665
  children: [
97840
98666
  /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
97841
98667
  dimColor: true,
@@ -98357,11 +99183,14 @@ function AgentMode({
98357
99183
  },
98358
99184
  onSubmit: async (message) => {
98359
99185
  try {
98360
- await appendSteering(join22(tasksDir, w.changeName), message);
99186
+ await appendSteering(join23(tasksDir, w.changeName), message);
98361
99187
  } catch (err) {
98362
99188
  appendLog(`! steering append failed for ${w.changeName}: ${err.message}`, "red");
98363
99189
  throw err;
98364
99190
  }
99191
+ try {
99192
+ await coordRef.current?.notifySteeringAppended?.(w.changeName, message);
99193
+ } catch {}
98365
99194
  const restarted = await coordRef.current?.restartWorker(w.changeName);
98366
99195
  if (restarted) {
98367
99196
  appendLog(` ${w.changeName}: steering applied, restarting worker`, "cyan");
@@ -98561,7 +99390,7 @@ var exports_list = {};
98561
99390
  __export(exports_list, {
98562
99391
  runList: () => runList
98563
99392
  });
98564
- import { join as join23 } from "path";
99393
+ import { join as join24 } from "path";
98565
99394
  function countTaskItems(content) {
98566
99395
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
98567
99396
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -98574,13 +99403,13 @@ function buildLocalRows(statesDir, projectRoot) {
98574
99403
  const sources = [{ dir: statesDir, label: "main" }];
98575
99404
  const worktreesRoot = worktreesDir2(projectRoot);
98576
99405
  for (const wt of storage.list(worktreesRoot)) {
98577
- sources.push({ dir: join23(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
99406
+ sources.push({ dir: join24(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
98578
99407
  }
98579
99408
  for (const { dir, label } of sources) {
98580
99409
  for (const entry of storage.list(dir)) {
98581
99410
  if (seen.has(entry))
98582
99411
  continue;
98583
- const raw = storage.read(join23(dir, entry, ".ralph-state.json"));
99412
+ const raw = storage.read(join24(dir, entry, ".ralph-state.json"));
98584
99413
  if (raw === null)
98585
99414
  continue;
98586
99415
  let state;
@@ -98595,7 +99424,7 @@ function buildLocalRows(statesDir, projectRoot) {
98595
99424
  const firstLine = promptRaw.split(`
98596
99425
  `).find((l) => l.trim() !== "") ?? "";
98597
99426
  let progress = "\u2014";
98598
- const tasksContent = storage.read(join23(dir, entry, "tasks.md"));
99427
+ const tasksContent = storage.read(join24(dir, entry, "tasks.md"));
98599
99428
  if (tasksContent !== null) {
98600
99429
  const { checked, unchecked } = countTaskItems(tasksContent);
98601
99430
  const total = checked + unchecked;
@@ -98989,6 +99818,7 @@ var init_list = __esm(() => {
98989
99818
  init_types2();
98990
99819
  init_worktree();
98991
99820
  init_config();
99821
+ init_linear();
98992
99822
  init_list_sort();
98993
99823
  localCmdRunner = {
98994
99824
  run: async (cmd, cwd2) => {
@@ -99011,8 +99841,8 @@ var exports_json_runner = {};
99011
99841
  __export(exports_json_runner, {
99012
99842
  runAgentJson: () => runAgentJson
99013
99843
  });
99014
- import { join as join24 } from "path";
99015
- import { mkdir as mkdir7 } from "fs/promises";
99844
+ import { join as join25 } from "path";
99845
+ import { mkdir as mkdir8 } from "fs/promises";
99016
99846
  import { homedir as homedir5 } from "os";
99017
99847
  function cleanOutputLine2(raw) {
99018
99848
  const clean = raw.replace(ANSI_STRIP_RE2, "").trim();
@@ -99036,7 +99866,7 @@ async function runAgentJson({
99036
99866
  statesDir,
99037
99867
  tasksDir
99038
99868
  }) {
99039
- await mkdir7(join24(homedir5(), ".ralph"), { recursive: true }).catch(() => {
99869
+ await mkdir8(join25(homedir5(), ".ralph"), { recursive: true }).catch(() => {
99040
99870
  return;
99041
99871
  });
99042
99872
  const cfgPath = await ensureRalphyConfig(projectRoot);
@@ -99190,8 +100020,8 @@ var exports_src2 = {};
99190
100020
  __export(exports_src2, {
99191
100021
  main: () => main2
99192
100022
  });
99193
- import { mkdir as mkdir8 } from "fs/promises";
99194
- import { join as join25 } from "path";
100023
+ import { mkdir as mkdir9 } from "fs/promises";
100024
+ import { join as join26 } from "path";
99195
100025
  async function main2(argv) {
99196
100026
  if (argv.includes("--help") || argv.includes("-h")) {
99197
100027
  printHelp2();
@@ -99225,9 +100055,9 @@ async function main2(argv) {
99225
100055
  });
99226
100056
  return typeof process.exitCode === "number" ? process.exitCode : 0;
99227
100057
  }
99228
- await mkdir8(statesDir, { recursive: true });
99229
- await mkdir8(tasksDir, { recursive: true });
99230
- await mkdir8(join25(projectRoot, ".ralph"), { recursive: true });
100058
+ await mkdir9(statesDir, { recursive: true });
100059
+ await mkdir9(tasksDir, { recursive: true });
100060
+ await mkdir9(join26(projectRoot, ".ralph"), { recursive: true });
99231
100061
  if (args.jsonOutput) {
99232
100062
  const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
99233
100063
  await runAgentJson2({ args, projectRoot, statesDir, tasksDir });