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
@@ -6,10 +6,11 @@ export type ClaudeModelAlias = 'sonnet' | 'haiku' | 'opus';
6
6
  * matching the pattern `claude-{family}-{version}` (e.g., `claude-opus-4-5-20251101`).
7
7
  */
8
8
  export type ClaudeModelName = ClaudeModelAlias | (string & { __brand?: 'FullModelId' });
9
- export type EffortLevel = 'low' | 'medium' | 'high';
9
+
10
+ /** Task complexity label for per-task effort frontmatter. Maps to models via effortMapping. */
11
+ export type TaskEffortLevel = 'low' | 'medium' | 'high';
10
12
 
11
13
  export type ModelScenario = 'plan' | 'execute' | 'nameGeneration' | 'failureAnalysis' | 'prGeneration' | 'config';
12
- export type EffortScenario = ModelScenario;
13
14
  export type CommitFormatType = 'task' | 'plan' | 'amend';
14
15
 
15
16
  export interface ModelsConfig {
@@ -21,13 +22,14 @@ export interface ModelsConfig {
21
22
  config: ClaudeModelName;
22
23
  }
23
24
 
24
- export interface EffortConfig {
25
- plan: EffortLevel;
26
- execute: EffortLevel;
27
- nameGeneration: EffortLevel;
28
- failureAnalysis: EffortLevel;
29
- prGeneration: EffortLevel;
30
- config: EffortLevel;
25
+ /**
26
+ * Maps task complexity labels to model names.
27
+ * Used to resolve per-task effort frontmatter to a model.
28
+ */
29
+ export interface EffortMappingConfig {
30
+ low: ClaudeModelName;
31
+ medium: ClaudeModelName;
32
+ high: ClaudeModelName;
31
33
  }
32
34
 
33
35
  export interface CommitFormatConfig {
@@ -55,16 +57,34 @@ export interface PricingConfig {
55
57
  haiku: ModelPricing;
56
58
  }
57
59
 
60
+ /** Display options for token usage summaries. */
61
+ export interface DisplayConfig {
62
+ /** Show estimated 5h rate limit window percentage. Default: true */
63
+ showRateLimitEstimate: boolean;
64
+ /** Show cache token counts in summaries. Default: true */
65
+ showCacheTokens: boolean;
66
+ }
67
+
68
+ /** Rate limit window configuration. */
69
+ export interface RateLimitWindowConfig {
70
+ /** Sonnet-equivalent token cap for the 5h window. Default: 88000 */
71
+ sonnetTokenCap: number;
72
+ }
73
+
58
74
  export interface RafConfig {
59
75
  models: ModelsConfig;
60
- effort: EffortConfig;
76
+ /** Maps task complexity labels (low/medium/high) to models. Used for per-task effort frontmatter. */
77
+ effortMapping: EffortMappingConfig;
61
78
  timeout: number;
62
79
  maxRetries: number;
63
80
  autoCommit: boolean;
64
81
  worktree: boolean;
82
+ /** Sync main branch with remote before worktree/PR operations. Default: true */
83
+ syncMainBranch: boolean;
65
84
  commitFormat: CommitFormatConfig;
66
- claudeCommand: string;
67
85
  pricing: PricingConfig;
86
+ display: DisplayConfig;
87
+ rateLimitWindow: RateLimitWindowConfig;
68
88
  }
69
89
 
70
90
  export const DEFAULT_CONFIG: RafConfig = {
@@ -76,25 +96,22 @@ export const DEFAULT_CONFIG: RafConfig = {
76
96
  prGeneration: 'sonnet',
77
97
  config: 'sonnet',
78
98
  },
79
- effort: {
80
- plan: 'high',
81
- execute: 'medium',
82
- nameGeneration: 'low',
83
- failureAnalysis: 'low',
84
- prGeneration: 'medium',
85
- config: 'medium',
99
+ effortMapping: {
100
+ low: 'haiku',
101
+ medium: 'sonnet',
102
+ high: 'opus',
86
103
  },
87
104
  timeout: 60,
88
105
  maxRetries: 3,
89
106
  autoCommit: true,
90
107
  worktree: false,
108
+ syncMainBranch: true,
91
109
  commitFormat: {
92
110
  task: '{prefix}[{projectId}:{taskId}] {description}',
93
111
  plan: '{prefix}[{projectId}] Plan: {projectName}',
94
112
  amend: '{prefix}[{projectId}] Amend: {projectName}',
95
113
  prefix: 'RAF',
96
114
  },
97
- claudeCommand: 'claude',
98
115
  pricing: {
99
116
  opus: {
100
117
  inputPerMTok: 15,
@@ -115,6 +132,13 @@ export const DEFAULT_CONFIG: RafConfig = {
115
132
  cacheCreatePerMTok: 1.25,
116
133
  },
117
134
  },
135
+ display: {
136
+ showRateLimitEstimate: true,
137
+ showCacheTokens: true,
138
+ },
139
+ rateLimitWindow: {
140
+ sonnetTokenCap: 88000,
141
+ },
118
142
  };
119
143
 
120
144
  /** Deep partial type for user config files — all fields optional at every level */
@@ -134,7 +158,9 @@ export const FULL_MODEL_ID_PATTERN = /^claude-[a-z]+-\d+(-\d+)*$/;
134
158
 
135
159
  /** @deprecated Use VALID_MODEL_ALIASES instead */
136
160
  export const VALID_MODELS = VALID_MODEL_ALIASES;
137
- export const VALID_EFFORTS: readonly EffortLevel[] = ['low', 'medium', 'high'];
161
+
162
+ /** Valid task effort levels for plan frontmatter. */
163
+ export const VALID_TASK_EFFORTS: readonly TaskEffortLevel[] = ['low', 'medium', 'high'];
138
164
 
139
165
  // Keep backward-compat exports used by other modules
140
166
  /** @deprecated Use DEFAULT_CONFIG instead */
@@ -142,7 +168,6 @@ export const DEFAULT_RAF_CONFIG = {
142
168
  defaultTimeout: DEFAULT_CONFIG.timeout,
143
169
  defaultMaxRetries: DEFAULT_CONFIG.maxRetries,
144
170
  autoCommit: DEFAULT_CONFIG.autoCommit,
145
- claudeCommand: DEFAULT_CONFIG.claudeCommand,
146
171
  };
147
172
 
148
173
  export interface PlanCommandOptions {
@@ -8,15 +8,16 @@ import {
8
8
  UserConfig,
9
9
  VALID_MODEL_ALIASES,
10
10
  FULL_MODEL_ID_PATTERN,
11
- VALID_EFFORTS,
12
11
  ClaudeModelName,
13
- EffortLevel,
12
+ TaskEffortLevel,
14
13
  ModelScenario,
15
- EffortScenario,
16
14
  CommitFormatType,
17
15
  PricingCategory,
18
16
  ModelPricing,
19
17
  PricingConfig,
18
+ DisplayConfig,
19
+ RateLimitWindowConfig,
20
+ EffortMappingConfig,
20
21
  } from '../types/config.js';
21
22
 
22
23
  const CONFIG_DIR = path.join(os.homedir(), '.raf');
@@ -36,8 +37,8 @@ export function getClaudeSettingsPath(): string {
36
37
  // ---- Validation ----
37
38
 
38
39
  const VALID_TOP_LEVEL_KEYS = new Set<string>([
39
- 'models', 'effort', 'timeout', 'maxRetries', 'autoCommit',
40
- 'worktree', 'commitFormat', 'claudeCommand', 'pricing',
40
+ 'models', 'effortMapping', 'timeout', 'maxRetries', 'autoCommit',
41
+ 'worktree', 'syncMainBranch', 'commitFormat', 'pricing', 'display', 'rateLimitWindow',
41
42
  ]);
42
43
 
43
44
  const VALID_PRICING_CATEGORIES = new Set<string>(['opus', 'sonnet', 'haiku']);
@@ -47,12 +48,14 @@ const VALID_MODEL_KEYS = new Set<string>([
47
48
  'plan', 'execute', 'nameGeneration', 'failureAnalysis', 'prGeneration', 'config',
48
49
  ]);
49
50
 
50
- const VALID_EFFORT_KEYS = new Set<string>([
51
- 'plan', 'execute', 'nameGeneration', 'failureAnalysis', 'prGeneration', 'config',
52
- ]);
51
+ const VALID_EFFORT_MAPPING_KEYS = new Set<string>(['low', 'medium', 'high']);
53
52
 
54
53
  const VALID_COMMIT_FORMAT_KEYS = new Set<string>(['task', 'plan', 'amend', 'prefix']);
55
54
 
55
+ const VALID_DISPLAY_KEYS = new Set<string>(['showRateLimitEstimate', 'showCacheTokens']);
56
+
57
+ const VALID_RATE_LIMIT_WINDOW_KEYS = new Set<string>(['sonnetTokenCap']);
58
+
56
59
  export class ConfigValidationError extends Error {
57
60
  constructor(message: string) {
58
61
  super(message);
@@ -99,16 +102,18 @@ export function validateConfig(config: unknown): UserConfig {
99
102
  }
100
103
  }
101
104
 
102
- // effort
103
- if (obj.effort !== undefined) {
104
- if (typeof obj.effort !== 'object' || obj.effort === null || Array.isArray(obj.effort)) {
105
- throw new ConfigValidationError('effort must be an object');
105
+ // effortMapping
106
+ if (obj.effortMapping !== undefined) {
107
+ if (typeof obj.effortMapping !== 'object' || obj.effortMapping === null || Array.isArray(obj.effortMapping)) {
108
+ throw new ConfigValidationError('effortMapping must be an object');
106
109
  }
107
- const effort = obj.effort as Record<string, unknown>;
108
- checkUnknownKeys(effort, VALID_EFFORT_KEYS, 'effort');
109
- for (const [key, val] of Object.entries(effort)) {
110
- if (typeof val !== 'string' || !(VALID_EFFORTS as readonly string[]).includes(val)) {
111
- throw new ConfigValidationError(`effort.${key} must be one of: ${VALID_EFFORTS.join(', ')}`);
110
+ const effortMapping = obj.effortMapping as Record<string, unknown>;
111
+ checkUnknownKeys(effortMapping, VALID_EFFORT_MAPPING_KEYS, 'effortMapping');
112
+ for (const [key, val] of Object.entries(effortMapping)) {
113
+ if (typeof val !== 'string' || !isValidModelName(val)) {
114
+ throw new ConfigValidationError(
115
+ `effortMapping.${key} must be a short alias (${VALID_MODEL_ALIASES.join(', ')}) or a full model ID (e.g., claude-sonnet-4-5-20250929)`
116
+ );
112
117
  }
113
118
  }
114
119
  }
@@ -141,6 +146,13 @@ export function validateConfig(config: unknown): UserConfig {
141
146
  }
142
147
  }
143
148
 
149
+ // syncMainBranch
150
+ if (obj.syncMainBranch !== undefined) {
151
+ if (typeof obj.syncMainBranch !== 'boolean') {
152
+ throw new ConfigValidationError('syncMainBranch must be a boolean');
153
+ }
154
+ }
155
+
144
156
  // commitFormat
145
157
  if (obj.commitFormat !== undefined) {
146
158
  if (typeof obj.commitFormat !== 'object' || obj.commitFormat === null || Array.isArray(obj.commitFormat)) {
@@ -155,13 +167,6 @@ export function validateConfig(config: unknown): UserConfig {
155
167
  }
156
168
  }
157
169
 
158
- // claudeCommand
159
- if (obj.claudeCommand !== undefined) {
160
- if (typeof obj.claudeCommand !== 'string' || obj.claudeCommand.trim() === '') {
161
- throw new ConfigValidationError('claudeCommand must be a non-empty string');
162
- }
163
- }
164
-
165
170
  // pricing
166
171
  if (obj.pricing !== undefined) {
167
172
  if (typeof obj.pricing !== 'object' || obj.pricing === null || Array.isArray(obj.pricing)) {
@@ -183,6 +188,34 @@ export function validateConfig(config: unknown): UserConfig {
183
188
  }
184
189
  }
185
190
 
191
+ // display
192
+ if (obj.display !== undefined) {
193
+ if (typeof obj.display !== 'object' || obj.display === null || Array.isArray(obj.display)) {
194
+ throw new ConfigValidationError('display must be an object');
195
+ }
196
+ const display = obj.display as Record<string, unknown>;
197
+ checkUnknownKeys(display, VALID_DISPLAY_KEYS, 'display');
198
+ for (const [key, val] of Object.entries(display)) {
199
+ if (typeof val !== 'boolean') {
200
+ throw new ConfigValidationError(`display.${key} must be a boolean`);
201
+ }
202
+ }
203
+ }
204
+
205
+ // rateLimitWindow
206
+ if (obj.rateLimitWindow !== undefined) {
207
+ if (typeof obj.rateLimitWindow !== 'object' || obj.rateLimitWindow === null || Array.isArray(obj.rateLimitWindow)) {
208
+ throw new ConfigValidationError('rateLimitWindow must be an object');
209
+ }
210
+ const rlw = obj.rateLimitWindow as Record<string, unknown>;
211
+ checkUnknownKeys(rlw, VALID_RATE_LIMIT_WINDOW_KEYS, 'rateLimitWindow');
212
+ if (rlw.sonnetTokenCap !== undefined) {
213
+ if (typeof rlw.sonnetTokenCap !== 'number' || rlw.sonnetTokenCap <= 0 || !Number.isFinite(rlw.sonnetTokenCap)) {
214
+ throw new ConfigValidationError('rateLimitWindow.sonnetTokenCap must be a positive number');
215
+ }
216
+ }
217
+ }
218
+
186
219
  return config as UserConfig;
187
220
  }
188
221
 
@@ -194,8 +227,8 @@ function deepMerge(defaults: RafConfig, overrides: UserConfig): RafConfig {
194
227
  if (overrides.models) {
195
228
  result.models = { ...defaults.models, ...overrides.models };
196
229
  }
197
- if (overrides.effort) {
198
- result.effort = { ...defaults.effort, ...overrides.effort };
230
+ if (overrides.effortMapping) {
231
+ result.effortMapping = { ...defaults.effortMapping, ...overrides.effortMapping };
199
232
  }
200
233
  if (overrides.commitFormat) {
201
234
  result.commitFormat = { ...defaults.commitFormat, ...overrides.commitFormat };
@@ -207,11 +240,17 @@ function deepMerge(defaults: RafConfig, overrides: UserConfig): RafConfig {
207
240
  haiku: { ...defaults.pricing.haiku, ...overrides.pricing.haiku },
208
241
  };
209
242
  }
243
+ if (overrides.display) {
244
+ result.display = { ...defaults.display, ...overrides.display };
245
+ }
246
+ if (overrides.rateLimitWindow) {
247
+ result.rateLimitWindow = { ...defaults.rateLimitWindow, ...overrides.rateLimitWindow };
248
+ }
210
249
  if (overrides.timeout !== undefined) result.timeout = overrides.timeout;
211
250
  if (overrides.maxRetries !== undefined) result.maxRetries = overrides.maxRetries;
212
251
  if (overrides.autoCommit !== undefined) result.autoCommit = overrides.autoCommit;
213
252
  if (overrides.worktree !== undefined) result.worktree = overrides.worktree;
214
- if (overrides.claudeCommand !== undefined) result.claudeCommand = overrides.claudeCommand;
253
+ if (overrides.syncMainBranch !== undefined) result.syncMainBranch = overrides.syncMainBranch;
215
254
 
216
255
  return result;
217
256
  }
@@ -226,7 +265,14 @@ export function resolveConfig(configPath?: string): RafConfig {
226
265
  const filePath = configPath ?? getConfigPath();
227
266
 
228
267
  if (!fs.existsSync(filePath)) {
229
- return { ...DEFAULT_CONFIG, models: { ...DEFAULT_CONFIG.models }, effort: { ...DEFAULT_CONFIG.effort }, commitFormat: { ...DEFAULT_CONFIG.commitFormat } };
268
+ return {
269
+ ...DEFAULT_CONFIG,
270
+ models: { ...DEFAULT_CONFIG.models },
271
+ effortMapping: { ...DEFAULT_CONFIG.effortMapping },
272
+ commitFormat: { ...DEFAULT_CONFIG.commitFormat },
273
+ display: { ...DEFAULT_CONFIG.display },
274
+ rateLimitWindow: { ...DEFAULT_CONFIG.rateLimitWindow },
275
+ };
230
276
  }
231
277
 
232
278
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -238,7 +284,7 @@ export function resolveConfig(configPath?: string): RafConfig {
238
284
  /**
239
285
  * @deprecated Use resolveConfig() instead. Kept for backward compatibility.
240
286
  */
241
- export function loadConfig(_rafDir: string): { defaultTimeout: number; defaultMaxRetries: number; autoCommit: boolean; claudeCommand: string } {
287
+ export function loadConfig(_rafDir: string): { defaultTimeout: number; defaultMaxRetries: number; autoCommit: boolean } {
242
288
  return { ...DEFAULT_RAF_CONFIG };
243
289
  }
244
290
 
@@ -273,8 +319,70 @@ export function getModel(scenario: ModelScenario): ClaudeModelName {
273
319
  return getResolvedConfig().models[scenario];
274
320
  }
275
321
 
276
- export function getEffort(scenario: EffortScenario): EffortLevel {
277
- return getResolvedConfig().effort[scenario];
322
+ /**
323
+ * Get the full effort mapping config.
324
+ */
325
+ export function getEffortMapping(): EffortMappingConfig {
326
+ return getResolvedConfig().effortMapping;
327
+ }
328
+
329
+ /**
330
+ * Resolve a task effort level to a model name using the effort mapping config.
331
+ */
332
+ export function resolveEffortToModel(effort: TaskEffortLevel): ClaudeModelName {
333
+ return getResolvedConfig().effortMapping[effort];
334
+ }
335
+
336
+ /**
337
+ * Model tier ordering for ceiling comparison.
338
+ * Higher tier = more capable/expensive model.
339
+ * haiku (1) < sonnet (2) < opus (3)
340
+ */
341
+ const MODEL_TIER_ORDER: Record<string, number> = {
342
+ haiku: 1,
343
+ sonnet: 2,
344
+ opus: 3,
345
+ };
346
+
347
+ /**
348
+ * Get the numeric tier of a model for comparison.
349
+ * Extracts family from full model IDs (e.g., 'claude-opus-4-6' -> 3).
350
+ * Unknown models default to highest tier (3) so they're never accidentally capped.
351
+ */
352
+ export function getModelTier(modelName: string): number {
353
+ // Check short aliases first
354
+ const tier = MODEL_TIER_ORDER[modelName];
355
+ if (tier !== undefined) {
356
+ return tier;
357
+ }
358
+
359
+ // Extract family from full model ID
360
+ const match = modelName.match(/^claude-([a-z]+)-/);
361
+ if (match && match[1]) {
362
+ const familyTier = MODEL_TIER_ORDER[match[1]];
363
+ if (familyTier !== undefined) {
364
+ return familyTier;
365
+ }
366
+ }
367
+
368
+ // Unknown model - default to highest tier (no cap)
369
+ return 3;
370
+ }
371
+
372
+ /**
373
+ * Apply ceiling to a model based on the configured models.execute ceiling.
374
+ * Returns the lower-tier model between the input and the ceiling.
375
+ */
376
+ export function applyModelCeiling(resolvedModel: string, ceiling?: string): string {
377
+ const ceilingModel = ceiling ?? getModel('execute');
378
+ const resolvedTier = getModelTier(resolvedModel);
379
+ const ceilingTier = getModelTier(ceilingModel);
380
+
381
+ // If resolved model is above ceiling, use ceiling instead
382
+ if (resolvedTier > ceilingTier) {
383
+ return ceilingModel;
384
+ }
385
+ return resolvedModel;
278
386
  }
279
387
 
280
388
  export function getCommitFormat(type: CommitFormatType): string {
@@ -301,8 +409,8 @@ export function getWorktreeDefault(): boolean {
301
409
  return getResolvedConfig().worktree;
302
410
  }
303
411
 
304
- export function getClaudeCommand(): string {
305
- return getResolvedConfig().claudeCommand;
412
+ export function getSyncMainBranch(): boolean {
413
+ return getResolvedConfig().syncMainBranch;
306
414
  }
307
415
 
308
416
  /**
@@ -327,6 +435,30 @@ export function getModelShortName(modelId: string): string {
327
435
  return modelId;
328
436
  }
329
437
 
438
+ /**
439
+ * Mapping of short model aliases to their current full model IDs.
440
+ * These should match the latest Claude model versions.
441
+ */
442
+ const MODEL_ALIAS_TO_FULL_ID: Record<string, string> = {
443
+ opus: 'claude-opus-4-6',
444
+ sonnet: 'claude-sonnet-4-5-20250929',
445
+ haiku: 'claude-haiku-4-5-20251001',
446
+ };
447
+
448
+ /**
449
+ * Resolve a model name to its full model ID.
450
+ * If already a full model ID, returns as-is.
451
+ * If a short alias (opus, sonnet, haiku), returns the corresponding full ID.
452
+ */
453
+ export function resolveFullModelId(modelName: string): string {
454
+ const fullId = MODEL_ALIAS_TO_FULL_ID[modelName];
455
+ if (fullId) {
456
+ return fullId;
457
+ }
458
+ // Already a full ID or unknown, return as-is
459
+ return modelName;
460
+ }
461
+
330
462
  /**
331
463
  * Map a full model ID (e.g., `claude-opus-4-6`) or short alias to a pricing category.
332
464
  * Returns null if the model cannot be mapped.
@@ -361,6 +493,41 @@ export function getPricingConfig(): PricingConfig {
361
493
  return getResolvedConfig().pricing;
362
494
  }
363
495
 
496
+ /**
497
+ * Get the full display config.
498
+ */
499
+ export function getDisplayConfig(): DisplayConfig {
500
+ return getResolvedConfig().display;
501
+ }
502
+
503
+ /**
504
+ * Get the full rate limit window config.
505
+ */
506
+ export function getRateLimitWindowConfig(): RateLimitWindowConfig {
507
+ return getResolvedConfig().rateLimitWindow;
508
+ }
509
+
510
+ /**
511
+ * Get whether to show rate limit estimate in token summaries.
512
+ */
513
+ export function getShowRateLimitEstimate(): boolean {
514
+ return getResolvedConfig().display.showRateLimitEstimate;
515
+ }
516
+
517
+ /**
518
+ * Get whether to show cache tokens in summaries.
519
+ */
520
+ export function getShowCacheTokens(): boolean {
521
+ return getResolvedConfig().display.showCacheTokens;
522
+ }
523
+
524
+ /**
525
+ * Get the Sonnet-equivalent token cap for the 5h rate limit window.
526
+ */
527
+ export function getSonnetTokenCap(): number {
528
+ return getResolvedConfig().rateLimitWindow.sonnetTokenCap;
529
+ }
530
+
364
531
  /**
365
532
  * Render a commit message template by replacing {placeholder} tokens with values.
366
533
  * Unknown placeholders are left as-is.
@@ -0,0 +1,140 @@
1
+ import { VALID_TASK_EFFORTS, TaskEffortLevel } from '../types/config.js';
2
+ import { isValidModelName } from './config.js';
3
+
4
+ /**
5
+ * Parsed frontmatter metadata from a plan file.
6
+ */
7
+ export interface PlanFrontmatter {
8
+ /** Task complexity label (low/medium/high). Used to resolve model via effortMapping. */
9
+ effort?: TaskEffortLevel;
10
+ /** Explicit model override (bypasses effort mapping but still subject to ceiling). */
11
+ model?: string;
12
+ }
13
+
14
+ /**
15
+ * Result of parsing frontmatter from a plan file.
16
+ */
17
+ export interface FrontmatterParseResult {
18
+ /** Parsed frontmatter metadata, if any valid keys were found. */
19
+ frontmatter: PlanFrontmatter;
20
+ /** Whether valid frontmatter was found (has at least effort or model). */
21
+ hasFrontmatter: boolean;
22
+ /** Warnings about invalid/unknown frontmatter values. */
23
+ warnings: string[];
24
+ }
25
+
26
+ /**
27
+ * Parse Obsidian-style frontmatter from plan file content.
28
+ *
29
+ * Supports two formats:
30
+ * 1. Standard format (preferred): `---` delimiter at the top and bottom
31
+ * 2. Legacy format (backward compatibility): properties followed by closing `---` only
32
+ *
33
+ * Standard format example:
34
+ * ```
35
+ * ---
36
+ * effort: medium
37
+ * model: sonnet
38
+ * ---
39
+ * # Task: ...
40
+ * ```
41
+ *
42
+ * Legacy format example:
43
+ * ```
44
+ * effort: medium
45
+ * model: sonnet
46
+ * ---
47
+ * # Task: ...
48
+ * ```
49
+ *
50
+ * The parser is lenient:
51
+ * - Ignores unknown keys (with a warning)
52
+ * - Handles missing `---` delimiter gracefully (returns empty frontmatter)
53
+ * - Invalid values produce warnings but don't throw
54
+ * - Case-insensitive value parsing for effort levels
55
+ */
56
+ export function parsePlanFrontmatter(content: string): FrontmatterParseResult {
57
+ const result: FrontmatterParseResult = {
58
+ frontmatter: {},
59
+ hasFrontmatter: false,
60
+ warnings: [],
61
+ };
62
+
63
+ const trimmedContent = content.trimStart();
64
+
65
+ let frontmatterSection: string;
66
+
67
+ if (trimmedContent.startsWith('---')) {
68
+ // Standard format: ---\nkey: value\n---
69
+ const afterOpener = trimmedContent.substring(3);
70
+ // Skip the rest of the opener line (handles "---\n" or "--- \n")
71
+ const openerEnd = afterOpener.indexOf('\n');
72
+ if (openerEnd === -1) {
73
+ // No newline after opening delimiter - no valid frontmatter
74
+ return result;
75
+ }
76
+ const rest = afterOpener.substring(openerEnd + 1);
77
+ const closerIndex = rest.indexOf('---');
78
+ if (closerIndex === -1) {
79
+ // No closing delimiter - no valid frontmatter
80
+ return result;
81
+ }
82
+ frontmatterSection = rest.substring(0, closerIndex);
83
+ } else {
84
+ // Legacy format: key: value\n---
85
+ const delimiterIndex = content.indexOf('---');
86
+ if (delimiterIndex === -1) {
87
+ // No delimiter found - no frontmatter
88
+ return result;
89
+ }
90
+ frontmatterSection = content.substring(0, delimiterIndex);
91
+ }
92
+
93
+ // Parse key: value lines
94
+ const lines = frontmatterSection.split('\n');
95
+ for (const line of lines) {
96
+ const trimmed = line.trim();
97
+ if (!trimmed) continue; // Skip empty lines
98
+
99
+ // Check if it looks like a markdown heading (starts with #) - even if it has a colon
100
+ if (trimmed.startsWith('#')) {
101
+ // Markdown content started before `---` - no valid frontmatter
102
+ return {
103
+ frontmatter: {},
104
+ hasFrontmatter: false,
105
+ warnings: ['Frontmatter section contains markdown content before closing delimiter'],
106
+ };
107
+ }
108
+
109
+ // Parse key: value format
110
+ const colonIndex = trimmed.indexOf(':');
111
+ if (colonIndex === -1) {
112
+ continue; // Skip non-property lines
113
+ }
114
+
115
+ const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
116
+ const value = trimmed.substring(colonIndex + 1).trim();
117
+
118
+ if (key === 'effort') {
119
+ const lowerValue = value.toLowerCase();
120
+ if ((VALID_TASK_EFFORTS as readonly string[]).includes(lowerValue)) {
121
+ result.frontmatter.effort = lowerValue as TaskEffortLevel;
122
+ result.hasFrontmatter = true;
123
+ } else {
124
+ result.warnings.push(`Invalid effort value: "${value}". Must be one of: ${VALID_TASK_EFFORTS.join(', ')}`);
125
+ }
126
+ } else if (key === 'model') {
127
+ if (isValidModelName(value)) {
128
+ result.frontmatter.model = value;
129
+ result.hasFrontmatter = true;
130
+ } else {
131
+ result.warnings.push(`Invalid model value: "${value}". Must be a short alias (sonnet, haiku, opus) or a full model ID.`);
132
+ }
133
+ } else {
134
+ // Unknown key - ignore with warning
135
+ result.warnings.push(`Unknown frontmatter key: "${key}"`);
136
+ }
137
+ }
138
+
139
+ return result;
140
+ }
@@ -1,9 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { logger } from './logger.js';
3
3
  import { sanitizeProjectName } from './validation.js';
4
- import { getModel, getClaudeCommand } from './config.js';
4
+ import { getModel } from './config.js';
5
5
 
6
- const NAME_GENERATION_PROMPT = `Generate a short, punchy, creative project name (1-3 words, kebab-case).
6
+ const NAME_GENERATION_PROMPT = `Output ONLY the kebab-case name. No introduction, no explanation, no quotes.
7
+
8
+ Generate a short, punchy, creative project name (1-3 words, kebab-case).
7
9
 
8
10
  Be creative! Use metaphors, analogies, or evocative words that capture the SPIRIT of the project.
9
11
  Don't literally describe what it does - make it memorable and fun.
@@ -15,26 +17,15 @@ Good examples:
15
17
  - Refactoring → 'spring-cleaning', 'phoenix', 'makeover'
16
18
  - New feature → 'moonshot', 'secret-sauce', 'magic-wand'
17
19
 
18
- Output ONLY the kebab-case name. No quotes, no explanation.
19
-
20
20
  Project description:`;
21
21
 
22
- const MULTI_NAME_GENERATION_PROMPT = `Generate 5 creative project names for the description below.
23
-
24
- IMPORTANT: Each name should use a DIFFERENT naming style:
25
- 1. **Metaphorical** - Use a metaphor or analogy (e.g., 'phoenix', 'lighthouse', 'compass')
26
- 2. **Fun/Playful** - Make it fun or quirky (e.g., 'turbo-boost', 'magic-beans', 'ninja-move')
27
- 3. **Action-oriented** - Focus on what it does with flair (e.g., 'bug-squasher', 'speed-demon', 'data-whisperer')
28
- 4. **Abstract** - Use abstract/poetic concepts (e.g., 'horizon', 'cascade', 'catalyst')
29
- 5. **Cultural reference** - Reference pop culture, mythology, or literature (e.g., 'atlas', 'merlin', 'gandalf')
22
+ const MULTI_NAME_GENERATION_PROMPT = `Output EXACTLY 5 project names, one per line. Do NOT include any introduction, explanation, preamble, numbering, or quotes.
30
23
 
31
24
  Rules:
32
- - Each name should be 1-3 words in kebab-case
33
- - Names must be lowercase with hyphens only
25
+ - Each name: 1-3 words, kebab-case, lowercase with hyphens only
26
+ - Use varied styles: metaphorical, playful, action-oriented, abstract, cultural reference
34
27
  - Make them memorable and evocative
35
- - If the project has many unrelated tasks, prefer abstract/metaphorical/fun names over descriptive ones
36
-
37
- Output format: ONLY output 5 names, one per line, no numbers, no explanations, no quotes.
28
+ - For projects with many unrelated tasks, prefer abstract/metaphorical names
38
29
 
39
30
  Project description:`;
40
31
 
@@ -45,9 +36,8 @@ Project description:`;
45
36
  function runClaudePrint(prompt: string): Promise<string | null> {
46
37
  return new Promise((resolve) => {
47
38
  const model = getModel('nameGeneration');
48
- const cmd = getClaudeCommand();
49
39
 
50
- const proc = spawn(cmd, [
40
+ const proc = spawn('claude', [
51
41
  '--model', model,
52
42
  '--no-session-persistence',
53
43
  '-p',