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.
- package/.claude/settings.local.json +3 -1
- package/CLAUDE.md +21 -4
- package/RAF/ahvrih-rate-forge/decisions.md +70 -0
- package/RAF/ahvrih-rate-forge/input.md +44 -0
- package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
- package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
- package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
- package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
- package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
- package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
- package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
- package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
- package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
- package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
- package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
- package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
- package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
- package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
- package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
- package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
- package/RAF/ahwidh-quick-fix-gremlin/decisions.md +37 -0
- package/RAF/ahwidh-quick-fix-gremlin/input.md +35 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/01-fix-name-generation-prompt.md +33 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/02-fix-amend-commit-scope.md +43 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/03-fix-diverged-main-branch-sync.md +32 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/04-wire-rate-limit-to-do-command.md +61 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/05-add-config-get-set-flags.md +125 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/06-sync-worktree-branch-before-execution.md +96 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/07-update-frontmatter-format.md +107 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/08-remove-plan-token-report.md +76 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/01-fix-name-generation-prompt.md +52 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/02-fix-amend-commit-scope.md +48 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/03-fix-diverged-main-branch-sync.md +49 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/04-wire-rate-limit-to-do-command.md +78 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/05-add-config-get-set-flags.md +101 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/06-sync-worktree-branch-before-execution.md +92 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/07-update-frontmatter-format.md +105 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/08-remove-plan-token-report.md +50 -0
- package/README.md +27 -7
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +209 -6
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +140 -21
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +27 -5
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +0 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +4 -9
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +3 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/pull-request.js +3 -3
- package/dist/core/pull-request.js.map +1 -1
- package/dist/core/state-derivation.d.ts +5 -0
- package/dist/core/state-derivation.d.ts.map +1 -1
- package/dist/core/state-derivation.js +14 -4
- package/dist/core/state-derivation.js.map +1 -1
- package/dist/core/worktree.d.ts +44 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +247 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +28 -11
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +28 -11
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +30 -13
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +14 -10
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +47 -4
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +176 -30
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +53 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +115 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +9 -19
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/session-parser.d.ts +44 -0
- package/dist/utils/session-parser.d.ts.map +1 -0
- package/dist/utils/session-parser.js +122 -0
- package/dist/utils/session-parser.js.map +1 -0
- package/dist/utils/terminal-symbols.d.ts +22 -3
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +52 -18
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +20 -0
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +57 -2
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +242 -7
- package/src/commands/do.ts +177 -23
- package/src/commands/plan.ts +27 -4
- package/src/core/claude-runner.ts +4 -16
- package/src/core/failure-analyzer.ts +3 -3
- package/src/core/pull-request.ts +3 -3
- package/src/core/state-derivation.ts +20 -4
- package/src/core/worktree.ts +266 -0
- package/src/prompts/amend.ts +28 -11
- package/src/prompts/config-docs.md +91 -29
- package/src/prompts/planning.ts +28 -11
- package/src/types/config.ts +46 -21
- package/src/utils/config.ts +200 -33
- package/src/utils/frontmatter.ts +140 -0
- package/src/utils/name-generator.ts +9 -19
- package/src/utils/terminal-symbols.ts +68 -16
- package/src/utils/token-tracker.ts +65 -2
- package/tests/unit/claude-runner-interactive.test.ts +8 -6
- package/tests/unit/claude-runner.test.ts +5 -66
- package/tests/unit/commit-planning-artifacts-worktree.test.ts +6 -14
- package/tests/unit/commit-planning-artifacts.test.ts +4 -12
- package/tests/unit/config-command.test.ts +176 -6
- package/tests/unit/config.test.ts +268 -45
- package/tests/unit/frontmatter.test.ts +276 -0
- package/tests/unit/name-generator.test.ts +1 -1
- package/tests/unit/post-execution-picker.test.ts +6 -0
- package/tests/unit/terminal-symbols.test.ts +142 -0
- package/tests/unit/token-tracker.test.ts +304 -1
- package/tests/unit/validation.test.ts +6 -4
- package/tests/unit/worktree.test.ts +309 -0
package/src/types/config.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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 {
|
package/src/utils/config.ts
CHANGED
|
@@ -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
|
-
|
|
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', '
|
|
40
|
-
'worktree', '
|
|
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
|
|
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
|
-
//
|
|
103
|
-
if (obj.
|
|
104
|
-
if (typeof obj.
|
|
105
|
-
throw new ConfigValidationError('
|
|
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
|
|
108
|
-
checkUnknownKeys(
|
|
109
|
-
for (const [key, val] of Object.entries(
|
|
110
|
-
if (typeof val !== 'string' || !(
|
|
111
|
-
throw new ConfigValidationError(
|
|
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.
|
|
198
|
-
result.
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
305
|
-
return getResolvedConfig().
|
|
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
|
|
4
|
+
import { getModel } from './config.js';
|
|
5
5
|
|
|
6
|
-
const NAME_GENERATION_PROMPT = `
|
|
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 = `
|
|
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
|
|
33
|
-
-
|
|
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
|
-
-
|
|
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(
|
|
40
|
+
const proc = spawn('claude', [
|
|
51
41
|
'--model', model,
|
|
52
42
|
'--no-session-persistence',
|
|
53
43
|
'-p',
|