rafcode 2.3.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 (109) hide show
  1. package/CLAUDE.md +19 -4
  2. package/RAF/ahvrih-rate-forge/decisions.md +70 -0
  3. package/RAF/ahvrih-rate-forge/input.md +44 -0
  4. package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
  5. package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
  6. package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
  7. package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
  8. package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
  9. package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
  10. package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
  11. package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
  12. package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
  13. package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
  14. package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
  15. package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
  16. package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
  17. package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
  18. package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
  19. package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
  20. package/README.md +27 -7
  21. package/dist/commands/config.d.ts.map +1 -1
  22. package/dist/commands/config.js +1 -6
  23. package/dist/commands/config.js.map +1 -1
  24. package/dist/commands/do.d.ts.map +1 -1
  25. package/dist/commands/do.js +106 -18
  26. package/dist/commands/do.js.map +1 -1
  27. package/dist/commands/plan.d.ts.map +1 -1
  28. package/dist/commands/plan.js +77 -2
  29. package/dist/commands/plan.js.map +1 -1
  30. package/dist/core/claude-runner.d.ts +6 -6
  31. package/dist/core/claude-runner.d.ts.map +1 -1
  32. package/dist/core/claude-runner.js +9 -10
  33. package/dist/core/claude-runner.js.map +1 -1
  34. package/dist/core/failure-analyzer.d.ts.map +1 -1
  35. package/dist/core/failure-analyzer.js +3 -3
  36. package/dist/core/failure-analyzer.js.map +1 -1
  37. package/dist/core/pull-request.js +3 -3
  38. package/dist/core/pull-request.js.map +1 -1
  39. package/dist/core/state-derivation.d.ts +5 -0
  40. package/dist/core/state-derivation.d.ts.map +1 -1
  41. package/dist/core/state-derivation.js +14 -4
  42. package/dist/core/state-derivation.js.map +1 -1
  43. package/dist/core/worktree.d.ts +32 -0
  44. package/dist/core/worktree.d.ts.map +1 -1
  45. package/dist/core/worktree.js +215 -0
  46. package/dist/core/worktree.js.map +1 -1
  47. package/dist/prompts/amend.d.ts.map +1 -1
  48. package/dist/prompts/amend.js +26 -11
  49. package/dist/prompts/amend.js.map +1 -1
  50. package/dist/prompts/planning.d.ts.map +1 -1
  51. package/dist/prompts/planning.js +26 -11
  52. package/dist/prompts/planning.js.map +1 -1
  53. package/dist/types/config.d.ts +30 -13
  54. package/dist/types/config.d.ts.map +1 -1
  55. package/dist/types/config.js +14 -10
  56. package/dist/types/config.js.map +1 -1
  57. package/dist/utils/config.d.ts +47 -4
  58. package/dist/utils/config.d.ts.map +1 -1
  59. package/dist/utils/config.js +176 -30
  60. package/dist/utils/config.js.map +1 -1
  61. package/dist/utils/frontmatter.d.ts +43 -0
  62. package/dist/utils/frontmatter.d.ts.map +1 -0
  63. package/dist/utils/frontmatter.js +85 -0
  64. package/dist/utils/frontmatter.js.map +1 -0
  65. package/dist/utils/name-generator.d.ts.map +1 -1
  66. package/dist/utils/name-generator.js +2 -3
  67. package/dist/utils/name-generator.js.map +1 -1
  68. package/dist/utils/session-parser.d.ts +44 -0
  69. package/dist/utils/session-parser.d.ts.map +1 -0
  70. package/dist/utils/session-parser.js +122 -0
  71. package/dist/utils/session-parser.js.map +1 -0
  72. package/dist/utils/terminal-symbols.d.ts +22 -3
  73. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  74. package/dist/utils/terminal-symbols.js +52 -18
  75. package/dist/utils/terminal-symbols.js.map +1 -1
  76. package/dist/utils/token-tracker.d.ts +20 -0
  77. package/dist/utils/token-tracker.d.ts.map +1 -1
  78. package/dist/utils/token-tracker.js +57 -2
  79. package/dist/utils/token-tracker.js.map +1 -1
  80. package/package.json +1 -1
  81. package/src/commands/config.ts +0 -7
  82. package/src/commands/do.ts +141 -20
  83. package/src/commands/plan.ts +87 -1
  84. package/src/core/claude-runner.ts +16 -17
  85. package/src/core/failure-analyzer.ts +3 -3
  86. package/src/core/pull-request.ts +3 -3
  87. package/src/core/state-derivation.ts +20 -4
  88. package/src/core/worktree.ts +230 -0
  89. package/src/prompts/amend.ts +26 -11
  90. package/src/prompts/config-docs.md +91 -29
  91. package/src/prompts/planning.ts +26 -11
  92. package/src/types/config.ts +46 -21
  93. package/src/utils/config.ts +200 -33
  94. package/src/utils/frontmatter.ts +110 -0
  95. package/src/utils/name-generator.ts +2 -3
  96. package/src/utils/session-parser.ts +161 -0
  97. package/src/utils/terminal-symbols.ts +68 -16
  98. package/src/utils/token-tracker.ts +65 -2
  99. package/tests/unit/claude-runner-interactive.test.ts +8 -6
  100. package/tests/unit/claude-runner.test.ts +5 -66
  101. package/tests/unit/config-command.test.ts +6 -6
  102. package/tests/unit/config.test.ts +268 -45
  103. package/tests/unit/frontmatter.test.ts +182 -0
  104. package/tests/unit/post-execution-picker.test.ts +5 -0
  105. package/tests/unit/session-parser.test.ts +301 -0
  106. package/tests/unit/terminal-symbols.test.ts +142 -0
  107. package/tests/unit/token-tracker.test.ts +304 -1
  108. package/tests/unit/validation.test.ts +6 -4
  109. package/tests/unit/worktree.test.ts +242 -0
@@ -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,
@@ -3,7 +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
+ import { getModel, getModelShortName } from '../utils/config.js';
7
7
  import { extractProjectName, getInputPath, getDecisionsPath, getOutcomesDir, TASK_ID_PATTERN } from '../utils/paths.js';
8
8
 
9
9
  export interface PrCreateResult {
@@ -354,10 +354,9 @@ export function filterClaudeOutput(output: string): string {
354
354
  * Call Claude to generate a PR body.
355
355
  */
356
356
  async function callClaudeForPrBody(prompt: string, timeoutMs: number): Promise<string> {
357
- const cmd = getClaudeCommand();
358
357
  let claudePath: string;
359
358
  try {
360
- claudePath = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
359
+ claudePath = execSync('which claude', { encoding: 'utf-8', stdio: 'pipe' }).trim();
361
360
  } catch {
362
361
  throw new Error('Claude CLI not found');
363
362
  }
@@ -369,6 +368,7 @@ async function callClaudeForPrBody(prompt: string, timeoutMs: number): Promise<s
369
368
  const prModel = getModel('prGeneration');
370
369
  const proc = spawn(claudePath, [
371
370
  '--model', prModel,
371
+ '--no-session-persistence',
372
372
  '--dangerously-skip-permissions',
373
373
  '-p',
374
374
  prompt,
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { getPlansDir, getOutcomesDir, getInputPath, decodeBase26, TASK_ID_PATTERN } from '../utils/paths.js';
4
+ import { parsePlanFrontmatter, type PlanFrontmatter } from '../utils/frontmatter.js';
4
5
 
5
6
  export type DerivedTaskStatus = 'pending' | 'completed' | 'failed' | 'blocked';
6
7
 
@@ -16,6 +17,10 @@ export interface DerivedTask {
16
17
  planFile: string;
17
18
  status: DerivedTaskStatus;
18
19
  dependencies: string[];
20
+ /** Frontmatter metadata parsed from the plan file. */
21
+ frontmatter?: PlanFrontmatter;
22
+ /** Warnings from frontmatter parsing. */
23
+ frontmatterWarnings?: string[];
19
24
  }
20
25
 
21
26
  export interface DerivedProjectState {
@@ -218,23 +223,34 @@ export function deriveProjectState(projectPath: string): DerivedProjectState {
218
223
  }
219
224
  }
220
225
 
221
- // First pass: Match plan files to outcomes and parse dependencies
226
+ // First pass: Match plan files to outcomes and parse dependencies + frontmatter
222
227
  for (const planFile of planFiles) {
223
228
  const match = planFile.match(new RegExp(`^(${TASK_ID_PATTERN})-(.+)\\.md$`));
224
229
  if (match && match[1]) {
225
230
  const taskId = match[1];
226
231
  const status = outcomeStatuses.get(taskId) ?? 'pending';
227
232
 
228
- // Read plan file to extract dependencies
233
+ // Read plan file to extract dependencies and frontmatter
229
234
  const planContent = fs.readFileSync(path.join(plansDir, planFile), 'utf-8');
230
235
  const dependencies = parseDependencies(planContent);
236
+ const frontmatterResult = parsePlanFrontmatter(planContent);
231
237
 
232
- tasks.push({
238
+ const task: DerivedTask = {
233
239
  id: taskId,
234
240
  planFile: path.join('plans', planFile),
235
241
  status,
236
242
  dependencies,
237
- });
243
+ };
244
+
245
+ // Only add frontmatter if valid metadata was found
246
+ if (frontmatterResult.hasFrontmatter) {
247
+ task.frontmatter = frontmatterResult.frontmatter;
248
+ }
249
+ if (frontmatterResult.warnings.length > 0) {
250
+ task.frontmatterWarnings = frontmatterResult.warnings;
251
+ }
252
+
253
+ tasks.push(task);
238
254
  }
239
255
  }
240
256
 
@@ -432,3 +432,233 @@ export function resolveWorktreeProjectByIdentifier(
432
432
  // Ambiguous or no match
433
433
  return null;
434
434
  }
435
+
436
+ export interface SyncMainBranchResult {
437
+ success: boolean;
438
+ /** The detected main branch name (e.g., 'main' or 'master') */
439
+ mainBranch: string | null;
440
+ /** Whether any changes were pulled */
441
+ hadChanges: boolean;
442
+ error?: string;
443
+ }
444
+
445
+ /**
446
+ * Detect the main branch name from the remote.
447
+ * Uses refs/remotes/origin/HEAD, falling back to main/master.
448
+ * Reuses the same logic as detectBaseBranch from pull-request.ts.
449
+ */
450
+ export function detectMainBranch(cwd?: string): string | null {
451
+ // Try to find the default branch from the remote
452
+ try {
453
+ const output = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
454
+ encoding: 'utf-8',
455
+ stdio: 'pipe',
456
+ ...(cwd ? { cwd } : {}),
457
+ }).trim();
458
+ // Output is like "refs/remotes/origin/main"
459
+ const parts = output.split('/');
460
+ return parts[parts.length - 1] ?? null;
461
+ } catch {
462
+ // Fallback: check for common branch names
463
+ }
464
+
465
+ // Try common default branches
466
+ for (const branch of ['main', 'master']) {
467
+ try {
468
+ execSync(`git rev-parse --verify "refs/heads/${branch}"`, {
469
+ encoding: 'utf-8',
470
+ stdio: 'pipe',
471
+ ...(cwd ? { cwd } : {}),
472
+ });
473
+ return branch;
474
+ } catch {
475
+ // Try next
476
+ }
477
+ }
478
+
479
+ return null;
480
+ }
481
+
482
+ /**
483
+ * Pull the main branch from remote before worktree creation.
484
+ * Uses fetch + merge --ff-only to safely update the main branch.
485
+ * This ensures the worktree is created from the latest remote state.
486
+ *
487
+ * Only pulls if currently on the main branch or if the main branch exists.
488
+ * Fails gracefully if there are conflicts or the branch has diverged.
489
+ *
490
+ * @param cwd - The directory to run git commands in (defaults to current directory)
491
+ */
492
+ export function pullMainBranch(cwd?: string): SyncMainBranchResult {
493
+ const mainBranch = detectMainBranch(cwd);
494
+
495
+ if (!mainBranch) {
496
+ return {
497
+ success: false,
498
+ mainBranch: null,
499
+ hadChanges: false,
500
+ error: 'Could not detect main branch (no origin/HEAD or main/master found)',
501
+ };
502
+ }
503
+
504
+ // Get current branch to check if we need to switch
505
+ const currentBranch = getCurrentBranch();
506
+
507
+ // If not on main, we need to fetch and update the main branch ref
508
+ // without checking it out (to avoid disrupting the user's work)
509
+ if (currentBranch !== mainBranch) {
510
+ // Fetch the main branch from origin
511
+ try {
512
+ execSync(`git fetch origin ${mainBranch}:${mainBranch}`, {
513
+ encoding: 'utf-8',
514
+ stdio: 'pipe',
515
+ ...(cwd ? { cwd } : {}),
516
+ });
517
+ logger.debug(`Fetched ${mainBranch} from origin`);
518
+ return {
519
+ success: true,
520
+ mainBranch,
521
+ hadChanges: true, // We fetched updates (may or may not have actual changes)
522
+ };
523
+ } catch (error) {
524
+ // This can fail if the local main has diverged from remote
525
+ // Try a simple fetch without updating the local ref
526
+ try {
527
+ execSync(`git fetch origin ${mainBranch}`, {
528
+ encoding: 'utf-8',
529
+ stdio: 'pipe',
530
+ ...(cwd ? { cwd } : {}),
531
+ });
532
+ logger.debug(`Fetched origin/${mainBranch} (local ${mainBranch} diverged, not updated)`);
533
+ return {
534
+ success: true,
535
+ mainBranch,
536
+ hadChanges: false,
537
+ error: `Local ${mainBranch} has diverged from origin, not updated`,
538
+ };
539
+ } catch (fetchError) {
540
+ const msg = fetchError instanceof Error ? fetchError.message : String(fetchError);
541
+ return {
542
+ success: false,
543
+ mainBranch,
544
+ hadChanges: false,
545
+ error: `Failed to fetch ${mainBranch}: ${msg}`,
546
+ };
547
+ }
548
+ }
549
+ }
550
+
551
+ // Currently on main branch - can do a regular pull with ff-only
552
+ // First, check for uncommitted changes that would block the pull
553
+ try {
554
+ const status = execSync('git status --porcelain', {
555
+ encoding: 'utf-8',
556
+ stdio: 'pipe',
557
+ ...(cwd ? { cwd } : {}),
558
+ }).trim();
559
+
560
+ if (status) {
561
+ return {
562
+ success: false,
563
+ mainBranch,
564
+ hadChanges: false,
565
+ error: `Cannot pull ${mainBranch}: uncommitted changes in working directory`,
566
+ };
567
+ }
568
+ } catch (error) {
569
+ const msg = error instanceof Error ? error.message : String(error);
570
+ return {
571
+ success: false,
572
+ mainBranch,
573
+ hadChanges: false,
574
+ error: `Failed to check git status: ${msg}`,
575
+ };
576
+ }
577
+
578
+ // Fetch and merge with ff-only
579
+ try {
580
+ execSync(`git fetch origin ${mainBranch}`, {
581
+ encoding: 'utf-8',
582
+ stdio: 'pipe',
583
+ ...(cwd ? { cwd } : {}),
584
+ });
585
+ } catch (error) {
586
+ const msg = error instanceof Error ? error.message : String(error);
587
+ return {
588
+ success: false,
589
+ mainBranch,
590
+ hadChanges: false,
591
+ error: `Failed to fetch from origin: ${msg}`,
592
+ };
593
+ }
594
+
595
+ try {
596
+ const output = execSync(`git merge --ff-only origin/${mainBranch}`, {
597
+ encoding: 'utf-8',
598
+ stdio: 'pipe',
599
+ ...(cwd ? { cwd } : {}),
600
+ });
601
+ const hadChanges = !output.includes('Already up to date');
602
+ return {
603
+ success: true,
604
+ mainBranch,
605
+ hadChanges,
606
+ };
607
+ } catch (error) {
608
+ const msg = error instanceof Error ? error.message : String(error);
609
+ return {
610
+ success: false,
611
+ mainBranch,
612
+ hadChanges: false,
613
+ error: `Cannot fast-forward ${mainBranch}: ${msg.includes('Not possible to fast-forward') ? 'branch has diverged from origin' : msg}`,
614
+ };
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Push the main branch to remote before PR creation.
620
+ * Ensures the PR base is up to date.
621
+ *
622
+ * @param cwd - The directory to run git commands in (defaults to current directory)
623
+ */
624
+ export function pushMainBranch(cwd?: string): SyncMainBranchResult {
625
+ const mainBranch = detectMainBranch(cwd);
626
+
627
+ if (!mainBranch) {
628
+ return {
629
+ success: false,
630
+ mainBranch: null,
631
+ hadChanges: false,
632
+ error: 'Could not detect main branch (no origin/HEAD or main/master found)',
633
+ };
634
+ }
635
+
636
+ try {
637
+ execSync(`git push origin ${mainBranch}`, {
638
+ encoding: 'utf-8',
639
+ stdio: 'pipe',
640
+ ...(cwd ? { cwd } : {}),
641
+ });
642
+ return {
643
+ success: true,
644
+ mainBranch,
645
+ hadChanges: true,
646
+ };
647
+ } catch (error) {
648
+ const msg = error instanceof Error ? error.message : String(error);
649
+ // Check if it's just "already up to date"
650
+ if (msg.includes('Everything up-to-date')) {
651
+ return {
652
+ success: true,
653
+ mainBranch,
654
+ hadChanges: false,
655
+ };
656
+ }
657
+ return {
658
+ success: false,
659
+ mainBranch,
660
+ hadChanges: false,
661
+ error: `Failed to push ${mainBranch}: ${msg}`,
662
+ };
663
+ }
664
+ }
@@ -135,9 +135,11 @@ After interviewing the user about all NEW tasks, create plan files starting from
135
135
  - ${projectPath}/plans/${encodeTaskId(nextTaskNumber + 1)}-task-name.md
136
136
  - etc.
137
137
 
138
- Each plan file should follow this structure:
138
+ Each plan file MUST have Obsidian-style frontmatter at the top, before the \`# Task:\` heading. The frontmatter format uses only a closing \`---\` delimiter (no opening delimiter):
139
139
 
140
140
  \`\`\`markdown
141
+ effort: medium
142
+ ---
141
143
  # Task: [Task Name]
142
144
 
143
145
  ## Objective
@@ -175,6 +177,23 @@ Each plan file should follow this structure:
175
177
  [Reference to existing task outcomes if relevant]
176
178
  \`\`\`
177
179
 
180
+ ### Frontmatter Requirements
181
+
182
+ The \`effort\` field is REQUIRED in every plan file. It indicates task complexity and determines which Claude model will execute the task:
183
+ - \`effort: low\` — Trivial/mechanical changes, simple one-file edits, config changes
184
+ - \`effort: medium\` — Well-scoped feature work, bug fixes with clear plans, multi-file changes following existing patterns
185
+ - \`effort: high\` — Architectural changes, complex logic, tasks requiring deep codebase understanding
186
+
187
+ Optionally, you can add an explicit \`model\` field to override the effort-based model selection:
188
+ \`\`\`markdown
189
+ effort: medium
190
+ model: opus
191
+ ---
192
+ # Task: ...
193
+ \`\`\`
194
+
195
+ This is rarely needed — prefer using the \`effort\` label so the user's config controls the actual model used.
196
+
178
197
  ### Step 5: Confirm Completion
179
198
 
180
199
  After creating all new plan files:
@@ -201,19 +220,15 @@ Planning complete! To exit this session and run your tasks:
201
220
  7. Specify task dependencies using the ## Dependencies section with task IDs only (e.g., "01, 02")
202
221
  8. Tasks without dependencies should omit the Dependencies section entirely
203
222
  9. Be specific - vague plans lead to poor execution
223
+ 10. ALWAYS include the \`effort\` frontmatter field in every plan file — assess each task's complexity
204
224
 
205
225
  ## Plan Output Style
206
226
 
207
- **CRITICAL**: Plans should be HIGH-LEVEL and CONCEPTUAL:
208
- - Describe WHAT needs to be done, not HOW to code it
209
- - Focus on architecture, data flow, and component interactions
210
- - NO code snippets or implementation details in plans
211
- - File paths ARE acceptable when referencing:
212
- - Existing project files to modify
213
- - Previous plan/outcome files for context
214
- - Project structure and directories
215
- - Let the executing agent decide implementation specifics
216
- - Plans guide the work; they don't prescribe exact code`;
227
+ Plans can include whatever level of detail you deem helpful for the executing agent. Use your judgment:
228
+ - Include implementation details when they clarify the approach
229
+ - Code snippets are acceptable when they help illustrate a specific pattern
230
+ - File paths are helpful when referencing existing project files, patterns, or directories
231
+ - Focus on clarity the goal is for the executing agent to understand what needs to be done`;
217
232
 
218
233
  const userMessage = `I want to add the following new tasks to this project:
219
234
 
@@ -37,24 +37,36 @@ Controls which Claude model is used for each scenario. Values can be a short ali
37
37
  | Key | Default | Description |
38
38
  |-----|---------|-------------|
39
39
  | `models.plan` | `"opus"` | Model used for planning sessions (`raf plan`) |
40
- | `models.execute` | `"opus"` | Model used for task execution (`raf do`) |
40
+ | `models.execute` | `"opus"` | Ceiling model for task execution (`raf do`). Per-task models from effort frontmatter are capped to this tier. Also used as the fallback when a plan has no effort frontmatter. |
41
41
  | `models.nameGeneration` | `"sonnet"` | Model used for generating project names |
42
42
  | `models.failureAnalysis` | `"haiku"` | Model used for analyzing task failures |
43
43
  | `models.prGeneration` | `"sonnet"` | Model used for generating PR titles and descriptions |
44
44
  | `models.config` | `"sonnet"` | Model used for the interactive config editor (`raf config`) |
45
45
 
46
- ### `effort` — Claude Effort Level
46
+ ### `effortMapping` — Task Effort to Model Mapping
47
47
 
48
- Controls the effort level passed to Claude for each scenario. All values must be one of: `"low"`, `"medium"`, `"high"`.
48
+ Maps task complexity labels (in plan frontmatter) to Claude models. When a plan file has `effort: medium`, RAF resolves the execution model using this mapping.
49
49
 
50
50
  | Key | Default | Description |
51
51
  |-----|---------|-------------|
52
- | `effort.plan` | `"high"` | Effort level for planning sessions |
53
- | `effort.execute` | `"medium"` | Effort level for task execution |
54
- | `effort.nameGeneration` | `"low"` | Effort level for project name generation |
55
- | `effort.failureAnalysis` | `"low"` | Effort level for failure analysis |
56
- | `effort.prGeneration` | `"medium"` | Effort level for PR generation |
57
- | `effort.config` | `"medium"` | Effort level for config editing sessions |
52
+ | `effortMapping.low` | `"haiku"` | Model for low-complexity tasks |
53
+ | `effortMapping.medium` | `"sonnet"` | Model for medium-complexity tasks |
54
+ | `effortMapping.high` | `"opus"` | Model for high-complexity tasks |
55
+
56
+ Values must be a short alias (`"sonnet"`, `"haiku"`, `"opus"`) or a full model ID.
57
+
58
+ **Interaction with `models.execute`**: The `models.execute` value acts as a ceiling. If a task's effort maps to a more expensive model than the ceiling, the ceiling model is used instead. This gives users budget control while allowing tasks to use cheaper models when appropriate.
59
+
60
+ Example:
61
+ ```json
62
+ {
63
+ "models": { "execute": "sonnet" },
64
+ "effortMapping": { "low": "haiku", "medium": "sonnet", "high": "opus" }
65
+ }
66
+ ```
67
+ - Task with `effort: low` → haiku (under ceiling)
68
+ - Task with `effort: medium` → sonnet (at ceiling)
69
+ - Task with `effort: high` → sonnet (capped to ceiling, not opus)
58
70
 
59
71
  ### `timeout` — Task Timeout
60
72
 
@@ -80,6 +92,18 @@ Controls the effort level passed to Claude for each scenario. All values must be
80
92
  - **Default**: `false`
81
93
  - **Description**: When `true`, `raf plan` and `raf do` default to worktree mode (isolated git worktree). Can be overridden per-command with `--worktree` flag.
82
94
 
95
+ ### `syncMainBranch` — Sync Main Branch with Remote
96
+
97
+ - **Type**: boolean
98
+ - **Default**: `true`
99
+ - **Description**: When `true`, RAF automatically syncs the main branch with the remote before worktree operations:
100
+ - **Before worktree creation** (`raf plan --worktree`): Pulls the main branch from remote to ensure the worktree starts from the latest code
101
+ - **Before PR creation** (post-execution "Create PR" action): Pushes the main branch to remote so the PR base is up to date
102
+
103
+ The main branch is auto-detected from `refs/remotes/origin/HEAD`, falling back to `main` or `master` if not set.
104
+
105
+ Failures in sync operations produce warnings but don't block the workflow. For example, if the local main branch has diverged from remote, the sync will warn and continue.
106
+
83
107
  ### `commitFormat` — Commit Message Templates
84
108
 
85
109
  Controls the format of git commit messages. Templates use `{placeholder}` syntax for variable substitution.
@@ -147,25 +171,62 @@ Example override:
147
171
 
148
172
  Only specify the fields you want to change — unset fields keep their defaults.
149
173
 
150
- ### `claudeCommand` — Claude CLI Path
174
+ ### `display` — Token Summary Display Options
175
+
176
+ Controls what information is shown in token usage summaries after tasks and in the grand total.
177
+
178
+ | Key | Default | Description |
179
+ |-----|---------|-------------|
180
+ | `display.showRateLimitEstimate` | `true` | Show estimated 5h rate limit window percentage (e.g., `~42% of 5h window`) |
181
+ | `display.showCacheTokens` | `true` | Show cache read/create token counts in summaries |
182
+
183
+ Example:
184
+
185
+ ```json
186
+ {
187
+ "display": {
188
+ "showRateLimitEstimate": false,
189
+ "showCacheTokens": true
190
+ }
191
+ }
192
+ ```
193
+
194
+ ### `rateLimitWindow` — Rate Limit Configuration
195
+
196
+ Controls the rate limit estimation calculation.
197
+
198
+ | Key | Default | Description |
199
+ |-----|---------|-------------|
200
+ | `rateLimitWindow.sonnetTokenCap` | `88000` | The Sonnet-equivalent token cap for the 5-hour window. All token usage is normalized to Sonnet-equivalent tokens using pricing ratios. |
201
+
202
+ The 5h window percentage is calculated as: `(estimatedCost / sonnetCostPerToken) / sonnetTokenCap * 100`
151
203
 
152
- - **Type**: string (non-empty)
153
- - **Default**: `"claude"`
154
- - **Description**: The command or path used to invoke the Claude CLI. Change this if `claude` is not on your PATH or you want to use a wrapper script.
204
+ Where `sonnetCostPerToken` is derived from the configured Sonnet pricing. Heavier models (Opus) consume the window faster than lighter ones (Haiku) in proportion to their API pricing ratios.
205
+
206
+ Example:
207
+
208
+ ```json
209
+ {
210
+ "rateLimitWindow": {
211
+ "sonnetTokenCap": 100000
212
+ }
213
+ }
214
+ ```
155
215
 
156
216
  ## Validation Rules
157
217
 
158
218
  The config is validated when loaded. Invalid configs cause an error with a descriptive message. The following rules are enforced:
159
219
 
160
220
  - **Unknown keys are rejected** at every nesting level. Typos like `"model"` instead of `"models"` will be caught.
161
- - **Model values** must be a short alias (`"sonnet"`, `"haiku"`, `"opus"`) or a full model ID matching the pattern `claude-{family}-{version}` (e.g., `"claude-sonnet-4-5-20250929"`).
162
- - **Effort values** must be exactly `"low"`, `"medium"`, or `"high"` (case-sensitive).
221
+ - **Model values** (`models.*`, `effortMapping.*`) must be a short alias (`"sonnet"`, `"haiku"`, `"opus"`) or a full model ID matching the pattern `claude-{family}-{version}` (e.g., `"claude-sonnet-4-5-20250929"`).
222
+ - **`effortMapping` keys** must be `"low"`, `"medium"`, or `"high"`.
163
223
  - **`timeout`** must be a positive finite number.
164
224
  - **`maxRetries`** must be a non-negative integer.
165
- - **`autoCommit`** and **`worktree`** must be booleans.
225
+ - **`autoCommit`**, **`worktree`**, and **`syncMainBranch`** must be booleans.
166
226
  - **`commitFormat` values** must be strings.
167
- - **`claudeCommand`** must be a non-empty string.
168
227
  - **`pricing`** categories must be `"opus"`, `"sonnet"`, or `"haiku"`. Each field must be a non-negative number.
228
+ - **`display` values** (`showRateLimitEstimate`, `showCacheTokens`) must be booleans.
229
+ - **`rateLimitWindow.sonnetTokenCap`** must be a positive number.
169
230
  - The config file must be valid JSON containing an object (not an array or primitive).
170
231
 
171
232
  ## CLI Precedence
@@ -199,14 +260,11 @@ Uses Sonnet instead of Opus for task execution. Everything else stays at default
199
260
  "plan": "sonnet",
200
261
  "execute": "sonnet"
201
262
  },
202
- "effort": {
203
- "plan": "medium"
204
- },
205
263
  "worktree": true
206
264
  }
207
265
  ```
208
266
 
209
- Uses Sonnet for both planning and execution, reduces planning effort, and defaults to worktree mode.
267
+ Uses Sonnet for planning and caps task execution at Sonnet (tasks with `effort: high` will use Sonnet instead of Opus). Defaults to worktree mode.
210
268
 
211
269
  ### Full — All Settings Explicit
212
270
 
@@ -220,29 +278,33 @@ Uses Sonnet for both planning and execution, reduces planning effort, and defaul
220
278
  "prGeneration": "sonnet",
221
279
  "config": "sonnet"
222
280
  },
223
- "effort": {
224
- "plan": "high",
225
- "execute": "medium",
226
- "nameGeneration": "low",
227
- "failureAnalysis": "low",
228
- "prGeneration": "medium",
229
- "config": "medium"
281
+ "effortMapping": {
282
+ "low": "haiku",
283
+ "medium": "sonnet",
284
+ "high": "opus"
230
285
  },
231
286
  "timeout": 60,
232
287
  "maxRetries": 3,
233
288
  "autoCommit": true,
234
289
  "worktree": false,
290
+ "syncMainBranch": true,
235
291
  "commitFormat": {
236
292
  "task": "{prefix}[{projectId}:{taskId}] {description}",
237
293
  "plan": "{prefix}[{projectId}] Plan: {projectName}",
238
294
  "amend": "{prefix}[{projectId}] Amend: {projectName}",
239
295
  "prefix": "RAF"
240
296
  },
241
- "claudeCommand": "claude",
242
297
  "pricing": {
243
298
  "opus": { "inputPerMTok": 15, "outputPerMTok": 75, "cacheReadPerMTok": 1.5, "cacheCreatePerMTok": 18.75 },
244
299
  "sonnet": { "inputPerMTok": 3, "outputPerMTok": 15, "cacheReadPerMTok": 0.3, "cacheCreatePerMTok": 3.75 },
245
300
  "haiku": { "inputPerMTok": 1, "outputPerMTok": 5, "cacheReadPerMTok": 0.1, "cacheCreatePerMTok": 1.25 }
301
+ },
302
+ "display": {
303
+ "showRateLimitEstimate": true,
304
+ "showCacheTokens": true
305
+ },
306
+ "rateLimitWindow": {
307
+ "sonnetTokenCap": 88000
246
308
  }
247
309
  }
248
310
  ```
@@ -80,9 +80,11 @@ After interviewing the user about all tasks, create plan files in the plans fold
80
80
  - ${projectPath}/plans/02-task-name.md
81
81
  - etc.
82
82
 
83
- Each plan file should follow this structure:
83
+ Each plan file MUST have Obsidian-style frontmatter at the top, before the \`# Task:\` heading. The frontmatter format uses only a closing \`---\` delimiter (no opening delimiter):
84
84
 
85
85
  \`\`\`markdown
86
+ effort: medium
87
+ ---
86
88
  # Task: [Task Name]
87
89
 
88
90
  ## Objective
@@ -117,6 +119,23 @@ Each plan file should follow this structure:
117
119
  [Any additional context, warnings, or considerations]
118
120
  \`\`\`
119
121
 
122
+ ### Frontmatter Requirements
123
+
124
+ The \`effort\` field is REQUIRED in every plan file. It indicates task complexity and determines which Claude model will execute the task:
125
+ - \`effort: low\` — Trivial/mechanical changes, simple one-file edits, config changes
126
+ - \`effort: medium\` — Well-scoped feature work, bug fixes with clear plans, multi-file changes following existing patterns
127
+ - \`effort: high\` — Architectural changes, complex logic, tasks requiring deep codebase understanding
128
+
129
+ Optionally, you can add an explicit \`model\` field to override the effort-based model selection:
130
+ \`\`\`markdown
131
+ effort: medium
132
+ model: opus
133
+ ---
134
+ # Task: ...
135
+ \`\`\`
136
+
137
+ This is rarely needed — prefer using the \`effort\` label so the user's config controls the actual model used.
138
+
120
139
  ### Step 4: Infer Task Dependencies
121
140
 
122
141
  For each task, analyze which other tasks must complete successfully before it can begin. Add a \`## Dependencies\` section to plan files that have prerequisites.
@@ -166,19 +185,15 @@ Planning complete! To exit this session and run your tasks:
166
185
  6. Only add Dependencies section when a task genuinely requires another to complete first
167
186
  7. Dependencies must only reference lower-numbered tasks to prevent circular dependencies
168
187
  8. Be specific - vague plans lead to poor execution
188
+ 9. ALWAYS include the \`effort\` frontmatter field in every plan file — assess each task's complexity
169
189
 
170
190
  ## Plan Output Style
171
191
 
172
- **CRITICAL**: Plans should be HIGH-LEVEL and CONCEPTUAL:
173
- - Describe WHAT needs to be done, not HOW to code it
174
- - Focus on architecture, data flow, and component interactions
175
- - NO code snippets or implementation details in plans
176
- - File paths ARE acceptable when referencing:
177
- - Existing project files to modify
178
- - Previous plan/outcome files for context
179
- - Project structure and directories
180
- - Let the executing agent decide implementation specifics
181
- - Plans guide the work; they don't prescribe exact code`;
192
+ Plans can include whatever level of detail you deem helpful for the executing agent. Use your judgment:
193
+ - Include implementation details when they clarify the approach
194
+ - Code snippets are acceptable when they help illustrate a specific pattern
195
+ - File paths are helpful when referencing existing project files, patterns, or directories
196
+ - Focus on clarity the goal is for the executing agent to understand what needs to be done`;
182
197
 
183
198
  const userMessage = `Here is my project description:
184
199