rafcode 2.1.0 → 2.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.
Files changed (130) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/CLAUDE.md +59 -11
  3. package/RAF/ahslfe-config-wizard/decisions.md +34 -0
  4. package/RAF/ahslfe-config-wizard/input.md +1 -0
  5. package/RAF/ahslfe-config-wizard/outcomes/01-define-config-schema.md +38 -0
  6. package/RAF/ahslfe-config-wizard/outcomes/02-refactor-codebase-to-use-config.md +67 -0
  7. package/RAF/ahslfe-config-wizard/outcomes/03-create-config-documentation.md +37 -0
  8. package/RAF/ahslfe-config-wizard/outcomes/04-implement-raf-config-command.md +47 -0
  9. package/RAF/ahslfe-config-wizard/outcomes/05-update-claude-md.md +26 -0
  10. package/RAF/ahslfe-config-wizard/plans/01-define-config-schema.md +73 -0
  11. package/RAF/ahslfe-config-wizard/plans/02-refactor-codebase-to-use-config.md +74 -0
  12. package/RAF/ahslfe-config-wizard/plans/03-create-config-documentation.md +57 -0
  13. package/RAF/ahslfe-config-wizard/plans/04-implement-raf-config-command.md +66 -0
  14. package/RAF/ahslfe-config-wizard/plans/05-update-claude-md.md +60 -0
  15. package/RAF/ahstvo-token-tracker/decisions.md +44 -0
  16. package/RAF/ahstvo-token-tracker/input.md +3 -0
  17. package/RAF/ahstvo-token-tracker/outcomes/01-full-model-id-support.md +43 -0
  18. package/RAF/ahstvo-token-tracker/outcomes/02-name-generation-no-session.md +33 -0
  19. package/RAF/ahstvo-token-tracker/outcomes/03-unify-stream-json-execution.md +48 -0
  20. package/RAF/ahstvo-token-tracker/outcomes/04-token-tracking-cost-calculation.md +53 -0
  21. package/RAF/ahstvo-token-tracker/outcomes/05-token-cost-console-reporting.md +57 -0
  22. package/RAF/ahstvo-token-tracker/outcomes/06-runtime-verbose-toggle.md +53 -0
  23. package/RAF/ahstvo-token-tracker/outcomes/07-readme-config-docs.md +36 -0
  24. package/RAF/ahstvo-token-tracker/plans/01-full-model-id-support.md +35 -0
  25. package/RAF/ahstvo-token-tracker/plans/02-name-generation-no-session.md +36 -0
  26. package/RAF/ahstvo-token-tracker/plans/03-unify-stream-json-execution.md +44 -0
  27. package/RAF/ahstvo-token-tracker/plans/04-token-tracking-cost-calculation.md +56 -0
  28. package/RAF/ahstvo-token-tracker/plans/05-token-cost-console-reporting.md +55 -0
  29. package/RAF/ahstvo-token-tracker/plans/06-runtime-verbose-toggle.md +48 -0
  30. package/RAF/ahstvo-token-tracker/plans/07-readme-config-docs.md +44 -0
  31. package/README.md +34 -0
  32. package/dist/commands/config.d.ts +3 -0
  33. package/dist/commands/config.d.ts.map +1 -0
  34. package/dist/commands/config.js +173 -0
  35. package/dist/commands/config.js.map +1 -0
  36. package/dist/commands/do.d.ts.map +1 -1
  37. package/dist/commands/do.js +50 -28
  38. package/dist/commands/do.js.map +1 -1
  39. package/dist/commands/plan.d.ts.map +1 -1
  40. package/dist/commands/plan.js +3 -2
  41. package/dist/commands/plan.js.map +1 -1
  42. package/dist/core/claude-runner.d.ts +17 -13
  43. package/dist/core/claude-runner.d.ts.map +1 -1
  44. package/dist/core/claude-runner.js +42 -257
  45. package/dist/core/claude-runner.js.map +1 -1
  46. package/dist/core/failure-analyzer.d.ts.map +1 -1
  47. package/dist/core/failure-analyzer.js +6 -3
  48. package/dist/core/failure-analyzer.js.map +1 -1
  49. package/dist/core/git.d.ts.map +1 -1
  50. package/dist/core/git.js +10 -3
  51. package/dist/core/git.js.map +1 -1
  52. package/dist/core/pull-request.d.ts +1 -1
  53. package/dist/core/pull-request.d.ts.map +1 -1
  54. package/dist/core/pull-request.js +7 -4
  55. package/dist/core/pull-request.js.map +1 -1
  56. package/dist/core/shutdown-handler.d.ts.map +1 -1
  57. package/dist/core/shutdown-handler.js +0 -4
  58. package/dist/core/shutdown-handler.js.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/parsers/stream-renderer.d.ts +16 -4
  62. package/dist/parsers/stream-renderer.d.ts.map +1 -1
  63. package/dist/parsers/stream-renderer.js +35 -5
  64. package/dist/parsers/stream-renderer.js.map +1 -1
  65. package/dist/prompts/execution.d.ts.map +1 -1
  66. package/dist/prompts/execution.js +11 -1
  67. package/dist/prompts/execution.js.map +1 -1
  68. package/dist/types/config.d.ts +95 -5
  69. package/dist/types/config.d.ts.map +1 -1
  70. package/dist/types/config.js +63 -3
  71. package/dist/types/config.js.map +1 -1
  72. package/dist/utils/config.d.ts +59 -7
  73. package/dist/utils/config.d.ts.map +1 -1
  74. package/dist/utils/config.js +276 -21
  75. package/dist/utils/config.js.map +1 -1
  76. package/dist/utils/name-generator.d.ts +3 -7
  77. package/dist/utils/name-generator.d.ts.map +1 -1
  78. package/dist/utils/name-generator.js +75 -61
  79. package/dist/utils/name-generator.js.map +1 -1
  80. package/dist/utils/terminal-symbols.d.ts +21 -0
  81. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  82. package/dist/utils/terminal-symbols.js +62 -0
  83. package/dist/utils/terminal-symbols.js.map +1 -1
  84. package/dist/utils/token-tracker.d.ts +45 -0
  85. package/dist/utils/token-tracker.d.ts.map +1 -0
  86. package/dist/utils/token-tracker.js +107 -0
  87. package/dist/utils/token-tracker.js.map +1 -0
  88. package/dist/utils/validation.d.ts +5 -5
  89. package/dist/utils/validation.d.ts.map +1 -1
  90. package/dist/utils/validation.js +10 -6
  91. package/dist/utils/validation.js.map +1 -1
  92. package/dist/utils/verbose-toggle.d.ts +33 -0
  93. package/dist/utils/verbose-toggle.d.ts.map +1 -0
  94. package/dist/utils/verbose-toggle.js +94 -0
  95. package/dist/utils/verbose-toggle.js.map +1 -0
  96. package/package.json +1 -1
  97. package/src/commands/config.ts +204 -0
  98. package/src/commands/do.ts +59 -27
  99. package/src/commands/plan.ts +3 -2
  100. package/src/core/claude-runner.ts +58 -311
  101. package/src/core/failure-analyzer.ts +6 -3
  102. package/src/core/git.ts +10 -3
  103. package/src/core/pull-request.ts +7 -4
  104. package/src/core/shutdown-handler.ts +0 -5
  105. package/src/index.ts +2 -0
  106. package/src/parsers/stream-renderer.ts +55 -8
  107. package/src/prompts/config-docs.md +331 -0
  108. package/src/prompts/execution.ts +13 -1
  109. package/src/types/config.ts +156 -8
  110. package/src/utils/config.ts +335 -21
  111. package/src/utils/name-generator.ts +84 -71
  112. package/src/utils/terminal-symbols.ts +68 -0
  113. package/src/utils/token-tracker.ts +135 -0
  114. package/src/utils/validation.ts +15 -10
  115. package/src/utils/verbose-toggle.ts +103 -0
  116. package/tests/unit/claude-runner.test.ts +216 -403
  117. package/tests/unit/config-command.test.ts +163 -0
  118. package/tests/unit/config.test.ts +608 -30
  119. package/tests/unit/name-generator.test.ts +99 -75
  120. package/tests/unit/pull-request.test.ts +2 -0
  121. package/tests/unit/stream-renderer.test.ts +83 -30
  122. package/tests/unit/terminal-symbols.test.ts +157 -0
  123. package/tests/unit/token-tracker.test.ts +352 -0
  124. package/tests/unit/verbose-toggle.test.ts +204 -0
  125. package/RAF/ahrtxf-session-sentinel/decisions.md +0 -19
  126. package/RAF/ahrtxf-session-sentinel/input.md +0 -1
  127. package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +0 -37
  128. package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +0 -45
  129. package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +0 -41
  130. package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +0 -51
@@ -140,8 +140,7 @@ describe('ClaudeRunner', () => {
140
140
  const runPromise = runner.run('test prompt', { timeout: 5 });
141
141
 
142
142
  // Emit some output and close the process normally
143
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
144
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
143
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>'));
145
144
  mockProc.emit('close', 0);
146
145
 
147
146
  const result = await runPromise;
@@ -163,9 +162,8 @@ describe('ClaudeRunner', () => {
163
162
 
164
163
  const runPromise1 = runner.run('first prompt', { timeout: 2 });
165
164
 
166
- // Complete first call quickly (NDJSON event)
167
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'output' }] } });
168
- mockProc1.stdout.emit('data', Buffer.from(event + '\n'));
165
+ // Complete first call quickly
166
+ mockProc1.stdout.emit('data', Buffer.from('output'));
169
167
  mockProc1.emit('close', 0);
170
168
  await runPromise1;
171
169
 
@@ -213,9 +211,8 @@ describe('ClaudeRunner', () => {
213
211
  // Advance a bit but not to timeout
214
212
  jest.advanceTimersByTime(30000);
215
213
 
216
- // Complete process (NDJSON event)
217
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'output' }] } });
218
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
214
+ // Complete process
215
+ mockProc.stdout.emit('data', Buffer.from('output'));
219
216
  mockProc.emit('close', 0);
220
217
 
221
218
  const result = await runPromise;
@@ -235,9 +232,8 @@ describe('ClaudeRunner', () => {
235
232
  jest.advanceTimersByTime(1000);
236
233
  expect(mockProc.kill).not.toHaveBeenCalled();
237
234
 
238
- // Complete the process normally before reaching the 60 minute timeout (NDJSON event)
239
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'output' }] } });
240
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
235
+ // Complete the process normally before reaching the 60 minute timeout
236
+ mockProc.stdout.emit('data', Buffer.from('output'));
241
237
  mockProc.emit('close', 0);
242
238
 
243
239
  const result = await runPromise;
@@ -309,9 +305,8 @@ describe('ClaudeRunner', () => {
309
305
  const runner = new ClaudeRunner();
310
306
  const runPromise = runner.run('test prompt', { timeout: 60 });
311
307
 
312
- // Emit context overflow message as NDJSON assistant event (run() now uses stream-json)
313
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Error: context length exceeded' }] } });
314
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
308
+ // Emit context overflow message (with newline so NDJSON line buffer processes it)
309
+ mockProc.stdout.emit('data', Buffer.from('Error: context length exceeded\n'));
315
310
 
316
311
  const result = await runPromise;
317
312
  expect(result.contextOverflow).toBe(true);
@@ -338,9 +333,8 @@ describe('ClaudeRunner', () => {
338
333
  const runner = new ClaudeRunner();
339
334
  const runPromise = runner.run('test prompt', { timeout: 60 });
340
335
 
341
- // Emit as NDJSON assistant event (run() now uses stream-json)
342
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: `Error: ${pattern}` }] } });
343
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
336
+ // Trailing newline needed for NDJSON line buffer processing
337
+ mockProc.stdout.emit('data', Buffer.from(`Error: ${pattern}\n`));
344
338
 
345
339
  const result = await runPromise;
346
340
  expect(result.contextOverflow).toBe(true);
@@ -362,18 +356,17 @@ describe('ClaudeRunner', () => {
362
356
  return proc;
363
357
  }
364
358
 
365
- it('should collect all stdout output from NDJSON events', async () => {
359
+ it('should collect all stdout output', async () => {
366
360
  const mockProc = createMockProcess();
367
361
  mockSpawn.mockReturnValue(mockProc);
368
362
 
369
363
  const runner = new ClaudeRunner();
370
364
  const runPromise = runner.run('test prompt', { timeout: 60 });
371
365
 
372
- // Emit NDJSON assistant events (run() now uses stream-json)
373
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk1' }] } });
374
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk2' }] } });
375
- const event3 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk3' }] } });
376
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n' + event2 + '\n' + event3 + '\n'));
366
+ // Emit multiple chunks
367
+ mockProc.stdout.emit('data', Buffer.from('chunk1'));
368
+ mockProc.stdout.emit('data', Buffer.from('chunk2'));
369
+ mockProc.stdout.emit('data', Buffer.from('chunk3'));
377
370
  mockProc.emit('close', 0);
378
371
 
379
372
  const result = await runPromise;
@@ -704,7 +697,7 @@ describe('ClaudeRunner', () => {
704
697
  expect(spawnArgs).toContain('--verbose');
705
698
  });
706
699
 
707
- it('should include --output-format stream-json and --verbose flags in run()', async () => {
700
+ it('should include --output-format stream-json and --verbose flags in run() (unified stream-json)', async () => {
708
701
  const mockProc = createMockProcess();
709
702
  mockSpawn.mockReturnValue(mockProc);
710
703
 
@@ -764,6 +757,169 @@ describe('ClaudeRunner', () => {
764
757
  });
765
758
  });
766
759
 
760
+ describe('usage data extraction', () => {
761
+ function createMockProcess() {
762
+ const stdout = new EventEmitter();
763
+ const stderr = new EventEmitter();
764
+ const proc = new EventEmitter() as any;
765
+ proc.stdout = stdout;
766
+ proc.stderr = stderr;
767
+ proc.kill = jest.fn();
768
+ return proc;
769
+ }
770
+
771
+ it('should return usageData from run() when result event has usage', async () => {
772
+ const mockProc = createMockProcess();
773
+ mockSpawn.mockReturnValue(mockProc);
774
+
775
+ const runner = new ClaudeRunner();
776
+ const runPromise = runner.run('test prompt', { timeout: 60 });
777
+
778
+ const assistantEvent = JSON.stringify({
779
+ type: 'assistant',
780
+ message: { content: [{ type: 'text', text: 'Done' }] },
781
+ });
782
+ const resultEvent = JSON.stringify({
783
+ type: 'result',
784
+ usage: { input_tokens: 1000, output_tokens: 500, cache_read_input_tokens: 200, cache_creation_input_tokens: 100 },
785
+ modelUsage: { 'claude-opus-4-6': { inputTokens: 1000, outputTokens: 500, cacheReadInputTokens: 200, cacheCreationInputTokens: 100 } },
786
+ });
787
+
788
+ mockProc.stdout.emit('data', Buffer.from(assistantEvent + '\n' + resultEvent + '\n'));
789
+ mockProc.emit('close', 0);
790
+
791
+ const result = await runPromise;
792
+ expect(result.usageData).toBeDefined();
793
+ expect(result.usageData!.inputTokens).toBe(1000);
794
+ expect(result.usageData!.outputTokens).toBe(500);
795
+ expect(result.usageData!.modelUsage['claude-opus-4-6']).toEqual({
796
+ inputTokens: 1000,
797
+ outputTokens: 500,
798
+ cacheReadInputTokens: 200,
799
+ cacheCreationInputTokens: 100,
800
+ });
801
+ });
802
+
803
+ it('should return usageData from runVerbose() when result event has usage', async () => {
804
+ const mockProc = createMockProcess();
805
+ mockSpawn.mockReturnValue(mockProc);
806
+
807
+ const runner = new ClaudeRunner();
808
+ const runPromise = runner.runVerbose('test prompt', { timeout: 60 });
809
+
810
+ const resultEvent = JSON.stringify({
811
+ type: 'result',
812
+ usage: { input_tokens: 2000, output_tokens: 800 },
813
+ modelUsage: { 'claude-sonnet-4-5-20250929': { inputTokens: 2000, outputTokens: 800 } },
814
+ });
815
+
816
+ mockProc.stdout.emit('data', Buffer.from(resultEvent + '\n'));
817
+ mockProc.emit('close', 0);
818
+
819
+ const result = await runPromise;
820
+ expect(result.usageData).toBeDefined();
821
+ expect(result.usageData!.inputTokens).toBe(2000);
822
+ expect(result.usageData!.outputTokens).toBe(800);
823
+ });
824
+
825
+ it('should return undefined usageData when no result event', async () => {
826
+ const mockProc = createMockProcess();
827
+ mockSpawn.mockReturnValue(mockProc);
828
+
829
+ const runner = new ClaudeRunner();
830
+ const runPromise = runner.run('test prompt', { timeout: 60 });
831
+
832
+ const assistantEvent = JSON.stringify({
833
+ type: 'assistant',
834
+ message: { content: [{ type: 'text', text: 'Output' }] },
835
+ });
836
+
837
+ mockProc.stdout.emit('data', Buffer.from(assistantEvent + '\n'));
838
+ mockProc.emit('close', 0);
839
+
840
+ const result = await runPromise;
841
+ expect(result.usageData).toBeUndefined();
842
+ });
843
+
844
+ it('should suppress display in run() but still capture usage data', async () => {
845
+ const mockProc = createMockProcess();
846
+ mockSpawn.mockReturnValue(mockProc);
847
+
848
+ // Spy on stdout.write to verify no display output in non-verbose mode
849
+ const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
850
+
851
+ const runner = new ClaudeRunner();
852
+ const runPromise = runner.run('test prompt', { timeout: 60 });
853
+
854
+ const toolEvent = JSON.stringify({
855
+ type: 'assistant',
856
+ message: { content: [
857
+ { type: 'text', text: 'Working...' },
858
+ { type: 'tool_use', name: 'Read', input: { file_path: '/test.ts' } },
859
+ ] },
860
+ });
861
+ const resultEvent = JSON.stringify({
862
+ type: 'result',
863
+ usage: { input_tokens: 100, output_tokens: 50 },
864
+ });
865
+
866
+ mockProc.stdout.emit('data', Buffer.from(toolEvent + '\n' + resultEvent + '\n'));
867
+ mockProc.emit('close', 0);
868
+
869
+ const result = await runPromise;
870
+
871
+ // No display output (run() is non-verbose)
872
+ expect(writeSpy).not.toHaveBeenCalled();
873
+
874
+ // But usage data is still captured
875
+ expect(result.usageData).toBeDefined();
876
+ expect(result.usageData!.inputTokens).toBe(100);
877
+
878
+ // And text content is captured
879
+ expect(result.output).toContain('Working...');
880
+
881
+ writeSpy.mockRestore();
882
+ });
883
+
884
+ it('should use verboseCheck callback to dynamically control display', async () => {
885
+ const mockProc = createMockProcess();
886
+ mockSpawn.mockReturnValue(mockProc);
887
+
888
+ const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
889
+
890
+ let dynamicVerbose = false;
891
+ const runner = new ClaudeRunner();
892
+ const runPromise = runner.run('test prompt', {
893
+ timeout: 60,
894
+ verboseCheck: () => dynamicVerbose,
895
+ });
896
+
897
+ // First event with verbose OFF — should not display
898
+ const event1 = JSON.stringify({
899
+ type: 'assistant',
900
+ message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/a.ts' } }] },
901
+ });
902
+ mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
903
+ expect(writeSpy).not.toHaveBeenCalled();
904
+
905
+ // Toggle verbose ON
906
+ dynamicVerbose = true;
907
+
908
+ // Second event with verbose ON — should display
909
+ const event2 = JSON.stringify({
910
+ type: 'assistant',
911
+ message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/b.ts' } }] },
912
+ });
913
+ mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
914
+ expect(writeSpy).toHaveBeenCalled();
915
+
916
+ mockProc.emit('close', 0);
917
+ await runPromise;
918
+
919
+ writeSpy.mockRestore();
920
+ });
921
+ });
922
+
767
923
  describe('retry isolation (timeout per attempt)', () => {
768
924
  function createMockProcess() {
769
925
  const stdout = new EventEmitter();
@@ -804,9 +960,8 @@ describe('ClaudeRunner', () => {
804
960
  jest.advanceTimersByTime(60000);
805
961
  expect(mockProc2.kill).not.toHaveBeenCalled();
806
962
 
807
- // Complete successfully (NDJSON event)
808
- const successEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'success' }] } });
809
- mockProc2.stdout.emit('data', Buffer.from(successEvent + '\n'));
963
+ // Complete successfully
964
+ mockProc2.stdout.emit('data', Buffer.from('success'));
810
965
  mockProc2.emit('close', 0);
811
966
 
812
967
  const result2 = await runPromise2;
@@ -827,9 +982,8 @@ describe('ClaudeRunner', () => {
827
982
  // Run for 4 minutes (240000ms)
828
983
  jest.advanceTimersByTime(240000);
829
984
 
830
- // Fail without timeout (NDJSON event)
831
- const failEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>FAILED</promise>' }] } });
832
- mockProc1.stdout.emit('data', Buffer.from(failEvent + '\n'));
985
+ // Fail without timeout
986
+ mockProc1.stdout.emit('data', Buffer.from('<promise>FAILED</promise>'));
833
987
  mockProc1.emit('close', 1);
834
988
 
835
989
  const result1 = await runPromise1;
@@ -879,9 +1033,8 @@ describe('ClaudeRunner', () => {
879
1033
  const runner = new ClaudeRunner();
880
1034
  const runPromise = runner.run('test prompt', { timeout: 60 });
881
1035
 
882
- // Emit output with completion marker (NDJSON event)
883
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Writing outcome...\n<promise>COMPLETE</promise>' }] } });
884
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1036
+ // Emit output with completion marker
1037
+ mockProc.stdout.emit('data', Buffer.from('Writing outcome...\n<promise>COMPLETE</promise>\n'));
885
1038
 
886
1039
  // Process should not be killed immediately
887
1040
  expect(mockProc.kill).not.toHaveBeenCalled();
@@ -906,9 +1059,8 @@ describe('ClaudeRunner', () => {
906
1059
  const runner = new ClaudeRunner();
907
1060
  const runPromise = runner.run('test prompt', { timeout: 60 });
908
1061
 
909
- // Emit output with failed marker (NDJSON event)
910
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>FAILED</promise>\nReason: test error' }] } });
911
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1062
+ // Emit output with failed marker
1063
+ mockProc.stdout.emit('data', Buffer.from('<promise>FAILED</promise>\nReason: test error'));
912
1064
 
913
1065
  // Advance past grace period
914
1066
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -925,15 +1077,13 @@ describe('ClaudeRunner', () => {
925
1077
  const runner = new ClaudeRunner();
926
1078
  const runPromise = runner.run('test prompt', { timeout: 60 });
927
1079
 
928
- // Emit partial output (no marker yet) as NDJSON
929
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Working on task...' }] } });
930
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
1080
+ // Emit partial output (no marker yet)
1081
+ mockProc.stdout.emit('data', Buffer.from('Working on task...\n'));
931
1082
  jest.advanceTimersByTime(10000);
932
1083
  expect(mockProc.kill).not.toHaveBeenCalled();
933
1084
 
934
- // Emit marker in second chunk as NDJSON
935
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
936
- mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
1085
+ // Emit marker in second chunk
1086
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
937
1087
 
938
1088
  // Grace period starts now - advance past it
939
1089
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -951,9 +1101,8 @@ describe('ClaudeRunner', () => {
951
1101
  const runner = new ClaudeRunner();
952
1102
  const runPromise = runner.run('test prompt', { timeout: 60 });
953
1103
 
954
- // Emit completion marker as NDJSON
955
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
956
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1104
+ // Emit completion marker
1105
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
957
1106
 
958
1107
  // Process exits naturally before grace period
959
1108
  jest.advanceTimersByTime(5000);
@@ -983,9 +1132,8 @@ describe('ClaudeRunner', () => {
983
1132
  outcomeFilePath: outcomePath,
984
1133
  });
985
1134
 
986
- // No output marker in stdout - just regular output (NDJSON event)
987
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Working on task...' }] } });
988
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1135
+ // No output marker in stdout - just regular output
1136
+ mockProc.stdout.emit('data', Buffer.from('Working on task...'));
989
1137
 
990
1138
  // After some time, outcome file appears with marker
991
1139
  jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS - 1);
@@ -1029,9 +1177,8 @@ describe('ClaudeRunner', () => {
1029
1177
  jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS * 5);
1030
1178
  expect(mockProc.kill).not.toHaveBeenCalled();
1031
1179
 
1032
- // Complete normally (NDJSON event)
1033
- const doneEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'done' }] } });
1034
- mockProc.stdout.emit('data', Buffer.from(doneEvent + '\n'));
1180
+ // Complete normally
1181
+ mockProc.stdout.emit('data', Buffer.from('done'));
1035
1182
  mockProc.emit('close', 0);
1036
1183
 
1037
1184
  await runPromise;
@@ -1064,16 +1211,14 @@ describe('ClaudeRunner', () => {
1064
1211
  const runner = new ClaudeRunner();
1065
1212
  const runPromise = runner.run('test prompt', { timeout: 60 });
1066
1213
 
1067
- // Emit first marker as NDJSON
1068
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1069
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
1214
+ // Emit first marker
1215
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1070
1216
 
1071
1217
  // Advance halfway through grace period
1072
1218
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2);
1073
1219
 
1074
- // Emit second marker (e.g., Claude reading back what it wrote) as NDJSON
1075
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Verified: <promise>COMPLETE</promise>' }] } });
1076
- mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
1220
+ // Emit second marker (e.g., Claude reading back what it wrote)
1221
+ mockProc.stdout.emit('data', Buffer.from('Verified: <promise>COMPLETE</promise>\n'));
1077
1222
 
1078
1223
  // Advance remaining grace period from FIRST detection
1079
1224
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2 + 1);
@@ -1129,9 +1274,8 @@ describe('ClaudeRunner', () => {
1129
1274
  commitContext,
1130
1275
  });
1131
1276
 
1132
- // Emit COMPLETE marker as NDJSON
1133
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1134
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1277
+ // Emit COMPLETE marker
1278
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1135
1279
 
1136
1280
  // Advance to grace period expiry - commit already verified
1137
1281
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1155,9 +1299,8 @@ describe('ClaudeRunner', () => {
1155
1299
  commitContext,
1156
1300
  });
1157
1301
 
1158
- // Emit COMPLETE marker as NDJSON
1159
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1160
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1302
+ // Emit COMPLETE marker
1303
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1161
1304
 
1162
1305
  // Advance past initial grace period - commit not found, should extend
1163
1306
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1191,9 +1334,8 @@ describe('ClaudeRunner', () => {
1191
1334
  commitContext,
1192
1335
  });
1193
1336
 
1194
- // Emit COMPLETE marker as NDJSON
1195
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1196
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1337
+ // Emit COMPLETE marker
1338
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1197
1339
 
1198
1340
  // Advance past initial grace period
1199
1341
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1219,9 +1361,8 @@ describe('ClaudeRunner', () => {
1219
1361
  commitContext,
1220
1362
  });
1221
1363
 
1222
- // Emit FAILED marker (not COMPLETE) as NDJSON
1223
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>FAILED</promise>' }] } });
1224
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1364
+ // Emit FAILED marker (not COMPLETE)
1365
+ mockProc.stdout.emit('data', Buffer.from('<promise>FAILED</promise>\n'));
1225
1366
 
1226
1367
  // Grace period should expire normally without extension
1227
1368
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1240,9 +1381,8 @@ describe('ClaudeRunner', () => {
1240
1381
  // No commitContext provided
1241
1382
  });
1242
1383
 
1243
- // Emit COMPLETE marker as NDJSON
1244
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1245
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1384
+ // Emit COMPLETE marker
1385
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1246
1386
 
1247
1387
  // Grace period should expire normally (no commit check)
1248
1388
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1267,9 +1407,8 @@ describe('ClaudeRunner', () => {
1267
1407
  commitContext,
1268
1408
  });
1269
1409
 
1270
- // Emit COMPLETE marker as NDJSON
1271
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1272
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1410
+ // Emit COMPLETE marker
1411
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1273
1412
 
1274
1413
  // Grace period expires - commit message doesn't match, should extend
1275
1414
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1302,9 +1441,8 @@ describe('ClaudeRunner', () => {
1302
1441
  commitContext,
1303
1442
  });
1304
1443
 
1305
- // Emit COMPLETE marker as NDJSON
1306
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1307
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1444
+ // Emit COMPLETE marker
1445
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1308
1446
 
1309
1447
  // Grace period expires - file not committed, should extend
1310
1448
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1321,329 +1459,4 @@ describe('ClaudeRunner', () => {
1321
1459
  await runPromise;
1322
1460
  });
1323
1461
  });
1324
-
1325
- describe('session ID extraction', () => {
1326
- function createMockProcess() {
1327
- const stdout = new EventEmitter();
1328
- const stderr = new EventEmitter();
1329
- const proc = new EventEmitter() as any;
1330
- proc.stdout = stdout;
1331
- proc.stderr = stderr;
1332
- proc.kill = jest.fn();
1333
- return proc;
1334
- }
1335
-
1336
- it('should extract sessionId from system init event in run()', async () => {
1337
- const mockProc = createMockProcess();
1338
- mockSpawn.mockReturnValue(mockProc);
1339
-
1340
- const runner = new ClaudeRunner();
1341
- const runPromise = runner.run('test prompt', { timeout: 60 });
1342
-
1343
- // Emit system init event with session_id
1344
- const initEvent = JSON.stringify({
1345
- type: 'system',
1346
- subtype: 'init',
1347
- session_id: 'test-session-123',
1348
- tools: ['Read', 'Write'],
1349
- model: 'claude-opus-4-6',
1350
- });
1351
- const textEvent = JSON.stringify({
1352
- type: 'assistant',
1353
- message: { content: [{ type: 'text', text: 'done' }] },
1354
- });
1355
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n' + textEvent + '\n'));
1356
- mockProc.emit('close', 0);
1357
-
1358
- const result = await runPromise;
1359
- expect(result.sessionId).toBe('test-session-123');
1360
- });
1361
-
1362
- it('should extract sessionId from system init event in runVerbose()', async () => {
1363
- const mockProc = createMockProcess();
1364
- mockSpawn.mockReturnValue(mockProc);
1365
-
1366
- const runner = new ClaudeRunner();
1367
- const runPromise = runner.runVerbose('test prompt', { timeout: 60 });
1368
-
1369
- // Emit system init event with session_id
1370
- const initEvent = JSON.stringify({
1371
- type: 'system',
1372
- subtype: 'init',
1373
- session_id: 'verbose-session-456',
1374
- tools: ['Read', 'Write'],
1375
- model: 'claude-opus-4-6',
1376
- });
1377
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n'));
1378
- mockProc.emit('close', 0);
1379
-
1380
- const result = await runPromise;
1381
- expect(result.sessionId).toBe('verbose-session-456');
1382
- });
1383
-
1384
- it('should return undefined sessionId when no system init event', async () => {
1385
- const mockProc = createMockProcess();
1386
- mockSpawn.mockReturnValue(mockProc);
1387
-
1388
- const runner = new ClaudeRunner();
1389
- const runPromise = runner.run('test prompt', { timeout: 60 });
1390
-
1391
- // Only emit assistant event, no system init
1392
- const textEvent = JSON.stringify({
1393
- type: 'assistant',
1394
- message: { content: [{ type: 'text', text: 'output' }] },
1395
- });
1396
- mockProc.stdout.emit('data', Buffer.from(textEvent + '\n'));
1397
- mockProc.emit('close', 0);
1398
-
1399
- const result = await runPromise;
1400
- expect(result.sessionId).toBeUndefined();
1401
- });
1402
-
1403
- it('should expose sessionId via getter on ClaudeRunner', async () => {
1404
- const mockProc = createMockProcess();
1405
- mockSpawn.mockReturnValue(mockProc);
1406
-
1407
- const runner = new ClaudeRunner();
1408
- expect(runner.sessionId).toBeUndefined();
1409
-
1410
- const runPromise = runner.run('test prompt', { timeout: 60 });
1411
-
1412
- const initEvent = JSON.stringify({
1413
- type: 'system',
1414
- subtype: 'init',
1415
- session_id: 'getter-session-789',
1416
- tools: [],
1417
- model: 'claude-opus-4-6',
1418
- });
1419
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n'));
1420
- mockProc.emit('close', 0);
1421
-
1422
- await runPromise;
1423
- expect(runner.sessionId).toBe('getter-session-789');
1424
- });
1425
-
1426
- it('should only capture the first sessionId (ignore duplicates)', async () => {
1427
- const mockProc = createMockProcess();
1428
- mockSpawn.mockReturnValue(mockProc);
1429
-
1430
- const runner = new ClaudeRunner();
1431
- const runPromise = runner.run('test prompt', { timeout: 60 });
1432
-
1433
- // Emit two system init events (shouldn't happen but test robustness)
1434
- const init1 = JSON.stringify({ type: 'system', subtype: 'init', session_id: 'first-session' });
1435
- const init2 = JSON.stringify({ type: 'system', subtype: 'init', session_id: 'second-session' });
1436
- mockProc.stdout.emit('data', Buffer.from(init1 + '\n' + init2 + '\n'));
1437
- mockProc.emit('close', 0);
1438
-
1439
- const result = await runPromise;
1440
- expect(result.sessionId).toBe('first-session');
1441
- });
1442
- });
1443
-
1444
- describe('runResume()', () => {
1445
- function createMockProcess() {
1446
- const stdout = new EventEmitter();
1447
- const stderr = new EventEmitter();
1448
- const proc = new EventEmitter() as any;
1449
- proc.stdout = stdout;
1450
- proc.stderr = stderr;
1451
- proc.kill = jest.fn().mockImplementation(() => {
1452
- setImmediate(() => proc.emit('close', 1));
1453
- });
1454
- return proc;
1455
- }
1456
-
1457
- it('should spawn Claude with --resume flag and session ID', async () => {
1458
- const mockProc = createMockProcess();
1459
- mockProc.kill = jest.fn();
1460
- mockSpawn.mockReturnValue(mockProc);
1461
-
1462
- const runner = new ClaudeRunner();
1463
- const runPromise = runner.runResume('test-session-abc', { timeout: 60 });
1464
-
1465
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'resumed output' }] } });
1466
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1467
- mockProc.emit('close', 0);
1468
-
1469
- await runPromise;
1470
-
1471
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1472
- expect(spawnArgs).toContain('--resume');
1473
- expect(spawnArgs).toContain('test-session-abc');
1474
-
1475
- // Verify --resume comes before the session ID
1476
- const resumeIndex = spawnArgs.indexOf('--resume');
1477
- expect(spawnArgs[resumeIndex + 1]).toBe('test-session-abc');
1478
- });
1479
-
1480
- it('should NOT include --model, --append-system-prompt, or -p flags', async () => {
1481
- const mockProc = createMockProcess();
1482
- mockProc.kill = jest.fn();
1483
- mockSpawn.mockReturnValue(mockProc);
1484
-
1485
- const runner = new ClaudeRunner({ model: 'sonnet' });
1486
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1487
-
1488
- mockProc.emit('close', 0);
1489
- await runPromise;
1490
-
1491
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1492
- expect(spawnArgs).not.toContain('--model');
1493
- expect(spawnArgs).not.toContain('--append-system-prompt');
1494
- expect(spawnArgs).not.toContain('-p');
1495
- });
1496
-
1497
- it('should include --dangerously-skip-permissions flag', async () => {
1498
- const mockProc = createMockProcess();
1499
- mockProc.kill = jest.fn();
1500
- mockSpawn.mockReturnValue(mockProc);
1501
-
1502
- const runner = new ClaudeRunner();
1503
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1504
-
1505
- mockProc.emit('close', 0);
1506
- await runPromise;
1507
-
1508
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1509
- expect(spawnArgs).toContain('--dangerously-skip-permissions');
1510
- });
1511
-
1512
- it('should include --output-format stream-json and --verbose flags', async () => {
1513
- const mockProc = createMockProcess();
1514
- mockProc.kill = jest.fn();
1515
- mockSpawn.mockReturnValue(mockProc);
1516
-
1517
- const runner = new ClaudeRunner();
1518
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1519
-
1520
- mockProc.emit('close', 0);
1521
- await runPromise;
1522
-
1523
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1524
- expect(spawnArgs).toContain('--output-format');
1525
- expect(spawnArgs).toContain('stream-json');
1526
- expect(spawnArgs).toContain('--verbose');
1527
- });
1528
-
1529
- it('should collect output from NDJSON events', async () => {
1530
- const mockProc = createMockProcess();
1531
- mockProc.kill = jest.fn();
1532
- mockSpawn.mockReturnValue(mockProc);
1533
-
1534
- const runner = new ClaudeRunner();
1535
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1536
-
1537
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk1' }] } });
1538
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk2' }] } });
1539
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n' + event2 + '\n'));
1540
- mockProc.emit('close', 0);
1541
-
1542
- const result = await runPromise;
1543
- expect(result.output).toBe('chunk1chunk2');
1544
- });
1545
-
1546
- it('should handle timeout correctly', async () => {
1547
- const mockProc = createMockProcess();
1548
- mockSpawn.mockReturnValue(mockProc);
1549
-
1550
- const runner = new ClaudeRunner();
1551
- const runPromise = runner.runResume('session-123', { timeout: 5 }); // 5 minutes
1552
-
1553
- // Fast forward past timeout
1554
- jest.advanceTimersByTime(300001);
1555
- expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
1556
-
1557
- const result = await runPromise;
1558
- expect(result.timedOut).toBe(true);
1559
- });
1560
-
1561
- it('should detect completion markers', async () => {
1562
- const mockProc = createMockProcess();
1563
- mockSpawn.mockReturnValue(mockProc);
1564
-
1565
- const runner = new ClaudeRunner();
1566
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1567
-
1568
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1569
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1570
-
1571
- // Grace period should start
1572
- expect(mockProc.kill).not.toHaveBeenCalled();
1573
- jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
1574
- expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
1575
-
1576
- await runPromise;
1577
- });
1578
-
1579
- it('should extract session ID from resumed session', async () => {
1580
- const mockProc = createMockProcess();
1581
- mockProc.kill = jest.fn();
1582
- mockSpawn.mockReturnValue(mockProc);
1583
-
1584
- const runner = new ClaudeRunner();
1585
- const runPromise = runner.runResume('old-session-123', { timeout: 60 });
1586
-
1587
- const initEvent = JSON.stringify({ type: 'system', subtype: 'init', session_id: 'new-session-456' });
1588
- const textEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'done' }] } });
1589
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n' + textEvent + '\n'));
1590
- mockProc.emit('close', 0);
1591
-
1592
- const result = await runPromise;
1593
- expect(result.sessionId).toBe('new-session-456');
1594
- expect(runner.sessionId).toBe('new-session-456');
1595
- });
1596
-
1597
- it('should pass cwd to spawn', async () => {
1598
- const mockProc = createMockProcess();
1599
- mockProc.kill = jest.fn();
1600
- mockSpawn.mockReturnValue(mockProc);
1601
-
1602
- const runner = new ClaudeRunner();
1603
- const runPromise = runner.runResume('session-123', { timeout: 60, cwd: '/worktree/path' });
1604
-
1605
- mockProc.emit('close', 0);
1606
- await runPromise;
1607
-
1608
- expect(mockSpawn).toHaveBeenCalledWith(
1609
- expect.any(String),
1610
- expect.any(Array),
1611
- expect.objectContaining({ cwd: '/worktree/path' })
1612
- );
1613
- });
1614
-
1615
- it('should detect context overflow', async () => {
1616
- jest.useRealTimers();
1617
-
1618
- const mockProc = createMockProcess();
1619
- mockSpawn.mockReturnValue(mockProc);
1620
-
1621
- const runner = new ClaudeRunner();
1622
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1623
-
1624
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Error: context length exceeded' }] } });
1625
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1626
-
1627
- const result = await runPromise;
1628
- expect(result.contextOverflow).toBe(true);
1629
- expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
1630
-
1631
- jest.useFakeTimers();
1632
- });
1633
-
1634
- it('should set CLAUDE_CODE_EFFORT_LEVEL env var when effortLevel is provided', async () => {
1635
- const mockProc = createMockProcess();
1636
- mockProc.kill = jest.fn();
1637
- mockSpawn.mockReturnValue(mockProc);
1638
-
1639
- const runner = new ClaudeRunner();
1640
- const runPromise = runner.runResume('session-123', { timeout: 60, effortLevel: 'medium' });
1641
-
1642
- mockProc.emit('close', 0);
1643
- await runPromise;
1644
-
1645
- const spawnOptions = mockSpawn.mock.calls[0][2];
1646
- expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
1647
- });
1648
- });
1649
1462
  });