rafcode 3.2.1 → 3.8.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 (200) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/CLAUDE.md +0 -1
  3. package/RAF/41-echo-chamber/decisions.md +13 -0
  4. package/RAF/41-echo-chamber/input.md +4 -0
  5. package/RAF/41-echo-chamber/outcomes/1-update-codex-model-defaults.md +24 -0
  6. package/RAF/41-echo-chamber/outcomes/2-e2e-test-codex-provider.md +74 -0
  7. package/RAF/41-echo-chamber/plans/1-update-codex-model-defaults.md +28 -0
  8. package/RAF/41-echo-chamber/plans/2-e2e-test-codex-provider.md +103 -0
  9. package/RAF/42-patch-parade/decisions.md +29 -0
  10. package/RAF/42-patch-parade/input.md +9 -0
  11. package/RAF/42-patch-parade/outcomes/1-fix-codex-model-resolution.md +36 -0
  12. package/RAF/42-patch-parade/outcomes/2-fix-provider-aware-name-generation.md +31 -0
  13. package/RAF/42-patch-parade/outcomes/3-fix-codex-error-event-rendering.md +32 -0
  14. package/RAF/42-patch-parade/outcomes/4-update-cli-help-docs.md +28 -0
  15. package/RAF/42-patch-parade/outcomes/5-update-default-codex-models-to-gpt-5-4.md +33 -0
  16. package/RAF/42-patch-parade/outcomes/6-unify-model-config-schema.md +89 -0
  17. package/RAF/42-patch-parade/plans/1-fix-codex-model-resolution.md +35 -0
  18. package/RAF/42-patch-parade/plans/2-fix-provider-aware-name-generation.md +38 -0
  19. package/RAF/42-patch-parade/plans/3-fix-codex-error-event-rendering.md +32 -0
  20. package/RAF/42-patch-parade/plans/4-update-cli-help-docs.md +31 -0
  21. package/RAF/42-patch-parade/plans/5-update-default-codex-models-to-gpt-5-4.md +35 -0
  22. package/RAF/42-patch-parade/plans/6-unify-model-config-schema.md +46 -0
  23. package/RAF/43-swiss-army/decisions.md +34 -0
  24. package/RAF/43-swiss-army/input.md +7 -0
  25. package/RAF/43-swiss-army/outcomes/1-fix-model-validation.md +21 -0
  26. package/RAF/43-swiss-army/outcomes/2-update-commit-format.md +31 -0
  27. package/RAF/43-swiss-army/outcomes/3-wire-reasoning-effort.md +28 -0
  28. package/RAF/43-swiss-army/outcomes/4-remove-provider-flag.md +27 -0
  29. package/RAF/43-swiss-army/outcomes/5-config-wizard-validation.md +23 -0
  30. package/RAF/43-swiss-army/outcomes/6-add-fast-mode.md +32 -0
  31. package/RAF/43-swiss-army/outcomes/7-config-preset.md +31 -0
  32. package/RAF/43-swiss-army/plans/1-fix-model-validation.md +38 -0
  33. package/RAF/43-swiss-army/plans/2-update-commit-format.md +46 -0
  34. package/RAF/43-swiss-army/plans/3-wire-reasoning-effort.md +39 -0
  35. package/RAF/43-swiss-army/plans/4-remove-provider-flag.md +43 -0
  36. package/RAF/43-swiss-army/plans/5-config-wizard-validation.md +42 -0
  37. package/RAF/43-swiss-army/plans/6-add-fast-mode.md +46 -0
  38. package/RAF/43-swiss-army/plans/7-config-preset.md +51 -0
  39. package/RAF/44-config-api-change/decisions.md +22 -0
  40. package/RAF/44-config-api-change/input.md +5 -0
  41. package/RAF/44-config-api-change/outcomes/1-restructure-config-subcommands.md +19 -0
  42. package/RAF/44-config-api-change/outcomes/2-move-preset-under-config.md +17 -0
  43. package/RAF/44-config-api-change/outcomes/3-update-existing-tests-for-config-api.md +14 -0
  44. package/RAF/44-config-api-change/outcomes/4-update-config-command-docs.md +11 -0
  45. package/RAF/44-config-api-change/outcomes/5-fix-codex-name-generation.md +18 -0
  46. package/RAF/44-config-api-change/plans/1-restructure-config-subcommands.md +37 -0
  47. package/RAF/44-config-api-change/plans/2-move-preset-under-config.md +38 -0
  48. package/RAF/44-config-api-change/plans/3-update-existing-tests-for-config-api.md +38 -0
  49. package/RAF/44-config-api-change/plans/4-update-config-command-docs.md +36 -0
  50. package/RAF/44-config-api-change/plans/5-fix-codex-name-generation.md +49 -0
  51. package/RAF/45-signal-cairn/decisions.md +7 -0
  52. package/RAF/45-signal-cairn/input.md +2 -0
  53. package/RAF/45-signal-cairn/outcomes/1-rename-provider-to-harness.md +19 -0
  54. package/RAF/45-signal-cairn/outcomes/2-normalize-model-display-names.md +18 -0
  55. package/RAF/45-signal-cairn/plans/1-rename-provider-to-harness.md +40 -0
  56. package/RAF/45-signal-cairn/plans/2-normalize-model-display-names.md +41 -0
  57. package/RAF/45-signal-lantern/decisions.md +10 -0
  58. package/RAF/45-signal-lantern/input.md +2 -0
  59. package/RAF/45-signal-lantern/outcomes/1-add-effort-and-fast-to-do-model-display.md +15 -0
  60. package/RAF/45-signal-lantern/outcomes/2-capture-codex-post-run-token-usage.md +15 -0
  61. package/RAF/45-signal-lantern/outcomes/3-show-codex-token-summaries-without-fake-cost.md +14 -0
  62. package/RAF/45-signal-lantern/plans/1-add-effort-and-fast-to-do-model-display.md +38 -0
  63. package/RAF/45-signal-lantern/plans/2-capture-codex-post-run-token-usage.md +37 -0
  64. package/RAF/45-signal-lantern/plans/3-show-codex-token-summaries-without-fake-cost.md +40 -0
  65. package/RAF/46-lantern-arc/decisions.md +19 -0
  66. package/RAF/46-lantern-arc/input.md +6 -0
  67. package/RAF/46-lantern-arc/outcomes/1-remove-spark-alias.md +16 -0
  68. package/RAF/46-lantern-arc/outcomes/2-clean-up-worktree-plan-command.md +30 -0
  69. package/RAF/46-lantern-arc/outcomes/3-fix-token-usage-accumulation.md +32 -0
  70. package/RAF/46-lantern-arc/outcomes/4-display-effort-in-compact-mode.md +22 -0
  71. package/RAF/46-lantern-arc/outcomes/5-codex-fast-mode-research.md +38 -0
  72. package/RAF/46-lantern-arc/outcomes/6-optimize-llm-prompts.md +39 -0
  73. package/RAF/46-lantern-arc/plans/1-remove-spark-alias.md +38 -0
  74. package/RAF/46-lantern-arc/plans/2-clean-up-worktree-plan-command.md +33 -0
  75. package/RAF/46-lantern-arc/plans/3-fix-token-usage-accumulation.md +33 -0
  76. package/RAF/46-lantern-arc/plans/4-display-effort-in-compact-mode.md +28 -0
  77. package/RAF/46-lantern-arc/plans/5-codex-fast-mode-research.md +34 -0
  78. package/RAF/46-lantern-arc/plans/6-optimize-llm-prompts.md +48 -0
  79. package/RAF/47-signal-trim/decisions.md +13 -0
  80. package/RAF/47-signal-trim/input.md +2 -0
  81. package/RAF/47-signal-trim/plans/1-remove-cache-from-status.md +73 -0
  82. package/README.md +47 -57
  83. package/dist/commands/config.d.ts.map +1 -1
  84. package/dist/commands/config.js +47 -49
  85. package/dist/commands/config.js.map +1 -1
  86. package/dist/commands/do.d.ts +2 -0
  87. package/dist/commands/do.d.ts.map +1 -1
  88. package/dist/commands/do.js +57 -44
  89. package/dist/commands/do.js.map +1 -1
  90. package/dist/commands/plan.d.ts.map +1 -1
  91. package/dist/commands/plan.js +36 -153
  92. package/dist/commands/plan.js.map +1 -1
  93. package/dist/commands/preset.d.ts +3 -0
  94. package/dist/commands/preset.d.ts.map +1 -0
  95. package/dist/commands/preset.js +158 -0
  96. package/dist/commands/preset.js.map +1 -0
  97. package/dist/core/claude-runner.d.ts +2 -0
  98. package/dist/core/claude-runner.d.ts.map +1 -1
  99. package/dist/core/claude-runner.js +36 -12
  100. package/dist/core/claude-runner.js.map +1 -1
  101. package/dist/core/codex-runner.d.ts +1 -0
  102. package/dist/core/codex-runner.d.ts.map +1 -1
  103. package/dist/core/codex-runner.js +26 -7
  104. package/dist/core/codex-runner.js.map +1 -1
  105. package/dist/core/failure-analyzer.js +2 -1
  106. package/dist/core/failure-analyzer.js.map +1 -1
  107. package/dist/core/git.d.ts +2 -2
  108. package/dist/core/git.d.ts.map +1 -1
  109. package/dist/core/git.js +53 -3
  110. package/dist/core/git.js.map +1 -1
  111. package/dist/core/pull-request.js +3 -3
  112. package/dist/core/pull-request.js.map +1 -1
  113. package/dist/core/runner-factory.d.ts +4 -4
  114. package/dist/core/runner-factory.d.ts.map +1 -1
  115. package/dist/core/runner-factory.js +8 -8
  116. package/dist/core/runner-factory.js.map +1 -1
  117. package/dist/core/runner-interface.d.ts +1 -1
  118. package/dist/core/runner-types.d.ts +17 -4
  119. package/dist/core/runner-types.d.ts.map +1 -1
  120. package/dist/parsers/codex-stream-renderer.d.ts +7 -0
  121. package/dist/parsers/codex-stream-renderer.d.ts.map +1 -1
  122. package/dist/parsers/codex-stream-renderer.js +37 -4
  123. package/dist/parsers/codex-stream-renderer.js.map +1 -1
  124. package/dist/prompts/amend.d.ts.map +1 -1
  125. package/dist/prompts/amend.js +29 -101
  126. package/dist/prompts/amend.js.map +1 -1
  127. package/dist/prompts/execution.d.ts.map +1 -1
  128. package/dist/prompts/execution.js +17 -34
  129. package/dist/prompts/execution.js.map +1 -1
  130. package/dist/prompts/planning.d.ts.map +1 -1
  131. package/dist/prompts/planning.js +21 -120
  132. package/dist/prompts/planning.js.map +1 -1
  133. package/dist/types/config.d.ts +33 -31
  134. package/dist/types/config.d.ts.map +1 -1
  135. package/dist/types/config.js +14 -28
  136. package/dist/types/config.js.map +1 -1
  137. package/dist/utils/config.d.ts +36 -16
  138. package/dist/utils/config.d.ts.map +1 -1
  139. package/dist/utils/config.js +209 -104
  140. package/dist/utils/config.js.map +1 -1
  141. package/dist/utils/name-generator.d.ts.map +1 -1
  142. package/dist/utils/name-generator.js +25 -12
  143. package/dist/utils/name-generator.js.map +1 -1
  144. package/dist/utils/terminal-symbols.d.ts +15 -2
  145. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  146. package/dist/utils/terminal-symbols.js +36 -4
  147. package/dist/utils/terminal-symbols.js.map +1 -1
  148. package/dist/utils/token-tracker.d.ts +6 -1
  149. package/dist/utils/token-tracker.d.ts.map +1 -1
  150. package/dist/utils/token-tracker.js +84 -51
  151. package/dist/utils/token-tracker.js.map +1 -1
  152. package/dist/utils/validation.d.ts +1 -2
  153. package/dist/utils/validation.d.ts.map +1 -1
  154. package/dist/utils/validation.js +4 -25
  155. package/dist/utils/validation.js.map +1 -1
  156. package/package.json +1 -1
  157. package/src/commands/config.ts +60 -63
  158. package/src/commands/do.ts +63 -51
  159. package/src/commands/plan.ts +34 -165
  160. package/src/commands/preset.ts +186 -0
  161. package/src/core/claude-runner.ts +45 -5
  162. package/src/core/codex-runner.ts +32 -7
  163. package/src/core/failure-analyzer.ts +2 -1
  164. package/src/core/git.ts +57 -3
  165. package/src/core/pull-request.ts +3 -3
  166. package/src/core/runner-factory.ts +9 -9
  167. package/src/core/runner-interface.ts +1 -1
  168. package/src/core/runner-types.ts +17 -4
  169. package/src/parsers/codex-stream-renderer.ts +47 -4
  170. package/src/prompts/amend.ts +29 -101
  171. package/src/prompts/config-docs.md +206 -62
  172. package/src/prompts/execution.ts +17 -34
  173. package/src/prompts/planning.ts +21 -120
  174. package/src/types/config.ts +47 -58
  175. package/src/utils/config.ts +248 -115
  176. package/src/utils/name-generator.ts +29 -13
  177. package/src/utils/terminal-symbols.ts +46 -6
  178. package/src/utils/token-tracker.ts +96 -57
  179. package/src/utils/validation.ts +5 -30
  180. package/tests/unit/amend-prompt.test.ts +3 -2
  181. package/tests/unit/claude-runner-interactive.test.ts +21 -3
  182. package/tests/unit/claude-runner.test.ts +39 -0
  183. package/tests/unit/codex-runner.test.ts +163 -0
  184. package/tests/unit/codex-stream-renderer.test.ts +127 -0
  185. package/tests/unit/command-output.test.ts +57 -0
  186. package/tests/unit/commit-planning-artifacts-worktree.test.ts +24 -7
  187. package/tests/unit/commit-planning-artifacts.test.ts +26 -4
  188. package/tests/unit/config-command.test.ts +215 -303
  189. package/tests/unit/config.test.ts +319 -235
  190. package/tests/unit/dependency-integration.test.ts +27 -1
  191. package/tests/unit/do-model-display.test.ts +35 -0
  192. package/tests/unit/execution-prompt.test.ts +49 -19
  193. package/tests/unit/name-generator.test.ts +82 -12
  194. package/tests/unit/plan-command-auto-flag.test.ts +7 -10
  195. package/tests/unit/plan-command.test.ts +14 -17
  196. package/tests/unit/planning-prompt.test.ts +9 -8
  197. package/tests/unit/terminal-symbols.test.ts +94 -3
  198. package/tests/unit/token-tracker.test.ts +180 -1
  199. package/tests/unit/validation.test.ts +9 -41
  200. package/tests/unit/worktree-flag-override.test.ts +0 -186
@@ -13,28 +13,23 @@ import {
13
13
  validateEnvironment,
14
14
  reportValidation,
15
15
  validateProjectName,
16
- resolveModelOption,
17
16
  } from '../utils/validation.js';
18
17
  import { logger } from '../utils/logger.js';
19
- import { getWorktreeDefault, getModel, getModelShortName, getSyncMainBranch } from '../utils/config.js';
18
+ import { formatModelDisplay, getModel } from '../utils/config.js';
19
+ import type { ModelEntry } from '../types/config.js';
20
20
  import { generateProjectNames } from '../utils/name-generator.js';
21
21
  import { pickProjectName } from '../ui/name-picker.js';
22
22
  import {
23
23
  getPlansDir,
24
24
  getRafDir,
25
- getNextProjectNumber,
26
- formatProjectNumber,
27
25
  resolveProjectIdentifierWithDetails,
28
26
  getInputPath,
29
- getDecisionsPath,
30
- getOutcomesDir,
31
27
  extractTaskNameFromPlanFile,
32
28
  decodeTaskId,
33
29
  encodeTaskId,
34
30
  TASK_ID_PATTERN,
35
31
  numericFileSort,
36
32
  } from '../utils/paths.js';
37
- import { sanitizeProjectName } from '../utils/validation.js';
38
33
  import {
39
34
  deriveProjectState,
40
35
  isProjectComplete,
@@ -43,21 +38,14 @@ import {
43
38
  import {
44
39
  getRepoBasename,
45
40
  getRepoRoot,
46
- createWorktree,
47
41
  validateWorktree,
48
- removeWorktree,
49
- pullMainBranch,
50
42
  resolveWorktreeProjectByIdentifier,
51
43
  } from '../core/worktree.js';
52
44
 
53
45
  interface PlanCommandOptions {
54
46
  amend?: boolean;
55
- model?: string;
56
- sonnet?: boolean;
57
47
  auto?: boolean;
58
- worktree?: boolean;
59
48
  resume?: string;
60
- provider?: string;
61
49
  }
62
50
 
63
51
  export function createPlanCommand(): Command {
@@ -68,30 +56,15 @@ export function createPlanCommand(): Command {
68
56
  '-a, --amend',
69
57
  'Add tasks to an existing project (requires project identifier as argument)'
70
58
  )
71
- .option('-m, --model <name>', 'Model to use (sonnet, haiku, opus)')
72
- .option('--sonnet', 'Use Sonnet model (shorthand for --model sonnet)')
73
59
  .option('-y, --auto', 'Skip permission prompts for file operations')
74
- .option('-w, --worktree', 'Create a git worktree for isolated planning')
75
- .option('--no-worktree', 'Disable worktree mode (overrides config)')
76
60
  .option('-r, --resume <identifier>', 'Resume a planning session for an existing project')
77
- .option('-p, --provider <provider>', 'CLI provider to use (claude, codex)')
78
61
  .action(async (projectName: string | undefined, options: PlanCommandOptions) => {
79
- // Validate and resolve model option
80
- let model: string;
81
- try {
82
- model = resolveModelOption(options.model, options.sonnet, 'plan');
83
- } catch (error) {
84
- logger.error((error as Error).message);
85
- process.exit(1);
86
- }
62
+ const modelEntry = getModel('plan');
87
63
 
88
64
  const autoMode = options.auto ?? false;
89
- const worktreeMode = options.worktree ?? getWorktreeDefault();
90
-
91
- const provider = options.provider as import('../types/config.js').HarnessProvider | undefined;
92
65
 
93
66
  if (options.resume) {
94
- await runResumeCommand(options.resume, model, provider);
67
+ await runResumeCommand(options.resume, modelEntry);
95
68
  } else if (options.amend) {
96
69
  if (!projectName) {
97
70
  logger.error('--amend requires a project identifier');
@@ -99,16 +72,16 @@ export function createPlanCommand(): Command {
99
72
  logger.error(' or: raf plan --amend <project>');
100
73
  process.exit(1);
101
74
  }
102
- await runAmendCommand(projectName, model, autoMode, provider);
75
+ await runAmendCommand(projectName, modelEntry, autoMode);
103
76
  } else {
104
- await runPlanCommand(projectName, model, autoMode, worktreeMode, provider);
77
+ await runPlanCommand(projectName, modelEntry, autoMode);
105
78
  }
106
79
  });
107
80
 
108
81
  return command;
109
82
  }
110
83
 
111
- async function runPlanCommand(projectName?: string, model?: string, autoMode: boolean = false, worktreeMode: boolean = false, provider?: import('../types/config.js').HarnessProvider): Promise<void> {
84
+ async function runPlanCommand(projectName?: string, modelEntry?: ModelEntry, autoMode: boolean = false): Promise<void> {
112
85
  // Validate environment
113
86
  const validation = validateEnvironment();
114
87
  reportValidation(validation);
@@ -151,7 +124,7 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
151
124
  });
152
125
 
153
126
  if (answer === 'amend') {
154
- await runAmendCommand(existingFolder, model, autoMode, provider);
127
+ await runAmendCommand(existingFolder, modelEntry, autoMode);
155
128
  return;
156
129
  } else if (answer === 'cancel') {
157
130
  logger.info('Aborted.');
@@ -161,15 +134,6 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
161
134
  }
162
135
  }
163
136
 
164
- // Validate git repo for worktree mode
165
- if (worktreeMode) {
166
- const repoRoot = getRepoRoot();
167
- if (!repoRoot) {
168
- logger.error('--worktree requires a git repository');
169
- process.exit(1);
170
- }
171
- }
172
-
173
137
  // Open editor for user input
174
138
  logger.info('Opening editor for project description...');
175
139
  logger.info('(Save and close the editor when done)');
@@ -197,7 +161,8 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
197
161
  // Get or generate project name
198
162
  let finalProjectName = projectName;
199
163
  if (!finalProjectName) {
200
- const nameModel = getModelShortName(getModel('nameGeneration'));
164
+ const nameEntry = getModel('nameGeneration');
165
+ const nameModel = formatModelDisplay(nameEntry.model);
201
166
  logger.info(`Generating project name suggestions with ${nameModel}...`);
202
167
  const suggestedNames = await generateProjectNames(cleanInput);
203
168
  logger.newline();
@@ -217,103 +182,32 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
217
182
  process.exit(1);
218
183
  }
219
184
 
220
- let projectPath: string;
221
- let worktreePath: string | null = null;
222
- let worktreeBranch: string | null = null;
223
-
224
- if (worktreeMode) {
225
- // Worktree mode: create worktree, then project folder inside it
226
- const repoBasename = getRepoBasename()!;
227
- const repoRoot = getRepoRoot()!;
228
- const rafDir = getRafDir();
229
-
230
- // Sync main branch before creating worktree (if enabled)
231
- if (getSyncMainBranch()) {
232
- const syncResult = pullMainBranch();
233
- if (syncResult.success) {
234
- if (syncResult.hadChanges) {
235
- logger.info(`Synced ${syncResult.mainBranch} from remote`);
236
- }
237
- } else {
238
- logger.warn(`Could not sync main branch: ${syncResult.error}`);
239
- }
240
- }
241
-
242
- // Compute project number from main repo's RAF directory (scan worktrees too)
243
- const projectNumber = getNextProjectNumber(rafDir, repoBasename);
244
- const sanitizedName = sanitizeProjectName(finalProjectName);
245
- const folderName = `${formatProjectNumber(projectNumber)}-${sanitizedName}`;
246
-
247
- // Create worktree
248
- const result = createWorktree(repoBasename, folderName);
249
- if (!result.success) {
250
- logger.error(`Failed to create worktree: ${result.error}`);
251
- process.exit(1);
252
- }
253
-
254
- worktreePath = result.worktreePath;
255
- worktreeBranch = result.branch;
256
-
257
- // Compute project path inside worktree at the same relative path
258
- const rafRelativePath = path.relative(repoRoot, rafDir);
259
- projectPath = path.join(worktreePath, rafRelativePath, folderName);
260
-
261
- // Create project folder structure inside the worktree
262
- fs.mkdirSync(projectPath, { recursive: true });
263
- fs.mkdirSync(getPlansDir(projectPath), { recursive: true });
264
- fs.mkdirSync(getOutcomesDir(projectPath), { recursive: true });
265
- fs.writeFileSync(getDecisionsPath(projectPath), '# Project Decisions\n');
185
+ // Standard mode: create project in main repo
186
+ const projectManager = new ProjectManager();
187
+ const projectPath = projectManager.createProject(finalProjectName);
266
188
 
267
- // Save input inside worktree project folder
268
- fs.writeFileSync(getInputPath(projectPath), userInput);
269
-
270
- logger.success(`Created worktree: ${worktreePath}`);
271
- logger.success(`Branch: ${worktreeBranch}`);
272
- logger.success(`Project: ${projectPath}`);
273
- logger.newline();
274
- } else {
275
- // Standard mode: create project in main repo
276
- const projectManager = new ProjectManager();
277
- projectPath = projectManager.createProject(finalProjectName);
278
-
279
- logger.success(`Created project: ${projectPath}`);
280
- logger.newline();
189
+ logger.success(`Created project: ${projectPath}`);
190
+ logger.newline();
281
191
 
282
- // Save input
283
- projectManager.saveInput(projectPath, userInput);
284
- }
192
+ // Save input
193
+ projectManager.saveInput(projectPath, userInput);
285
194
 
286
195
  // Set up shutdown handler
287
- const claudeRunner = createRunner({ model, provider });
196
+ const claudeRunner = createRunner({ model: modelEntry?.model, harness: modelEntry?.harness, reasoningEffort: modelEntry?.reasoningEffort, fast: modelEntry?.fast });
288
197
  shutdownHandler.init();
289
198
  shutdownHandler.registerClaudeRunner(claudeRunner);
290
199
 
291
200
  // Register cleanup callback
292
201
  shutdownHandler.onShutdown(() => {
293
- if (worktreeMode && worktreePath) {
294
- // Clean up worktree if no plans were created
295
- const plansDir = getPlansDir(projectPath);
296
- const hasPlanFiles = fs.existsSync(plansDir) &&
297
- fs.readdirSync(plansDir).filter((f) => f.endsWith('.md')).length > 0;
298
- if (!hasPlanFiles) {
299
- const removal = removeWorktree(worktreePath);
300
- if (removal.success) {
301
- logger.debug(`Cleaned up worktree: ${worktreePath}`);
302
- } else {
303
- logger.warn(`Failed to clean up worktree: ${removal.error}`);
304
- }
305
- }
306
- } else {
307
- const projectManager = new ProjectManager();
308
- projectManager.cleanupEmptyProject(projectPath);
309
- }
202
+ const projectManager = new ProjectManager();
203
+ projectManager.cleanupEmptyProject(projectPath);
310
204
  });
311
205
 
312
206
  // Run planning session
313
207
  logger.info('Starting planning session...');
314
208
  logger.info('The planner will interview you about each task.');
315
- if (model) {
316
- logger.info(`Using model: ${model}`);
209
+ if (modelEntry) {
210
+ logger.info(`Using model: ${formatModelDisplay(modelEntry.model, modelEntry.harness, { includeHarness: true })}`);
317
211
  }
318
212
  if (autoMode) {
319
213
  logger.warn('Auto mode enabled: permission prompts will be skipped.');
@@ -323,14 +217,11 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
323
217
  const { systemPrompt, userMessage } = getPlanningPrompt({
324
218
  projectPath,
325
219
  inputContent: userInput,
326
- worktreeMode,
327
220
  });
328
221
 
329
222
  try {
330
223
  const exitCode = await claudeRunner.runInteractive(systemPrompt, userMessage, {
331
224
  dangerouslySkipPermissions: autoMode,
332
- // Run session in the worktree root if in worktree mode
333
- cwd: worktreePath ?? undefined,
334
225
  });
335
226
 
336
227
  if (exitCode !== 0) {
@@ -357,45 +248,23 @@ async function runPlanCommand(projectName?: string, model?: string, autoMode: bo
357
248
  // Commit planning artifacts (input.md, decisions.md, and plan files)
358
249
  const planAbsolutePaths = planFiles.map((f) => path.join(plansDir, f));
359
250
  await commitPlanningArtifacts(projectPath, {
360
- cwd: worktreePath ?? undefined,
361
251
  additionalFiles: planAbsolutePaths,
362
252
  });
363
253
 
364
254
  logger.newline();
365
- if (worktreeMode) {
366
- logger.info(`Worktree: ${worktreePath}`);
367
- logger.info(`Branch: ${worktreeBranch}`);
368
- logger.info(`Run 'raf do ${finalProjectName}' to execute the plans.`);
369
- } else {
370
- logger.info(`Run 'raf do ${finalProjectName}' to execute the plans.`);
371
- }
255
+ logger.info(`Run 'raf do ${finalProjectName}' to execute the plans.`);
372
256
  }
373
257
  } catch (error) {
374
258
  logger.error(`Planning failed: ${error}`);
375
259
  throw error;
376
260
  } finally {
377
- if (worktreeMode && worktreePath) {
378
- // Clean up worktree if no plans were created
379
- const plansDir = getPlansDir(projectPath);
380
- const hasPlanFiles = fs.existsSync(plansDir) &&
381
- fs.readdirSync(plansDir).filter((f) => f.endsWith('.md')).length > 0;
382
- if (!hasPlanFiles) {
383
- const removal = removeWorktree(worktreePath);
384
- if (removal.success) {
385
- logger.debug(`Cleaned up worktree: ${worktreePath}`);
386
- } else {
387
- logger.warn(`Failed to clean up worktree: ${removal.error}`);
388
- }
389
- }
390
- } else if (!worktreeMode) {
391
- // Cleanup empty project folder if no plans were created (standard mode only)
392
- const projectManager = new ProjectManager();
393
- projectManager.cleanupEmptyProject(projectPath);
394
- }
261
+ // Cleanup empty project folder if no plans were created
262
+ const projectManager = new ProjectManager();
263
+ projectManager.cleanupEmptyProject(projectPath);
395
264
  }
396
265
  }
397
266
 
398
- async function runAmendCommand(identifier: string, model?: string, autoMode: boolean = false, provider?: import('../types/config.js').HarnessProvider): Promise<void> {
267
+ async function runAmendCommand(identifier: string, modelEntry?: ModelEntry, autoMode: boolean = false): Promise<void> {
399
268
  // Validate environment
400
269
  const validation = validateEnvironment();
401
270
  reportValidation(validation);
@@ -528,15 +397,15 @@ async function runAmendCommand(identifier: string, model?: string, autoMode: boo
528
397
  fs.writeFileSync(inputPath, updatedInput);
529
398
 
530
399
  // Set up shutdown handler
531
- const claudeRunner = createRunner({ model, provider });
400
+ const claudeRunner = createRunner({ model: modelEntry?.model, harness: modelEntry?.harness, reasoningEffort: modelEntry?.reasoningEffort, fast: modelEntry?.fast });
532
401
  shutdownHandler.init();
533
402
  shutdownHandler.registerClaudeRunner(claudeRunner);
534
403
 
535
404
  // Run amend planning session
536
405
  logger.info('Starting amendment session...');
537
406
  logger.info('The planner will interview you about each new task.');
538
- if (model) {
539
- logger.info(`Using model: ${model}`);
407
+ if (modelEntry) {
408
+ logger.info(`Using model: ${formatModelDisplay(modelEntry.model, modelEntry.harness, { includeHarness: true })}`);
540
409
  }
541
410
  if (autoMode) {
542
411
  logger.warn('Auto mode enabled: permission prompts will be skipped.');
@@ -636,7 +505,7 @@ ${taskList}
636
505
  `;
637
506
  }
638
507
 
639
- async function runResumeCommand(identifier: string, model?: string, provider?: import('../types/config.js').HarnessProvider): Promise<void> {
508
+ async function runResumeCommand(identifier: string, modelEntry?: ModelEntry): Promise<void> {
640
509
  // Validate environment
641
510
  const validation = validateEnvironment();
642
511
  reportValidation(validation);
@@ -700,13 +569,13 @@ async function runResumeCommand(identifier: string, model?: string, provider?: i
700
569
  }
701
570
 
702
571
  logger.info(`Project: ${folderName}`);
703
- if (model) {
704
- logger.info(`Model: ${model}`);
572
+ if (modelEntry) {
573
+ logger.info(`Model: ${formatModelDisplay(modelEntry.model, modelEntry.harness, { includeHarness: true })}`);
705
574
  }
706
575
  logger.newline();
707
576
 
708
577
  // Set up shutdown handler
709
- const claudeRunner = createRunner({ model, provider });
578
+ const claudeRunner = createRunner({ model: modelEntry?.model, harness: modelEntry?.harness, reasoningEffort: modelEntry?.reasoningEffort, fast: modelEntry?.fast });
710
579
  shutdownHandler.init();
711
580
  shutdownHandler.registerClaudeRunner(claudeRunner);
712
581
 
@@ -0,0 +1,186 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { Command } from 'commander';
5
+ import { logger } from '../utils/logger.js';
6
+ import {
7
+ getConfigPath,
8
+ validateConfig,
9
+ ConfigValidationError,
10
+ } from '../utils/config.js';
11
+
12
+ const PRESETS_DIR = path.join(os.homedir(), '.raf', 'presets');
13
+
14
+ function ensurePresetsDir(): void {
15
+ if (!fs.existsSync(PRESETS_DIR)) {
16
+ fs.mkdirSync(PRESETS_DIR, { recursive: true });
17
+ }
18
+ }
19
+
20
+ function getPresetPath(name: string): string {
21
+ return path.join(PRESETS_DIR, `${name}.json`);
22
+ }
23
+
24
+ function validatePresetName(name: string): void {
25
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
26
+ logger.error('Preset name must contain only letters, numbers, hyphens, and underscores.');
27
+ process.exit(1);
28
+ }
29
+ }
30
+
31
+ function savePreset(name: string): void {
32
+ validatePresetName(name);
33
+ ensurePresetsDir();
34
+
35
+ const configPath = getConfigPath();
36
+ if (!fs.existsSync(configPath)) {
37
+ logger.error('No config file found. Run `raf config wizard` or `raf config set <key> <value>` to create one first.');
38
+ process.exit(1);
39
+ }
40
+
41
+ const presetPath = getPresetPath(name);
42
+ const configContent = fs.readFileSync(configPath, 'utf-8');
43
+
44
+ // Validate the config before saving
45
+ try {
46
+ const parsed = JSON.parse(configContent);
47
+ validateConfig(parsed);
48
+ } catch (err) {
49
+ if (err instanceof ConfigValidationError) {
50
+ logger.error(`Current config is invalid: ${err.message}`);
51
+ process.exit(1);
52
+ }
53
+ throw err;
54
+ }
55
+
56
+ const existed = fs.existsSync(presetPath);
57
+ fs.writeFileSync(presetPath, configContent);
58
+ logger.info(`${existed ? 'Updated' : 'Saved'} preset "${name}" at ${presetPath}`);
59
+ }
60
+
61
+ function loadPreset(name: string): void {
62
+ validatePresetName(name);
63
+ const presetPath = getPresetPath(name);
64
+
65
+ if (!fs.existsSync(presetPath)) {
66
+ logger.error(`Preset "${name}" not found. Run \`raf config preset list\` to see available presets.`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const content = fs.readFileSync(presetPath, 'utf-8');
71
+
72
+ // Validate preset before applying
73
+ let parsed: unknown;
74
+ try {
75
+ parsed = JSON.parse(content);
76
+ } catch {
77
+ logger.error(`Preset "${name}" contains invalid JSON.`);
78
+ process.exit(1);
79
+ }
80
+
81
+ try {
82
+ validateConfig(parsed);
83
+ } catch (err) {
84
+ if (err instanceof ConfigValidationError) {
85
+ logger.error(`Preset "${name}" is invalid: ${err.message}`);
86
+ process.exit(1);
87
+ }
88
+ throw err;
89
+ }
90
+
91
+ const configPath = getConfigPath();
92
+ const configDir = path.dirname(configPath);
93
+ if (!fs.existsSync(configDir)) {
94
+ fs.mkdirSync(configDir, { recursive: true });
95
+ }
96
+ fs.writeFileSync(configPath, content);
97
+ logger.info(`Loaded preset "${name}" into ${configPath}`);
98
+
99
+ // Show a brief summary of what was loaded
100
+ const config = parsed as Record<string, unknown>;
101
+ const keys = Object.keys(config);
102
+ if (keys.length > 0) {
103
+ logger.info(` Keys: ${keys.join(', ')}`);
104
+ }
105
+ }
106
+
107
+ function listPresets(): void {
108
+ ensurePresetsDir();
109
+
110
+ const files = fs.readdirSync(PRESETS_DIR).filter(f => f.endsWith('.json'));
111
+ if (files.length === 0) {
112
+ logger.info('No presets saved. Use `raf config preset save <name>` to create one.');
113
+ return;
114
+ }
115
+
116
+ logger.info(`Found ${files.length} preset(s):\n`);
117
+ for (const file of files) {
118
+ const name = path.basename(file, '.json');
119
+ const filePath = path.join(PRESETS_DIR, file);
120
+ try {
121
+ const content = fs.readFileSync(filePath, 'utf-8');
122
+ const config = JSON.parse(content) as Record<string, unknown>;
123
+
124
+ // Build a brief summary from models
125
+ const summary: string[] = [];
126
+ if (config.models && typeof config.models === 'object') {
127
+ const models = config.models as Record<string, { model?: string; harness?: string }>;
128
+ const harnesses = new Set<string>();
129
+ for (const entry of Object.values(models)) {
130
+ if (entry?.harness) harnesses.add(entry.harness);
131
+ }
132
+ if (harnesses.size > 0) {
133
+ summary.push(`harnesses: ${[...harnesses].join(', ')}`);
134
+ }
135
+ if (models.execute?.model) {
136
+ summary.push(`execute: ${models.execute.model}`);
137
+ }
138
+ }
139
+
140
+ const info = summary.length > 0 ? ` (${summary.join('; ')})` : '';
141
+ logger.info(` ${name}${info}`);
142
+ } catch {
143
+ logger.info(` ${name} (invalid JSON)`);
144
+ }
145
+ }
146
+ }
147
+
148
+ function deletePreset(name: string): void {
149
+ validatePresetName(name);
150
+ const presetPath = getPresetPath(name);
151
+
152
+ if (!fs.existsSync(presetPath)) {
153
+ logger.error(`Preset "${name}" not found.`);
154
+ process.exit(1);
155
+ }
156
+
157
+ fs.unlinkSync(presetPath);
158
+ logger.info(`Deleted preset "${name}".`);
159
+ }
160
+
161
+ export function createPresetCommand(): Command {
162
+ const preset = new Command('preset')
163
+ .description('Manage named config presets under `raf config preset`');
164
+
165
+ preset
166
+ .command('save <name>')
167
+ .description('Save current config as a named preset')
168
+ .action((name: string) => savePreset(name));
169
+
170
+ preset
171
+ .command('load <name>')
172
+ .description('Load a preset into the active config (overwrites current config)')
173
+ .action((name: string) => loadPreset(name));
174
+
175
+ preset
176
+ .command('list')
177
+ .description('List all saved presets')
178
+ .action(() => listPresets());
179
+
180
+ preset
181
+ .command('delete <name>')
182
+ .description('Delete a saved preset')
183
+ .action((name: string) => deletePreset(name));
184
+
185
+ return preset;
186
+ }
@@ -4,6 +4,7 @@ import { execSync, spawn } from 'node:child_process';
4
4
  import { logger } from '../utils/logger.js';
5
5
  import { renderStreamEvent } from '../parsers/stream-renderer.js';
6
6
  import { getModel } from '../utils/config.js';
7
+ import { mergeUsageData } from '../utils/token-tracker.js';
7
8
  import type { ICliRunner } from './runner-interface.js';
8
9
  import type { RunnerOptions, RunnerConfig, RunResult } from './runner-types.js';
9
10
  import { createCompletionDetector } from './completion-detector.js';
@@ -32,9 +33,13 @@ export class ClaudeRunner implements ICliRunner {
32
33
  private activeProcess: pty.IPty | null = null;
33
34
  private killed = false;
34
35
  private model: string;
36
+ private reasoningEffort?: string;
37
+ private fast?: boolean;
35
38
 
36
39
  constructor(config: RunnerConfig = {}) {
37
- this.model = config.model ?? getModel('execute');
40
+ this.model = config.model ?? getModel('execute').model;
41
+ this.reasoningEffort = config.reasoningEffort;
42
+ this.fast = config.fast;
38
43
  }
39
44
 
40
45
  /**
@@ -55,6 +60,16 @@ export class ClaudeRunner implements ICliRunner {
55
60
  return new Promise((resolve) => {
56
61
  const args = ['--model', this.model];
57
62
 
63
+ // Add reasoning effort flag when configured
64
+ if (this.reasoningEffort) {
65
+ args.push('--effort', this.reasoningEffort);
66
+ }
67
+
68
+ // Add fast mode when configured
69
+ if (this.fast) {
70
+ args.push('--settings', '{"fastMode": true}');
71
+ }
72
+
58
73
  // Add --dangerously-skip-permissions if requested (for --auto mode)
59
74
  if (dangerouslySkipPermissions) {
60
75
  args.push('--dangerously-skip-permissions');
@@ -147,6 +162,16 @@ export class ClaudeRunner implements ICliRunner {
147
162
  return new Promise((resolve) => {
148
163
  const args = ['--resume', '--model', this.model];
149
164
 
165
+ // Add reasoning effort flag when configured
166
+ if (this.reasoningEffort) {
167
+ args.push('--effort', this.reasoningEffort);
168
+ }
169
+
170
+ // Add fast mode when configured
171
+ if (this.fast) {
172
+ args.push('--settings', '{"fastMode": true}');
173
+ }
174
+
150
175
  logger.debug(`Starting session resume picker with model: ${this.model}`);
151
176
 
152
177
  this.activeProcess = pty.spawn(getClaudePath(), args, {
@@ -272,10 +297,23 @@ export class ClaudeRunner implements ICliRunner {
272
297
  // including tool calls, file operations, and token usage in the result event.
273
298
  // --dangerously-skip-permissions bypasses interactive prompts
274
299
  // -p enables print mode (non-interactive)
275
- const proc = spawn(claudePath, [
300
+ const execArgs = [
276
301
  '--dangerously-skip-permissions',
277
302
  '--model',
278
303
  this.model,
304
+ ];
305
+
306
+ // Add reasoning effort flag when configured
307
+ if (this.reasoningEffort) {
308
+ execArgs.push('--effort', this.reasoningEffort);
309
+ }
310
+
311
+ // Add fast mode when configured
312
+ if (this.fast) {
313
+ execArgs.push('--settings', '{"fastMode": true}');
314
+ }
315
+
316
+ execArgs.push(
279
317
  '--append-system-prompt',
280
318
  prompt,
281
319
  '--output-format',
@@ -283,7 +321,9 @@ export class ClaudeRunner implements ICliRunner {
283
321
  '--verbose',
284
322
  '-p',
285
323
  'Execute the task as described in the system prompt.',
286
- ], {
324
+ );
325
+
326
+ const proc = spawn(claudePath, execArgs, {
287
327
  cwd,
288
328
  env: process.env,
289
329
  stdio: ['ignore', 'pipe', 'pipe'], // no stdin needed
@@ -346,7 +386,7 @@ export class ClaudeRunner implements ICliRunner {
346
386
 
347
387
  // Capture usage data from result events
348
388
  if (rendered.usageData) {
349
- usageData = rendered.usageData;
389
+ usageData = mergeUsageData(usageData, rendered.usageData);
350
390
  }
351
391
 
352
392
  if (shouldDisplay() && rendered.display) {
@@ -368,7 +408,7 @@ export class ClaudeRunner implements ICliRunner {
368
408
  output += rendered.textContent;
369
409
  }
370
410
  if (rendered.usageData) {
371
- usageData = rendered.usageData;
411
+ usageData = mergeUsageData(usageData, rendered.usageData);
372
412
  }
373
413
  if (shouldDisplay() && rendered.display) {
374
414
  process.stdout.write(rendered.display);