rafcode 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/RAF/ahrren-turbo-finder/decisions.md +19 -0
  3. package/RAF/ahrren-turbo-finder/input.md +2 -0
  4. package/RAF/ahrren-turbo-finder/outcomes/01-worktree-auto-detect.md +40 -0
  5. package/RAF/ahrren-turbo-finder/outcomes/02-medium-effort-do.md +34 -0
  6. package/RAF/ahrren-turbo-finder/plans/01-worktree-auto-detect.md +44 -0
  7. package/RAF/ahrren-turbo-finder/plans/02-medium-effort-do.md +39 -0
  8. package/RAF/ahrtxf-session-sentinel/decisions.md +19 -0
  9. package/RAF/ahrtxf-session-sentinel/input.md +1 -0
  10. package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +37 -0
  11. package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +45 -0
  12. package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +41 -0
  13. package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +51 -0
  14. package/dist/commands/do.d.ts.map +1 -1
  15. package/dist/commands/do.js +61 -20
  16. package/dist/commands/do.js.map +1 -1
  17. package/dist/core/claude-runner.d.ts +19 -0
  18. package/dist/core/claude-runner.d.ts.map +1 -1
  19. package/dist/core/claude-runner.js +199 -29
  20. package/dist/core/claude-runner.js.map +1 -1
  21. package/dist/core/shutdown-handler.d.ts.map +1 -1
  22. package/dist/core/shutdown-handler.js +4 -0
  23. package/dist/core/shutdown-handler.js.map +1 -1
  24. package/dist/core/worktree.d.ts +18 -0
  25. package/dist/core/worktree.d.ts.map +1 -1
  26. package/dist/core/worktree.js +61 -0
  27. package/dist/core/worktree.js.map +1 -1
  28. package/dist/parsers/stream-renderer.d.ts +3 -0
  29. package/dist/parsers/stream-renderer.d.ts.map +1 -1
  30. package/dist/parsers/stream-renderer.js +1 -1
  31. package/dist/parsers/stream-renderer.js.map +1 -1
  32. package/dist/types/config.d.ts +1 -0
  33. package/dist/types/config.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/commands/do.ts +67 -21
  36. package/src/core/claude-runner.ts +244 -31
  37. package/src/core/shutdown-handler.ts +5 -0
  38. package/src/core/worktree.ts +77 -0
  39. package/src/parsers/stream-renderer.ts +4 -1
  40. package/src/types/config.ts +1 -0
  41. package/tests/unit/claude-runner-interactive.test.ts +24 -0
  42. package/tests/unit/claude-runner.test.ts +509 -55
  43. package/tests/unit/post-execution-picker.test.ts +1 -0
  44. package/tests/unit/stream-renderer.test.ts +30 -0
  45. package/tests/unit/worktree.test.ts +102 -0
@@ -50,6 +50,12 @@ export interface ClaudeRunnerOptions {
50
50
  /** Path to the outcome file that should be committed. */
51
51
  outcomeFilePath: string;
52
52
  };
53
+ /**
54
+ * Claude Code reasoning effort level.
55
+ * Sets CLAUDE_CODE_EFFORT_LEVEL env var for the spawned process.
56
+ * Only applied in non-interactive modes (run, runVerbose).
57
+ */
58
+ effortLevel?: 'low' | 'medium' | 'high';
53
59
  }
54
60
 
55
61
  export interface ClaudeRunnerConfig {
@@ -65,6 +71,7 @@ export interface RunResult {
65
71
  exitCode: number;
66
72
  timedOut: boolean;
67
73
  contextOverflow: boolean;
74
+ sessionId?: string;
68
75
  }
69
76
 
70
77
  const CONTEXT_OVERFLOW_PATTERNS = [
@@ -252,11 +259,20 @@ export class ClaudeRunner {
252
259
  private activeProcess: pty.IPty | null = null;
253
260
  private killed = false;
254
261
  private model: string;
262
+ private _sessionId: string | undefined;
255
263
 
256
264
  constructor(config: ClaudeRunnerConfig = {}) {
257
265
  this.model = config.model ?? 'opus';
258
266
  }
259
267
 
268
+ /**
269
+ * Get the session ID captured from the most recent Claude session.
270
+ * Available after the system.init event is received during run() or runVerbose().
271
+ */
272
+ get sessionId(): string | undefined {
273
+ return this._sessionId;
274
+ }
275
+
260
276
  /**
261
277
  * Run Claude interactively with stdin/stdout passthrough.
262
278
  * Used for planning phase where user interaction is needed.
@@ -367,7 +383,7 @@ export class ClaudeRunner {
367
383
  * - Default timeout is 60 minutes if not specified
368
384
  */
369
385
  async run(prompt: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
370
- const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext } = options;
386
+ const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel } = options;
371
387
  // Ensure timeout is a positive number, fallback to 60 minutes
372
388
  const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
373
389
  const timeoutMs = validatedTimeout * 60 * 1000;
@@ -377,14 +393,20 @@ export class ClaudeRunner {
377
393
  let stderr = '';
378
394
  let timedOut = false;
379
395
  let contextOverflow = false;
396
+ let sessionId: string | undefined;
380
397
 
381
398
  const claudePath = getClaudePath();
382
399
 
383
400
  logger.debug(`Starting Claude execution session with model: ${this.model}`);
384
401
  logger.debug(`Claude path: ${claudePath}`);
385
402
 
386
- // Use --append-system-prompt to add RAF instructions to system prompt
387
- // This gives RAF instructions stronger precedence than passing as user message
403
+ // Build env, optionally injecting effort level
404
+ const env = effortLevel
405
+ ? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
406
+ : process.env;
407
+
408
+ // Use --output-format stream-json to get structured events including session_id.
409
+ // Unlike runVerbose(), output is parsed silently without writing to stdout.
388
410
  // --dangerously-skip-permissions bypasses interactive prompts
389
411
  // -p enables print mode (non-interactive)
390
412
  const proc = spawn(claudePath, [
@@ -393,11 +415,14 @@ export class ClaudeRunner {
393
415
  this.model,
394
416
  '--append-system-prompt',
395
417
  prompt,
418
+ '--output-format',
419
+ 'stream-json',
420
+ '--verbose',
396
421
  '-p',
397
422
  'Execute the task as described in the system prompt.',
398
423
  ], {
399
424
  cwd,
400
- env: process.env,
425
+ env,
401
426
  stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
402
427
  });
403
428
 
@@ -408,6 +433,9 @@ export class ClaudeRunner {
408
433
  const timeoutHandle = setTimeout(() => {
409
434
  timedOut = true;
410
435
  logger.warn('Claude session timed out');
436
+ if (sessionId) {
437
+ logger.info(`Session ID: ${sessionId}`);
438
+ }
411
439
  proc.kill('SIGTERM');
412
440
  }, timeoutMs);
413
441
 
@@ -418,22 +446,47 @@ export class ClaudeRunner {
418
446
  commitContext,
419
447
  );
420
448
 
421
- // Collect stdout
449
+ // Buffer for incomplete NDJSON lines (data chunks may split across line boundaries)
450
+ let lineBuffer = '';
451
+
452
+ // Collect stdout - parse NDJSON silently (no display output)
422
453
  proc.stdout.on('data', (data) => {
423
- const text = data.toString();
424
- output += text;
425
-
426
- // Check for completion marker to start grace period
427
- completionDetector.checkOutput(output);
428
-
429
- // Check for context overflow
430
- for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
431
- if (pattern.test(text)) {
432
- contextOverflow = true;
433
- logger.warn('Context overflow detected');
434
- proc.kill('SIGTERM');
435
- break;
454
+ lineBuffer += data.toString();
455
+
456
+ // Process complete lines from the NDJSON stream
457
+ let newlineIndex: number;
458
+ while ((newlineIndex = lineBuffer.indexOf('\n')) !== -1) {
459
+ const line = lineBuffer.substring(0, newlineIndex);
460
+ lineBuffer = lineBuffer.substring(newlineIndex + 1);
461
+
462
+ const result = renderStreamEvent(line);
463
+
464
+ if (result.sessionId && !sessionId) {
465
+ sessionId = result.sessionId;
466
+ this._sessionId = sessionId;
436
467
  }
468
+
469
+ if (result.textContent) {
470
+ output += result.textContent;
471
+
472
+ // Check for completion marker to start grace period
473
+ completionDetector.checkOutput(output);
474
+
475
+ // Check for context overflow
476
+ for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
477
+ if (pattern.test(result.textContent)) {
478
+ contextOverflow = true;
479
+ logger.warn('Context overflow detected');
480
+ if (sessionId) {
481
+ logger.info(`Session ID: ${sessionId}`);
482
+ }
483
+ proc.kill('SIGTERM');
484
+ break;
485
+ }
486
+ }
487
+ }
488
+
489
+ // Silently discard display output (unlike runVerbose)
437
490
  }
438
491
  });
439
492
 
@@ -443,6 +496,17 @@ export class ClaudeRunner {
443
496
  });
444
497
 
445
498
  proc.on('close', (exitCode) => {
499
+ // Process any remaining data in the line buffer
500
+ if (lineBuffer.trim()) {
501
+ const result = renderStreamEvent(lineBuffer);
502
+ if (result.sessionId && !sessionId) {
503
+ sessionId = result.sessionId;
504
+ }
505
+ if (result.textContent) {
506
+ output += result.textContent;
507
+ }
508
+ }
509
+
446
510
  clearTimeout(timeoutHandle);
447
511
  completionDetector.cleanup();
448
512
  this.activeProcess = null;
@@ -456,6 +520,7 @@ export class ClaudeRunner {
456
520
  exitCode: exitCode ?? (this.killed ? 130 : 1),
457
521
  timedOut,
458
522
  contextOverflow,
523
+ sessionId,
459
524
  });
460
525
  });
461
526
  });
@@ -474,7 +539,7 @@ export class ClaudeRunner {
474
539
  * - Default timeout is 60 minutes if not specified
475
540
  */
476
541
  async runVerbose(prompt: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
477
- const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext } = options;
542
+ const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel } = options;
478
543
  // Ensure timeout is a positive number, fallback to 60 minutes
479
544
  const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
480
545
  const timeoutMs = validatedTimeout * 60 * 1000;
@@ -484,6 +549,7 @@ export class ClaudeRunner {
484
549
  let stderr = '';
485
550
  let timedOut = false;
486
551
  let contextOverflow = false;
552
+ let sessionId: string | undefined;
487
553
 
488
554
  const claudePath = getClaudePath();
489
555
 
@@ -491,6 +557,11 @@ export class ClaudeRunner {
491
557
  logger.debug(`Prompt length: ${prompt.length}, timeout: ${timeoutMs}ms, cwd: ${cwd}`);
492
558
  logger.debug(`Claude path: ${claudePath}`);
493
559
 
560
+ // Build env, optionally injecting effort level
561
+ const env = effortLevel
562
+ ? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
563
+ : process.env;
564
+
494
565
  logger.debug('Spawning process...');
495
566
  // Use --output-format stream-json --verbose to get real-time streaming events
496
567
  // including tool calls, file operations, and intermediate output.
@@ -509,7 +580,7 @@ export class ClaudeRunner {
509
580
  'Execute the task as described in the system prompt.',
510
581
  ], {
511
582
  cwd,
512
- env: process.env,
583
+ env,
513
584
  stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
514
585
  });
515
586
 
@@ -521,6 +592,9 @@ export class ClaudeRunner {
521
592
  const timeoutHandle = setTimeout(() => {
522
593
  timedOut = true;
523
594
  logger.warn('Claude session timed out');
595
+ if (sessionId) {
596
+ logger.info(`Session ID: ${sessionId}`);
597
+ }
524
598
  proc.kill('SIGTERM');
525
599
  }, timeoutMs);
526
600
 
@@ -549,27 +623,35 @@ export class ClaudeRunner {
549
623
  const line = lineBuffer.substring(0, newlineIndex);
550
624
  lineBuffer = lineBuffer.substring(newlineIndex + 1);
551
625
 
552
- const { display, textContent } = renderStreamEvent(line);
626
+ const result = renderStreamEvent(line);
627
+
628
+ if (result.sessionId && !sessionId) {
629
+ sessionId = result.sessionId;
630
+ this._sessionId = sessionId;
631
+ }
553
632
 
554
- if (textContent) {
555
- output += textContent;
633
+ if (result.textContent) {
634
+ output += result.textContent;
556
635
 
557
636
  // Check for completion marker to start grace period
558
637
  completionDetector.checkOutput(output);
559
638
 
560
639
  // Check for context overflow
561
640
  for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
562
- if (pattern.test(textContent)) {
641
+ if (pattern.test(result.textContent)) {
563
642
  contextOverflow = true;
564
643
  logger.warn('Context overflow detected');
644
+ if (sessionId) {
645
+ logger.info(`Session ID: ${sessionId}`);
646
+ }
565
647
  proc.kill('SIGTERM');
566
648
  break;
567
649
  }
568
650
  }
569
651
  }
570
652
 
571
- if (display) {
572
- process.stdout.write(display);
653
+ if (result.display) {
654
+ process.stdout.write(result.display);
573
655
  }
574
656
  }
575
657
  });
@@ -582,12 +664,15 @@ export class ClaudeRunner {
582
664
  proc.on('close', (exitCode) => {
583
665
  // Process any remaining data in the line buffer
584
666
  if (lineBuffer.trim()) {
585
- const { display, textContent } = renderStreamEvent(lineBuffer);
586
- if (textContent) {
587
- output += textContent;
667
+ const result = renderStreamEvent(lineBuffer);
668
+ if (result.sessionId && !sessionId) {
669
+ sessionId = result.sessionId;
588
670
  }
589
- if (display) {
590
- process.stdout.write(display);
671
+ if (result.textContent) {
672
+ output += result.textContent;
673
+ }
674
+ if (result.display) {
675
+ process.stdout.write(result.display);
591
676
  }
592
677
  }
593
678
 
@@ -605,6 +690,134 @@ export class ClaudeRunner {
605
690
  exitCode: exitCode ?? (this.killed ? 130 : 1),
606
691
  timedOut,
607
692
  contextOverflow,
693
+ sessionId,
694
+ });
695
+ });
696
+ });
697
+ }
698
+
699
+ /**
700
+ * Resume an interrupted Claude session by session ID.
701
+ * Spawns Claude with --resume flag only — no prompt, model, or system prompt flags.
702
+ * Uses stream-json output format for session ID extraction and completion detection.
703
+ */
704
+ async runResume(sessionId: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
705
+ const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel } = options;
706
+ const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
707
+ const timeoutMs = validatedTimeout * 60 * 1000;
708
+
709
+ return new Promise((resolve) => {
710
+ let output = '';
711
+ let stderr = '';
712
+ let timedOut = false;
713
+ let contextOverflow = false;
714
+ let newSessionId: string | undefined;
715
+
716
+ const claudePath = getClaudePath();
717
+
718
+ logger.debug(`Resuming Claude session: ${sessionId}`);
719
+
720
+ const env = effortLevel
721
+ ? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
722
+ : process.env;
723
+
724
+ // Resume with minimal flags: --resume, --dangerously-skip-permissions, --output-format stream-json --verbose
725
+ // Do NOT pass --model, --append-system-prompt, or -p — Claude restores these from the session
726
+ const proc = spawn(claudePath, [
727
+ '--resume',
728
+ sessionId,
729
+ '--dangerously-skip-permissions',
730
+ '--output-format',
731
+ 'stream-json',
732
+ '--verbose',
733
+ ], {
734
+ cwd,
735
+ env,
736
+ stdio: ['ignore', 'pipe', 'pipe'],
737
+ });
738
+
739
+ this.activeProcess = proc as any;
740
+
741
+ const timeoutHandle = setTimeout(() => {
742
+ timedOut = true;
743
+ logger.warn('Resumed Claude session timed out');
744
+ if (newSessionId) {
745
+ logger.info(`Session ID: ${newSessionId}`);
746
+ }
747
+ proc.kill('SIGTERM');
748
+ }, timeoutMs);
749
+
750
+ const completionDetector = createCompletionDetector(
751
+ () => proc.kill('SIGTERM'),
752
+ outcomeFilePath,
753
+ commitContext,
754
+ );
755
+
756
+ let lineBuffer = '';
757
+
758
+ proc.stdout.on('data', (data) => {
759
+ lineBuffer += data.toString();
760
+
761
+ let newlineIndex: number;
762
+ while ((newlineIndex = lineBuffer.indexOf('\n')) !== -1) {
763
+ const line = lineBuffer.substring(0, newlineIndex);
764
+ lineBuffer = lineBuffer.substring(newlineIndex + 1);
765
+
766
+ const result = renderStreamEvent(line);
767
+
768
+ if (result.sessionId && !newSessionId) {
769
+ newSessionId = result.sessionId;
770
+ this._sessionId = newSessionId;
771
+ }
772
+
773
+ if (result.textContent) {
774
+ output += result.textContent;
775
+ completionDetector.checkOutput(output);
776
+
777
+ for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
778
+ if (pattern.test(result.textContent)) {
779
+ contextOverflow = true;
780
+ logger.warn('Context overflow detected');
781
+ if (newSessionId) {
782
+ logger.info(`Session ID: ${newSessionId}`);
783
+ }
784
+ proc.kill('SIGTERM');
785
+ break;
786
+ }
787
+ }
788
+ }
789
+ }
790
+ });
791
+
792
+ proc.stderr.on('data', (data) => {
793
+ stderr += data.toString();
794
+ });
795
+
796
+ proc.on('close', (exitCode) => {
797
+ if (lineBuffer.trim()) {
798
+ const result = renderStreamEvent(lineBuffer);
799
+ if (result.sessionId && !newSessionId) {
800
+ newSessionId = result.sessionId;
801
+ }
802
+ if (result.textContent) {
803
+ output += result.textContent;
804
+ }
805
+ }
806
+
807
+ clearTimeout(timeoutHandle);
808
+ completionDetector.cleanup();
809
+ this.activeProcess = null;
810
+
811
+ if (stderr) {
812
+ logger.debug(`Claude stderr: ${stderr}`);
813
+ }
814
+
815
+ resolve({
816
+ output,
817
+ exitCode: exitCode ?? (this.killed ? 130 : 1),
818
+ timedOut,
819
+ contextOverflow,
820
+ sessionId: newSessionId,
608
821
  });
609
822
  });
610
823
  });
@@ -54,6 +54,11 @@ class ShutdownHandler {
54
54
  this.isShuttingDown = true;
55
55
  logger.info(`\nReceived ${signal}, shutting down gracefully...`);
56
56
 
57
+ // Print session ID if available (for resumption/debugging)
58
+ if (this.claudeRunner?.sessionId) {
59
+ logger.info(`Session ID: ${this.claudeRunner.sessionId}`);
60
+ }
61
+
57
62
  // Kill Claude process if running
58
63
  if (this.claudeRunner?.isRunning()) {
59
64
  logger.debug('Killing Claude process...');
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { logger } from '../utils/logger.js';
6
+ import { extractProjectNumber, extractProjectName, isBase26Prefix, decodeBase26 } from '../utils/paths.js';
6
7
 
7
8
  export interface WorktreeCreateResult {
8
9
  success: boolean;
@@ -355,3 +356,79 @@ export function listWorktreeProjects(repoBasename: string): string[] {
355
356
  return [];
356
357
  }
357
358
  }
359
+
360
+ export interface WorktreeProjectResolution {
361
+ /** The worktree project folder name (e.g., "ahrren-turbo-finder") */
362
+ folder: string;
363
+ /** The worktree root path (e.g., ~/.raf/worktrees/RAF/ahrren-turbo-finder) */
364
+ worktreeRoot: string;
365
+ }
366
+
367
+ /**
368
+ * Resolve a project identifier against worktree folder names.
369
+ * Uses the same matching strategy as `resolveProjectIdentifierWithDetails`:
370
+ * 1. Full folder name match (exact, case-insensitive)
371
+ * 2. Base26 prefix match (6-char ID)
372
+ * 3. Project name match (the portion after the prefix)
373
+ *
374
+ * @param repoBasename - The basename of the current git repo
375
+ * @param identifier - The project identifier to resolve
376
+ * @returns The matched worktree project info or null if not found
377
+ */
378
+ export function resolveWorktreeProjectByIdentifier(
379
+ repoBasename: string,
380
+ identifier: string,
381
+ ): WorktreeProjectResolution | null {
382
+ const wtProjectDirs = listWorktreeProjects(repoBasename);
383
+ if (wtProjectDirs.length === 0) return null;
384
+
385
+ const lowerIdentifier = identifier.toLowerCase();
386
+
387
+ // 1. Full folder name match (exact, case-insensitive)
388
+ for (const dir of wtProjectDirs) {
389
+ if (dir.toLowerCase() === lowerIdentifier) {
390
+ return {
391
+ folder: dir,
392
+ worktreeRoot: computeWorktreePath(repoBasename, dir),
393
+ };
394
+ }
395
+ }
396
+
397
+ // 2. Base26 prefix match
398
+ if (isBase26Prefix(identifier)) {
399
+ const targetNumber = decodeBase26(identifier);
400
+ if (targetNumber !== null) {
401
+ for (const dir of wtProjectDirs) {
402
+ const prefix = extractProjectNumber(dir);
403
+ if (prefix) {
404
+ const dirNumber = decodeBase26(prefix);
405
+ if (dirNumber === targetNumber) {
406
+ return {
407
+ folder: dir,
408
+ worktreeRoot: computeWorktreePath(repoBasename, dir),
409
+ };
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ // 3. Project name match (case-insensitive)
417
+ const nameMatches: string[] = [];
418
+ for (const dir of wtProjectDirs) {
419
+ const name = extractProjectName(dir);
420
+ if (name && name.toLowerCase() === lowerIdentifier) {
421
+ nameMatches.push(dir);
422
+ }
423
+ }
424
+
425
+ if (nameMatches.length === 1) {
426
+ return {
427
+ folder: nameMatches[0]!,
428
+ worktreeRoot: computeWorktreePath(repoBasename, nameMatches[0]!),
429
+ };
430
+ }
431
+
432
+ // Ambiguous or no match
433
+ return null;
434
+ }
@@ -11,6 +11,7 @@
11
11
  export interface StreamEvent {
12
12
  type: string;
13
13
  subtype?: string;
14
+ session_id?: string;
14
15
  message?: {
15
16
  content?: ContentBlock[];
16
17
  };
@@ -72,6 +73,8 @@ export interface RenderResult {
72
73
  display: string;
73
74
  /** Text content to accumulate for output parsing (completion markers, etc.) */
74
75
  textContent: string;
76
+ /** Session ID extracted from system init event, if present */
77
+ sessionId?: string;
75
78
  }
76
79
 
77
80
  /**
@@ -93,7 +96,7 @@ export function renderStreamEvent(line: string): RenderResult {
93
96
 
94
97
  switch (event.type) {
95
98
  case 'system':
96
- return { display: '', textContent: '' };
99
+ return { display: '', textContent: '', sessionId: event.session_id };
97
100
 
98
101
  case 'assistant':
99
102
  return renderAssistant(event);
@@ -29,6 +29,7 @@ export interface DoCommandOptions {
29
29
  model?: ClaudeModelName;
30
30
  sonnet?: boolean;
31
31
  worktree?: boolean;
32
+ resume?: string;
32
33
  }
33
34
 
34
35
  export interface StatusCommandOptions {
@@ -244,6 +244,30 @@ describe('ClaudeRunner - runInteractive', () => {
244
244
  });
245
245
  });
246
246
 
247
+ describe('effort level (not applied in interactive mode)', () => {
248
+ it('should NOT set CLAUDE_CODE_EFFORT_LEVEL in runInteractive env', async () => {
249
+ const mockProc = createMockPtyProcess();
250
+ const mockStdin = createMockStdin();
251
+ const mockStdout = createMockStdout();
252
+
253
+ Object.defineProperty(process, 'stdin', { value: mockStdin, configurable: true });
254
+ Object.defineProperty(process, 'stdout', { value: mockStdout, configurable: true });
255
+
256
+ mockPtySpawn.mockReturnValue(mockProc);
257
+
258
+ const runner = new ClaudeRunner();
259
+ // Even if effortLevel were somehow passed, interactive mode should use process.env as-is
260
+ const runPromise = runner.runInteractive('system', 'user');
261
+
262
+ const spawnOptions = mockPtySpawn.mock.calls[0][2];
263
+ // Interactive mode passes process.env directly, no effort level override
264
+ expect(spawnOptions.env).not.toHaveProperty('CLAUDE_CODE_EFFORT_LEVEL');
265
+
266
+ mockProc._exitCallback({ exitCode: 0 });
267
+ await runPromise;
268
+ });
269
+ });
270
+
247
271
  describe('--dangerously-skip-permissions flag', () => {
248
272
  it('should NOT include --dangerously-skip-permissions by default', async () => {
249
273
  const mockProc = createMockPtyProcess();