rafcode 2.2.0 → 2.4.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 (125) hide show
  1. package/CLAUDE.md +19 -4
  2. package/RAF/ahtahs-token-reaper/decisions.md +37 -0
  3. package/RAF/ahtahs-token-reaper/input.md +20 -0
  4. package/RAF/ahtahs-token-reaper/outcomes/01-extend-token-tracker-data-model.md +42 -0
  5. package/RAF/ahtahs-token-reaper/outcomes/02-accumulate-usage-in-retry-loop.md +31 -0
  6. package/RAF/ahtahs-token-reaper/outcomes/03-per-attempt-display-formatting.md +60 -0
  7. package/RAF/ahtahs-token-reaper/outcomes/04-add-model-name-to-claude-call-logs.md +57 -0
  8. package/RAF/ahtahs-token-reaper/outcomes/05-handle-invalid-config-in-raf-config.md +46 -0
  9. package/RAF/ahtahs-token-reaper/outcomes/06-fix-verbose-toggle-timer-display.md +38 -0
  10. package/RAF/ahtahs-token-reaper/plans/01-extend-token-tracker-data-model.md +36 -0
  11. package/RAF/ahtahs-token-reaper/plans/02-accumulate-usage-in-retry-loop.md +36 -0
  12. package/RAF/ahtahs-token-reaper/plans/03-per-attempt-display-formatting.md +43 -0
  13. package/RAF/ahtahs-token-reaper/plans/04-add-model-name-to-claude-call-logs.md +38 -0
  14. package/RAF/ahtahs-token-reaper/plans/05-handle-invalid-config-in-raf-config.md +36 -0
  15. package/RAF/ahtahs-token-reaper/plans/06-fix-verbose-toggle-timer-display.md +40 -0
  16. package/RAF/ahvrih-rate-forge/decisions.md +70 -0
  17. package/RAF/ahvrih-rate-forge/input.md +44 -0
  18. package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
  19. package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
  20. package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
  21. package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
  22. package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
  23. package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
  24. package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
  25. package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
  26. package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
  27. package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
  28. package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
  29. package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
  30. package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
  31. package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
  32. package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
  33. package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
  34. package/README.md +27 -7
  35. package/dist/commands/config.d.ts.map +1 -1
  36. package/dist/commands/config.js +24 -7
  37. package/dist/commands/config.js.map +1 -1
  38. package/dist/commands/do.d.ts.map +1 -1
  39. package/dist/commands/do.js +122 -27
  40. package/dist/commands/do.js.map +1 -1
  41. package/dist/commands/plan.d.ts.map +1 -1
  42. package/dist/commands/plan.js +79 -3
  43. package/dist/commands/plan.js.map +1 -1
  44. package/dist/core/claude-runner.d.ts +6 -6
  45. package/dist/core/claude-runner.d.ts.map +1 -1
  46. package/dist/core/claude-runner.js +9 -10
  47. package/dist/core/claude-runner.js.map +1 -1
  48. package/dist/core/failure-analyzer.d.ts.map +1 -1
  49. package/dist/core/failure-analyzer.js +3 -3
  50. package/dist/core/failure-analyzer.js.map +1 -1
  51. package/dist/core/pull-request.d.ts.map +1 -1
  52. package/dist/core/pull-request.js +5 -3
  53. package/dist/core/pull-request.js.map +1 -1
  54. package/dist/core/state-derivation.d.ts +5 -0
  55. package/dist/core/state-derivation.d.ts.map +1 -1
  56. package/dist/core/state-derivation.js +14 -4
  57. package/dist/core/state-derivation.js.map +1 -1
  58. package/dist/core/worktree.d.ts +32 -0
  59. package/dist/core/worktree.d.ts.map +1 -1
  60. package/dist/core/worktree.js +215 -0
  61. package/dist/core/worktree.js.map +1 -1
  62. package/dist/prompts/amend.d.ts.map +1 -1
  63. package/dist/prompts/amend.js +26 -11
  64. package/dist/prompts/amend.js.map +1 -1
  65. package/dist/prompts/planning.d.ts.map +1 -1
  66. package/dist/prompts/planning.js +26 -11
  67. package/dist/prompts/planning.js.map +1 -1
  68. package/dist/types/config.d.ts +30 -13
  69. package/dist/types/config.d.ts.map +1 -1
  70. package/dist/types/config.js +14 -10
  71. package/dist/types/config.js.map +1 -1
  72. package/dist/utils/config.d.ts +53 -4
  73. package/dist/utils/config.d.ts.map +1 -1
  74. package/dist/utils/config.js +197 -30
  75. package/dist/utils/config.js.map +1 -1
  76. package/dist/utils/frontmatter.d.ts +43 -0
  77. package/dist/utils/frontmatter.d.ts.map +1 -0
  78. package/dist/utils/frontmatter.js +85 -0
  79. package/dist/utils/frontmatter.js.map +1 -0
  80. package/dist/utils/name-generator.d.ts.map +1 -1
  81. package/dist/utils/name-generator.js +2 -3
  82. package/dist/utils/name-generator.js.map +1 -1
  83. package/dist/utils/session-parser.d.ts +44 -0
  84. package/dist/utils/session-parser.d.ts.map +1 -0
  85. package/dist/utils/session-parser.js +122 -0
  86. package/dist/utils/session-parser.js.map +1 -0
  87. package/dist/utils/terminal-symbols.d.ts +28 -5
  88. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  89. package/dist/utils/terminal-symbols.js +77 -18
  90. package/dist/utils/terminal-symbols.js.map +1 -1
  91. package/dist/utils/token-tracker.d.ts +31 -1
  92. package/dist/utils/token-tracker.d.ts.map +1 -1
  93. package/dist/utils/token-tracker.js +94 -4
  94. package/dist/utils/token-tracker.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/commands/config.ts +26 -7
  97. package/src/commands/do.ts +157 -29
  98. package/src/commands/plan.ts +89 -2
  99. package/src/core/claude-runner.ts +16 -17
  100. package/src/core/failure-analyzer.ts +3 -3
  101. package/src/core/pull-request.ts +5 -3
  102. package/src/core/state-derivation.ts +20 -4
  103. package/src/core/worktree.ts +230 -0
  104. package/src/prompts/amend.ts +26 -11
  105. package/src/prompts/config-docs.md +91 -29
  106. package/src/prompts/planning.ts +26 -11
  107. package/src/types/config.ts +46 -21
  108. package/src/utils/config.ts +222 -33
  109. package/src/utils/frontmatter.ts +110 -0
  110. package/src/utils/name-generator.ts +2 -3
  111. package/src/utils/session-parser.ts +161 -0
  112. package/src/utils/terminal-symbols.ts +105 -18
  113. package/src/utils/token-tracker.ts +109 -4
  114. package/tests/unit/claude-runner-interactive.test.ts +8 -6
  115. package/tests/unit/claude-runner.test.ts +5 -66
  116. package/tests/unit/config-command.test.ts +84 -5
  117. package/tests/unit/config.test.ts +292 -45
  118. package/tests/unit/frontmatter.test.ts +182 -0
  119. package/tests/unit/post-execution-picker.test.ts +5 -0
  120. package/tests/unit/session-parser.test.ts +301 -0
  121. package/tests/unit/terminal-symbols.test.ts +263 -33
  122. package/tests/unit/timer-verbose-integration.test.ts +170 -0
  123. package/tests/unit/token-tracker.test.ts +653 -17
  124. package/tests/unit/validation.test.ts +6 -4
  125. package/tests/unit/worktree.test.ts +242 -0
@@ -9,10 +9,12 @@ import { logger } from '../utils/logger.js';
9
9
  import {
10
10
  getConfigPath,
11
11
  getModel,
12
- getEffort,
12
+ getModelShortName,
13
13
  validateConfig,
14
14
  ConfigValidationError,
15
+ resetConfigCache,
15
16
  } from '../utils/config.js';
17
+ import { DEFAULT_CONFIG } from '../types/config.js';
16
18
 
17
19
  interface ConfigCommandOptions {
18
20
  reset?: boolean;
@@ -153,11 +155,28 @@ async function handleReset(): Promise<void> {
153
155
 
154
156
  async function runConfigSession(initialPrompt?: string): Promise<void> {
155
157
  const configPath = getConfigPath();
156
- const model = getModel('config');
157
- const effort = getEffort('config');
158
158
 
159
- // Set effort level env var for the Claude session
160
- process.env['CLAUDE_CODE_EFFORT_LEVEL'] = effort;
159
+ // Try to load config, but fall back to defaults if it's broken
160
+ // This allows raf config to be used to fix a broken config file
161
+ let model: string;
162
+ let configError: Error | null = null;
163
+
164
+ try {
165
+ model = getModel('config');
166
+ } catch (error) {
167
+ // Config file has errors - fall back to defaults so the session can launch
168
+ configError = error instanceof Error ? error : new Error(String(error));
169
+ model = DEFAULT_CONFIG.models.config;
170
+ // Clear the cached config so subsequent calls don't use the broken cache
171
+ resetConfigCache();
172
+ }
173
+
174
+ // Warn user if config has errors, before starting the session
175
+ if (configError) {
176
+ logger.warn(`Config file has errors, using defaults: ${configError.message}`);
177
+ logger.warn('Fix the config in this session or run `raf config --reset` to start fresh.');
178
+ logger.newline();
179
+ }
161
180
 
162
181
  // Load config docs
163
182
  let configDocs: string;
@@ -181,8 +200,8 @@ async function runConfigSession(initialPrompt?: string): Promise<void> {
181
200
  shutdownHandler.init();
182
201
  shutdownHandler.registerClaudeRunner(claudeRunner);
183
202
 
184
- logger.info('Starting config session with Claude...');
185
- logger.info(`Using model: ${model}`);
203
+ const configModel = getModelShortName(model);
204
+ logger.info(`Starting config session with ${configModel}...`);
186
205
  logger.newline();
187
206
 
188
207
  try {
@@ -13,7 +13,9 @@ import { getRafDir, extractProjectNumber, extractProjectName, extractTaskNameFro
13
13
  import { pickPendingProject, getPendingProjects, getPendingWorktreeProjects } from '../ui/project-picker.js';
14
14
  import type { PendingProjectInfo } from '../ui/project-picker.js';
15
15
  import { logger } from '../utils/logger.js';
16
- import { getConfig, getEffort, getWorktreeDefault } from '../utils/config.js';
16
+ import { getConfig, getWorktreeDefault, getModel, getModelShortName, resolveFullModelId, getSyncMainBranch, resolveEffortToModel, applyModelCeiling } from '../utils/config.js';
17
+ import type { PlanFrontmatter } from '../utils/frontmatter.js';
18
+ import { getVersion } from '../utils/version.js';
17
19
  import { createTaskTimer, formatElapsedTime } from '../utils/timer.js';
18
20
  import { createStatusLine } from '../utils/status-line.js';
19
21
  import {
@@ -49,6 +51,8 @@ import {
49
51
  mergeWorktreeBranch,
50
52
  removeWorktree,
51
53
  resolveWorktreeProjectByIdentifier,
54
+ pushMainBranch,
55
+ pullMainBranch,
52
56
  } from '../core/worktree.js';
53
57
  import { createPullRequest, prPreflight } from '../core/pull-request.js';
54
58
  import type { DoCommandOptions } from '../types/config.js';
@@ -61,6 +65,74 @@ import type { DoCommandOptions } from '../types/config.js';
61
65
  */
62
66
  export type PostExecutionAction = 'merge' | 'pr' | 'leave';
63
67
 
68
+ /**
69
+ * Result of resolving a task's model from frontmatter.
70
+ */
71
+ interface TaskModelResolution {
72
+ /** The resolved model (after ceiling is applied). */
73
+ model: string;
74
+ /** Whether a warning should be logged about missing frontmatter. */
75
+ missingFrontmatter: boolean;
76
+ /** Frontmatter parsing warnings to log. */
77
+ warnings: string[];
78
+ }
79
+
80
+ /**
81
+ * Resolve the execution model for a task from its frontmatter metadata.
82
+ *
83
+ * Resolution order:
84
+ * 1. Explicit `model` in frontmatter (subject to ceiling)
85
+ * 2. `effort` in frontmatter resolved via effortMapping (subject to ceiling)
86
+ * 3. Fallback to models.execute (the ceiling, with a warning)
87
+ *
88
+ * @param frontmatter - Parsed frontmatter from the plan file
89
+ * @param frontmatterWarnings - Warnings from frontmatter parsing
90
+ * @param ceilingModel - The ceiling model (usually models.execute from config)
91
+ * @param isRetry - Whether this is a retry attempt (escalates to ceiling)
92
+ */
93
+ function resolveTaskModel(
94
+ frontmatter: PlanFrontmatter | undefined,
95
+ frontmatterWarnings: string[] | undefined,
96
+ ceilingModel: string,
97
+ isRetry: boolean,
98
+ ): TaskModelResolution {
99
+ const warnings = frontmatterWarnings ? [...frontmatterWarnings] : [];
100
+
101
+ // Retry escalation: always use the ceiling model on retry
102
+ if (isRetry) {
103
+ return { model: ceilingModel, missingFrontmatter: false, warnings };
104
+ }
105
+
106
+ // No frontmatter - fallback to ceiling with warning
107
+ if (!frontmatter) {
108
+ return {
109
+ model: ceilingModel,
110
+ missingFrontmatter: true,
111
+ warnings,
112
+ };
113
+ }
114
+
115
+ // Explicit model in frontmatter - apply ceiling
116
+ if (frontmatter.model) {
117
+ const model = applyModelCeiling(frontmatter.model, ceilingModel);
118
+ return { model, missingFrontmatter: false, warnings };
119
+ }
120
+
121
+ // Effort-based resolution - apply ceiling
122
+ if (frontmatter.effort) {
123
+ const mappedModel = resolveEffortToModel(frontmatter.effort);
124
+ const model = applyModelCeiling(mappedModel, ceilingModel);
125
+ return { model, missingFrontmatter: false, warnings };
126
+ }
127
+
128
+ // Frontmatter present but no effort or model - fallback to ceiling with warning
129
+ return {
130
+ model: ceilingModel,
131
+ missingFrontmatter: true,
132
+ warnings,
133
+ };
134
+ }
135
+
64
136
  /**
65
137
  * Format failure history for console output.
66
138
  * Shows attempts that failed before eventual success or final failure.
@@ -166,6 +238,18 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
166
238
  // Record original branch before any worktree operations
167
239
  originalBranch = getCurrentBranch() ?? undefined;
168
240
 
241
+ // Sync main branch before worktree operations (if enabled)
242
+ if (getSyncMainBranch()) {
243
+ const syncResult = pullMainBranch();
244
+ if (syncResult.success) {
245
+ if (syncResult.hadChanges) {
246
+ logger.info(`Synced ${syncResult.mainBranch} from remote`);
247
+ }
248
+ } else {
249
+ logger.warn(`Could not sync main branch: ${syncResult.error}`);
250
+ }
251
+ }
252
+
169
253
  if (!projectIdentifier) {
170
254
  // Auto-discovery flow
171
255
  const selected = await discoverAndPickWorktreeProject(repoBasename, rafDir, rafRelativePath);
@@ -394,7 +478,6 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
394
478
  force,
395
479
  maxRetries,
396
480
  autoCommit,
397
- showModel: true,
398
481
  model,
399
482
  worktreeCwd: worktreeRoot,
400
483
  }
@@ -500,6 +583,19 @@ async function executePostAction(
500
583
 
501
584
  case 'pr': {
502
585
  logger.newline();
586
+
587
+ // Push main branch to remote before PR creation (if enabled)
588
+ if (getSyncMainBranch()) {
589
+ const syncResult = pushMainBranch();
590
+ if (syncResult.success) {
591
+ if (syncResult.hadChanges) {
592
+ logger.info(`Pushed ${syncResult.mainBranch} to remote`);
593
+ }
594
+ } else {
595
+ logger.warn(`Could not push main branch: ${syncResult.error}`);
596
+ }
597
+ }
598
+
503
599
  logger.info(`Creating PR for branch "${worktreeBranch}"...`);
504
600
 
505
601
  const prResult = await createPullRequest(worktreeBranch, projectPath, { cwd: worktreeRoot });
@@ -658,7 +754,6 @@ interface SingleProjectOptions {
658
754
  force: boolean;
659
755
  maxRetries: number;
660
756
  autoCommit: boolean;
661
- showModel: boolean;
662
757
  model: string;
663
758
  /** Worktree root directory. When set, Claude runs with cwd in the worktree. */
664
759
  worktreeCwd?: string;
@@ -669,7 +764,7 @@ async function executeSingleProject(
669
764
  projectName: string,
670
765
  options: SingleProjectOptions
671
766
  ): Promise<ProjectExecutionResult> {
672
- const { timeout, verbose, debug, force, maxRetries, autoCommit, showModel, model, worktreeCwd } = options;
767
+ const { timeout, verbose, debug, force, maxRetries, autoCommit, model, worktreeCwd } = options;
673
768
 
674
769
  if (!validatePlansExist(projectPath)) {
675
770
  return {
@@ -709,11 +804,12 @@ async function executeSingleProject(
709
804
  : state.tasks.filter((t) => t.status !== 'completed').map((t) => t.id)
710
805
  );
711
806
 
712
- // Set up shutdown handler
713
- const claudeRunner = new ClaudeRunner({ model });
807
+ // Set up shutdown handler - we'll register runners dynamically per-task
714
808
  const projectManager = new ProjectManager();
715
809
  shutdownHandler.init();
716
- shutdownHandler.registerClaudeRunner(claudeRunner);
810
+
811
+ // The ceiling model for all tasks (can be overridden per-task, subject to this ceiling)
812
+ const ceilingModel = model;
717
813
 
718
814
  // Initialize token tracker for usage reporting
719
815
  const tokenTracker = new TokenTracker();
@@ -725,15 +821,13 @@ async function executeSingleProject(
725
821
  // Start project timer
726
822
  const projectStartTime = Date.now();
727
823
 
824
+ // Resolve and display version + ceiling model info (before any tasks run)
825
+ const fullCeilingModelId = resolveFullModelId(ceilingModel);
826
+ logger.dim(`RAF v${getVersion()} | Ceiling: ${fullCeilingModelId}`);
827
+
728
828
  if (verbose) {
729
829
  logger.info(`Executing project: ${projectName}`);
730
830
  logger.info(`Tasks: ${state.tasks.length}, Task timeout: ${timeout} minutes`);
731
-
732
- // Log Claude model name
733
- if (showModel && model) {
734
- logger.info(`Using model: ${model}`);
735
- }
736
-
737
831
  logger.newline();
738
832
  } else {
739
833
  // Minimal mode: show project header
@@ -905,28 +999,63 @@ async function executeSingleProject(
905
999
  let attempts = 0;
906
1000
  let lastOutput = '';
907
1001
  let failureReason = '';
908
- let lastUsageData: import('../types/config.js').UsageData | undefined;
1002
+ // Collect usage data from all attempts (for accurate token tracking across retries)
1003
+ const attemptUsageData: import('../types/config.js').UsageData[] = [];
909
1004
  // Track failure history for each attempt (attempt number -> reason)
910
1005
  const failureHistory: Array<{ attempt: number; reason: string }> = [];
911
1006
 
912
1007
  // Set up timer for elapsed time tracking
913
1008
  const statusLine = createStatusLine();
914
1009
  const timer = createTaskTimer(verbose ? undefined : (elapsed) => {
1010
+ // When verbose is toggled ON at runtime, clear the status line and skip updates
1011
+ if (verboseToggle.isVerbose) {
1012
+ statusLine.clear();
1013
+ return;
1014
+ }
915
1015
  // Show running status with task name and timer (updates in place)
916
1016
  statusLine.update(formatTaskProgress(taskNumber, totalTasks, 'running', displayName, elapsed, taskId));
917
1017
  });
918
1018
  timer.start();
919
1019
 
1020
+ // Log frontmatter warnings once before the retry loop
1021
+ if (task.frontmatterWarnings && task.frontmatterWarnings.length > 0) {
1022
+ for (const warning of task.frontmatterWarnings) {
1023
+ logger.warn(` Frontmatter warning: ${warning}`);
1024
+ }
1025
+ }
1026
+
920
1027
  while (!success && attempts < maxRetries) {
921
1028
  attempts++;
1029
+ const isRetry = attempts > 1;
1030
+
1031
+ // Resolve the model for this attempt (escalates to ceiling on retry)
1032
+ const modelResolution = resolveTaskModel(
1033
+ task.frontmatter,
1034
+ undefined, // warnings already logged above
1035
+ ceilingModel,
1036
+ isRetry,
1037
+ );
1038
+
1039
+ // Log missing frontmatter warning on first attempt only
1040
+ if (!isRetry && modelResolution.missingFrontmatter) {
1041
+ logger.warn(` No effort frontmatter found — using ceiling model`);
1042
+ }
1043
+
1044
+ // Create a runner for this attempt's model
1045
+ const taskRunner = new ClaudeRunner({ model: modelResolution.model });
1046
+ shutdownHandler.registerClaudeRunner(taskRunner);
922
1047
 
923
- if (verbose && attempts > 1) {
924
- logger.info(` Retry ${attempts}/${maxRetries} for task ${taskLabel}...`);
1048
+ if (verbose && isRetry) {
1049
+ const retryModel = resolveFullModelId(modelResolution.model);
1050
+ logger.info(` Retry ${attempts}/${maxRetries} for task ${taskLabel} (model: ${retryModel})...`);
1051
+ } else if (verbose && !isRetry) {
1052
+ const taskModel = resolveFullModelId(modelResolution.model);
1053
+ logger.info(` Model: ${taskModel}`);
925
1054
  }
926
1055
 
927
1056
  // Build execution prompt (inside loop to include retry context on retries)
928
1057
  // Check if previous outcome file exists for retry context
929
- const previousOutcomeFileForRetry = attempts > 1 && fs.existsSync(outcomeFilePath)
1058
+ const previousOutcomeFileForRetry = isRetry && fs.existsSync(outcomeFilePath)
930
1059
  ? outcomeFilePath
931
1060
  : undefined;
932
1061
 
@@ -955,22 +1084,20 @@ async function executeSingleProject(
955
1084
  } : undefined;
956
1085
 
957
1086
  // Run Claude (use worktree root as cwd if in worktree mode)
958
- const executeEffort = getEffort('execute');
959
1087
  const runnerOptions = {
960
1088
  timeout,
961
1089
  outcomeFilePath,
962
1090
  commitContext,
963
1091
  cwd: worktreeCwd,
964
- effortLevel: executeEffort,
965
1092
  verboseCheck: () => verboseToggle.isVerbose,
966
1093
  };
967
1094
  const result = verbose
968
- ? await claudeRunner.runVerbose(prompt, runnerOptions)
969
- : await claudeRunner.run(prompt, runnerOptions);
1095
+ ? await taskRunner.runVerbose(prompt, runnerOptions)
1096
+ : await taskRunner.run(prompt, runnerOptions);
970
1097
 
971
1098
  lastOutput = result.output;
972
1099
  if (result.usageData) {
973
- lastUsageData = result.usageData;
1100
+ attemptUsageData.push(result.usageData);
974
1101
  }
975
1102
 
976
1103
  // Parse result
@@ -1088,9 +1215,9 @@ Task completed. No detailed report provided.
1088
1215
  }
1089
1216
 
1090
1217
  // Track and display token usage for this task
1091
- if (lastUsageData) {
1092
- const entry = tokenTracker.addTask(task.id, lastUsageData);
1093
- logger.dim(formatTaskTokenSummary(entry.usage, entry.cost));
1218
+ if (attemptUsageData.length > 0) {
1219
+ const entry = tokenTracker.addTask(task.id, attemptUsageData);
1220
+ logger.dim(formatTaskTokenSummary(entry, (u) => tokenTracker.calculateCost(u)));
1094
1221
  }
1095
1222
 
1096
1223
  completedInSession.add(task.id);
@@ -1108,16 +1235,17 @@ Task completed. No detailed report provided.
1108
1235
 
1109
1236
  if (verbose) {
1110
1237
  logger.error(` Task ${taskLabel} failed: ${failureReason} (${elapsedFormatted})`);
1111
- logger.info(' Analyzing failure...');
1238
+ const analysisModel = getModelShortName(getModel('failureAnalysis'));
1239
+ logger.info(` Analyzing failure with ${analysisModel}...`);
1112
1240
  } else {
1113
1241
  // Minimal mode: show failed task line
1114
1242
  logger.info(formatTaskProgress(taskNumber, totalTasks, 'failed', displayName, elapsedMs, task.id));
1115
1243
  }
1116
1244
 
1117
1245
  // Track token usage even for failed tasks (partial data still useful for totals)
1118
- if (lastUsageData) {
1119
- const entry = tokenTracker.addTask(task.id, lastUsageData);
1120
- logger.dim(formatTaskTokenSummary(entry.usage, entry.cost));
1246
+ if (attemptUsageData.length > 0) {
1247
+ const entry = tokenTracker.addTask(task.id, attemptUsageData);
1248
+ logger.dim(formatTaskTokenSummary(entry, (u) => tokenTracker.calculateCost(u)));
1121
1249
  }
1122
1250
 
1123
1251
  // Analyze failure and generate structured report
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
3
4
  import { Command } from 'commander';
4
5
  import { ProjectManager } from '../core/project-manager.js';
5
6
  import { ClaudeRunner } from '../core/claude-runner.js';
@@ -15,7 +16,10 @@ import {
15
16
  resolveModelOption,
16
17
  } from '../utils/validation.js';
17
18
  import { logger } from '../utils/logger.js';
18
- import { getWorktreeDefault } from '../utils/config.js';
19
+ import { getWorktreeDefault, getModel, getModelShortName, getDisplayConfig, getPricingConfig, getSyncMainBranch } from '../utils/config.js';
20
+ import { TokenTracker } from '../utils/token-tracker.js';
21
+ import { parseSessionById } from '../utils/session-parser.js';
22
+ import { formatTokenTotalSummary, TokenSummaryOptions } from '../utils/terminal-symbols.js';
19
23
  import { generateProjectNames } from '../utils/name-generator.js';
20
24
  import { pickProjectName } from '../ui/name-picker.js';
21
25
  import {
@@ -48,6 +52,7 @@ import {
48
52
  validateWorktree,
49
53
  removeWorktree,
50
54
  computeWorktreeBaseDir,
55
+ pullMainBranch,
51
56
  } from '../core/worktree.js';
52
57
 
53
58
  interface PlanCommandOptions {
@@ -155,7 +160,8 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
155
160
  // Get or generate project name
156
161
  let finalProjectName = projectName;
157
162
  if (!finalProjectName) {
158
- logger.info('Generating project name suggestions...');
163
+ const nameModel = getModelShortName(getModel('nameGeneration'));
164
+ logger.info(`Generating project name suggestions with ${nameModel}...`);
159
165
  const suggestedNames = await generateProjectNames(cleanInput);
160
166
  logger.newline();
161
167
 
@@ -184,6 +190,18 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
184
190
  const repoRoot = getRepoRoot()!;
185
191
  const rafDir = getRafDir();
186
192
 
193
+ // Sync main branch before creating worktree (if enabled)
194
+ if (getSyncMainBranch()) {
195
+ const syncResult = pullMainBranch();
196
+ if (syncResult.success) {
197
+ if (syncResult.hadChanges) {
198
+ logger.info(`Synced ${syncResult.mainBranch} from remote`);
199
+ }
200
+ } else {
201
+ logger.warn(`Could not sync main branch: ${syncResult.error}`);
202
+ }
203
+ }
204
+
187
205
  // Compute project number from main repo's RAF directory
188
206
  const projectNumber = getNextProjectNumber(rafDir);
189
207
  const sanitizedName = sanitizeProjectName(finalProjectName);
@@ -271,17 +289,25 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
271
289
  worktreeMode,
272
290
  });
273
291
 
292
+ // Generate session ID for token tracking
293
+ const sessionId = crypto.randomUUID();
294
+ const sessionCwd = worktreePath ?? process.cwd();
295
+
274
296
  try {
275
297
  const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
276
298
  dangerouslySkipPermissions: autoMode,
277
299
  // Run Claude session in the worktree root if in worktree mode
278
300
  cwd: worktreePath ?? undefined,
301
+ sessionId,
279
302
  });
280
303
 
281
304
  if (exitCode !== 0) {
282
305
  logger.warn(`Claude exited with code ${exitCode}`);
283
306
  }
284
307
 
308
+ // Parse session file and display token usage summary
309
+ displayPlanSessionTokenSummary(sessionId, sessionCwd);
310
+
285
311
  // Check for created plan files
286
312
  const plansDir = getPlansDir(projectPath);
287
313
  const planFiles = fs.existsSync(plansDir)
@@ -411,6 +437,18 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
411
437
  logger.info(`Recreated worktree from branch: ${folderName}`);
412
438
  } else {
413
439
  // No branch — create fresh worktree and copy project files
440
+ // Sync main branch before creating worktree (if enabled)
441
+ if (getSyncMainBranch()) {
442
+ const syncResult = pullMainBranch();
443
+ if (syncResult.success) {
444
+ if (syncResult.hadChanges) {
445
+ logger.info(`Synced ${syncResult.mainBranch} from remote`);
446
+ }
447
+ } else {
448
+ logger.warn(`Could not sync main branch: ${syncResult.error}`);
449
+ }
450
+ }
451
+
414
452
  const result = createWorktree(repoBasename, folderName);
415
453
  if (!result.success) {
416
454
  logger.error(`Failed to create worktree: ${result.error}`);
@@ -566,17 +604,25 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
566
604
  worktreeMode,
567
605
  });
568
606
 
607
+ // Generate session ID for token tracking
608
+ const sessionId = crypto.randomUUID();
609
+ const sessionCwd = worktreePath ?? process.cwd();
610
+
569
611
  try {
570
612
  const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
571
613
  dangerouslySkipPermissions: autoMode,
572
614
  // Run Claude session in the worktree root if in worktree mode
573
615
  cwd: worktreePath ?? undefined,
616
+ sessionId,
574
617
  });
575
618
 
576
619
  if (exitCode !== 0) {
577
620
  logger.warn(`Claude exited with code ${exitCode}`);
578
621
  }
579
622
 
623
+ // Parse session file and display token usage summary
624
+ displayPlanSessionTokenSummary(sessionId, sessionCwd);
625
+
580
626
  // Check for new plan files
581
627
  const allPlanFiles = fs.existsSync(plansDir)
582
628
  ? fs.readdirSync(plansDir).filter((f) => f.endsWith('.md')).sort()
@@ -652,3 +698,44 @@ ${taskList}
652
698
  # Describe what you want to add below:
653
699
  `;
654
700
  }
701
+
702
+ /**
703
+ * Display token usage summary for a plan/amend session.
704
+ * Parses the Claude session file and displays formatted usage data.
705
+ */
706
+ function displayPlanSessionTokenSummary(sessionId: string, cwd: string): void {
707
+ const result = parseSessionById(sessionId, cwd);
708
+
709
+ if (!result.success) {
710
+ // Session file not found or couldn't be parsed - just log debug and continue
711
+ logger.debug(`Could not parse session file: ${result.error}`);
712
+ return;
713
+ }
714
+
715
+ // Check if there's any usage data
716
+ const totalTokens = result.usage.inputTokens + result.usage.outputTokens;
717
+ if (totalTokens === 0) {
718
+ logger.debug('No token usage data found in session file');
719
+ return;
720
+ }
721
+
722
+ // Create tracker and add the session as a single "task"
723
+ const pricingConfig = getPricingConfig();
724
+ const tracker = new TokenTracker(pricingConfig);
725
+ const entry = tracker.addTask('plan', [result.usage]);
726
+
727
+ // Get display options
728
+ const displayConfig = getDisplayConfig();
729
+ const options: TokenSummaryOptions = {
730
+ showCacheTokens: displayConfig.showCacheTokens,
731
+ showRateLimitEstimate: displayConfig.showRateLimitEstimate,
732
+ rateLimitPercentage: displayConfig.showRateLimitEstimate
733
+ ? tracker.calculateRateLimitPercentage(entry.cost.totalCost)
734
+ : undefined,
735
+ };
736
+
737
+ // Display the summary
738
+ logger.newline();
739
+ const summary = formatTokenTotalSummary(result.usage, entry.cost, options);
740
+ console.log(summary);
741
+ }
@@ -6,12 +6,11 @@ import { logger } from '../utils/logger.js';
6
6
  import { renderStreamEvent } from '../parsers/stream-renderer.js';
7
7
  import type { UsageData } from '../types/config.js';
8
8
  import { getHeadCommitHash, getHeadCommitMessage, isFileCommittedInHead } from './git.js';
9
- import { getClaudeCommand, getModel } from '../utils/config.js';
9
+ import { getModel } from '../utils/config.js';
10
10
 
11
11
  function getClaudePath(): string {
12
- const cmd = getClaudeCommand();
13
12
  try {
14
- return execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
13
+ return execSync('which claude', { encoding: 'utf-8' }).trim();
15
14
  } catch {
16
15
  throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
17
16
  }
@@ -32,6 +31,12 @@ export interface ClaudeRunnerOptions {
32
31
  * Claude will still ask planning interview questions.
33
32
  */
34
33
  dangerouslySkipPermissions?: boolean;
34
+ /**
35
+ * Session ID for Claude CLI. When provided, passed as --session-id to enable
36
+ * locating the session file after the session ends for token tracking.
37
+ * Only used in interactive mode (runInteractive).
38
+ */
39
+ sessionId?: string;
35
40
  /**
36
41
  * Path to the outcome file. When provided, enables completion detection:
37
42
  * - Monitors stdout for completion markers (<promise>COMPLETE/FAILED</promise>)
@@ -53,12 +58,6 @@ export interface ClaudeRunnerOptions {
53
58
  /** Path to the outcome file that should be committed. */
54
59
  outcomeFilePath: string;
55
60
  };
56
- /**
57
- * Claude Code reasoning effort level.
58
- * Sets CLAUDE_CODE_EFFORT_LEVEL env var for the spawned process.
59
- * Only applied in non-interactive modes (run, runVerbose).
60
- */
61
- effortLevel?: 'low' | 'medium' | 'high';
62
61
  /**
63
62
  * Dynamic verbose display callback. When provided, called for each stream event
64
63
  * to determine whether to write display output to stdout. Overrides the static
@@ -287,7 +286,7 @@ export class ClaudeRunner {
287
286
  userMessage: string,
288
287
  options: ClaudeRunnerOptions = {}
289
288
  ): Promise<number> {
290
- const { cwd = process.cwd(), dangerouslySkipPermissions = false } = options;
289
+ const { cwd = process.cwd(), dangerouslySkipPermissions = false, sessionId } = options;
291
290
 
292
291
  return new Promise((resolve) => {
293
292
  const args = ['--model', this.model];
@@ -297,6 +296,11 @@ export class ClaudeRunner {
297
296
  args.push('--dangerously-skip-permissions');
298
297
  }
299
298
 
299
+ // Add --session-id if provided (for token tracking)
300
+ if (sessionId) {
301
+ args.push('--session-id', sessionId);
302
+ }
303
+
300
304
  // System instructions via --append-system-prompt
301
305
  args.push('--append-system-prompt', systemPrompt);
302
306
 
@@ -415,7 +419,7 @@ export class ClaudeRunner {
415
419
  options: ClaudeRunnerOptions,
416
420
  verbose: boolean,
417
421
  ): Promise<RunResult> {
418
- const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, effortLevel, verboseCheck } = options;
422
+ const { timeout = 60, cwd = process.cwd(), outcomeFilePath, commitContext, verboseCheck } = options;
419
423
  // Ensure timeout is a positive number, fallback to 60 minutes
420
424
  const validatedTimeout = Number(timeout) > 0 ? Number(timeout) : 60;
421
425
  const timeoutMs = validatedTimeout * 60 * 1000;
@@ -437,11 +441,6 @@ export class ClaudeRunner {
437
441
  logger.debug(`Prompt length: ${prompt.length}, timeout: ${timeoutMs}ms, cwd: ${cwd}`);
438
442
  logger.debug(`Claude path: ${claudePath}`);
439
443
 
440
- // Build env, optionally injecting effort level
441
- const env = effortLevel
442
- ? { ...process.env, CLAUDE_CODE_EFFORT_LEVEL: effortLevel }
443
- : process.env;
444
-
445
444
  logger.debug('Spawning process...');
446
445
  // Use --output-format stream-json --verbose to get real-time streaming events
447
446
  // including tool calls, file operations, and token usage in the result event.
@@ -460,7 +459,7 @@ export class ClaudeRunner {
460
459
  'Execute the task as described in the system prompt.',
461
460
  ], {
462
461
  cwd,
463
- env,
462
+ env: process.env,
464
463
  stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
465
464
  });
466
465
 
@@ -1,6 +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
+ import { getModel } from '../utils/config.js';
4
4
 
5
5
  /**
6
6
  * Failure types that can be detected programmatically without using the API.
@@ -213,9 +213,8 @@ function extractRelevantOutput(output: string, maxLines: number): string {
213
213
  * Get the path to Claude CLI.
214
214
  */
215
215
  function getClaudePath(): string {
216
- const cmd = getClaudeCommand();
217
216
  try {
218
- return execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
217
+ return execSync('which claude', { encoding: 'utf-8' }).trim();
219
218
  } catch {
220
219
  throw new Error('Claude CLI not found. Please ensure it is installed and in your PATH.');
221
220
  }
@@ -312,6 +311,7 @@ Respond with ONLY a markdown report in this exact format:
312
311
  const failureModel = getModel('failureAnalysis');
313
312
  const proc = spawn(claudePath, [
314
313
  '--model', failureModel,
314
+ '--no-session-persistence',
315
315
  '--dangerously-skip-permissions',
316
316
  '-p',
317
317
  prompt,