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.
- package/.claude/settings.local.json +3 -1
- package/RAF/ahrren-turbo-finder/decisions.md +19 -0
- package/RAF/ahrren-turbo-finder/input.md +2 -0
- package/RAF/ahrren-turbo-finder/outcomes/01-worktree-auto-detect.md +40 -0
- package/RAF/ahrren-turbo-finder/outcomes/02-medium-effort-do.md +34 -0
- package/RAF/ahrren-turbo-finder/plans/01-worktree-auto-detect.md +44 -0
- package/RAF/ahrren-turbo-finder/plans/02-medium-effort-do.md +39 -0
- package/RAF/ahrtxf-session-sentinel/decisions.md +19 -0
- package/RAF/ahrtxf-session-sentinel/input.md +1 -0
- package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +37 -0
- package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +45 -0
- package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +41 -0
- package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +51 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +61 -20
- package/dist/commands/do.js.map +1 -1
- package/dist/core/claude-runner.d.ts +19 -0
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +199 -29
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/shutdown-handler.d.ts.map +1 -1
- package/dist/core/shutdown-handler.js +4 -0
- package/dist/core/shutdown-handler.js.map +1 -1
- package/dist/core/worktree.d.ts +18 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +61 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +3 -0
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +1 -1
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/do.ts +67 -21
- package/src/core/claude-runner.ts +244 -31
- package/src/core/shutdown-handler.ts +5 -0
- package/src/core/worktree.ts +77 -0
- package/src/parsers/stream-renderer.ts +4 -1
- package/src/types/config.ts +1 -0
- package/tests/unit/claude-runner-interactive.test.ts +24 -0
- package/tests/unit/claude-runner.test.ts +509 -55
- package/tests/unit/post-execution-picker.test.ts +1 -0
- package/tests/unit/stream-renderer.test.ts +30 -0
- 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
|
-
//
|
|
387
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
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
|
|
586
|
-
if (
|
|
587
|
-
|
|
667
|
+
const result = renderStreamEvent(lineBuffer);
|
|
668
|
+
if (result.sessionId && !sessionId) {
|
|
669
|
+
sessionId = result.sessionId;
|
|
588
670
|
}
|
|
589
|
-
if (
|
|
590
|
-
|
|
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...');
|
package/src/core/worktree.ts
CHANGED
|
@@ -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);
|
package/src/types/config.ts
CHANGED
|
@@ -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();
|