rafcode 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) 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/RAF/ahtahs-token-reaper/decisions.md +37 -0
  32. package/RAF/ahtahs-token-reaper/input.md +20 -0
  33. package/RAF/ahtahs-token-reaper/outcomes/01-extend-token-tracker-data-model.md +42 -0
  34. package/RAF/ahtahs-token-reaper/outcomes/02-accumulate-usage-in-retry-loop.md +31 -0
  35. package/RAF/ahtahs-token-reaper/outcomes/03-per-attempt-display-formatting.md +60 -0
  36. package/RAF/ahtahs-token-reaper/outcomes/04-add-model-name-to-claude-call-logs.md +57 -0
  37. package/RAF/ahtahs-token-reaper/outcomes/05-handle-invalid-config-in-raf-config.md +46 -0
  38. package/RAF/ahtahs-token-reaper/outcomes/06-fix-verbose-toggle-timer-display.md +38 -0
  39. package/RAF/ahtahs-token-reaper/plans/01-extend-token-tracker-data-model.md +36 -0
  40. package/RAF/ahtahs-token-reaper/plans/02-accumulate-usage-in-retry-loop.md +36 -0
  41. package/RAF/ahtahs-token-reaper/plans/03-per-attempt-display-formatting.md +43 -0
  42. package/RAF/ahtahs-token-reaper/plans/04-add-model-name-to-claude-call-logs.md +38 -0
  43. package/RAF/ahtahs-token-reaper/plans/05-handle-invalid-config-in-raf-config.md +36 -0
  44. package/RAF/ahtahs-token-reaper/plans/06-fix-verbose-toggle-timer-display.md +40 -0
  45. package/README.md +34 -0
  46. package/dist/commands/config.d.ts +3 -0
  47. package/dist/commands/config.d.ts.map +1 -0
  48. package/dist/commands/config.js +195 -0
  49. package/dist/commands/config.js.map +1 -0
  50. package/dist/commands/do.d.ts.map +1 -1
  51. package/dist/commands/do.js +55 -7
  52. package/dist/commands/do.js.map +1 -1
  53. package/dist/commands/plan.d.ts.map +1 -1
  54. package/dist/commands/plan.js +5 -3
  55. package/dist/commands/plan.js.map +1 -1
  56. package/dist/core/claude-runner.d.ts +19 -2
  57. package/dist/core/claude-runner.d.ts.map +1 -1
  58. package/dist/core/claude-runner.js +43 -96
  59. package/dist/core/claude-runner.js.map +1 -1
  60. package/dist/core/failure-analyzer.d.ts.map +1 -1
  61. package/dist/core/failure-analyzer.js +6 -3
  62. package/dist/core/failure-analyzer.js.map +1 -1
  63. package/dist/core/git.d.ts.map +1 -1
  64. package/dist/core/git.js +10 -3
  65. package/dist/core/git.js.map +1 -1
  66. package/dist/core/pull-request.d.ts +1 -1
  67. package/dist/core/pull-request.d.ts.map +1 -1
  68. package/dist/core/pull-request.js +9 -4
  69. package/dist/core/pull-request.js.map +1 -1
  70. package/dist/index.js +2 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/parsers/stream-renderer.d.ts +16 -1
  73. package/dist/parsers/stream-renderer.d.ts.map +1 -1
  74. package/dist/parsers/stream-renderer.js +34 -4
  75. package/dist/parsers/stream-renderer.js.map +1 -1
  76. package/dist/prompts/execution.d.ts.map +1 -1
  77. package/dist/prompts/execution.js +11 -1
  78. package/dist/prompts/execution.js.map +1 -1
  79. package/dist/types/config.d.ts +95 -4
  80. package/dist/types/config.d.ts.map +1 -1
  81. package/dist/types/config.js +63 -3
  82. package/dist/types/config.js.map +1 -1
  83. package/dist/utils/config.d.ts +65 -7
  84. package/dist/utils/config.d.ts.map +1 -1
  85. package/dist/utils/config.js +297 -21
  86. package/dist/utils/config.js.map +1 -1
  87. package/dist/utils/name-generator.d.ts +3 -7
  88. package/dist/utils/name-generator.d.ts.map +1 -1
  89. package/dist/utils/name-generator.js +75 -61
  90. package/dist/utils/name-generator.js.map +1 -1
  91. package/dist/utils/terminal-symbols.d.ts +25 -0
  92. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  93. package/dist/utils/terminal-symbols.js +87 -0
  94. package/dist/utils/terminal-symbols.js.map +1 -1
  95. package/dist/utils/token-tracker.d.ts +55 -0
  96. package/dist/utils/token-tracker.d.ts.map +1 -0
  97. package/dist/utils/token-tracker.js +142 -0
  98. package/dist/utils/token-tracker.js.map +1 -0
  99. package/dist/utils/validation.d.ts +5 -5
  100. package/dist/utils/validation.d.ts.map +1 -1
  101. package/dist/utils/validation.js +10 -6
  102. package/dist/utils/validation.js.map +1 -1
  103. package/dist/utils/verbose-toggle.d.ts +33 -0
  104. package/dist/utils/verbose-toggle.d.ts.map +1 -0
  105. package/dist/utils/verbose-toggle.js +94 -0
  106. package/dist/utils/verbose-toggle.js.map +1 -0
  107. package/package.json +1 -1
  108. package/src/commands/config.ts +230 -0
  109. package/src/commands/do.ts +64 -6
  110. package/src/commands/plan.ts +5 -3
  111. package/src/core/claude-runner.ts +59 -115
  112. package/src/core/failure-analyzer.ts +6 -3
  113. package/src/core/git.ts +10 -3
  114. package/src/core/pull-request.ts +9 -4
  115. package/src/index.ts +2 -0
  116. package/src/parsers/stream-renderer.ts +54 -4
  117. package/src/prompts/config-docs.md +331 -0
  118. package/src/prompts/execution.ts +13 -1
  119. package/src/types/config.ts +156 -7
  120. package/src/utils/config.ts +357 -21
  121. package/src/utils/name-generator.ts +84 -71
  122. package/src/utils/terminal-symbols.ts +103 -0
  123. package/src/utils/token-tracker.ts +177 -0
  124. package/src/utils/validation.ts +15 -10
  125. package/src/utils/verbose-toggle.ts +103 -0
  126. package/tests/unit/claude-runner.test.ts +171 -7
  127. package/tests/unit/config-command.test.ts +242 -0
  128. package/tests/unit/config.test.ts +632 -30
  129. package/tests/unit/name-generator.test.ts +99 -75
  130. package/tests/unit/pull-request.test.ts +2 -0
  131. package/tests/unit/stream-renderer.test.ts +83 -0
  132. package/tests/unit/terminal-symbols.test.ts +245 -0
  133. package/tests/unit/timer-verbose-integration.test.ts +170 -0
  134. package/tests/unit/token-tracker.test.ts +685 -0
  135. package/tests/unit/verbose-toggle.test.ts +204 -0
@@ -4,11 +4,14 @@ import type { IDisposable } from 'node-pty';
4
4
  import { execSync, spawn } from 'node:child_process';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { renderStreamEvent } from '../parsers/stream-renderer.js';
7
+ import type { UsageData } from '../types/config.js';
7
8
  import { getHeadCommitHash, getHeadCommitMessage, isFileCommittedInHead } from './git.js';
9
+ import { getClaudeCommand, getModel } from '../utils/config.js';
8
10
 
9
11
  function getClaudePath(): string {
12
+ const cmd = getClaudeCommand();
10
13
  try {
11
- return execSync('which claude', { encoding: 'utf-8' }).trim();
14
+ return execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
12
15
  } catch {
13
16
  throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
14
17
  }
@@ -56,6 +59,12 @@ export interface ClaudeRunnerOptions {
56
59
  * Only applied in non-interactive modes (run, runVerbose).
57
60
  */
58
61
  effortLevel?: 'low' | 'medium' | 'high';
62
+ /**
63
+ * Dynamic verbose display callback. When provided, called for each stream event
64
+ * to determine whether to write display output to stdout. Overrides the static
65
+ * verbose parameter in _runStreamJson. Used by the runtime verbose toggle.
66
+ */
67
+ verboseCheck?: () => boolean;
59
68
  }
60
69
 
61
70
  export interface ClaudeRunnerConfig {
@@ -71,6 +80,8 @@ export interface RunResult {
71
80
  exitCode: number;
72
81
  timedOut: boolean;
73
82
  contextOverflow: boolean;
83
+ /** Token usage data from the stream-json result event. */
84
+ usageData?: UsageData;
74
85
  }
75
86
 
76
87
  const CONTEXT_OVERFLOW_PATTERNS = [
@@ -260,7 +271,7 @@ export class ClaudeRunner {
260
271
  private model: string;
261
272
 
262
273
  constructor(config: ClaudeRunnerConfig = {}) {
263
- this.model = config.model ?? 'opus';
274
+ this.model = config.model ?? getModel('execute');
264
275
  }
265
276
 
266
277
  /**
@@ -362,8 +373,8 @@ export class ClaudeRunner {
362
373
 
363
374
  /**
364
375
  * Run Claude non-interactively and collect output.
365
- * Used for execution phase where we parse the results.
366
- * Uses child_process.spawn with -p flag for prompt (like ralphy).
376
+ * Uses stream-json format internally to capture token usage data.
377
+ * Tool display is suppressed (non-verbose mode).
367
378
  *
368
379
  * TIMEOUT BEHAVIOR:
369
380
  * - The timeout is applied per individual call to this method
@@ -373,103 +384,7 @@ export class ClaudeRunner {
373
384
  * - Default timeout is 60 minutes if not specified
374
385
  */
375
386
  async run(prompt: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
376
- const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel } = options;
377
- // Ensure timeout is a positive number, fallback to 60 minutes
378
- const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
379
- const timeoutMs = validatedTimeout * 60 * 1000;
380
-
381
- return new Promise((resolve) => {
382
- let output = '';
383
- let stderr = '';
384
- let timedOut = false;
385
- let contextOverflow = false;
386
-
387
- const claudePath = getClaudePath();
388
-
389
- logger.debug(`Starting Claude execution session with model: ${this.model}`);
390
- logger.debug(`Claude path: ${claudePath}`);
391
-
392
- // Build env, optionally injecting effort level
393
- const env = effortLevel
394
- ? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
395
- : process.env;
396
-
397
- // Use --append-system-prompt to add RAF instructions to system prompt
398
- // This gives RAF instructions stronger precedence than passing as user message
399
- // --dangerously-skip-permissions bypasses interactive prompts
400
- // -p enables print mode (non-interactive)
401
- const proc = spawn(claudePath, [
402
- '--dangerously-skip-permissions',
403
- '--model',
404
- this.model,
405
- '--append-system-prompt',
406
- prompt,
407
- '-p',
408
- 'Execute the task as described in the system prompt.',
409
- ], {
410
- cwd,
411
- env,
412
- stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
413
- });
414
-
415
- // Track this process
416
- this.activeProcess = proc as any;
417
-
418
- // Set up timeout
419
- const timeoutHandle = setTimeout(() => {
420
- timedOut = true;
421
- logger.warn('Claude session timed out');
422
- proc.kill('SIGTERM');
423
- }, timeoutMs);
424
-
425
- // Set up completion detection (stdout marker + outcome file polling)
426
- const completionDetector = createCompletionDetector(
427
- () => proc.kill('SIGTERM'),
428
- outcomeFilePath,
429
- commitContext,
430
- );
431
-
432
- // Collect stdout
433
- proc.stdout.on('data', (data) => {
434
- const text = data.toString();
435
- output += text;
436
-
437
- // Check for completion marker to start grace period
438
- completionDetector.checkOutput(output);
439
-
440
- // Check for context overflow
441
- for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
442
- if (pattern.test(text)) {
443
- contextOverflow = true;
444
- logger.warn('Context overflow detected');
445
- proc.kill('SIGTERM');
446
- break;
447
- }
448
- }
449
- });
450
-
451
- // Collect stderr
452
- proc.stderr.on('data', (data) => {
453
- stderr += data.toString();
454
- });
455
-
456
- proc.on('close', (exitCode) => {
457
- clearTimeout(timeoutHandle);
458
- completionDetector.cleanup();
459
- this.activeProcess = null;
460
-
461
- if (stderr) {
462
- logger.debug(`Claude stderr: ${stderr}`);
463
- }
464
-
465
- resolve({
466
- output,
467
- exitCode: exitCode ?? (this.killed ? 130 : 1),
468
- timedOut,
469
- contextOverflow,
470
- });
471
- });
472
- });
387
+ return this._runStreamJson(prompt, options, false);
473
388
  }
474
389
 
475
390
  /**
@@ -485,20 +400,40 @@ export class ClaudeRunner {
485
400
  * - Default timeout is 60 minutes if not specified
486
401
  */
487
402
  async runVerbose(prompt: string, options: ClaudeRunnerOptions = {}): Promise<RunResult> {
488
- const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel } = options;
403
+ return this._runStreamJson(prompt, options, true);
404
+ }
405
+
406
+ /**
407
+ * Internal unified execution method using stream-json format.
408
+ * Both run() and runVerbose() delegate to this method.
409
+ *
410
+ * @param verbose - When true, tool descriptions and text are printed to stdout.
411
+ * When false, display output is suppressed but usage data is still captured.
412
+ */
413
+ private async _runStreamJson(
414
+ prompt: string,
415
+ options: ClaudeRunnerOptions,
416
+ verbose: boolean,
417
+ ): Promise<RunResult> {
418
+ const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel, verboseCheck } = options;
489
419
  // Ensure timeout is a positive number, fallback to 60 minutes
490
420
  const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
491
421
  const timeoutMs = validatedTimeout * 60 * 1000;
492
422
 
423
+ // When verboseCheck callback is provided, use it to dynamically determine display.
424
+ // Otherwise, fall back to the static verbose parameter.
425
+ const shouldDisplay = verboseCheck ?? (() => verbose);
426
+
493
427
  return new Promise((resolve) => {
494
428
  let output = '';
495
429
  let stderr = '';
496
430
  let timedOut = false;
497
431
  let contextOverflow = false;
432
+ let usageData: import('../types/config.js').UsageData | undefined;
498
433
 
499
434
  const claudePath = getClaudePath();
500
435
 
501
- logger.debug(`Starting Claude execution session (verbose/stream-json) with model: ${this.model}`);
436
+ logger.debug(`Starting Claude execution session (stream-json, verbose=${verbose}) with model: ${this.model}`);
502
437
  logger.debug(`Prompt length: ${prompt.length}, timeout: ${timeoutMs}ms, cwd: ${cwd}`);
503
438
  logger.debug(`Claude path: ${claudePath}`);
504
439
 
@@ -509,7 +444,7 @@ export class ClaudeRunner {
509
444
 
510
445
  logger.debug('Spawning process...');
511
446
  // Use --output-format stream-json --verbose to get real-time streaming events
512
- // including tool calls, file operations, and intermediate output.
447
+ // including tool calls, file operations, and token usage in the result event.
513
448
  // --dangerously-skip-permissions bypasses interactive prompts
514
449
  // -p enables print mode (non-interactive)
515
450
  const proc = spawn(claudePath, [
@@ -565,17 +500,17 @@ export class ClaudeRunner {
565
500
  const line = lineBuffer.substring(0, newlineIndex);
566
501
  lineBuffer = lineBuffer.substring(newlineIndex + 1);
567
502
 
568
- const { display, textContent } = renderStreamEvent(line);
503
+ const rendered = renderStreamEvent(line);
569
504
 
570
- if (textContent) {
571
- output += textContent;
505
+ if (rendered.textContent) {
506
+ output += rendered.textContent;
572
507
 
573
508
  // Check for completion marker to start grace period
574
509
  completionDetector.checkOutput(output);
575
510
 
576
511
  // Check for context overflow
577
512
  for (const pattern of CONTEXT_OVERFLOW_PATTERNS) {
578
- if (pattern.test(textContent)) {
513
+ if (pattern.test(rendered.textContent)) {
579
514
  contextOverflow = true;
580
515
  logger.warn('Context overflow detected');
581
516
  proc.kill('SIGTERM');
@@ -584,8 +519,13 @@ export class ClaudeRunner {
584
519
  }
585
520
  }
586
521
 
587
- if (display) {
588
- process.stdout.write(display);
522
+ // Capture usage data from result events
523
+ if (rendered.usageData) {
524
+ usageData = rendered.usageData;
525
+ }
526
+
527
+ if (shouldDisplay() && rendered.display) {
528
+ process.stdout.write(rendered.display);
589
529
  }
590
530
  }
591
531
  });
@@ -598,12 +538,15 @@ export class ClaudeRunner {
598
538
  proc.on('close', (exitCode) => {
599
539
  // Process any remaining data in the line buffer
600
540
  if (lineBuffer.trim()) {
601
- const { display, textContent } = renderStreamEvent(lineBuffer);
602
- if (textContent) {
603
- output += textContent;
541
+ const rendered = renderStreamEvent(lineBuffer);
542
+ if (rendered.textContent) {
543
+ output += rendered.textContent;
544
+ }
545
+ if (rendered.usageData) {
546
+ usageData = rendered.usageData;
604
547
  }
605
- if (display) {
606
- process.stdout.write(display);
548
+ if (shouldDisplay() && rendered.display) {
549
+ process.stdout.write(rendered.display);
607
550
  }
608
551
  }
609
552
 
@@ -621,6 +564,7 @@ export class ClaudeRunner {
621
564
  exitCode: exitCode ?? (this.killed ? 130 : 1),
622
565
  timedOut,
623
566
  contextOverflow,
567
+ usageData,
624
568
  });
625
569
  });
626
570
  });
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { execSync } from 'node:child_process';
3
+ import { getModel, getClaudeCommand } from '../utils/config.js';
3
4
 
4
5
  /**
5
6
  * Failure types that can be detected programmatically without using the API.
@@ -212,8 +213,9 @@ function extractRelevantOutput(output: string, maxLines: number): string {
212
213
  * Get the path to Claude CLI.
213
214
  */
214
215
  function getClaudePath(): string {
216
+ const cmd = getClaudeCommand();
215
217
  try {
216
- return execSync('which claude', { encoding: 'utf-8' }).trim();
218
+ return execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
217
219
  } catch {
218
220
  throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
219
221
  }
@@ -306,9 +308,10 @@ Respond with ONLY a markdown report in this exact format:
306
308
 
307
309
  const claudePath = getClaudePath();
308
310
 
309
- // Use haiku model for fast, cost-effective analysis
311
+ // Use configured model for failure analysis
312
+ const failureModel = getModel('failureAnalysis');
310
313
  const proc = spawn(claudePath, [
311
- '--model', 'haiku',
314
+ '--model', failureModel,
312
315
  '--dangerously-skip-permissions',
313
316
  '-p',
314
317
  prompt,
package/src/core/git.ts CHANGED
@@ -2,6 +2,7 @@ import { execSync } from 'node:child_process';
2
2
  import * as path from 'node:path';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import { extractProjectNumber, extractProjectName } from '../utils/paths.js';
5
+ import { getCommitFormat, getCommitPrefix, renderCommitMessage } from '../utils/config.js';
5
6
 
6
7
  export interface GitStatus {
7
8
  isRepo: boolean;
@@ -246,9 +247,15 @@ export async function commitPlanningArtifacts(projectPath: string, options?: { c
246
247
  const inputFile = path.join(projectPath, 'input.md');
247
248
  const decisionsFile = path.join(projectPath, 'decisions.md');
248
249
 
249
- // Build commit message
250
- const prefix = options?.isAmend ? 'Amend' : 'Plan';
251
- const commitMessage = `RAF[${projectNumber}] ${prefix}: ${projectName}`;
250
+ // Build commit message from config template
251
+ const formatType = options?.isAmend ? 'amend' as const : 'plan' as const;
252
+ const template = getCommitFormat(formatType);
253
+ const commitPrefix = getCommitPrefix();
254
+ const commitMessage = renderCommitMessage(template, {
255
+ prefix: commitPrefix,
256
+ projectId: projectNumber,
257
+ projectName,
258
+ });
252
259
 
253
260
  // Build list of files to stage (absolute paths)
254
261
  const absoluteFiles = [inputFile, decisionsFile, ...(options?.additionalFiles ?? [])];
@@ -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 { getModel, getClaudeCommand, getModelShortName } from '../utils/config.js';
6
7
  import { extractProjectName, getInputPath, getDecisionsPath, getOutcomesDir, TASK_ID_PATTERN } from '../utils/paths.js';
7
8
 
8
9
  export interface PrCreateResult {
@@ -222,7 +223,7 @@ export function readProjectContext(projectPath: string): {
222
223
  }
223
224
 
224
225
  /**
225
- * Generate a PR body by calling Claude Sonnet to summarize project context.
226
+ * Generate a PR body by calling Claude to summarize project context.
226
227
  * Falls back to a simple body if Claude is unavailable.
227
228
  */
228
229
  export async function generatePrBody(
@@ -278,6 +279,8 @@ Respond with ONLY the PR body in this exact format (no extra text, no code fence
278
279
  [1-3 bullet points describing how to verify these changes work correctly]`;
279
280
 
280
281
  try {
282
+ const prModel = getModelShortName(getModel('prGeneration'));
283
+ logger.info(`Generating PR with ${prModel}...`);
281
284
  const body = await callClaudeForPrBody(prompt, timeoutMs);
282
285
  return body;
283
286
  } catch (error) {
@@ -348,12 +351,13 @@ export function filterClaudeOutput(output: string): string {
348
351
  }
349
352
 
350
353
  /**
351
- * Call Claude Sonnet to generate a PR body.
354
+ * Call Claude to generate a PR body.
352
355
  */
353
356
  async function callClaudeForPrBody(prompt: string, timeoutMs: number): Promise<string> {
357
+ const cmd = getClaudeCommand();
354
358
  let claudePath: string;
355
359
  try {
356
- claudePath = execSync('which claude', { encoding: 'utf-8', stdio: 'pipe' }).trim();
360
+ claudePath = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
357
361
  } catch {
358
362
  throw new Error('Claude CLI not found');
359
363
  }
@@ -362,8 +366,9 @@ async function callClaudeForPrBody(prompt: string, timeoutMs: number): Promise<s
362
366
  let output = '';
363
367
  let stderr = '';
364
368
 
369
+ const prModel = getModel('prGeneration');
365
370
  const proc = spawn(claudePath, [
366
- '--model', 'sonnet',
371
+ '--model', prModel,
367
372
  '--dangerously-skip-permissions',
368
373
  '-p',
369
374
  prompt,
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { createPlanCommand } from './commands/plan.js';
5
5
  import { createDoCommand } from './commands/do.js';
6
6
  import { createStatusCommand } from './commands/status.js';
7
7
  import { createMigrateCommand } from './commands/migrate.js';
8
+ import { createConfigCommand } from './commands/config.js';
8
9
  import { getVersion } from './utils/version.js';
9
10
 
10
11
  const program = new Command();
@@ -18,5 +19,6 @@ program.addCommand(createPlanCommand());
18
19
  program.addCommand(createDoCommand());
19
20
  program.addCommand(createStatusCommand());
20
21
  program.addCommand(createMigrateCommand());
22
+ program.addCommand(createConfigCommand());
21
23
 
22
24
  program.parse();
@@ -5,9 +5,11 @@
5
5
  * - system (init): Session initialization info
6
6
  * - assistant: Claude's response with text or tool_use content blocks
7
7
  * - user: Tool results (tool_result content blocks)
8
- * - result: Final result with success/failure status
8
+ * - result: Final result with success/failure status and token usage
9
9
  */
10
10
 
11
+ import type { UsageData, ModelTokenUsage } from '../types/config.js';
12
+
11
13
  export interface StreamEvent {
12
14
  type: string;
13
15
  subtype?: string;
@@ -15,6 +17,18 @@ export interface StreamEvent {
15
17
  content?: ContentBlock[];
16
18
  };
17
19
  result?: string;
20
+ usage?: {
21
+ input_tokens?: number;
22
+ output_tokens?: number;
23
+ cache_read_input_tokens?: number;
24
+ cache_creation_input_tokens?: number;
25
+ };
26
+ modelUsage?: Record<string, {
27
+ inputTokens?: number;
28
+ outputTokens?: number;
29
+ cacheReadInputTokens?: number;
30
+ cacheCreationInputTokens?: number;
31
+ }>;
18
32
  tool_use_result?: {
19
33
  type?: string;
20
34
  file?: {
@@ -72,6 +86,8 @@ export interface RenderResult {
72
86
  display: string;
73
87
  /** Text content to accumulate for output parsing (completion markers, etc.) */
74
88
  textContent: string;
89
+ /** Usage data extracted from result events (only present on result events) */
90
+ usageData?: UsageData;
75
91
  }
76
92
 
77
93
  /**
@@ -132,8 +148,42 @@ function renderAssistant(event: StreamEvent): RenderResult {
132
148
  return { display, textContent };
133
149
  }
134
150
 
135
- function renderResult(_event: StreamEvent): RenderResult {
151
+ function renderResult(event: StreamEvent): RenderResult {
136
152
  // The result event's text duplicates the last assistant message,
137
- // which is already captured. Skip to avoid double-counting.
138
- return { display: '', textContent: '' };
153
+ // which is already captured. Skip text to avoid double-counting.
154
+ // But extract usage data if present.
155
+ const usageData = extractUsageData(event);
156
+ return { display: '', textContent: '', usageData };
157
+ }
158
+
159
+ /**
160
+ * Extract usage data from a stream-json result event.
161
+ * Returns undefined if no usage data is present.
162
+ */
163
+ function extractUsageData(event: StreamEvent): UsageData | undefined {
164
+ if (!event.usage && !event.modelUsage) {
165
+ return undefined;
166
+ }
167
+
168
+ const usage = event.usage;
169
+ const modelUsage: Record<string, ModelTokenUsage> = {};
170
+
171
+ if (event.modelUsage) {
172
+ for (const [model, data] of Object.entries(event.modelUsage)) {
173
+ modelUsage[model] = {
174
+ inputTokens: data.inputTokens ?? 0,
175
+ outputTokens: data.outputTokens ?? 0,
176
+ cacheReadInputTokens: data.cacheReadInputTokens ?? 0,
177
+ cacheCreationInputTokens: data.cacheCreationInputTokens ?? 0,
178
+ };
179
+ }
180
+ }
181
+
182
+ return {
183
+ inputTokens: usage?.input_tokens ?? 0,
184
+ outputTokens: usage?.output_tokens ?? 0,
185
+ cacheReadInputTokens: usage?.cache_read_input_tokens ?? 0,
186
+ cacheCreationInputTokens: usage?.cache_creation_input_tokens ?? 0,
187
+ modelUsage,
188
+ };
139
189
  }