rafcode 2.3.0 → 2.4.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/CLAUDE.md +21 -4
  3. package/RAF/ahvrih-rate-forge/decisions.md +70 -0
  4. package/RAF/ahvrih-rate-forge/input.md +44 -0
  5. package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
  6. package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
  7. package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
  8. package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
  9. package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
  10. package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
  11. package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
  12. package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
  13. package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
  14. package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
  15. package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
  16. package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
  17. package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
  18. package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
  19. package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
  20. package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
  21. package/RAF/ahwidh-quick-fix-gremlin/decisions.md +37 -0
  22. package/RAF/ahwidh-quick-fix-gremlin/input.md +35 -0
  23. package/RAF/ahwidh-quick-fix-gremlin/outcomes/01-fix-name-generation-prompt.md +33 -0
  24. package/RAF/ahwidh-quick-fix-gremlin/outcomes/02-fix-amend-commit-scope.md +43 -0
  25. package/RAF/ahwidh-quick-fix-gremlin/outcomes/03-fix-diverged-main-branch-sync.md +32 -0
  26. package/RAF/ahwidh-quick-fix-gremlin/outcomes/04-wire-rate-limit-to-do-command.md +61 -0
  27. package/RAF/ahwidh-quick-fix-gremlin/outcomes/05-add-config-get-set-flags.md +125 -0
  28. package/RAF/ahwidh-quick-fix-gremlin/outcomes/06-sync-worktree-branch-before-execution.md +96 -0
  29. package/RAF/ahwidh-quick-fix-gremlin/outcomes/07-update-frontmatter-format.md +107 -0
  30. package/RAF/ahwidh-quick-fix-gremlin/outcomes/08-remove-plan-token-report.md +76 -0
  31. package/RAF/ahwidh-quick-fix-gremlin/plans/01-fix-name-generation-prompt.md +52 -0
  32. package/RAF/ahwidh-quick-fix-gremlin/plans/02-fix-amend-commit-scope.md +48 -0
  33. package/RAF/ahwidh-quick-fix-gremlin/plans/03-fix-diverged-main-branch-sync.md +49 -0
  34. package/RAF/ahwidh-quick-fix-gremlin/plans/04-wire-rate-limit-to-do-command.md +78 -0
  35. package/RAF/ahwidh-quick-fix-gremlin/plans/05-add-config-get-set-flags.md +101 -0
  36. package/RAF/ahwidh-quick-fix-gremlin/plans/06-sync-worktree-branch-before-execution.md +92 -0
  37. package/RAF/ahwidh-quick-fix-gremlin/plans/07-update-frontmatter-format.md +105 -0
  38. package/RAF/ahwidh-quick-fix-gremlin/plans/08-remove-plan-token-report.md +50 -0
  39. package/README.md +27 -7
  40. package/dist/commands/config.d.ts.map +1 -1
  41. package/dist/commands/config.js +209 -6
  42. package/dist/commands/config.js.map +1 -1
  43. package/dist/commands/do.d.ts.map +1 -1
  44. package/dist/commands/do.js +140 -21
  45. package/dist/commands/do.js.map +1 -1
  46. package/dist/commands/plan.d.ts.map +1 -1
  47. package/dist/commands/plan.js +27 -5
  48. package/dist/commands/plan.js.map +1 -1
  49. package/dist/core/claude-runner.d.ts +0 -6
  50. package/dist/core/claude-runner.d.ts.map +1 -1
  51. package/dist/core/claude-runner.js +4 -9
  52. package/dist/core/claude-runner.js.map +1 -1
  53. package/dist/core/failure-analyzer.d.ts.map +1 -1
  54. package/dist/core/failure-analyzer.js +3 -3
  55. package/dist/core/failure-analyzer.js.map +1 -1
  56. package/dist/core/pull-request.js +3 -3
  57. package/dist/core/pull-request.js.map +1 -1
  58. package/dist/core/state-derivation.d.ts +5 -0
  59. package/dist/core/state-derivation.d.ts.map +1 -1
  60. package/dist/core/state-derivation.js +14 -4
  61. package/dist/core/state-derivation.js.map +1 -1
  62. package/dist/core/worktree.d.ts +44 -0
  63. package/dist/core/worktree.d.ts.map +1 -1
  64. package/dist/core/worktree.js +247 -0
  65. package/dist/core/worktree.js.map +1 -1
  66. package/dist/prompts/amend.d.ts.map +1 -1
  67. package/dist/prompts/amend.js +28 -11
  68. package/dist/prompts/amend.js.map +1 -1
  69. package/dist/prompts/planning.d.ts.map +1 -1
  70. package/dist/prompts/planning.js +28 -11
  71. package/dist/prompts/planning.js.map +1 -1
  72. package/dist/types/config.d.ts +30 -13
  73. package/dist/types/config.d.ts.map +1 -1
  74. package/dist/types/config.js +14 -10
  75. package/dist/types/config.js.map +1 -1
  76. package/dist/utils/config.d.ts +47 -4
  77. package/dist/utils/config.d.ts.map +1 -1
  78. package/dist/utils/config.js +176 -30
  79. package/dist/utils/config.js.map +1 -1
  80. package/dist/utils/frontmatter.d.ts +53 -0
  81. package/dist/utils/frontmatter.d.ts.map +1 -0
  82. package/dist/utils/frontmatter.js +115 -0
  83. package/dist/utils/frontmatter.js.map +1 -0
  84. package/dist/utils/name-generator.d.ts.map +1 -1
  85. package/dist/utils/name-generator.js +9 -19
  86. package/dist/utils/name-generator.js.map +1 -1
  87. package/dist/utils/session-parser.d.ts +44 -0
  88. package/dist/utils/session-parser.d.ts.map +1 -0
  89. package/dist/utils/session-parser.js +122 -0
  90. package/dist/utils/session-parser.js.map +1 -0
  91. package/dist/utils/terminal-symbols.d.ts +22 -3
  92. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  93. package/dist/utils/terminal-symbols.js +52 -18
  94. package/dist/utils/terminal-symbols.js.map +1 -1
  95. package/dist/utils/token-tracker.d.ts +20 -0
  96. package/dist/utils/token-tracker.d.ts.map +1 -1
  97. package/dist/utils/token-tracker.js +57 -2
  98. package/dist/utils/token-tracker.js.map +1 -1
  99. package/package.json +1 -1
  100. package/src/commands/config.ts +242 -7
  101. package/src/commands/do.ts +177 -23
  102. package/src/commands/plan.ts +27 -4
  103. package/src/core/claude-runner.ts +4 -16
  104. package/src/core/failure-analyzer.ts +3 -3
  105. package/src/core/pull-request.ts +3 -3
  106. package/src/core/state-derivation.ts +20 -4
  107. package/src/core/worktree.ts +266 -0
  108. package/src/prompts/amend.ts +28 -11
  109. package/src/prompts/config-docs.md +91 -29
  110. package/src/prompts/planning.ts +28 -11
  111. package/src/types/config.ts +46 -21
  112. package/src/utils/config.ts +200 -33
  113. package/src/utils/frontmatter.ts +140 -0
  114. package/src/utils/name-generator.ts +9 -19
  115. package/src/utils/terminal-symbols.ts +68 -16
  116. package/src/utils/token-tracker.ts +65 -2
  117. package/tests/unit/claude-runner-interactive.test.ts +8 -6
  118. package/tests/unit/claude-runner.test.ts +5 -66
  119. package/tests/unit/commit-planning-artifacts-worktree.test.ts +6 -14
  120. package/tests/unit/commit-planning-artifacts.test.ts +4 -12
  121. package/tests/unit/config-command.test.ts +176 -6
  122. package/tests/unit/config.test.ts +268 -45
  123. package/tests/unit/frontmatter.test.ts +276 -0
  124. package/tests/unit/name-generator.test.ts +1 -1
  125. package/tests/unit/post-execution-picker.test.ts +6 -0
  126. package/tests/unit/terminal-symbols.test.ts +142 -0
  127. package/tests/unit/token-tracker.test.ts +304 -1
  128. package/tests/unit/validation.test.ts +6 -4
  129. package/tests/unit/worktree.test.ts +309 -0
@@ -7,6 +7,16 @@ import { formatElapsedTime } from './timer.js';
7
7
  import type { UsageData } from '../types/config.js';
8
8
  import type { CostBreakdown, TaskUsageEntry } from './token-tracker.js';
9
9
 
10
+ /** Options for token summary formatting. */
11
+ export interface TokenSummaryOptions {
12
+ /** Whether to show cache token counts. Default: true */
13
+ showCacheTokens?: boolean;
14
+ /** Whether to show rate limit percentage. Default: true */
15
+ showRateLimitEstimate?: boolean;
16
+ /** Rate limit percentage to display (requires showRateLimitEstimate: true) */
17
+ rateLimitPercentage?: number;
18
+ }
19
+
10
20
  /**
11
21
  * Visual symbols for terminal output using dots/symbols style.
12
22
  */
@@ -145,6 +155,17 @@ export function formatCost(cost: number): string {
145
155
  return `$${cost.toFixed(2)}`;
146
156
  }
147
157
 
158
+ /**
159
+ * Formats a rate limit percentage for display.
160
+ * Uses tilde (~) prefix to indicate estimate.
161
+ */
162
+ export function formatRateLimitPercentage(percentage: number): string {
163
+ if (percentage === 0) return '~0% of 5h window';
164
+ if (percentage < 0.1) return `~${percentage.toFixed(2)}% of 5h window`;
165
+ if (percentage < 1) return `~${percentage.toFixed(1)}% of 5h window`;
166
+ return `~${Math.round(percentage)}% of 5h window`;
167
+ }
168
+
148
169
  /**
149
170
  * Formats a single line of token usage (for a single attempt or total).
150
171
  * Used internally by formatTaskTokenSummary.
@@ -153,67 +174,94 @@ function formatTokenLine(
153
174
  usage: UsageData,
154
175
  costValue: number,
155
176
  prefix: string = '',
156
- indent: string = ' '
177
+ indent: string = ' ',
178
+ options: TokenSummaryOptions = {}
157
179
  ): string {
180
+ const { showCacheTokens = true, showRateLimitEstimate = true, rateLimitPercentage } = options;
158
181
  const parts: string[] = [];
159
182
  const tokenPart = `${formatNumber(usage.inputTokens)} in / ${formatNumber(usage.outputTokens)} out`;
160
183
  parts.push(prefix ? `${prefix}: ${tokenPart}` : `Tokens: ${tokenPart}`);
161
184
 
162
- const cacheTotal = usage.cacheReadInputTokens + usage.cacheCreationInputTokens;
163
- if (cacheTotal > 0) {
164
- if (usage.cacheReadInputTokens > 0 && usage.cacheCreationInputTokens > 0) {
165
- parts.push(`Cache: ${formatNumber(usage.cacheReadInputTokens)} read / ${formatNumber(usage.cacheCreationInputTokens)} created`);
166
- } else if (usage.cacheReadInputTokens > 0) {
167
- parts.push(`Cache: ${formatNumber(usage.cacheReadInputTokens)} read`);
168
- } else {
169
- parts.push(`Cache: ${formatNumber(usage.cacheCreationInputTokens)} created`);
185
+ if (showCacheTokens) {
186
+ const cacheTotal = usage.cacheReadInputTokens + usage.cacheCreationInputTokens;
187
+ if (cacheTotal > 0) {
188
+ if (usage.cacheReadInputTokens > 0 && usage.cacheCreationInputTokens > 0) {
189
+ parts.push(`Cache: ${formatNumber(usage.cacheReadInputTokens)} read / ${formatNumber(usage.cacheCreationInputTokens)} created`);
190
+ } else if (usage.cacheReadInputTokens > 0) {
191
+ parts.push(`Cache: ${formatNumber(usage.cacheReadInputTokens)} read`);
192
+ } else {
193
+ parts.push(`Cache: ${formatNumber(usage.cacheCreationInputTokens)} created`);
194
+ }
170
195
  }
171
196
  }
172
197
 
173
198
  parts.push(`Est. cost: ${formatCost(costValue)}`);
199
+
200
+ if (showRateLimitEstimate && rateLimitPercentage !== undefined) {
201
+ parts.push(formatRateLimitPercentage(rateLimitPercentage));
202
+ }
203
+
174
204
  return `${indent}${parts.join(' | ')}`;
175
205
  }
176
206
 
177
207
  /**
178
208
  * Formats a per-task token usage summary.
179
- * For single-attempt tasks: " Tokens: 5,234 in / 1,023 out | Cache: 18,500 read | Est. cost: $0.42"
209
+ * For single-attempt tasks: " Tokens: 5,234 in / 1,023 out | Cache: 18,500 read | Est. cost: $0.42 | ~2% of 5h window"
180
210
  * For multi-attempt tasks: shows per-attempt breakdown plus total.
181
211
  *
182
212
  * @param entry - The TaskUsageEntry containing accumulated usage, cost, and attempts array
183
213
  * @param calculateAttemptCost - Optional function to calculate cost for a single attempt's UsageData
214
+ * @param options - Display options for showing cache tokens and rate limit percentage
184
215
  */
185
216
  export function formatTaskTokenSummary(
186
217
  entry: TaskUsageEntry,
187
- calculateAttemptCost?: (usage: UsageData) => CostBreakdown
218
+ calculateAttemptCost?: (usage: UsageData) => CostBreakdown,
219
+ options: TokenSummaryOptions = {}
188
220
  ): string {
189
221
  // Single-attempt: render exactly as before (no per-attempt breakdown)
190
222
  if (entry.attempts.length <= 1) {
191
- return formatTokenLine(entry.usage, entry.cost.totalCost);
223
+ return formatTokenLine(entry.usage, entry.cost.totalCost, '', ' ', options);
192
224
  }
193
225
 
194
226
  // Multi-attempt: show per-attempt lines plus total
227
+ // Per-attempt lines don't show rate limit (only show on total)
228
+ const perAttemptOptions: TokenSummaryOptions = {
229
+ ...options,
230
+ showRateLimitEstimate: false,
231
+ rateLimitPercentage: undefined,
232
+ };
233
+
195
234
  const lines: string[] = [];
196
235
  entry.attempts.forEach((attemptUsage, i) => {
197
236
  const attemptCost = calculateAttemptCost
198
237
  ? calculateAttemptCost(attemptUsage).totalCost
199
238
  : 0;
200
- lines.push(formatTokenLine(attemptUsage, attemptCost, `Attempt ${i + 1}`, ' '));
239
+ lines.push(formatTokenLine(attemptUsage, attemptCost, `Attempt ${i + 1}`, ' ', perAttemptOptions));
201
240
  });
202
- lines.push(formatTokenLine(entry.usage, entry.cost.totalCost, 'Total', ' '));
241
+ lines.push(formatTokenLine(entry.usage, entry.cost.totalCost, 'Total', ' ', options));
203
242
  return lines.join('\n');
204
243
  }
205
244
 
206
245
  /**
207
246
  * Formats the grand total token usage summary block.
208
247
  * Displayed after all tasks complete.
248
+ *
249
+ * @param usage - Total usage data
250
+ * @param cost - Total cost breakdown
251
+ * @param options - Display options for cache tokens and rate limit
209
252
  */
210
- export function formatTokenTotalSummary(usage: UsageData, cost: CostBreakdown): string {
253
+ export function formatTokenTotalSummary(
254
+ usage: UsageData,
255
+ cost: CostBreakdown,
256
+ options: TokenSummaryOptions = {}
257
+ ): string {
258
+ const { showCacheTokens = true, showRateLimitEstimate = true, rateLimitPercentage } = options;
211
259
  const lines: string[] = [];
212
260
  const divider = '── Token Usage Summary ──────────────────';
213
261
  lines.push(divider);
214
262
  lines.push(`Total tokens: ${formatNumber(usage.inputTokens)} in / ${formatNumber(usage.outputTokens)} out`);
215
263
 
216
- if (usage.cacheReadInputTokens > 0 || usage.cacheCreationInputTokens > 0) {
264
+ if (showCacheTokens && (usage.cacheReadInputTokens > 0 || usage.cacheCreationInputTokens > 0)) {
217
265
  const cacheParts: string[] = [];
218
266
  if (usage.cacheReadInputTokens > 0) {
219
267
  cacheParts.push(`${formatNumber(usage.cacheReadInputTokens)} read`);
@@ -225,6 +273,10 @@ export function formatTokenTotalSummary(usage: UsageData, cost: CostBreakdown):
225
273
  }
226
274
 
227
275
  lines.push(`Estimated cost: ${formatCost(cost.totalCost)}`);
276
+
277
+ if (showRateLimitEstimate && rateLimitPercentage !== undefined) {
278
+ lines.push(formatRateLimitPercentage(rateLimitPercentage));
279
+ }
228
280
  lines.push('─────────────────────────────────────────');
229
281
  return lines.join('\n');
230
282
  }
@@ -1,5 +1,5 @@
1
1
  import { UsageData, PricingConfig } from '../types/config.js';
2
- import { resolveModelPricingCategory, getPricingConfig } from './config.js';
2
+ import { resolveModelPricingCategory, getPricingConfig, getRateLimitWindowConfig } from './config.js';
3
3
 
4
4
  /** Cost breakdown for a single task or accumulated total. */
5
5
  export interface CostBreakdown {
@@ -21,6 +21,29 @@ export interface TaskUsageEntry {
21
21
  attempts: UsageData[];
22
22
  }
23
23
 
24
+ /**
25
+ * Sum multiple CostBreakdown objects into a single total.
26
+ */
27
+ export function sumCostBreakdowns(costs: CostBreakdown[]): CostBreakdown {
28
+ const result: CostBreakdown = {
29
+ inputCost: 0,
30
+ outputCost: 0,
31
+ cacheReadCost: 0,
32
+ cacheCreateCost: 0,
33
+ totalCost: 0,
34
+ };
35
+
36
+ for (const cost of costs) {
37
+ result.inputCost += cost.inputCost;
38
+ result.outputCost += cost.outputCost;
39
+ result.cacheReadCost += cost.cacheReadCost;
40
+ result.cacheCreateCost += cost.cacheCreateCost;
41
+ result.totalCost += cost.totalCost;
42
+ }
43
+
44
+ return result;
45
+ }
46
+
24
47
  /**
25
48
  * Merge multiple UsageData objects into a single accumulated UsageData.
26
49
  * Sums all token fields and merges modelUsage maps.
@@ -72,10 +95,15 @@ export class TokenTracker {
72
95
  /**
73
96
  * Record usage data from a completed task.
74
97
  * Accepts an array of UsageData (one per attempt) and accumulates them.
98
+ * Cost is calculated per-attempt to avoid underreporting when some attempts
99
+ * have modelUsage and others only have aggregate fields.
75
100
  */
76
101
  addTask(taskId: string, attempts: UsageData[]): TaskUsageEntry {
77
102
  const usage = accumulateUsage(attempts);
78
- const cost = this.calculateCost(usage);
103
+ // Calculate cost per-attempt, then sum. This ensures attempts with only
104
+ // aggregate fields use sonnet fallback pricing independently.
105
+ const perAttemptCosts = attempts.map((attempt) => this.calculateCost(attempt));
106
+ const cost = sumCostBreakdowns(perAttemptCosts);
79
107
  const entry: TaskUsageEntry = { taskId, usage, cost, attempts };
80
108
  this.entries.push(entry);
81
109
  return entry;
@@ -174,4 +202,39 @@ export class TokenTracker {
174
202
  result.totalCost = result.inputCost + result.outputCost + result.cacheReadCost + result.cacheCreateCost;
175
203
  return result;
176
204
  }
205
+
206
+ /**
207
+ * Calculate the 5h rate limit window percentage for a given cost.
208
+ * Converts cost to Sonnet-equivalent tokens using the configured Sonnet pricing,
209
+ * then divides by the configured cap.
210
+ *
211
+ * @param totalCost - The total cost in dollars
212
+ * @param sonnetTokenCap - Optional override for the Sonnet-equivalent token cap (defaults to config value)
213
+ * @returns The percentage of the 5h window consumed (0-100+)
214
+ */
215
+ calculateRateLimitPercentage(totalCost: number, sonnetTokenCap?: number): number {
216
+ if (totalCost === 0) return 0;
217
+
218
+ // Get the configured cap or use the provided override
219
+ const cap = sonnetTokenCap ?? getRateLimitWindowConfig().sonnetTokenCap;
220
+
221
+ // Calculate the average Sonnet cost per token
222
+ // Using the average of input and output pricing (simplified approach)
223
+ const sonnetPricing = this.pricingConfig.sonnet;
224
+ const avgSonnetCostPerToken = (sonnetPricing.inputPerMTok + sonnetPricing.outputPerMTok) / 2 / 1_000_000;
225
+
226
+ // Convert cost to Sonnet-equivalent tokens
227
+ const sonnetEquivalentTokens = totalCost / avgSonnetCostPerToken;
228
+
229
+ // Calculate percentage
230
+ return (sonnetEquivalentTokens / cap) * 100;
231
+ }
232
+
233
+ /**
234
+ * Get the cumulative 5h window percentage across all recorded tasks.
235
+ */
236
+ getCumulativeRateLimitPercentage(sonnetTokenCap?: number): number {
237
+ const totals = this.getTotals();
238
+ return this.calculateRateLimitPercentage(totals.cost.totalCost, sonnetTokenCap);
239
+ }
177
240
  }
@@ -144,7 +144,9 @@ describe('ClaudeRunner - runInteractive', () => {
144
144
 
145
145
  const spawnArgs = mockPtySpawn.mock.calls[0][1] as string[];
146
146
  expect(spawnArgs).toContain('--model');
147
- expect(spawnArgs).toContain('opus');
147
+ // Default model comes from config, could be short alias or full model ID
148
+ const modelArgIndex = spawnArgs.indexOf('--model');
149
+ expect(spawnArgs[modelArgIndex + 1]).toMatch(/^(opus|sonnet|haiku|claude-(opus|sonnet|haiku)-.+)$/);
148
150
 
149
151
  mockProc._exitCallback({ exitCode: 0 });
150
152
  await runPromise;
@@ -244,8 +246,8 @@ describe('ClaudeRunner - runInteractive', () => {
244
246
  });
245
247
  });
246
248
 
247
- describe('effort level (not applied in interactive mode)', () => {
248
- it('should NOT set CLAUDE_CODE_EFFORT_LEVEL in runInteractive env', async () => {
249
+ describe('environment passing', () => {
250
+ it('should pass process.env to pty spawn in runInteractive', async () => {
249
251
  const mockProc = createMockPtyProcess();
250
252
  const mockStdin = createMockStdin();
251
253
  const mockStdout = createMockStdout();
@@ -256,12 +258,12 @@ describe('ClaudeRunner - runInteractive', () => {
256
258
  mockPtySpawn.mockReturnValue(mockProc);
257
259
 
258
260
  const runner = new ClaudeRunner();
259
- // Even if effortLevel were somehow passed, interactive mode should use process.env as-is
260
261
  const runPromise = runner.runInteractive('system', 'user');
261
262
 
262
263
  const spawnOptions = mockPtySpawn.mock.calls[0][2];
263
- // Interactive mode passes process.env directly, no effort level override
264
- expect(spawnOptions.env).not.toHaveProperty('CLAUDE_CODE_EFFORT_LEVEL');
264
+ // Interactive mode passes process.env directly
265
+ // Note: effortLevel option was removed from ClaudeRunner in favor of per-task model resolution
266
+ expect(spawnOptions.env).toBeDefined();
265
267
 
266
268
  mockProc._exitCallback({ exitCode: 0 });
267
269
  await runPromise;
@@ -473,7 +473,9 @@ describe('ClaudeRunner', () => {
473
473
  });
474
474
  });
475
475
 
476
- describe('effort level', () => {
476
+ // Note: effortLevel option was removed from ClaudeRunner in favor of per-task model resolution
477
+ // via plan frontmatter. See effortMapping config and frontmatter.ts for the new approach.
478
+ describe('environment handling', () => {
477
479
  function createMockProcess() {
478
480
  const stdout = new EventEmitter();
479
481
  const stderr = new EventEmitter();
@@ -484,35 +486,7 @@ describe('ClaudeRunner', () => {
484
486
  return proc;
485
487
  }
486
488
 
487
- it('should set CLAUDE_CODE_EFFORT_LEVEL env var in run() when effortLevel is provided', async () => {
488
- const mockProc = createMockProcess();
489
- mockSpawn.mockReturnValue(mockProc);
490
-
491
- const runner = new ClaudeRunner();
492
- const runPromise = runner.run('test prompt', { timeout: 60, effortLevel: 'medium' });
493
-
494
- mockProc.emit('close', 0);
495
- await runPromise;
496
-
497
- const spawnOptions = mockSpawn.mock.calls[0][2];
498
- expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
499
- });
500
-
501
- it('should set CLAUDE_CODE_EFFORT_LEVEL env var in runVerbose() when effortLevel is provided', async () => {
502
- const mockProc = createMockProcess();
503
- mockSpawn.mockReturnValue(mockProc);
504
-
505
- const runner = new ClaudeRunner();
506
- const runPromise = runner.runVerbose('test prompt', { timeout: 60, effortLevel: 'medium' });
507
-
508
- mockProc.emit('close', 0);
509
- await runPromise;
510
-
511
- const spawnOptions = mockSpawn.mock.calls[0][2];
512
- expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
513
- });
514
-
515
- it('should NOT set CLAUDE_CODE_EFFORT_LEVEL when effortLevel is not provided in run()', async () => {
489
+ it('should pass process.env to child process in run()', async () => {
516
490
  const mockProc = createMockProcess();
517
491
  mockSpawn.mockReturnValue(mockProc);
518
492
 
@@ -523,11 +497,10 @@ describe('ClaudeRunner', () => {
523
497
  await runPromise;
524
498
 
525
499
  const spawnOptions = mockSpawn.mock.calls[0][2];
526
- // env should be process.env directly (no CLAUDE_CODE_EFFORT_LEVEL override)
527
500
  expect(spawnOptions.env).toBe(process.env);
528
501
  });
529
502
 
530
- it('should NOT set CLAUDE_CODE_EFFORT_LEVEL when effortLevel is not provided in runVerbose()', async () => {
503
+ it('should pass process.env to child process in runVerbose()', async () => {
531
504
  const mockProc = createMockProcess();
532
505
  mockSpawn.mockReturnValue(mockProc);
533
506
 
@@ -538,42 +511,8 @@ describe('ClaudeRunner', () => {
538
511
  await runPromise;
539
512
 
540
513
  const spawnOptions = mockSpawn.mock.calls[0][2];
541
- // env should be process.env directly (no CLAUDE_CODE_EFFORT_LEVEL override)
542
514
  expect(spawnOptions.env).toBe(process.env);
543
515
  });
544
-
545
- it('should support different effort levels', async () => {
546
- for (const level of ['low', 'medium', 'high'] as const) {
547
- const mockProc = createMockProcess();
548
- mockSpawn.mockReturnValue(mockProc);
549
-
550
- const runner = new ClaudeRunner();
551
- const runPromise = runner.run('test prompt', { timeout: 60, effortLevel: level });
552
-
553
- mockProc.emit('close', 0);
554
- await runPromise;
555
-
556
- const spawnOptions = mockSpawn.mock.calls[mockSpawn.mock.calls.length - 1][2];
557
- expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe(level);
558
- }
559
- });
560
-
561
- it('should preserve other env vars when effortLevel is set', async () => {
562
- const mockProc = createMockProcess();
563
- mockSpawn.mockReturnValue(mockProc);
564
-
565
- const runner = new ClaudeRunner();
566
- const runPromise = runner.run('test prompt', { timeout: 60, effortLevel: 'medium' });
567
-
568
- mockProc.emit('close', 0);
569
- await runPromise;
570
-
571
- const spawnOptions = mockSpawn.mock.calls[0][2];
572
- // Should have PATH from process.env
573
- expect(spawnOptions.env.PATH).toBe(process.env.PATH);
574
- // And the injected effort level
575
- expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
576
- });
577
516
  });
578
517
 
579
518
  describe('system prompt append flag', () => {
@@ -162,13 +162,9 @@ describe('commitPlanningArtifacts - worktree integration', () => {
162
162
  '# Task: New Task'
163
163
  );
164
164
 
165
- // Call commitPlanningArtifacts with additional files
166
- const additionalFiles = [
167
- path.join(wtProjectPath, 'plans', '02-new-task.md'),
168
- ];
165
+ // Call commitPlanningArtifacts (plan files not included in amend commit)
169
166
  await commitPlanningArtifacts(wtProjectPath, {
170
167
  cwd: worktreePath,
171
- additionalFiles,
172
168
  isAmend: true,
173
169
  });
174
170
 
@@ -176,11 +172,11 @@ describe('commitPlanningArtifacts - worktree integration', () => {
176
172
  const lastMsg = getLastCommitMessage(worktreePath);
177
173
  expect(lastMsg).toMatch(/RAF\[aatest\] Amend: my-project/);
178
174
 
179
- // Verify all three files are in the commit
175
+ // Verify only input.md and decisions.md are in the commit (not plan files)
180
176
  const committedFiles = getLastCommitFiles(worktreePath);
181
177
  expect(committedFiles).toContain(`RAF/${projectFolder}/input.md`);
182
178
  expect(committedFiles).toContain(`RAF/${projectFolder}/decisions.md`);
183
- expect(committedFiles).toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
179
+ expect(committedFiles).not.toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
184
180
  });
185
181
 
186
182
  it('should commit after worktree recreation from branch', async () => {
@@ -234,13 +230,9 @@ describe('commitPlanningArtifacts - worktree integration', () => {
234
230
  '# Task: New Task'
235
231
  );
236
232
 
237
- // Call commitPlanningArtifacts with worktree cwd
238
- const additionalFiles = [
239
- path.join(recreatedProjectPath, 'plans', '02-new-task.md'),
240
- ];
233
+ // Call commitPlanningArtifacts (plan files not included in amend commit)
241
234
  await commitPlanningArtifacts(recreatedProjectPath, {
242
235
  cwd: recreatedWtPath,
243
- additionalFiles,
244
236
  isAmend: true,
245
237
  });
246
238
 
@@ -248,11 +240,11 @@ describe('commitPlanningArtifacts - worktree integration', () => {
248
240
  const lastMsg = getLastCommitMessage(recreatedWtPath);
249
241
  expect(lastMsg).toMatch(/RAF\[aatest\] Amend: my-project/);
250
242
 
251
- // Verify all files are in the commit
243
+ // Verify only input.md and decisions.md are in the commit (not plan files)
252
244
  const committedFiles = getLastCommitFiles(recreatedWtPath);
253
245
  expect(committedFiles).toContain(`RAF/${projectFolder}/input.md`);
254
246
  expect(committedFiles).toContain(`RAF/${projectFolder}/decisions.md`);
255
- expect(committedFiles).toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
247
+ expect(committedFiles).not.toContain(`RAF/${projectFolder}/plans/02-new-task.md`);
256
248
  });
257
249
 
258
250
  it('should work when only some files have changed', async () => {
@@ -252,7 +252,7 @@ describe('commitPlanningArtifacts', () => {
252
252
  );
253
253
  });
254
254
 
255
- it('should stage additional files when provided', async () => {
255
+ it('should not stage plan files in amend mode', async () => {
256
256
  mockExecSync.mockImplementation((cmd: unknown) => {
257
257
  const cmdStr = cmd as string;
258
258
  if (cmdStr.includes('rev-parse')) {
@@ -262,32 +262,24 @@ describe('commitPlanningArtifacts', () => {
262
262
  return '';
263
263
  }
264
264
  if (cmdStr.includes('git diff --cached')) {
265
- return 'RAF/aaaaar-decision-vault/input.md\nRAF/aaaaar-decision-vault/plans/04-new-task.md\n';
265
+ return 'RAF/aaaaar-decision-vault/input.md\n';
266
266
  }
267
267
  return '';
268
268
  });
269
269
 
270
- const additionalFiles = [
271
- '/Users/test/RAF/aaaaar-decision-vault/plans/04-new-task.md',
272
- '/Users/test/RAF/aaaaar-decision-vault/plans/05-another-task.md',
273
- ];
274
-
275
270
  await commitPlanningArtifacts('/Users/test/RAF/aaaaar-decision-vault', {
276
- additionalFiles,
277
271
  isAmend: true,
278
272
  });
279
273
 
280
- // Verify git add called for all 4 files (input, decisions, 2 plans)
274
+ // Verify git add called for only 2 files (input, decisions)
281
275
  const addCalls = mockExecSync.mock.calls.filter(
282
276
  (call) => (call[0] as string).includes('git add')
283
277
  );
284
- expect(addCalls.length).toBe(4);
278
+ expect(addCalls.length).toBe(2);
285
279
 
286
280
  const addCmds = addCalls.map((c) => c[0] as string);
287
281
  expect(addCmds.some((cmd) => cmd.includes('input.md'))).toBe(true);
288
282
  expect(addCmds.some((cmd) => cmd.includes('decisions.md'))).toBe(true);
289
- expect(addCmds.some((cmd) => cmd.includes('04-new-task.md'))).toBe(true);
290
- expect(addCmds.some((cmd) => cmd.includes('05-another-task.md'))).toBe(true);
291
283
  });
292
284
 
293
285
  it('should pass cwd to isGitRepo for worktree support', async () => {