@herbcaudill/ralph 1.0.2 → 1.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/dist/index.js CHANGED
@@ -134,14 +134,183 @@ var EnhancedTextInput = ({
134
134
  };
135
135
 
136
136
  // src/components/SessionRunner.tsx
137
- import { appendFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
138
- import { join as join10, basename } from "path";
137
+ import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync3 } from "fs";
138
+ import { join as join12, basename } from "path";
139
+
140
+ // ../shared/dist/persistence/SessionPersister.js
141
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "fs";
142
+ import { appendFile, readFile } from "fs/promises";
143
+ import { join } from "path";
144
+ var SessionPersister = class {
145
+ constructor(storageDir) {
146
+ this.storageDir = storageDir;
147
+ if (!existsSync(storageDir)) {
148
+ mkdirSync(storageDir, { recursive: true });
149
+ }
150
+ }
151
+ /** Append an event to a session's JSONL file. */
152
+ async appendEvent(sessionId, event, app) {
153
+ const filePath = this.sessionPath(sessionId, app);
154
+ const dir = this.getAppDir(app);
155
+ if (!existsSync(dir)) {
156
+ mkdirSync(dir, { recursive: true });
157
+ }
158
+ const line = JSON.stringify(event) + "\n";
159
+ await appendFile(filePath, line, "utf-8");
160
+ }
161
+ /** Read all events for a session. */
162
+ async readEvents(sessionId, app) {
163
+ const filePath = this.sessionPath(sessionId, app);
164
+ if (!existsSync(filePath))
165
+ return [];
166
+ const content = await readFile(filePath, "utf-8");
167
+ return content.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
168
+ }
169
+ /** Read events since a given timestamp. */
170
+ async readEventsSince(sessionId, since, app) {
171
+ const events = await this.readEvents(sessionId, app);
172
+ return events.filter((e) => e.timestamp >= since);
173
+ }
174
+ /**
175
+ * List all session IDs (derived from JSONL filenames).
176
+ * If app is provided, lists sessions only from that app's directory.
177
+ * If app is undefined, lists sessions from all directories including root.
178
+ */
179
+ listSessions(app) {
180
+ return this.listSessionsWithApp(app).map((s) => s.sessionId);
181
+ }
182
+ /**
183
+ * List all sessions with their app namespace.
184
+ * If app is provided, lists sessions only from that app's directory.
185
+ * If app is undefined, lists sessions from all directories including root.
186
+ */
187
+ listSessionsWithApp(app) {
188
+ if (!existsSync(this.storageDir))
189
+ return [];
190
+ if (app !== void 0) {
191
+ const appDir = this.getAppDir(app);
192
+ if (!existsSync(appDir))
193
+ return [];
194
+ return readdirSync(appDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ sessionId: f.replace(/\.jsonl$/, ""), app }));
195
+ }
196
+ const sessions = [];
197
+ const entries = readdirSync(this.storageDir, { withFileTypes: true });
198
+ for (const entry of entries) {
199
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
200
+ sessions.push({ sessionId: entry.name.replace(/\.jsonl$/, ""), app: void 0 });
201
+ } else if (entry.isDirectory()) {
202
+ const appDir = join(this.storageDir, entry.name);
203
+ for (const file of readdirSync(appDir)) {
204
+ if (file.endsWith(".jsonl")) {
205
+ sessions.push({ sessionId: file.replace(/\.jsonl$/, ""), app: entry.name });
206
+ }
207
+ }
208
+ }
209
+ }
210
+ return sessions;
211
+ }
212
+ /** Get the most recently created session ID, or null if none. */
213
+ getLatestSessionId(app) {
214
+ const sessions = this.listSessions(app);
215
+ if (sessions.length === 0)
216
+ return null;
217
+ let latest = null;
218
+ for (const id of sessions) {
219
+ const filePath = app ? this.sessionPath(id, app) : this.findSessionPath(id);
220
+ if (!filePath || !existsSync(filePath))
221
+ continue;
222
+ const stat = statSync(filePath);
223
+ if (!latest || stat.birthtimeMs > latest.birthtime) {
224
+ latest = { id, birthtime: stat.birthtimeMs };
225
+ }
226
+ }
227
+ return latest?.id ?? null;
228
+ }
229
+ /** Delete a session's JSONL file. */
230
+ deleteSession(sessionId, app) {
231
+ const filePath = this.sessionPath(sessionId, app);
232
+ if (existsSync(filePath)) {
233
+ unlinkSync(filePath);
234
+ }
235
+ }
236
+ /** Read session metadata from the first event (session_created) in the JSONL file. */
237
+ readSessionMetadata(sessionId, app) {
238
+ const filePath = this.sessionPath(sessionId, app);
239
+ if (!existsSync(filePath))
240
+ return null;
241
+ try {
242
+ const content = readFileSync(filePath, "utf-8");
243
+ const firstLine = content.split("\n").find((line) => line.trim());
244
+ if (!firstLine)
245
+ return null;
246
+ const event = JSON.parse(firstLine);
247
+ if (event.type === "session_created") {
248
+ return {
249
+ adapter: event.adapter ?? "claude",
250
+ cwd: event.cwd,
251
+ createdAt: event.timestamp ?? 0,
252
+ app: event.app,
253
+ systemPrompt: event.systemPrompt
254
+ };
255
+ }
256
+ return null;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ /** Check if a session exists. */
262
+ hasSession(sessionId, app) {
263
+ return existsSync(this.sessionPath(sessionId, app));
264
+ }
265
+ /** Get the full file path for a session. */
266
+ getSessionPath(sessionId, app) {
267
+ return this.sessionPath(sessionId, app);
268
+ }
269
+ /** Get the directory for an app. */
270
+ getAppDir(app) {
271
+ return app ? join(this.storageDir, app) : this.storageDir;
272
+ }
273
+ /** Get the file path for a session. */
274
+ sessionPath(sessionId, app) {
275
+ return join(this.getAppDir(app), `${sessionId}.jsonl`);
276
+ }
277
+ /** Find the session path by searching root and app directories. */
278
+ findSessionPath(sessionId) {
279
+ const rootPath = join(this.storageDir, `${sessionId}.jsonl`);
280
+ if (existsSync(rootPath))
281
+ return rootPath;
282
+ if (existsSync(this.storageDir)) {
283
+ const entries = readdirSync(this.storageDir, { withFileTypes: true });
284
+ for (const entry of entries) {
285
+ if (entry.isDirectory()) {
286
+ const appPath = join(this.storageDir, entry.name, `${sessionId}.jsonl`);
287
+ if (existsSync(appPath))
288
+ return appPath;
289
+ }
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+ };
295
+
296
+ // ../shared/dist/persistence/getDefaultStorageDir.js
297
+ import { homedir, platform } from "os";
298
+ import { join as join2 } from "path";
299
+ function getDefaultStorageDir() {
300
+ if (platform() === "win32") {
301
+ const localAppData = process.env.LOCALAPPDATA ?? join2(homedir(), "AppData", "Local");
302
+ return join2(localAppData, "ralph", "agent-sessions");
303
+ }
304
+ return join2(homedir(), ".local", "share", "ralph", "agent-sessions");
305
+ }
306
+
307
+ // src/components/SessionRunner.tsx
139
308
  import { query } from "@anthropic-ai/claude-agent-sdk";
140
309
 
141
310
  // src/lib/addTodo.ts
142
311
  import { execSync } from "child_process";
143
- import { existsSync, readFileSync, writeFileSync } from "fs";
144
- import { join } from "path";
312
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
313
+ import { join as join3 } from "path";
145
314
 
146
315
  // src/lib/insertTodo.ts
147
316
  var insertTodo = (content, description) => {
@@ -164,8 +333,8 @@ ${content}`;
164
333
 
165
334
  // src/lib/addTodo.ts
166
335
  var addTodo = (description, cwd = process.cwd()) => {
167
- const todoPath = join(cwd, ".ralph", "todo.md");
168
- const content = existsSync(todoPath) ? readFileSync(todoPath, "utf-8") : "";
336
+ const todoPath = join3(cwd, ".ralph", "todo.md");
337
+ const content = existsSync2(todoPath) ? readFileSync2(todoPath, "utf-8") : "";
169
338
  const newContent = insertTodo(content, description);
170
339
  writeFileSync(todoPath, newContent);
171
340
  let indexContent = "";
@@ -196,8 +365,8 @@ var addTodo = (description, cwd = process.cwd()) => {
196
365
  };
197
366
 
198
367
  // src/lib/getProgress.ts
199
- import { existsSync as existsSync2 } from "fs";
200
- import { join as join3 } from "path";
368
+ import { existsSync as existsSync3 } from "fs";
369
+ import { join as join5 } from "path";
201
370
 
202
371
  // src/lib/getBeadsProgress.ts
203
372
  import { execSync as execSync2 } from "child_process";
@@ -234,13 +403,13 @@ var getBeadsProgress = (initialCount, startupTimestamp) => {
234
403
  };
235
404
 
236
405
  // src/lib/getTodoProgress.ts
237
- import { readFileSync as readFileSync2 } from "fs";
238
- import { join as join2 } from "path";
239
- var ralphDir = join2(process.cwd(), ".ralph");
240
- var todoFile = join2(ralphDir, "todo.md");
406
+ import { readFileSync as readFileSync3 } from "fs";
407
+ import { join as join4 } from "path";
408
+ var ralphDir = join4(process.cwd(), ".ralph");
409
+ var todoFile = join4(ralphDir, "todo.md");
241
410
  var getTodoProgress = () => {
242
411
  try {
243
- const content = readFileSync2(todoFile, "utf-8");
412
+ const content = readFileSync3(todoFile, "utf-8");
244
413
  const uncheckedMatches = content.match(/- \[ \]/g);
245
414
  const unchecked = uncheckedMatches ? uncheckedMatches.length : 0;
246
415
  const checkedMatches = content.match(/- \[[xX]\]/g);
@@ -253,22 +422,22 @@ var getTodoProgress = () => {
253
422
  };
254
423
 
255
424
  // src/lib/getProgress.ts
256
- var beadsDir = join3(process.cwd(), ".beads");
257
- var ralphDir2 = join3(process.cwd(), ".ralph");
258
- var todoFile2 = join3(ralphDir2, "todo.md");
425
+ var beadsDir = join5(process.cwd(), ".beads");
426
+ var ralphDir2 = join5(process.cwd(), ".ralph");
427
+ var todoFile2 = join5(ralphDir2, "todo.md");
259
428
  var getProgress = (initialCount, startupTimestamp) => {
260
- if (existsSync2(beadsDir)) {
429
+ if (existsSync3(beadsDir)) {
261
430
  return getBeadsProgress(initialCount, startupTimestamp);
262
431
  }
263
- if (existsSync2(todoFile2)) {
432
+ if (existsSync3(todoFile2)) {
264
433
  return getTodoProgress();
265
434
  }
266
435
  return { type: "none", completed: 0, total: 0 };
267
436
  };
268
437
 
269
438
  // src/lib/captureStartupSnapshot.ts
270
- import { existsSync as existsSync3 } from "fs";
271
- import { join as join5 } from "path";
439
+ import { existsSync as existsSync4 } from "fs";
440
+ import { join as join7 } from "path";
272
441
 
273
442
  // src/lib/captureBeadsSnapshot.ts
274
443
  import { execSync as execSync3 } from "child_process";
@@ -300,13 +469,13 @@ var captureBeadsSnapshot = () => {
300
469
  };
301
470
 
302
471
  // src/lib/captureTodoSnapshot.ts
303
- import { readFileSync as readFileSync3 } from "fs";
304
- import { join as join4 } from "path";
305
- var ralphDir3 = join4(process.cwd(), ".ralph");
306
- var todoFile3 = join4(ralphDir3, "todo.md");
472
+ import { readFileSync as readFileSync4 } from "fs";
473
+ import { join as join6 } from "path";
474
+ var ralphDir3 = join6(process.cwd(), ".ralph");
475
+ var todoFile3 = join6(ralphDir3, "todo.md");
307
476
  var captureTodoSnapshot = () => {
308
477
  try {
309
- const content = readFileSync3(todoFile3, "utf-8");
478
+ const content = readFileSync4(todoFile3, "utf-8");
310
479
  const uncheckedMatches = content.match(/- \[ \]/g);
311
480
  const unchecked = uncheckedMatches ? uncheckedMatches.length : 0;
312
481
  const checkedMatches = content.match(/- \[[xX]\]/g);
@@ -322,14 +491,14 @@ var captureTodoSnapshot = () => {
322
491
  };
323
492
 
324
493
  // src/lib/captureStartupSnapshot.ts
325
- var beadsDir2 = join5(process.cwd(), ".beads");
326
- var ralphDir4 = join5(process.cwd(), ".ralph");
327
- var todoFile4 = join5(ralphDir4, "todo.md");
494
+ var beadsDir2 = join7(process.cwd(), ".beads");
495
+ var ralphDir4 = join7(process.cwd(), ".ralph");
496
+ var todoFile4 = join7(ralphDir4, "todo.md");
328
497
  var captureStartupSnapshot = () => {
329
- if (existsSync3(beadsDir2)) {
498
+ if (existsSync4(beadsDir2)) {
330
499
  return captureBeadsSnapshot();
331
500
  }
332
- if (existsSync3(todoFile4)) {
501
+ if (existsSync4(todoFile4)) {
333
502
  return captureTodoSnapshot();
334
503
  }
335
504
  return void 0;
@@ -350,54 +519,66 @@ var ProgressBar = ({ completed, total, width = 12, repoName: repoName3 }) => {
350
519
  return /* @__PURE__ */ React2.createElement(Text2, null, repoName3 && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, repoName3), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " \u2502 ")), /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, filled), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, empty), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", completed, "/", total, " "));
351
520
  };
352
521
 
353
- // src/lib/beadsClient.ts
522
+ // ../../../beads-sdk/dist/exec.js
523
+ import { spawn } from "child_process";
524
+
525
+ // ../../../beads-sdk/dist/socket.js
354
526
  import { createConnection } from "net";
355
- import { join as join6 } from "path";
356
- import { existsSync as existsSync4 } from "fs";
357
- var SOCKET_PATH = join6(process.cwd(), ".beads", "bd.sock");
358
- var BeadsClient = class _BeadsClient {
359
- socket = null;
360
- connected = false;
361
- /**
362
- * Check if the beads daemon socket exists.
363
- */
364
- static socketExists() {
365
- return existsSync4(SOCKET_PATH);
366
- }
367
- /**
368
- * Connect to the beads daemon.
369
- */
527
+ import { join as join8 } from "path";
528
+ import { existsSync as existsSync5 } from "fs";
529
+ var DaemonSocket = class {
530
+ constructor(options = {}) {
531
+ this.socket = null;
532
+ this.connected = false;
533
+ const cwd = options.cwd ?? process.cwd();
534
+ this.socketPath = join8(cwd, ".beads", "bd.sock");
535
+ this.connectTimeout = options.connectTimeout ?? 2e3;
536
+ this.requestTimeout = options.requestTimeout ?? 5e3;
537
+ }
538
+ /** Check if the beads daemon socket file exists. */
539
+ socketExists() {
540
+ return existsSync5(this.socketPath);
541
+ }
542
+ /** Check if connected to the daemon. */
543
+ get isConnected() {
544
+ return this.connected && this.socket !== null;
545
+ }
546
+ /** Connect to the beads daemon. Returns true if connected, false otherwise. */
370
547
  async connect() {
371
- if (!_BeadsClient.socketExists()) {
548
+ if (!this.socketExists())
372
549
  return false;
373
- }
374
- return new Promise((resolve) => {
375
- this.socket = createConnection(SOCKET_PATH);
550
+ if (this.connected && this.socket)
551
+ return true;
552
+ return new Promise((resolve2) => {
553
+ this.socket = createConnection(this.socketPath);
376
554
  const timeout = setTimeout(() => {
377
555
  this.socket?.destroy();
378
556
  this.socket = null;
379
- resolve(false);
380
- }, 2e3);
557
+ resolve2(false);
558
+ }, this.connectTimeout);
381
559
  this.socket.on("connect", () => {
382
560
  clearTimeout(timeout);
383
561
  this.connected = true;
384
- resolve(true);
562
+ resolve2(true);
385
563
  });
386
564
  this.socket.on("error", () => {
387
565
  clearTimeout(timeout);
388
566
  this.socket = null;
389
- resolve(false);
567
+ this.connected = false;
568
+ resolve2(false);
569
+ });
570
+ this.socket.on("close", () => {
571
+ this.socket = null;
572
+ this.connected = false;
390
573
  });
391
574
  });
392
575
  }
393
- /**
394
- * Send an RPC request and wait for response.
395
- */
576
+ /** Send an RPC request and wait for response. */
396
577
  async execute(operation, args = {}) {
397
578
  if (!this.socket || !this.connected) {
398
579
  return null;
399
580
  }
400
- return new Promise((resolve) => {
581
+ return new Promise((resolve2) => {
401
582
  const request = { operation, args };
402
583
  const requestLine = JSON.stringify(request) + "\n";
403
584
  let responseData = "";
@@ -407,24 +588,24 @@ var BeadsClient = class _BeadsClient {
407
588
  cleanup();
408
589
  try {
409
590
  const response = JSON.parse(responseData.trim());
410
- if (response.success && response.data) {
411
- resolve(response.data);
591
+ if (response.success && response.data !== void 0) {
592
+ resolve2(response.data);
412
593
  } else {
413
- resolve(null);
594
+ resolve2(null);
414
595
  }
415
596
  } catch {
416
- resolve(null);
597
+ resolve2(null);
417
598
  }
418
599
  }
419
600
  };
420
601
  const onError = () => {
421
602
  cleanup();
422
- resolve(null);
603
+ resolve2(null);
423
604
  };
424
605
  const timeout = setTimeout(() => {
425
606
  cleanup();
426
- resolve(null);
427
- }, 5e3);
607
+ resolve2(null);
608
+ }, this.requestTimeout);
428
609
  const cleanup = () => {
429
610
  clearTimeout(timeout);
430
611
  this.socket?.off("data", onData);
@@ -437,21 +618,18 @@ var BeadsClient = class _BeadsClient {
437
618
  }
438
619
  /**
439
620
  * Get mutations since a given timestamp.
621
+ * Returns all mutation events that occurred after the specified timestamp.
440
622
  */
441
623
  async getMutations(since = 0) {
442
624
  const result = await this.execute("get_mutations", { since });
443
625
  return result ?? [];
444
626
  }
445
- /**
446
- * Get ready issues (no blockers).
447
- */
627
+ /** Get ready issues (open and unblocked). */
448
628
  async getReady() {
449
629
  const result = await this.execute("ready", {});
450
630
  return result ?? [];
451
631
  }
452
- /**
453
- * Close the connection.
454
- */
632
+ /** Close the connection to the daemon. */
455
633
  close() {
456
634
  if (this.socket) {
457
635
  this.socket.destroy();
@@ -460,50 +638,63 @@ var BeadsClient = class _BeadsClient {
460
638
  }
461
639
  }
462
640
  };
463
- function watchForNewIssues(onNewIssue, interval = 5e3) {
464
- let lastTimestamp = Date.now();
641
+ function watchMutations(onMutation, options = {}) {
642
+ const { cwd, interval = 1e3, since } = options;
643
+ let lastTimestamp = since ?? Date.now();
465
644
  let client = null;
466
645
  let timeoutId = null;
467
646
  let stopped = false;
468
647
  const poll = async () => {
469
- if (stopped) return;
648
+ if (stopped)
649
+ return;
470
650
  if (!client) {
471
- client = new BeadsClient();
651
+ client = new DaemonSocket({ cwd });
472
652
  const connected = await client.connect();
473
653
  if (!connected) {
474
- timeoutId = setTimeout(poll, interval);
654
+ if (!stopped)
655
+ timeoutId = setTimeout(poll, interval);
475
656
  return;
476
657
  }
477
658
  }
478
659
  try {
479
660
  const mutations = await client.getMutations(lastTimestamp);
480
661
  for (const mutation of mutations) {
481
- if (mutation.Type === "create") {
482
- onNewIssue(mutation);
483
- }
662
+ onMutation(mutation);
484
663
  const mutationTime = new Date(mutation.Timestamp).getTime();
485
- if (mutationTime > lastTimestamp) {
664
+ if (mutationTime > lastTimestamp)
486
665
  lastTimestamp = mutationTime;
487
- }
488
666
  }
489
667
  } catch {
490
668
  client?.close();
491
669
  client = null;
492
670
  }
493
- if (!stopped) {
671
+ if (!stopped)
494
672
  timeoutId = setTimeout(poll, interval);
495
- }
496
673
  };
497
674
  poll();
498
675
  return () => {
499
676
  stopped = true;
500
677
  if (timeoutId) {
501
678
  clearTimeout(timeoutId);
679
+ timeoutId = null;
502
680
  }
503
681
  client?.close();
682
+ client = null;
504
683
  };
505
684
  }
506
685
 
686
+ // src/lib/beadsClient.ts
687
+ function watchForNewIssues(onNewIssue, interval = 5e3) {
688
+ return watchMutations(
689
+ (event) => {
690
+ if (event.Type === "create") {
691
+ onNewIssue(event);
692
+ }
693
+ },
694
+ { interval }
695
+ );
696
+ }
697
+
507
698
  // src/lib/debug.ts
508
699
  var isDebugEnabled = (namespace) => {
509
700
  const debugEnv = process.env.RALPH_DEBUG;
@@ -561,9 +752,9 @@ var MessageQueue = class {
561
752
  return;
562
753
  }
563
754
  if (this.resolvers.length > 0) {
564
- const resolve = this.resolvers.shift();
755
+ const resolve2 = this.resolvers.shift();
565
756
  log(`push() resolving pending next() call (${this.resolvers.length} resolvers remaining)`);
566
- resolve({ value: message, done: false });
757
+ resolve2({ value: message, done: false });
567
758
  } else {
568
759
  this.queue.push(message);
569
760
  log(`push() added to queue (queue length: ${this.queue.length})`);
@@ -579,9 +770,9 @@ var MessageQueue = class {
579
770
  }
580
771
  log(`close() called - resolving ${this.resolvers.length} pending resolvers`);
581
772
  this.closed = true;
582
- for (const resolve of this.resolvers) {
773
+ for (const resolve2 of this.resolvers) {
583
774
  log(`close() resolving pending resolver with done=true`);
584
- resolve({ value: void 0, done: true });
775
+ resolve2({ value: void 0, done: true });
585
776
  }
586
777
  this.resolvers = [];
587
778
  log(`close() complete`);
@@ -620,8 +811,8 @@ var MessageQueue = class {
620
811
  log(
621
812
  `next() #${callId}: queue empty, creating pending resolver (${this.resolvers.length + 1} total)`
622
813
  );
623
- return new Promise((resolve) => {
624
- this.resolvers.push(resolve);
814
+ return new Promise((resolve2) => {
815
+ this.resolvers.push(resolve2);
625
816
  });
626
817
  }
627
818
  };
@@ -656,43 +847,6 @@ var useTerminalSize = () => {
656
847
  return size;
657
848
  };
658
849
 
659
- // src/lib/getNextLogFile.ts
660
- import { existsSync as existsSync6, mkdirSync } from "fs";
661
- import { join as join8 } from "path";
662
-
663
- // src/lib/findMaxLogNumber.ts
664
- import { readdirSync, existsSync as existsSync5 } from "fs";
665
- import { join as join7 } from "path";
666
- var EVENT_LOG_PATTERN = /^events-(\d+)\.jsonl$/;
667
- var findMaxLogNumber = () => {
668
- const ralphDir6 = join7(process.cwd(), ".ralph");
669
- if (!existsSync5(ralphDir6)) {
670
- return 0;
671
- }
672
- const files = readdirSync(ralphDir6);
673
- let maxNumber = 0;
674
- for (const file of files) {
675
- const match = file.match(EVENT_LOG_PATTERN);
676
- if (match) {
677
- const num = parseInt(match[1], 10);
678
- if (num > maxNumber) {
679
- maxNumber = num;
680
- }
681
- }
682
- }
683
- return maxNumber;
684
- };
685
-
686
- // src/lib/getNextLogFile.ts
687
- var getNextLogFile = () => {
688
- const ralphDir6 = join8(process.cwd(), ".ralph");
689
- if (!existsSync6(ralphDir6)) {
690
- mkdirSync(ralphDir6, { recursive: true });
691
- }
692
- const maxNumber = findMaxLogNumber();
693
- return join8(ralphDir6, `events-${maxNumber + 1}.jsonl`);
694
- };
695
-
696
850
  // src/lib/parseTaskLifecycle.ts
697
851
  function parseTaskLifecycleEvent(text) {
698
852
  const startingMatch = text.match(/<start_task>([a-z]+-[a-z0-9]+(?:\.[a-z0-9]+)*)<\/start_task>/i);
@@ -713,31 +867,95 @@ function parseTaskLifecycleEvent(text) {
713
867
  }
714
868
 
715
869
  // src/lib/getPromptContent.ts
716
- import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
717
- import { join as join9, dirname } from "path";
870
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
871
+ import { join as join11, dirname as dirname4 } from "path";
872
+ import { fileURLToPath as fileURLToPath2 } from "url";
873
+
874
+ // ../shared/dist/prompts/loadPrompt.js
875
+ import { existsSync as existsSync7, readFileSync as readFileSync5, copyFileSync, mkdirSync as mkdirSync2 } from "fs";
876
+ import { join as join9, dirname as dirname2 } from "path";
877
+
878
+ // ../shared/dist/prompts/getWorkspaceRoot.js
879
+ import { existsSync as existsSync6 } from "fs";
880
+ import { dirname, resolve } from "path";
881
+ function getWorkspaceRoot(cwd = process.cwd()) {
882
+ const start = resolve(cwd);
883
+ let current = start;
884
+ while (true) {
885
+ const gitPath = resolve(current, ".git");
886
+ if (existsSync6(gitPath)) {
887
+ return current;
888
+ }
889
+ const parent = dirname(current);
890
+ if (parent === current) {
891
+ return start;
892
+ }
893
+ current = parent;
894
+ }
895
+ }
896
+
897
+ // ../shared/dist/prompts/loadPrompt.js
898
+ var WORKFLOW_PLACEHOLDER = "{WORKFLOW}";
899
+ function loadSessionPrompt(options) {
900
+ const { templatesDir, cwd = process.cwd() } = options;
901
+ const workspaceRoot = getWorkspaceRoot(cwd);
902
+ const corePromptPath = join9(templatesDir, "core.prompt.md");
903
+ if (!existsSync7(corePromptPath)) {
904
+ throw new Error(`Core prompt not found at ${corePromptPath}`);
905
+ }
906
+ const corePrompt = readFileSync5(corePromptPath, "utf-8");
907
+ const customWorkflowPath = join9(workspaceRoot, ".ralph", "workflow.prompt.md");
908
+ const defaultWorkflowPath = join9(templatesDir, "workflow.prompt.md");
909
+ let workflowContent;
910
+ let hasCustomWorkflow2;
911
+ let workflowPath;
912
+ if (existsSync7(customWorkflowPath)) {
913
+ workflowContent = readFileSync5(customWorkflowPath, "utf-8");
914
+ hasCustomWorkflow2 = true;
915
+ workflowPath = customWorkflowPath;
916
+ } else if (existsSync7(defaultWorkflowPath)) {
917
+ workflowContent = readFileSync5(defaultWorkflowPath, "utf-8");
918
+ hasCustomWorkflow2 = false;
919
+ workflowPath = defaultWorkflowPath;
920
+ } else {
921
+ throw new Error(`Workflow file not found at ${customWorkflowPath} or ${defaultWorkflowPath}`);
922
+ }
923
+ const content = corePrompt.replace(WORKFLOW_PLACEHOLDER, workflowContent);
924
+ return {
925
+ content,
926
+ hasCustomWorkflow: hasCustomWorkflow2,
927
+ workflowPath
928
+ };
929
+ }
930
+
931
+ // ../shared/dist/prompts/templatesDir.js
932
+ import { dirname as dirname3, join as join10 } from "path";
718
933
  import { fileURLToPath } from "url";
934
+ var TEMPLATES_DIR = join10(dirname3(fileURLToPath(import.meta.url)), "..", "..", "templates");
935
+
936
+ // src/lib/getPromptContent.ts
719
937
  var getPromptContent = () => {
720
- const __dirname = dirname(fileURLToPath(import.meta.url));
721
- const ralphDir6 = join9(process.cwd(), ".ralph");
722
- const promptFile = join9(ralphDir6, "prompt.md");
723
- const todoFile7 = join9(ralphDir6, "todo.md");
724
- const beadsDir3 = join9(process.cwd(), ".beads");
725
- const templatesDir = join9(__dirname, "..", "..", "templates");
726
- if (existsSync7(promptFile)) {
727
- return readFileSync4(promptFile, "utf-8");
728
- }
729
- const useBeadsTemplate = existsSync7(beadsDir3) || !existsSync7(todoFile7);
730
- const templateFile = useBeadsTemplate ? "prompt-beads.md" : "prompt-todos.md";
731
- const templatePath = join9(templatesDir, templateFile);
732
- if (existsSync7(templatePath)) {
733
- return readFileSync4(templatePath, "utf-8");
734
- }
735
- return "Work on the highest-priority task.";
938
+ const __dirname = dirname4(fileURLToPath2(import.meta.url));
939
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
940
+ const ralphDir6 = join11(workspaceRoot, ".ralph");
941
+ const promptFile = join11(ralphDir6, "prompt.prompt.md");
942
+ let templatesDir = join11(__dirname, "..", "..", "templates");
943
+ if (!existsSync8(join11(templatesDir, "core.prompt.md"))) {
944
+ templatesDir = join11(__dirname, "..", "templates");
945
+ }
946
+ if (existsSync8(promptFile)) {
947
+ return readFileSync6(promptFile, "utf-8");
948
+ }
949
+ const { content } = loadSessionPrompt({
950
+ templatesDir,
951
+ cwd: workspaceRoot
952
+ });
953
+ return content;
736
954
  };
737
955
 
738
956
  // src/lib/sdkMessageToEvent.ts
739
957
  var sdkMessageToEvent = (message) => {
740
- if (message.type === "assistant" || message.type === "user" || message.type === "result") {
958
+ if (message.type === "assistant" || message.type === "result") {
741
959
  return message;
742
960
  }
743
961
  return null;
@@ -1083,7 +1301,7 @@ var renderStaticItem = (item) => {
1083
1301
  return /* @__PURE__ */ React4.createElement(Header, { claudeVersion: item.claudeVersion, ralphVersion: item.ralphVersion });
1084
1302
  }
1085
1303
  if (item.type === "session") {
1086
- return /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Gradient2, { colors: ["#30A6E4", "#EBC635"] }, /* @__PURE__ */ React4.createElement(BigText2, { text: `R${item.session}`, font: "tiny" })));
1304
+ return /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Gradient2, { colors: ["#30A6E4", "#EBC635"] }, /* @__PURE__ */ React4.createElement(BigText2, { text: `R${item.session}`, font: "tiny" })), item.sessionId && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "session ", item.sessionId.slice(0, 8)), /* @__PURE__ */ React4.createElement(Text4, null, " ")));
1087
1305
  }
1088
1306
  const lines = formatContentBlock(item.block);
1089
1307
  return /* @__PURE__ */ React4.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, lines.map((line, i) => /* @__PURE__ */ React4.createElement(Text4, { key: i }, line || " ")));
@@ -1091,8 +1309,8 @@ var renderStaticItem = (item) => {
1091
1309
 
1092
1310
  // src/components/SessionRunner.tsx
1093
1311
  var log2 = createDebugLogger("session");
1094
- var ralphDir5 = join10(process.cwd(), ".ralph");
1095
- var todoFile5 = join10(ralphDir5, "todo.md");
1312
+ var ralphDir5 = join12(process.cwd(), ".ralph");
1313
+ var todoFile5 = join12(ralphDir5, "todo.md");
1096
1314
  var repoName = basename(process.cwd());
1097
1315
  var SessionRunner = ({
1098
1316
  totalSessions,
@@ -1129,7 +1347,9 @@ var SessionRunner = ({
1129
1347
  const [isPaused, setIsPaused] = useState3(false);
1130
1348
  const isPausedRef = useRef(false);
1131
1349
  const [currentTaskId, setCurrentTaskId] = useState3(null);
1132
- const [currentTaskTitle, setCurrentTaskTitle] = useState3(null);
1350
+ const [sessionId, setSessionId] = useState3(null);
1351
+ const sessionIdRef = useRef(null);
1352
+ const taskCompletedAbortRef = useRef(false);
1133
1353
  const [staticItems, setStaticItems] = useState3([
1134
1354
  { type: "header", claudeVersion, ralphVersion, key: "header" }
1135
1355
  ]);
@@ -1261,10 +1481,11 @@ var SessionRunner = ({
1261
1481
  }, [isWatching, startupSnapshot]);
1262
1482
  useEffect3(() => {
1263
1483
  const newItems = [];
1264
- if (currentSession > lastSessionRef.current) {
1484
+ if (currentSession > lastSessionRef.current && sessionId) {
1265
1485
  newItems.push({
1266
1486
  type: "session",
1267
1487
  session: currentSession,
1488
+ sessionId,
1268
1489
  key: `session-${currentSession}`
1269
1490
  });
1270
1491
  lastSessionRef.current = currentSession;
@@ -1280,7 +1501,7 @@ var SessionRunner = ({
1280
1501
  if (newItems.length > 0) {
1281
1502
  setStaticItems((prev) => [...prev, ...newItems]);
1282
1503
  }
1283
- }, [events, currentSession]);
1504
+ }, [events, currentSession, sessionId]);
1284
1505
  useEffect3(() => {
1285
1506
  if (currentSession > totalSessions) {
1286
1507
  exit();
@@ -1296,16 +1517,12 @@ var SessionRunner = ({
1296
1517
  }
1297
1518
  return;
1298
1519
  }
1299
- if (!logFileRef.current) {
1300
- logFileRef.current = getNextLogFile();
1301
- writeFileSync2(logFileRef.current, "");
1302
- }
1303
- const logFile = logFileRef.current;
1520
+ logFileRef.current = null;
1304
1521
  setEvents([]);
1305
1522
  const promptContent = getPromptContent();
1306
- const todoExists = existsSync8(todoFile5);
1523
+ const todoExists = existsSync9(todoFile5);
1307
1524
  setHasTodoFile(todoExists);
1308
- const todoContent = todoExists ? readFileSync5(todoFile5, "utf-8") : "";
1525
+ const todoContent = todoExists ? readFileSync7(todoFile5, "utf-8") : "";
1309
1526
  const roundHeader = `# Ralph, round ${currentSession}
1310
1527
 
1311
1528
  `;
@@ -1315,7 +1532,10 @@ var SessionRunner = ({
1315
1532
 
1316
1533
  ${todoContent}` : `${roundHeader}${promptContent}`;
1317
1534
  const abortController = new AbortController();
1535
+ taskCompletedAbortRef.current = false;
1318
1536
  setIsRunning(true);
1537
+ sessionIdRef.current = null;
1538
+ setSessionId(null);
1319
1539
  const messageQueue = new MessageQueue();
1320
1540
  messageQueueRef.current = messageQueue;
1321
1541
  messageQueue.push(createUserMessage(fullPrompt));
@@ -1341,7 +1561,18 @@ ${todoContent}` : `${roundHeader}${promptContent}`;
1341
1561
  }
1342
1562
  })) {
1343
1563
  log2(`Received message type: ${message.type}`);
1344
- appendFileSync(logFile, JSON.stringify(message) + "\n");
1564
+ if (!logFileRef.current && message.type === "system" && "session_id" in message && typeof message.session_id === "string") {
1565
+ const storageDir = join12(getDefaultStorageDir(), "ralph");
1566
+ if (!existsSync9(storageDir)) {
1567
+ mkdirSync3(storageDir, { recursive: true });
1568
+ }
1569
+ logFileRef.current = join12(storageDir, `${message.session_id.slice(0, 8)}.jsonl`);
1570
+ sessionIdRef.current = message.session_id;
1571
+ setSessionId(message.session_id);
1572
+ }
1573
+ if (logFileRef.current) {
1574
+ appendFileSync(logFileRef.current, JSON.stringify(message) + "\n");
1575
+ }
1345
1576
  const event = sdkMessageToEvent(message);
1346
1577
  if (event) {
1347
1578
  setEvents((prev) => [...prev, event]);
@@ -1356,28 +1587,33 @@ ${todoContent}` : `${roundHeader}${promptContent}`;
1356
1587
  if (taskInfo) {
1357
1588
  if (taskInfo.action === "starting") {
1358
1589
  setCurrentTaskId(taskInfo.taskId ?? null);
1359
- setCurrentTaskTitle(taskInfo.taskTitle ?? null);
1360
- log2(
1361
- `Task started: ${taskInfo.taskId}${taskInfo.taskTitle ? ` - ${taskInfo.taskTitle}` : ""}`
1362
- );
1363
- const taskStartedEvent = {
1364
- type: "ralph_task_started",
1365
- taskId: taskInfo.taskId,
1366
- taskTitle: taskInfo.taskTitle,
1367
- session: currentSession
1368
- };
1369
- appendFileSync(logFile, JSON.stringify(taskStartedEvent) + "\n");
1590
+ log2(`Task started: ${taskInfo.taskId}`);
1591
+ if (logFileRef.current) {
1592
+ const taskStartedEvent = {
1593
+ type: "ralph_task_started",
1594
+ taskId: taskInfo.taskId,
1595
+ session: currentSession,
1596
+ sessionId: sessionIdRef.current
1597
+ };
1598
+ appendFileSync(logFileRef.current, JSON.stringify(taskStartedEvent) + "\n");
1599
+ }
1370
1600
  } else if (taskInfo.action === "completed") {
1371
- log2(
1372
- `Task completed: ${taskInfo.taskId}${taskInfo.taskTitle ? ` - ${taskInfo.taskTitle}` : ""}`
1373
- );
1374
- const taskCompletedEvent = {
1375
- type: "ralph_task_completed",
1376
- taskId: taskInfo.taskId,
1377
- taskTitle: taskInfo.taskTitle,
1378
- session: currentSession
1379
- };
1380
- appendFileSync(logFile, JSON.stringify(taskCompletedEvent) + "\n");
1601
+ log2(`Task completed: ${taskInfo.taskId}`);
1602
+ if (logFileRef.current) {
1603
+ const taskCompletedEvent = {
1604
+ type: "ralph_task_completed",
1605
+ taskId: taskInfo.taskId,
1606
+ session: currentSession,
1607
+ sessionId: sessionIdRef.current
1608
+ };
1609
+ appendFileSync(
1610
+ logFileRef.current,
1611
+ JSON.stringify(taskCompletedEvent) + "\n"
1612
+ );
1613
+ }
1614
+ log2(`Aborting session after task completion to enforce session boundary`);
1615
+ taskCompletedAbortRef.current = true;
1616
+ abortController.abort();
1381
1617
  }
1382
1618
  }
1383
1619
  }
@@ -1423,6 +1659,22 @@ ${todoContent}` : `${roundHeader}${promptContent}`;
1423
1659
  messageQueue.close();
1424
1660
  messageQueueRef.current = null;
1425
1661
  if (abortController.signal.aborted) {
1662
+ if (taskCompletedAbortRef.current) {
1663
+ log2(`Session aborted after task completion \u2014 advancing to next session`);
1664
+ taskCompletedAbortRef.current = false;
1665
+ if (stopAfterCurrentRef.current) {
1666
+ log2(`Stop after current requested - exiting gracefully`);
1667
+ exit();
1668
+ process.exit(0);
1669
+ return;
1670
+ }
1671
+ if (isPausedRef.current) {
1672
+ log2(`Paused after session ${currentSession}`);
1673
+ return;
1674
+ }
1675
+ setTimeout(() => setCurrentSession((i) => i + 1), 500);
1676
+ return;
1677
+ }
1426
1678
  log2(`Abort signal detected`);
1427
1679
  return;
1428
1680
  }
@@ -1459,7 +1711,7 @@ ${todoContent}` : `${roundHeader}${promptContent}`;
1459
1711
  color: userMessageStatus.type === "success" ? "green" : userMessageStatus.type === "error" ? "red" : "yellow"
1460
1712
  },
1461
1713
  userMessageStatus.text
1462
- ), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2500".repeat(columns))), /* @__PURE__ */ React5.createElement(Box3, { marginTop: 1, justifyContent: "space-between" }, isWatching ? detectedIssue ? /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " New issue: ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, detectedIssue.IssueID), detectedIssue.Title ? ` - ${detectedIssue.Title}` : "") : /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "Waiting for new issues ", /* @__PURE__ */ React5.createElement(Spinner, { type: "simpleDotsScrolling" })) : isPaused && !isRunning ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, "\u23F8 Paused after round ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " ", /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Ctrl-P to resume)")) : isRunning ? stopAfterCurrent ? /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " Stopping after round", " ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " completes...", " ", /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Ctrl-S pressed)")) : isPaused ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " Pausing after round", " ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " completes...", " ", /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Ctrl-P pressed)")) : /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " Running round ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " ", "(max ", totalSessions, ")") : /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "simpleDotsScrolling" }), " Waiting for Ralph to start..."), progressData.type !== "none" && progressData.total > 0 && /* @__PURE__ */ React5.createElement(
1714
+ ), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2500".repeat(columns))), /* @__PURE__ */ React5.createElement(Box3, { marginTop: 1, justifyContent: "space-between" }, isWatching ? detectedIssue ? /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " New issue: ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, detectedIssue.IssueID), detectedIssue.Title ? ` - ${detectedIssue.Title}` : "") : /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "Waiting for new issues ", /* @__PURE__ */ React5.createElement(Spinner, { type: "simpleDotsScrolling" })) : isPaused && !isRunning ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, "\u23F8 Paused after round ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " ", /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Ctrl-P to resume)")) : isRunning ? stopAfterCurrent ? /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " Stopping after round", " ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " completes...", " ", /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Ctrl-S pressed)")) : isPaused ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " Pausing after round", " ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " completes...", " ", /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Ctrl-P pressed)")) : /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "dots" }), " Running round ", /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, currentSession), " ", "(max ", totalSessions, ")", sessionId && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " ", sessionId.slice(0, 8))) : /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, /* @__PURE__ */ React5.createElement(Spinner, { type: "simpleDotsScrolling" }), " Waiting for Ralph to start..."), progressData.type !== "none" && progressData.total > 0 && /* @__PURE__ */ React5.createElement(
1463
1715
  ProgressBar,
1464
1716
  {
1465
1717
  completed: progressData.completed,
@@ -1472,7 +1724,7 @@ ${todoContent}` : `${roundHeader}${promptContent}`;
1472
1724
  // src/components/ReplayLog.tsx
1473
1725
  import React8, { useState as useState5, useEffect as useEffect5 } from "react";
1474
1726
  import { Box as Box6, Text as Text8, useApp as useApp2 } from "ink";
1475
- import { readFileSync as readFileSync6 } from "fs";
1727
+ import { readFileSync as readFileSync8 } from "fs";
1476
1728
 
1477
1729
  // src/components/EventDisplay.tsx
1478
1730
  import React6, { useMemo, useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
@@ -1711,7 +1963,7 @@ var ReplayLog = ({
1711
1963
  const [error, setError] = useState5();
1712
1964
  useEffect5(() => {
1713
1965
  try {
1714
- const content = readFileSync6(filePath, "utf-8");
1966
+ const content = readFileSync8(filePath, "utf-8");
1715
1967
  const eventStrings = content.split(/\n\n+/).filter((s) => s.trim());
1716
1968
  const parsedEvents = [];
1717
1969
  for (const eventStr of eventStrings) {
@@ -1745,8 +1997,8 @@ var ReplayLog = ({
1745
1997
  // src/components/JsonOutput.tsx
1746
1998
  import React9, { useState as useState6, useEffect as useEffect6, useRef as useRef3 } from "react";
1747
1999
  import { useApp as useApp3, Text as Text9 } from "ink";
1748
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
1749
- import { join as join11, basename as basename2 } from "path";
2000
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
2001
+ import { join as join13, basename as basename2 } from "path";
1750
2002
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1751
2003
 
1752
2004
  // src/lib/parseStdinCommand.ts
@@ -1838,7 +2090,7 @@ var outputEvent = (event) => {
1838
2090
 
1839
2091
  // src/components/JsonOutput.tsx
1840
2092
  var log5 = createDebugLogger("session");
1841
- var todoFile6 = join11(process.cwd(), ".ralph", "todo.md");
2093
+ var todoFile6 = join13(process.cwd(), ".ralph", "todo.md");
1842
2094
  var repoName2 = basename2(process.cwd());
1843
2095
  var JsonOutput = ({ totalSessions, agent }) => {
1844
2096
  const { exit } = useApp3();
@@ -1847,14 +2099,13 @@ var JsonOutput = ({ totalSessions, agent }) => {
1847
2099
  const [isRunning, setIsRunning] = useState6(false);
1848
2100
  const [startupSnapshot] = useState6(() => captureStartupSnapshot());
1849
2101
  const messageQueueRef = useRef3(null);
1850
- const logFileRef = useRef3(null);
1851
2102
  const [stopAfterCurrent, setStopAfterCurrent] = useState6(false);
1852
2103
  const stopAfterCurrentRef = useRef3(false);
1853
2104
  const [isPaused, setIsPaused] = useState6(false);
1854
2105
  const isPausedRef = useRef3(false);
1855
2106
  const stdinCleanupRef = useRef3(null);
1856
2107
  const currentTaskIdRef = useRef3(null);
1857
- const currentTaskTitleRef = useRef3(null);
2108
+ const sessionIdRef = useRef3(null);
1858
2109
  useEffect6(() => {
1859
2110
  stopAfterCurrentRef.current = stopAfterCurrent;
1860
2111
  }, [stopAfterCurrent]);
@@ -1906,13 +2157,9 @@ var JsonOutput = ({ totalSessions, agent }) => {
1906
2157
  process.exit(0);
1907
2158
  return;
1908
2159
  }
1909
- if (!logFileRef.current) {
1910
- logFileRef.current = getNextLogFile();
1911
- writeFileSync3(logFileRef.current, "");
1912
- }
1913
2160
  const promptContent = getPromptContent();
1914
- const todoExists = existsSync9(todoFile6);
1915
- const todoContent = todoExists ? readFileSync7(todoFile6, "utf-8") : "";
2161
+ const todoExists = existsSync10(todoFile6);
2162
+ const todoContent = todoExists ? readFileSync9(todoFile6, "utf-8") : "";
1916
2163
  const fullPrompt = todoContent ? `${promptContent}
1917
2164
 
1918
2165
  ## Current Todo List
@@ -1920,14 +2167,7 @@ var JsonOutput = ({ totalSessions, agent }) => {
1920
2167
  ${todoContent}` : promptContent;
1921
2168
  const abortController = new AbortController();
1922
2169
  setIsRunning(true);
1923
- outputEvent({
1924
- type: "ralph_session_start",
1925
- session: currentSession,
1926
- totalSessions,
1927
- repo: repoName2,
1928
- taskId: currentTaskIdRef.current,
1929
- taskTitle: currentTaskTitleRef.current
1930
- });
2170
+ sessionIdRef.current = null;
1931
2171
  const messageQueue = new MessageQueue();
1932
2172
  messageQueueRef.current = messageQueue;
1933
2173
  messageQueue.push(createUserMessage(fullPrompt));
@@ -1952,6 +2192,17 @@ ${todoContent}` : promptContent;
1952
2192
  }
1953
2193
  })) {
1954
2194
  log5(`Received message type: ${message.type}`);
2195
+ if (!sessionIdRef.current && message.type === "system" && "session_id" in message && typeof message.session_id === "string") {
2196
+ sessionIdRef.current = message.session_id;
2197
+ outputEvent({
2198
+ type: "ralph_session_start",
2199
+ session: currentSession,
2200
+ totalSessions,
2201
+ repo: repoName2,
2202
+ taskId: currentTaskIdRef.current,
2203
+ sessionId: message.session_id
2204
+ });
2205
+ }
1955
2206
  outputEvent(message);
1956
2207
  if (message.type === "assistant") {
1957
2208
  const assistantMessage = message.message;
@@ -1963,25 +2214,20 @@ ${todoContent}` : promptContent;
1963
2214
  if (taskInfo) {
1964
2215
  if (taskInfo.action === "starting") {
1965
2216
  currentTaskIdRef.current = taskInfo.taskId ?? null;
1966
- currentTaskTitleRef.current = taskInfo.taskTitle ?? null;
1967
- log5(
1968
- `Task started: ${taskInfo.taskId}${taskInfo.taskTitle ? ` - ${taskInfo.taskTitle}` : ""}`
1969
- );
2217
+ log5(`Task started: ${taskInfo.taskId}`);
1970
2218
  outputEvent({
1971
2219
  type: "ralph_task_started",
1972
2220
  taskId: taskInfo.taskId,
1973
- taskTitle: taskInfo.taskTitle,
1974
- session: currentSession
2221
+ session: currentSession,
2222
+ sessionId: sessionIdRef.current
1975
2223
  });
1976
2224
  } else if (taskInfo.action === "completed") {
1977
- log5(
1978
- `Task completed: ${taskInfo.taskId}${taskInfo.taskTitle ? ` - ${taskInfo.taskTitle}` : ""}`
1979
- );
2225
+ log5(`Task completed: ${taskInfo.taskId}`);
1980
2226
  outputEvent({
1981
2227
  type: "ralph_task_completed",
1982
2228
  taskId: taskInfo.taskId,
1983
- taskTitle: taskInfo.taskTitle,
1984
- session: currentSession
2229
+ session: currentSession,
2230
+ sessionId: sessionIdRef.current
1985
2231
  });
1986
2232
  }
1987
2233
  }
@@ -2005,7 +2251,7 @@ ${todoContent}` : promptContent;
2005
2251
  type: "ralph_session_end",
2006
2252
  session: currentSession,
2007
2253
  taskId: currentTaskIdRef.current,
2008
- taskTitle: currentTaskTitleRef.current
2254
+ sessionId: sessionIdRef.current
2009
2255
  });
2010
2256
  if (stopAfterCurrentRef.current) {
2011
2257
  log5(`Stop after current requested - exiting gracefully`);
@@ -2090,26 +2336,26 @@ var App = ({
2090
2336
  // src/components/InitRalph.tsx
2091
2337
  import { Text as Text10, Box as Box7 } from "ink";
2092
2338
  import React11, { useEffect as useEffect7, useState as useState7 } from "react";
2093
- import { existsSync as existsSync11, readFileSync as readFileSync8, appendFileSync as appendFileSync2, writeFileSync as writeFileSync4 } from "fs";
2094
- import { join as join13, dirname as dirname3 } from "path";
2095
- import { fileURLToPath as fileURLToPath2 } from "url";
2339
+ import { existsSync as existsSync12 } from "fs";
2340
+ import { join as join15, dirname as dirname6 } from "path";
2341
+ import { fileURLToPath as fileURLToPath3 } from "url";
2096
2342
 
2097
2343
  // src/lib/copyTemplates.ts
2098
- import { existsSync as existsSync10, mkdirSync as mkdirSync2, copyFileSync } from "fs";
2099
- import { join as join12, dirname as dirname2 } from "path";
2344
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2 } from "fs";
2345
+ import { join as join14, dirname as dirname5 } from "path";
2100
2346
  function copyTemplates(templatesDir, destDir, files) {
2101
2347
  const result = { created: [], skipped: [], errors: [] };
2102
2348
  for (const { src, dest } of files) {
2103
- const srcPath = join12(templatesDir, src);
2104
- const destPath = join12(destDir, dest);
2105
- const destDirPath = dirname2(destPath);
2106
- if (!existsSync10(destDirPath)) {
2107
- mkdirSync2(destDirPath, { recursive: true });
2349
+ const srcPath = join14(templatesDir, src);
2350
+ const destPath = join14(destDir, dest);
2351
+ const destDirPath = dirname5(destPath);
2352
+ if (!existsSync11(destDirPath)) {
2353
+ mkdirSync4(destDirPath, { recursive: true });
2108
2354
  }
2109
- if (existsSync10(destPath)) {
2355
+ if (existsSync11(destPath)) {
2110
2356
  result.skipped.push(dest);
2111
- } else if (existsSync10(srcPath)) {
2112
- copyFileSync(srcPath, destPath);
2357
+ } else if (existsSync11(srcPath)) {
2358
+ copyFileSync2(srcPath, destPath);
2113
2359
  result.created.push(dest);
2114
2360
  } else {
2115
2361
  result.errors.push(`Template not found: ${src}`);
@@ -2120,20 +2366,20 @@ function copyTemplates(templatesDir, destDir, files) {
2120
2366
 
2121
2367
  // src/components/InitRalph.tsx
2122
2368
  function InitRalph() {
2123
- const __dirname = dirname3(fileURLToPath2(import.meta.url));
2369
+ const __dirname = dirname6(fileURLToPath3(import.meta.url));
2124
2370
  const [status, setStatus] = useState7("checking");
2125
2371
  const [createdFiles, setCreatedFiles] = useState7([]);
2126
2372
  const [skippedFiles, setSkippedFiles] = useState7([]);
2127
2373
  const [errors, setErrors] = useState7([]);
2128
2374
  useEffect7(() => {
2129
- const ralphDir6 = join13(process.cwd(), ".ralph");
2130
- const claudeDir = join13(process.cwd(), ".claude");
2131
- if (existsSync11(join13(ralphDir6, "workflow.md"))) {
2375
+ const ralphDir6 = join15(process.cwd(), ".ralph");
2376
+ const claudeDir = join15(process.cwd(), ".claude");
2377
+ if (existsSync12(join15(ralphDir6, "workflow.md"))) {
2132
2378
  setStatus("exists");
2133
2379
  return;
2134
2380
  }
2135
2381
  const initialize = async () => {
2136
- const templatesDir = join13(__dirname, "..", "..", "templates");
2382
+ const templatesDir = join15(__dirname, "..", "..", "templates");
2137
2383
  setStatus("creating");
2138
2384
  try {
2139
2385
  const allCreated = [];
@@ -2159,21 +2405,6 @@ function InitRalph() {
2159
2405
  allCreated.push(...agentsResult.created.map((f) => `.claude/${f}`));
2160
2406
  allSkipped.push(...agentsResult.skipped.map((f) => `.claude/${f}`));
2161
2407
  allErrors.push(...agentsResult.errors);
2162
- const gitignorePath = join13(process.cwd(), ".gitignore");
2163
- const eventsLogEntry = ".ralph/events-*.jsonl";
2164
- if (existsSync11(gitignorePath)) {
2165
- const content = readFileSync8(gitignorePath, "utf-8");
2166
- if (!content.includes(eventsLogEntry)) {
2167
- const newline = content.endsWith("\n") ? "" : "\n";
2168
- appendFileSync2(gitignorePath, `${newline}${eventsLogEntry}
2169
- `);
2170
- allCreated.push("(added .ralph/events-*.jsonl to .gitignore)");
2171
- }
2172
- } else {
2173
- writeFileSync4(gitignorePath, `${eventsLogEntry}
2174
- `);
2175
- allCreated.push("(created .gitignore with .ralph/events-*.jsonl)");
2176
- }
2177
2408
  setCreatedFiles(allCreated);
2178
2409
  setSkippedFiles(allSkipped);
2179
2410
  setErrors(allErrors);
@@ -2240,20 +2471,17 @@ var getDefaultSessions = () => {
2240
2471
  };
2241
2472
 
2242
2473
  // src/lib/getLatestLogFile.ts
2243
- import { join as join14 } from "path";
2244
2474
  var getLatestLogFile = () => {
2245
- const maxNumber = findMaxLogNumber();
2246
- if (maxNumber === 0) {
2247
- return void 0;
2248
- }
2249
- const ralphDir6 = join14(process.cwd(), ".ralph");
2250
- return join14(ralphDir6, `events-${maxNumber}.jsonl`);
2475
+ const persister = new SessionPersister(getDefaultStorageDir());
2476
+ const latestId = persister.getLatestSessionId("ralph");
2477
+ if (!latestId) return void 0;
2478
+ return persister.getSessionPath(latestId, "ralph");
2251
2479
  };
2252
2480
 
2253
2481
  // package.json
2254
2482
  var package_default = {
2255
2483
  name: "@herbcaudill/ralph",
2256
- version: "1.0.2",
2484
+ version: "1.2.0",
2257
2485
  description: "Autonomous AI session engine for Claude CLI",
2258
2486
  type: "module",
2259
2487
  main: "./dist/index.js",
@@ -2271,9 +2499,8 @@ var package_default = {
2271
2499
  dev: "tsc --watch",
2272
2500
  typecheck: "tsc --noEmit",
2273
2501
  ralph: "tsx src/index.ts",
2274
- "test:all": "pnpm typecheck && vitest run",
2275
2502
  test: "vitest run",
2276
- "test:e2e": "vitest --config vitest.e2e.config.ts",
2503
+ "test:pw": "vitest --config vitest.e2e.config.ts",
2277
2504
  "test:watch": "vitest --watch",
2278
2505
  "test:ui": "vitest --ui",
2279
2506
  format: "prettier --write . --log-level silent",
@@ -2294,7 +2521,7 @@ var package_default = {
2294
2521
  url: "https://github.com/HerbCaudill/ralph.git"
2295
2522
  },
2296
2523
  dependencies: {
2297
- "@anthropic-ai/claude-agent-sdk": "^0.2.7",
2524
+ "@anthropic-ai/claude-agent-sdk": "^0.2.29",
2298
2525
  chalk: "^5.6.2",
2299
2526
  commander: "^14.0.2",
2300
2527
  ink: "^6.6.0",
@@ -2306,6 +2533,7 @@ var package_default = {
2306
2533
  react: "^19.2.3"
2307
2534
  },
2308
2535
  devDependencies: {
2536
+ "@herbcaudill/beads-sdk": "link:../../../beads-sdk",
2309
2537
  "@herbcaudill/ralph-shared": "workspace:*",
2310
2538
  "@types/node": "^24.10.1",
2311
2539
  "@types/react": "^19.2.8",
@@ -2372,10 +2600,10 @@ program.command("todo [description...]").description("add a todo item and commit
2372
2600
  input: process.stdin,
2373
2601
  output: process.stdout
2374
2602
  });
2375
- description = await new Promise((resolve) => {
2603
+ description = await new Promise((resolve2) => {
2376
2604
  rl.question("Todo: ", (answer) => {
2377
2605
  rl.close();
2378
- resolve(answer.trim());
2606
+ resolve2(answer.trim());
2379
2607
  });
2380
2608
  });
2381
2609
  if (!description) {