rafcode 3.0.0 → 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 (235) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/CLAUDE.md +0 -1
  3. package/RAF/38-dual-wielder/decisions.md +9 -0
  4. package/RAF/38-dual-wielder/input.md +6 -1
  5. package/RAF/38-dual-wielder/outcomes/8-e2e-test-codex-provider.md +139 -0
  6. package/RAF/38-dual-wielder/plans/8-e2e-test-codex-provider.md +95 -0
  7. package/RAF/39-pathless-rover/decisions.md +16 -0
  8. package/RAF/39-pathless-rover/input.md +2 -0
  9. package/RAF/39-pathless-rover/outcomes/1-fix-codex-stream-renderer.md +21 -0
  10. package/RAF/39-pathless-rover/outcomes/2-wire-provider-flag.md +28 -0
  11. package/RAF/39-pathless-rover/outcomes/3-remove-worktree-flag-do.md +41 -0
  12. package/RAF/39-pathless-rover/outcomes/4-remove-worktree-flag-plan-amend.md +30 -0
  13. package/RAF/39-pathless-rover/outcomes/5-update-prompts-and-docs.md +26 -0
  14. package/RAF/39-pathless-rover/plans/1-fix-codex-stream-renderer.md +43 -0
  15. package/RAF/39-pathless-rover/plans/2-wire-provider-flag.md +48 -0
  16. package/RAF/39-pathless-rover/plans/3-remove-worktree-flag-do.md +41 -0
  17. package/RAF/39-pathless-rover/plans/4-remove-worktree-flag-plan-amend.md +43 -0
  18. package/RAF/39-pathless-rover/plans/5-update-prompts-and-docs.md +31 -0
  19. package/RAF/40-numeric-order-fix/decisions.md +7 -0
  20. package/RAF/40-numeric-order-fix/input.md +19 -0
  21. package/RAF/40-numeric-order-fix/outcomes/1-fix-numeric-sort-order.md +18 -0
  22. package/RAF/40-numeric-order-fix/outcomes/2-add-npm-keywords.md +10 -0
  23. package/RAF/40-numeric-order-fix/plans/1-fix-numeric-sort-order.md +48 -0
  24. package/RAF/40-numeric-order-fix/plans/2-add-npm-keywords.md +23 -0
  25. package/RAF/41-echo-chamber/decisions.md +13 -0
  26. package/RAF/41-echo-chamber/input.md +4 -0
  27. package/RAF/41-echo-chamber/outcomes/1-update-codex-model-defaults.md +24 -0
  28. package/RAF/41-echo-chamber/outcomes/2-e2e-test-codex-provider.md +74 -0
  29. package/RAF/41-echo-chamber/plans/1-update-codex-model-defaults.md +28 -0
  30. package/RAF/41-echo-chamber/plans/2-e2e-test-codex-provider.md +103 -0
  31. package/RAF/42-patch-parade/decisions.md +29 -0
  32. package/RAF/42-patch-parade/input.md +9 -0
  33. package/RAF/42-patch-parade/outcomes/1-fix-codex-model-resolution.md +36 -0
  34. package/RAF/42-patch-parade/outcomes/2-fix-provider-aware-name-generation.md +31 -0
  35. package/RAF/42-patch-parade/outcomes/3-fix-codex-error-event-rendering.md +32 -0
  36. package/RAF/42-patch-parade/outcomes/4-update-cli-help-docs.md +28 -0
  37. package/RAF/42-patch-parade/outcomes/5-update-default-codex-models-to-gpt-5-4.md +33 -0
  38. package/RAF/42-patch-parade/outcomes/6-unify-model-config-schema.md +89 -0
  39. package/RAF/42-patch-parade/plans/1-fix-codex-model-resolution.md +35 -0
  40. package/RAF/42-patch-parade/plans/2-fix-provider-aware-name-generation.md +38 -0
  41. package/RAF/42-patch-parade/plans/3-fix-codex-error-event-rendering.md +32 -0
  42. package/RAF/42-patch-parade/plans/4-update-cli-help-docs.md +31 -0
  43. package/RAF/42-patch-parade/plans/5-update-default-codex-models-to-gpt-5-4.md +35 -0
  44. package/RAF/42-patch-parade/plans/6-unify-model-config-schema.md +46 -0
  45. package/RAF/43-swiss-army/decisions.md +34 -0
  46. package/RAF/43-swiss-army/input.md +7 -0
  47. package/RAF/43-swiss-army/outcomes/1-fix-model-validation.md +21 -0
  48. package/RAF/43-swiss-army/outcomes/2-update-commit-format.md +31 -0
  49. package/RAF/43-swiss-army/outcomes/3-wire-reasoning-effort.md +28 -0
  50. package/RAF/43-swiss-army/outcomes/4-remove-provider-flag.md +27 -0
  51. package/RAF/43-swiss-army/outcomes/5-config-wizard-validation.md +23 -0
  52. package/RAF/43-swiss-army/outcomes/6-add-fast-mode.md +32 -0
  53. package/RAF/43-swiss-army/outcomes/7-config-preset.md +31 -0
  54. package/RAF/43-swiss-army/plans/1-fix-model-validation.md +38 -0
  55. package/RAF/43-swiss-army/plans/2-update-commit-format.md +46 -0
  56. package/RAF/43-swiss-army/plans/3-wire-reasoning-effort.md +39 -0
  57. package/RAF/43-swiss-army/plans/4-remove-provider-flag.md +43 -0
  58. package/RAF/43-swiss-army/plans/5-config-wizard-validation.md +42 -0
  59. package/RAF/43-swiss-army/plans/6-add-fast-mode.md +46 -0
  60. package/RAF/43-swiss-army/plans/7-config-preset.md +51 -0
  61. package/RAF/44-config-api-change/decisions.md +22 -0
  62. package/RAF/44-config-api-change/input.md +5 -0
  63. package/RAF/44-config-api-change/outcomes/1-restructure-config-subcommands.md +19 -0
  64. package/RAF/44-config-api-change/outcomes/2-move-preset-under-config.md +17 -0
  65. package/RAF/44-config-api-change/outcomes/3-update-existing-tests-for-config-api.md +14 -0
  66. package/RAF/44-config-api-change/outcomes/4-update-config-command-docs.md +11 -0
  67. package/RAF/44-config-api-change/outcomes/5-fix-codex-name-generation.md +18 -0
  68. package/RAF/44-config-api-change/plans/1-restructure-config-subcommands.md +37 -0
  69. package/RAF/44-config-api-change/plans/2-move-preset-under-config.md +38 -0
  70. package/RAF/44-config-api-change/plans/3-update-existing-tests-for-config-api.md +38 -0
  71. package/RAF/44-config-api-change/plans/4-update-config-command-docs.md +36 -0
  72. package/RAF/44-config-api-change/plans/5-fix-codex-name-generation.md +49 -0
  73. package/RAF/45-signal-cairn/decisions.md +7 -0
  74. package/RAF/45-signal-cairn/input.md +2 -0
  75. package/RAF/45-signal-cairn/outcomes/1-rename-provider-to-harness.md +19 -0
  76. package/RAF/45-signal-cairn/outcomes/2-normalize-model-display-names.md +18 -0
  77. package/RAF/45-signal-cairn/plans/1-rename-provider-to-harness.md +40 -0
  78. package/RAF/45-signal-cairn/plans/2-normalize-model-display-names.md +41 -0
  79. package/RAF/45-signal-lantern/decisions.md +10 -0
  80. package/RAF/45-signal-lantern/input.md +2 -0
  81. package/RAF/45-signal-lantern/outcomes/1-add-effort-and-fast-to-do-model-display.md +15 -0
  82. package/RAF/45-signal-lantern/outcomes/2-capture-codex-post-run-token-usage.md +15 -0
  83. package/RAF/45-signal-lantern/outcomes/3-show-codex-token-summaries-without-fake-cost.md +14 -0
  84. package/RAF/45-signal-lantern/plans/1-add-effort-and-fast-to-do-model-display.md +38 -0
  85. package/RAF/45-signal-lantern/plans/2-capture-codex-post-run-token-usage.md +37 -0
  86. package/RAF/45-signal-lantern/plans/3-show-codex-token-summaries-without-fake-cost.md +40 -0
  87. package/RAF/46-lantern-arc/decisions.md +19 -0
  88. package/RAF/46-lantern-arc/input.md +6 -0
  89. package/RAF/46-lantern-arc/outcomes/1-remove-spark-alias.md +16 -0
  90. package/RAF/46-lantern-arc/outcomes/2-clean-up-worktree-plan-command.md +30 -0
  91. package/RAF/46-lantern-arc/outcomes/3-fix-token-usage-accumulation.md +32 -0
  92. package/RAF/46-lantern-arc/outcomes/4-display-effort-in-compact-mode.md +22 -0
  93. package/RAF/46-lantern-arc/outcomes/5-codex-fast-mode-research.md +38 -0
  94. package/RAF/46-lantern-arc/outcomes/6-optimize-llm-prompts.md +39 -0
  95. package/RAF/46-lantern-arc/plans/1-remove-spark-alias.md +38 -0
  96. package/RAF/46-lantern-arc/plans/2-clean-up-worktree-plan-command.md +33 -0
  97. package/RAF/46-lantern-arc/plans/3-fix-token-usage-accumulation.md +33 -0
  98. package/RAF/46-lantern-arc/plans/4-display-effort-in-compact-mode.md +28 -0
  99. package/RAF/46-lantern-arc/plans/5-codex-fast-mode-research.md +34 -0
  100. package/RAF/46-lantern-arc/plans/6-optimize-llm-prompts.md +48 -0
  101. package/RAF/47-signal-trim/decisions.md +13 -0
  102. package/RAF/47-signal-trim/input.md +2 -0
  103. package/RAF/47-signal-trim/plans/1-remove-cache-from-status.md +73 -0
  104. package/README.md +50 -63
  105. package/dist/commands/config.d.ts.map +1 -1
  106. package/dist/commands/config.js +47 -49
  107. package/dist/commands/config.js.map +1 -1
  108. package/dist/commands/do.d.ts +2 -0
  109. package/dist/commands/do.d.ts.map +1 -1
  110. package/dist/commands/do.js +91 -230
  111. package/dist/commands/do.js.map +1 -1
  112. package/dist/commands/plan.d.ts.map +1 -1
  113. package/dist/commands/plan.js +54 -259
  114. package/dist/commands/plan.js.map +1 -1
  115. package/dist/commands/preset.d.ts +3 -0
  116. package/dist/commands/preset.d.ts.map +1 -0
  117. package/dist/commands/preset.js +158 -0
  118. package/dist/commands/preset.js.map +1 -0
  119. package/dist/core/claude-runner.d.ts +2 -0
  120. package/dist/core/claude-runner.d.ts.map +1 -1
  121. package/dist/core/claude-runner.js +36 -12
  122. package/dist/core/claude-runner.js.map +1 -1
  123. package/dist/core/codex-runner.d.ts +1 -0
  124. package/dist/core/codex-runner.d.ts.map +1 -1
  125. package/dist/core/codex-runner.js +26 -7
  126. package/dist/core/codex-runner.js.map +1 -1
  127. package/dist/core/failure-analyzer.js +2 -1
  128. package/dist/core/failure-analyzer.js.map +1 -1
  129. package/dist/core/git.d.ts +2 -2
  130. package/dist/core/git.d.ts.map +1 -1
  131. package/dist/core/git.js +53 -3
  132. package/dist/core/git.js.map +1 -1
  133. package/dist/core/project-manager.d.ts.map +1 -1
  134. package/dist/core/project-manager.js +2 -2
  135. package/dist/core/project-manager.js.map +1 -1
  136. package/dist/core/pull-request.js +5 -5
  137. package/dist/core/pull-request.js.map +1 -1
  138. package/dist/core/runner-factory.d.ts +4 -4
  139. package/dist/core/runner-factory.d.ts.map +1 -1
  140. package/dist/core/runner-factory.js +8 -8
  141. package/dist/core/runner-factory.js.map +1 -1
  142. package/dist/core/runner-interface.d.ts +1 -1
  143. package/dist/core/runner-types.d.ts +17 -4
  144. package/dist/core/runner-types.d.ts.map +1 -1
  145. package/dist/core/state-derivation.js +3 -3
  146. package/dist/core/state-derivation.js.map +1 -1
  147. package/dist/parsers/codex-stream-renderer.d.ts +28 -4
  148. package/dist/parsers/codex-stream-renderer.d.ts.map +1 -1
  149. package/dist/parsers/codex-stream-renderer.js +110 -0
  150. package/dist/parsers/codex-stream-renderer.js.map +1 -1
  151. package/dist/prompts/amend.d.ts +0 -1
  152. package/dist/prompts/amend.d.ts.map +1 -1
  153. package/dist/prompts/amend.js +31 -104
  154. package/dist/prompts/amend.js.map +1 -1
  155. package/dist/prompts/execution.d.ts.map +1 -1
  156. package/dist/prompts/execution.js +17 -34
  157. package/dist/prompts/execution.js.map +1 -1
  158. package/dist/prompts/planning.d.ts.map +1 -1
  159. package/dist/prompts/planning.js +23 -123
  160. package/dist/prompts/planning.js.map +1 -1
  161. package/dist/types/config.d.ts +33 -32
  162. package/dist/types/config.d.ts.map +1 -1
  163. package/dist/types/config.js +14 -28
  164. package/dist/types/config.js.map +1 -1
  165. package/dist/utils/config.d.ts +36 -16
  166. package/dist/utils/config.d.ts.map +1 -1
  167. package/dist/utils/config.js +209 -104
  168. package/dist/utils/config.js.map +1 -1
  169. package/dist/utils/name-generator.d.ts.map +1 -1
  170. package/dist/utils/name-generator.js +25 -12
  171. package/dist/utils/name-generator.js.map +1 -1
  172. package/dist/utils/paths.d.ts +5 -0
  173. package/dist/utils/paths.d.ts.map +1 -1
  174. package/dist/utils/paths.js +9 -0
  175. package/dist/utils/paths.js.map +1 -1
  176. package/dist/utils/terminal-symbols.d.ts +15 -2
  177. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  178. package/dist/utils/terminal-symbols.js +36 -4
  179. package/dist/utils/terminal-symbols.js.map +1 -1
  180. package/dist/utils/token-tracker.d.ts +6 -1
  181. package/dist/utils/token-tracker.d.ts.map +1 -1
  182. package/dist/utils/token-tracker.js +84 -51
  183. package/dist/utils/token-tracker.js.map +1 -1
  184. package/dist/utils/validation.d.ts +1 -2
  185. package/dist/utils/validation.d.ts.map +1 -1
  186. package/dist/utils/validation.js +4 -25
  187. package/dist/utils/validation.js.map +1 -1
  188. package/package.json +7 -2
  189. package/src/commands/config.ts +60 -63
  190. package/src/commands/do.ts +96 -262
  191. package/src/commands/plan.ts +55 -279
  192. package/src/commands/preset.ts +186 -0
  193. package/src/core/claude-runner.ts +45 -5
  194. package/src/core/codex-runner.ts +32 -7
  195. package/src/core/failure-analyzer.ts +2 -1
  196. package/src/core/git.ts +57 -3
  197. package/src/core/project-manager.ts +2 -1
  198. package/src/core/pull-request.ts +5 -5
  199. package/src/core/runner-factory.ts +9 -9
  200. package/src/core/runner-interface.ts +1 -1
  201. package/src/core/runner-types.ts +17 -4
  202. package/src/core/state-derivation.ts +3 -3
  203. package/src/parsers/codex-stream-renderer.ts +149 -4
  204. package/src/prompts/amend.ts +30 -105
  205. package/src/prompts/config-docs.md +206 -62
  206. package/src/prompts/execution.ts +17 -34
  207. package/src/prompts/planning.ts +23 -124
  208. package/src/types/config.ts +47 -59
  209. package/src/utils/config.ts +248 -115
  210. package/src/utils/name-generator.ts +29 -13
  211. package/src/utils/paths.ts +10 -0
  212. package/src/utils/terminal-symbols.ts +46 -6
  213. package/src/utils/token-tracker.ts +96 -57
  214. package/src/utils/validation.ts +5 -30
  215. package/tests/unit/amend-prompt.test.ts +3 -2
  216. package/tests/unit/claude-runner-interactive.test.ts +21 -3
  217. package/tests/unit/claude-runner.test.ts +39 -0
  218. package/tests/unit/codex-runner.test.ts +163 -0
  219. package/tests/unit/codex-stream-renderer.test.ts +127 -0
  220. package/tests/unit/command-output.test.ts +57 -0
  221. package/tests/unit/commit-planning-artifacts-worktree.test.ts +24 -7
  222. package/tests/unit/commit-planning-artifacts.test.ts +26 -4
  223. package/tests/unit/config-command.test.ts +215 -303
  224. package/tests/unit/config.test.ts +319 -235
  225. package/tests/unit/dependency-integration.test.ts +27 -1
  226. package/tests/unit/do-model-display.test.ts +35 -0
  227. package/tests/unit/execution-prompt.test.ts +49 -19
  228. package/tests/unit/name-generator.test.ts +82 -12
  229. package/tests/unit/plan-command-auto-flag.test.ts +7 -10
  230. package/tests/unit/plan-command.test.ts +14 -17
  231. package/tests/unit/planning-prompt.test.ts +9 -8
  232. package/tests/unit/terminal-symbols.test.ts +94 -3
  233. package/tests/unit/token-tracker.test.ts +180 -1
  234. package/tests/unit/validation.test.ts +9 -41
  235. package/tests/unit/worktree-flag-override.test.ts +0 -186
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  SYMBOLS,
3
+ formatModelMetadata,
3
4
  formatTaskProgress,
4
5
  formatProjectHeader,
5
6
  formatSummary,
@@ -15,6 +16,28 @@ import type { UsageData } from '../../src/types/config.js';
15
16
  import type { CostBreakdown, TaskUsageEntry } from '../../src/utils/token-tracker.js';
16
17
 
17
18
  describe('Terminal Symbols', () => {
19
+ describe('formatModelMetadata', () => {
20
+ it('should format model only when no metadata is present', () => {
21
+ expect(formatModelMetadata('sonnet')).toBe('sonnet');
22
+ });
23
+
24
+ it('should append effort when present', () => {
25
+ expect(formatModelMetadata('sonnet', { effort: 'low' })).toBe('sonnet, low');
26
+ });
27
+
28
+ it('should append fast when enabled', () => {
29
+ expect(formatModelMetadata('sonnet', { fast: true })).toBe('sonnet, fast');
30
+ });
31
+
32
+ it('should append effort and fast in order', () => {
33
+ expect(formatModelMetadata('sonnet', { effort: 'low', fast: true })).toBe('sonnet, low, fast');
34
+ });
35
+
36
+ it('should omit falsy fast values', () => {
37
+ expect(formatModelMetadata('sonnet', { effort: 'low', fast: false })).toBe('sonnet, low');
38
+ });
39
+ });
40
+
18
41
  describe('SYMBOLS', () => {
19
42
  it('should have all required symbols', () => {
20
43
  expect(SYMBOLS.running).toBe('●');
@@ -115,6 +138,11 @@ describe('Terminal Symbols', () => {
115
138
  expect(result).toBe('● auth-login (sonnet) 1m 23s');
116
139
  });
117
140
 
141
+ it('should show model metadata in parentheses for running task with time', () => {
142
+ const result = formatTaskProgress(1, 5, 'running', 'auth-login', 83000, undefined, 'sonnet', { effort: 'low', fast: true });
143
+ expect(result).toBe('● auth-login (sonnet, low, fast) 1m 23s');
144
+ });
145
+
118
146
  it('should show model name in parentheses for running task without time', () => {
119
147
  const result = formatTaskProgress(1, 5, 'running', 'auth-login', undefined, undefined, 'opus');
120
148
  expect(result).toBe('● auth-login (opus) 1/5');
@@ -125,6 +153,11 @@ describe('Terminal Symbols', () => {
125
153
  expect(result).toBe('✓ setup-db (haiku) 2m 34s');
126
154
  });
127
155
 
156
+ it('should omit effort when unavailable and omit falsy fast', () => {
157
+ const result = formatTaskProgress(3, 5, 'completed', 'setup-db', 154000, undefined, 'haiku', { fast: false });
158
+ expect(result).toBe('✓ setup-db (haiku) 2m 34s');
159
+ });
160
+
128
161
  it('should show model name in parentheses for completed task without time', () => {
129
162
  const result = formatTaskProgress(3, 5, 'completed', 'setup-db', undefined, undefined, 'sonnet');
130
163
  expect(result).toBe('✓ setup-db (sonnet) 3/5');
@@ -145,6 +178,11 @@ describe('Terminal Symbols', () => {
145
178
  expect(result).toBe('● 001-auth-login (sonnet) 1m 23s');
146
179
  });
147
180
 
181
+ it('should show normalized canonical model names in task progress', () => {
182
+ const result = formatTaskProgress(1, 5, 'running', 'auth-login', 83000, '001', 'gpt-5.4');
183
+ expect(result).toBe('● 001-auth-login (gpt-5.4) 1m 23s');
184
+ });
185
+
148
186
  it('should show model name for blocked task without time', () => {
149
187
  const result = formatTaskProgress(2, 5, 'blocked', 'depends-on-failed', undefined, undefined, 'sonnet');
150
188
  expect(result).toBe('⊘ depends-on-failed (sonnet) 2/5');
@@ -164,6 +202,14 @@ describe('Terminal Symbols', () => {
164
202
  const result = formatTaskProgress(2, 5, 'completed', 'setup-db', 154000, '002', 'opus');
165
203
  expect(result).toBe('✓ 002-setup-db (opus) 2m 34s');
166
204
  });
205
+
206
+ it('should keep truncation stable with model metadata', () => {
207
+ const longName = 'this-is-a-very-long-task-name-that-should-be-truncated-for-display';
208
+ const result = formatTaskProgress(1, 1, 'running', longName, 1000, '001', 'sonnet', { effort: 'low', fast: true });
209
+
210
+ expect(result).toContain('001-this-is-a-very-long-task-name-that-shou…');
211
+ expect(result).toContain('(sonnet, low, fast) 1s');
212
+ });
167
213
  });
168
214
 
169
215
  describe('formatProjectHeader', () => {
@@ -317,6 +363,10 @@ describe('Terminal Symbols', () => {
317
363
  expect(formatCost(0)).toBe('$0.00');
318
364
  });
319
365
 
366
+ it('should format unavailable cost', () => {
367
+ expect(formatCost(null)).toBe('unavailable');
368
+ });
369
+
320
370
  it('should format normal costs with 2 decimals', () => {
321
371
  expect(formatCost(1.23)).toBe('$1.23');
322
372
  });
@@ -341,10 +391,11 @@ describe('Terminal Symbols', () => {
341
391
  cacheReadInputTokens: 0,
342
392
  cacheCreationInputTokens: 0,
343
393
  modelUsage: {},
394
+ totalCostUsd: 0,
344
395
  ...overrides,
345
396
  });
346
397
 
347
- const makeCost = (total: number): CostBreakdown => ({
398
+ const makeCost = (total: number | null): CostBreakdown => ({
348
399
  totalCost: total,
349
400
  });
350
401
 
@@ -391,6 +442,12 @@ describe('Terminal Symbols', () => {
391
442
  const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42), []));
392
443
  expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cost: $0.42');
393
444
  });
445
+
446
+ it('should omit cost when exact cost is unknown', () => {
447
+ const usage = makeUsage({ totalCostUsd: null });
448
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(null)));
449
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out');
450
+ });
394
451
  });
395
452
 
396
453
  describe('multi-attempt tasks', () => {
@@ -460,6 +517,34 @@ describe('Terminal Symbols', () => {
460
517
  expect(lines[2]).toContain('Attempt 3');
461
518
  expect(lines[3]).toContain('Total');
462
519
  });
520
+
521
+ it('should omit cost for attempts and totals with unknown exact cost', () => {
522
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, totalCostUsd: null });
523
+ const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400, totalCostUsd: null });
524
+ const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600, totalCostUsd: null });
525
+ const entry = makeEntry(totalUsage, makeCost(null), [attempt1, attempt2]);
526
+
527
+ const result = formatTaskTokenSummary(entry);
528
+ const lines = result.split('\n');
529
+
530
+ expect(lines[0]).toBe(' Attempt 1: 1,000 in / 200 out');
531
+ expect(lines[1]).toBe(' Attempt 2: 2,000 in / 400 out');
532
+ expect(lines[2]).toBe(' Total: 3,000 in / 600 out');
533
+ });
534
+
535
+ it('should preserve exact zero cost instead of omitting it', () => {
536
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, totalCostUsd: 0 });
537
+ const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400, totalCostUsd: null });
538
+ const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600, totalCostUsd: 0 });
539
+ const entry = makeEntry(totalUsage, makeCost(0), [attempt1, attempt2]);
540
+
541
+ const result = formatTaskTokenSummary(entry);
542
+ const lines = result.split('\n');
543
+
544
+ expect(lines[0]).toBe(' Attempt 1: 1,000 in / 200 out | Cost: $0.00');
545
+ expect(lines[1]).toBe(' Attempt 2: 2,000 in / 400 out');
546
+ expect(lines[2]).toBe(' Total: 3,000 in / 600 out | Cost: $0.00');
547
+ });
463
548
  });
464
549
  });
465
550
 
@@ -474,7 +559,7 @@ describe('Terminal Symbols', () => {
474
559
  ...overrides,
475
560
  });
476
561
 
477
- const makeCost = (total: number): CostBreakdown => ({
562
+ const makeCost = (total: number | null): CostBreakdown => ({
478
563
  totalCost: total,
479
564
  });
480
565
 
@@ -528,6 +613,11 @@ describe('Terminal Symbols', () => {
528
613
  );
529
614
  expect(result).not.toContain('Cache:');
530
615
  });
616
+
617
+ it('should omit total cost when exact cost is unknown', () => {
618
+ const result = formatTokenTotalSummary(makeUsage(), makeCost(null));
619
+ expect(result).not.toContain('Total cost:');
620
+ });
531
621
  });
532
622
 
533
623
  describe('formatTaskTokenSummary with options', () => {
@@ -537,10 +627,11 @@ describe('Terminal Symbols', () => {
537
627
  cacheReadInputTokens: 0,
538
628
  cacheCreationInputTokens: 0,
539
629
  modelUsage: {},
630
+ totalCostUsd: 0,
540
631
  ...overrides,
541
632
  });
542
633
 
543
- const makeCost = (total: number): CostBreakdown => ({
634
+ const makeCost = (total: number | null): CostBreakdown => ({
544
635
  totalCost: total,
545
636
  });
546
637
 
@@ -1,4 +1,4 @@
1
- import { TokenTracker, CostBreakdown, accumulateUsage, sumCostBreakdowns } from '../../src/utils/token-tracker.js';
1
+ import { TokenTracker, CostBreakdown, accumulateUsage, mergeUsageData, sumCostBreakdowns } from '../../src/utils/token-tracker.js';
2
2
  import { UsageData } from '../../src/types/config.js';
3
3
 
4
4
  function makeUsage(overrides: Partial<UsageData> = {}): UsageData {
@@ -85,6 +85,26 @@ describe('TokenTracker', () => {
85
85
  expect(entry.cost.totalCost).toBe(25.0);
86
86
  });
87
87
 
88
+ it('should mark total cost unavailable when any attempt has unknown exact cost', () => {
89
+ const tracker = new TokenTracker();
90
+ const attempt1 = makeUsage({
91
+ inputTokens: 100,
92
+ outputTokens: 50,
93
+ totalCostUsd: null,
94
+ });
95
+ const attempt2 = makeUsage({
96
+ inputTokens: 200,
97
+ outputTokens: 75,
98
+ totalCostUsd: 0.5,
99
+ });
100
+
101
+ const entry = tracker.addTask('01', [attempt1, attempt2]);
102
+ expect(entry.usage.inputTokens).toBe(300);
103
+ expect(entry.usage.outputTokens).toBe(125);
104
+ expect(entry.cost.totalCost).toBeNull();
105
+ expect(entry.usage.totalCostUsd).toBeNull();
106
+ });
107
+
88
108
  it('should store attempts array in entry', () => {
89
109
  const tracker = new TokenTracker();
90
110
  const usage = makeUsage({ inputTokens: 100, totalCostUsd: 0.01 });
@@ -129,6 +149,27 @@ describe('TokenTracker', () => {
129
149
  expect(totals.usage.outputTokens).toBe(750_000);
130
150
  });
131
151
 
152
+ it('should keep grand total cost unavailable when any task cost is unknown', () => {
153
+ const tracker = new TokenTracker();
154
+
155
+ tracker.addTask('01', [makeUsage({
156
+ inputTokens: 100,
157
+ outputTokens: 10,
158
+ totalCostUsd: null,
159
+ })]);
160
+
161
+ tracker.addTask('02', [makeUsage({
162
+ inputTokens: 200,
163
+ outputTokens: 20,
164
+ totalCostUsd: 1.25,
165
+ })]);
166
+
167
+ const totals = tracker.getTotals();
168
+ expect(totals.usage.inputTokens).toBe(300);
169
+ expect(totals.cost.totalCost).toBeNull();
170
+ expect(totals.usage.totalCostUsd).toBeNull();
171
+ });
172
+
132
173
  it('should return empty totals when no tasks added', () => {
133
174
  const tracker = new TokenTracker();
134
175
  const totals = tracker.getTotals();
@@ -330,6 +371,21 @@ describe('TokenTracker', () => {
330
371
  expect(result.totalCostUsd).toBe(1.5);
331
372
  });
332
373
 
374
+ it('should keep accumulated cost unavailable when any attempt has unknown cost', () => {
375
+ const attempt1 = makeUsage({
376
+ inputTokens: 100,
377
+ totalCostUsd: null,
378
+ });
379
+ const attempt2 = makeUsage({
380
+ inputTokens: 200,
381
+ totalCostUsd: 1.0,
382
+ });
383
+
384
+ const result = accumulateUsage([attempt1, attempt2]);
385
+ expect(result.inputTokens).toBe(300);
386
+ expect(result.totalCostUsd).toBeNull();
387
+ });
388
+
333
389
  it('should merge modelUsage for same model across attempts', () => {
334
390
  const attempt1 = makeUsage({
335
391
  modelUsage: {
@@ -440,6 +496,121 @@ describe('TokenTracker', () => {
440
496
  });
441
497
  });
442
498
 
499
+ describe('mergeUsageData', () => {
500
+ it('should initialize usage when existing is undefined', () => {
501
+ const incoming = makeUsage({
502
+ inputTokens: 10,
503
+ outputTokens: 5,
504
+ totalCostUsd: 0.25,
505
+ modelUsage: {
506
+ 'claude-opus-4-6': {
507
+ inputTokens: 10,
508
+ outputTokens: 5,
509
+ cacheReadInputTokens: 0,
510
+ cacheCreationInputTokens: 0,
511
+ costUsd: 0.25,
512
+ },
513
+ },
514
+ });
515
+
516
+ const result = mergeUsageData(undefined, incoming);
517
+ expect(result).toEqual(incoming);
518
+ });
519
+
520
+ it('should return existing usage when incoming is undefined', () => {
521
+ const existing = makeUsage({
522
+ inputTokens: 10,
523
+ outputTokens: 5,
524
+ });
525
+
526
+ const result = mergeUsageData(existing, undefined);
527
+ expect(result).toEqual(existing);
528
+ });
529
+
530
+ it('should sum usage fields and model usage across both inputs', () => {
531
+ const existing = makeUsage({
532
+ inputTokens: 100,
533
+ outputTokens: 40,
534
+ cacheReadInputTokens: 10,
535
+ cacheCreationInputTokens: 5,
536
+ totalCostUsd: 1.0,
537
+ modelUsage: {
538
+ 'claude-opus-4-6': {
539
+ inputTokens: 100,
540
+ outputTokens: 40,
541
+ cacheReadInputTokens: 10,
542
+ cacheCreationInputTokens: 5,
543
+ costUsd: 1.0,
544
+ },
545
+ },
546
+ });
547
+ const incoming = makeUsage({
548
+ inputTokens: 50,
549
+ outputTokens: 20,
550
+ cacheReadInputTokens: 4,
551
+ cacheCreationInputTokens: 2,
552
+ totalCostUsd: 0.5,
553
+ modelUsage: {
554
+ 'claude-opus-4-6': {
555
+ inputTokens: 50,
556
+ outputTokens: 20,
557
+ cacheReadInputTokens: 4,
558
+ cacheCreationInputTokens: 2,
559
+ costUsd: 0.5,
560
+ },
561
+ },
562
+ });
563
+
564
+ const result = mergeUsageData(existing, incoming);
565
+ expect(result).toEqual({
566
+ inputTokens: 150,
567
+ outputTokens: 60,
568
+ cacheReadInputTokens: 14,
569
+ cacheCreationInputTokens: 7,
570
+ totalCostUsd: 1.5,
571
+ modelUsage: {
572
+ 'claude-opus-4-6': {
573
+ inputTokens: 150,
574
+ outputTokens: 60,
575
+ cacheReadInputTokens: 14,
576
+ cacheCreationInputTokens: 7,
577
+ costUsd: 1.5,
578
+ },
579
+ },
580
+ });
581
+ });
582
+
583
+ it('should treat missing numeric fields as zero while merging', () => {
584
+ const existing = makeUsage({
585
+ inputTokens: 20,
586
+ outputTokens: 10,
587
+ totalCostUsd: null,
588
+ });
589
+ const incoming = {
590
+ inputTokens: 5,
591
+ modelUsage: {
592
+ codex: {
593
+ inputTokens: 5,
594
+ },
595
+ },
596
+ } as unknown as UsageData;
597
+
598
+ const result = mergeUsageData(existing, incoming);
599
+ expect(result!.inputTokens).toBe(25);
600
+ expect(result!.outputTokens).toBe(10);
601
+ expect(result!.cacheReadInputTokens).toBe(0);
602
+ expect(result!.cacheCreationInputTokens).toBe(0);
603
+ expect(result!.totalCostUsd).toBeNull();
604
+ expect(result!.modelUsage.codex).toEqual({
605
+ inputTokens: 5,
606
+ outputTokens: 0,
607
+ cacheReadInputTokens: 0,
608
+ cacheCreationInputTokens: 0,
609
+ costUsd: null,
610
+ });
611
+ });
612
+ });
613
+
443
614
  describe('sumCostBreakdowns', () => {
444
615
  it('should return zero breakdown for empty array', () => {
445
616
  const result = sumCostBreakdowns([]);
@@ -464,5 +635,13 @@ describe('TokenTracker', () => {
464
635
  const result = sumCostBreakdowns([cost1, cost2]);
465
636
  expect(result.totalCost).toBe(49.5);
466
637
  });
638
+
639
+ it('should keep total cost unavailable when any breakdown is unknown', () => {
640
+ const result = sumCostBreakdowns([
641
+ { totalCost: 33 },
642
+ { totalCost: null },
643
+ ]);
644
+ expect(result.totalCost).toBeNull();
645
+ });
467
646
  });
468
647
  });
@@ -2,7 +2,6 @@ import {
2
2
  validateProjectName,
3
3
  sanitizeProjectName,
4
4
  validateModelName,
5
- resolveModelOption,
6
5
  } from '../../src/utils/validation.js';
7
6
 
8
7
  describe('Validation', () => {
@@ -67,6 +66,11 @@ describe('Validation', () => {
67
66
  expect(validateModelName('OPUS')).toBe('opus');
68
67
  });
69
68
 
69
+ it('should accept Codex model names', () => {
70
+ expect(validateModelName('gpt-5.4')).toBe('gpt-5.4');
71
+ expect(validateModelName('gpt54')).toBe('gpt54');
72
+ });
73
+
70
74
  it('should reject invalid model names', () => {
71
75
  expect(validateModelName('gpt4')).toBeNull();
72
76
  expect(validateModelName('claude')).toBeNull();
@@ -75,46 +79,10 @@ describe('Validation', () => {
75
79
  });
76
80
  });
77
81
 
78
- describe('resolveModelOption', () => {
79
- it('should return a valid model as default', () => {
80
- // Default comes from config, could be short alias or full model ID
81
- const result = resolveModelOption();
82
- expect(result).toMatch(/^(opus|sonnet|haiku|claude-(opus|sonnet|haiku)-.+)$/);
83
- expect(resolveModelOption(undefined, undefined)).toBe(result);
84
- expect(resolveModelOption(undefined, false)).toBe(result);
85
- });
86
-
87
- it('should use --model flag when provided', () => {
88
- expect(resolveModelOption('sonnet')).toBe('sonnet');
89
- expect(resolveModelOption('haiku')).toBe('haiku');
90
- expect(resolveModelOption('opus')).toBe('opus');
91
- });
92
-
93
- it('should normalize model name to lowercase', () => {
94
- expect(resolveModelOption('SONNET')).toBe('sonnet');
95
- expect(resolveModelOption('Haiku')).toBe('haiku');
96
- });
97
-
98
- it('should use --sonnet shorthand', () => {
99
- expect(resolveModelOption(undefined, true)).toBe('sonnet');
100
- });
101
-
102
- it('should throw error for conflicting flags', () => {
103
- expect(() => resolveModelOption('haiku', true)).toThrow(
104
- 'Cannot specify both --model and --sonnet flags'
105
- );
106
- expect(() => resolveModelOption('opus', true)).toThrow(
107
- 'Cannot specify both --model and --sonnet flags'
108
- );
109
- });
110
-
111
- it('should throw error for invalid model name', () => {
112
- expect(() => resolveModelOption('gpt4')).toThrow(
113
- 'Invalid model name: "gpt4". Valid options: sonnet, haiku, opus'
114
- );
115
- expect(() => resolveModelOption('invalid')).toThrow(
116
- 'Invalid model name: "invalid". Valid options: sonnet, haiku, opus'
117
- );
82
+ describe('resolveModelOption removed', () => {
83
+ it('should not be exported from validation module', async () => {
84
+ const validationModule = await import('../../src/utils/validation.js');
85
+ expect('resolveModelOption' in validationModule).toBe(false);
118
86
  });
119
87
  });
120
88
  });
@@ -1,186 +0,0 @@
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 { createPlanCommand } from '../../src/commands/plan.js';
6
- import { createDoCommand } from '../../src/commands/do.js';
7
- import { getWorktreeDefault, resetConfigCache, saveConfig } from '../../src/utils/config.js';
8
-
9
- describe('Worktree Flag Override', () => {
10
- let tempDir: string;
11
- let originalHome: string | undefined;
12
-
13
- beforeEach(() => {
14
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-worktree-flag-test-'));
15
- originalHome = process.env.HOME;
16
- process.env.HOME = tempDir;
17
- resetConfigCache();
18
- });
19
-
20
- afterEach(() => {
21
- process.env.HOME = originalHome;
22
- fs.rmSync(tempDir, { recursive: true, force: true });
23
- resetConfigCache();
24
- });
25
-
26
- describe('Commander.js --no-worktree flag parsing', () => {
27
- it('should parse --worktree as true', () => {
28
- const planCommand = createPlanCommand();
29
- // Use parseOptions instead of parse to avoid running the action
30
- planCommand.parseOptions(['--worktree']);
31
- const opts = planCommand.opts();
32
- expect(opts.worktree).toBe(true);
33
- });
34
-
35
- it('should parse --no-worktree as false', () => {
36
- const planCommand = createPlanCommand();
37
- planCommand.parseOptions(['--no-worktree']);
38
- const opts = planCommand.opts();
39
- expect(opts.worktree).toBe(false);
40
- });
41
-
42
- it('should parse omitted flag as undefined', () => {
43
- const planCommand = createPlanCommand();
44
- planCommand.parseOptions([]);
45
- const opts = planCommand.opts();
46
- expect(opts.worktree).toBeUndefined();
47
- });
48
-
49
- it('should parse --worktree for do command as true', () => {
50
- const doCommand = createDoCommand();
51
- doCommand.parseOptions(['--worktree']);
52
- const opts = doCommand.opts();
53
- expect(opts.worktree).toBe(true);
54
- });
55
-
56
- it('should parse --no-worktree for do command as false', () => {
57
- const doCommand = createDoCommand();
58
- doCommand.parseOptions(['--no-worktree']);
59
- const opts = doCommand.opts();
60
- expect(opts.worktree).toBe(false);
61
- });
62
-
63
- it('should parse omitted flag for do command as undefined', () => {
64
- const doCommand = createDoCommand();
65
- doCommand.parseOptions([]);
66
- const opts = doCommand.opts();
67
- expect(opts.worktree).toBeUndefined();
68
- });
69
- });
70
-
71
- describe('Config resolution with --no-worktree flag', () => {
72
- it('should resolve to true when --worktree flag is passed (regardless of config)', () => {
73
- // Simulate: options.worktree = true (from --worktree flag)
74
- const options = { worktree: true };
75
- // With nullish coalescing, explicit true takes precedence
76
- const resolved = options.worktree ?? getWorktreeDefault();
77
- expect(resolved).toBe(true);
78
- });
79
-
80
- it('should resolve to false when --no-worktree flag is passed (regardless of config)', () => {
81
- // Simulate: options.worktree = false (from --no-worktree flag)
82
- const options = { worktree: false };
83
- // With nullish coalescing, explicit false takes precedence
84
- const resolved = options.worktree ?? getWorktreeDefault();
85
- expect(resolved).toBe(false);
86
- });
87
-
88
- it('should resolve to config default when no flag is passed', () => {
89
- // Simulate: options.worktree = undefined (no flag passed)
90
- const options = { worktree: undefined };
91
- // With nullish coalescing, undefined falls back to getWorktreeDefault()
92
- const resolved = options.worktree ?? getWorktreeDefault();
93
- // We can't assert a specific value here since it depends on the user's actual config
94
- expect(typeof resolved).toBe('boolean');
95
- });
96
- });
97
-
98
- describe('Tri-state behavior verification', () => {
99
- it('should correctly handle all three states (true/false/undefined) in plan command', () => {
100
- // State 1: --worktree (explicit true)
101
- const planCmd1 = createPlanCommand();
102
- planCmd1.parseOptions(['--worktree']);
103
- const opts1 = planCmd1.opts();
104
- expect(opts1.worktree).toBe(true);
105
- const resolved1 = opts1.worktree ?? getWorktreeDefault();
106
- expect(resolved1).toBe(true);
107
-
108
- // State 2: --no-worktree (explicit false)
109
- const planCmd2 = createPlanCommand();
110
- planCmd2.parseOptions(['--no-worktree']);
111
- const opts2 = planCmd2.opts();
112
- expect(opts2.worktree).toBe(false);
113
- const resolved2 = opts2.worktree ?? getWorktreeDefault();
114
- expect(resolved2).toBe(false);
115
-
116
- // State 3: omitted (undefined, falls back to config)
117
- const planCmd3 = createPlanCommand();
118
- planCmd3.parseOptions([]);
119
- const opts3 = planCmd3.opts();
120
- expect(opts3.worktree).toBeUndefined();
121
- const resolved3 = opts3.worktree ?? getWorktreeDefault();
122
- // Should be a boolean (actual value depends on config)
123
- expect(typeof resolved3).toBe('boolean');
124
- });
125
-
126
- it('should correctly handle all three states (true/false/undefined) in do command', () => {
127
- // State 1: --worktree (explicit true)
128
- const doCmd1 = createDoCommand();
129
- doCmd1.parseOptions(['--worktree']);
130
- const opts1 = doCmd1.opts();
131
- expect(opts1.worktree).toBe(true);
132
- const resolved1 = opts1.worktree ?? getWorktreeDefault();
133
- expect(resolved1).toBe(true);
134
-
135
- // State 2: --no-worktree (explicit false)
136
- const doCmd2 = createDoCommand();
137
- doCmd2.parseOptions(['--no-worktree']);
138
- const opts2 = doCmd2.opts();
139
- expect(opts2.worktree).toBe(false);
140
- const resolved2 = opts2.worktree ?? getWorktreeDefault();
141
- expect(resolved2).toBe(false);
142
-
143
- // State 3: omitted (undefined, falls back to config)
144
- const doCmd3 = createDoCommand();
145
- doCmd3.parseOptions([]);
146
- const opts3 = doCmd3.opts();
147
- expect(opts3.worktree).toBeUndefined();
148
- const resolved3 = opts3.worktree ?? getWorktreeDefault();
149
- // Should be a boolean (actual value depends on config)
150
- expect(typeof resolved3).toBe('boolean');
151
- });
152
- });
153
-
154
- describe('Override semantics', () => {
155
- it('--no-worktree should override config default (explicit false takes precedence)', () => {
156
- const planCommand = createPlanCommand();
157
- planCommand.parseOptions(['--no-worktree']);
158
- const opts = planCommand.opts();
159
- const resolved = opts.worktree ?? getWorktreeDefault();
160
-
161
- expect(opts.worktree).toBe(false); // Flag sets explicit false
162
- expect(resolved).toBe(false); // Final result is false (flag takes precedence)
163
- });
164
-
165
- it('--worktree should override config default (explicit true takes precedence)', () => {
166
- const doCommand = createDoCommand();
167
- doCommand.parseOptions(['--worktree']);
168
- const opts = doCommand.opts();
169
- const resolved = opts.worktree ?? getWorktreeDefault();
170
-
171
- expect(opts.worktree).toBe(true); // Flag sets explicit true
172
- expect(resolved).toBe(true); // Final result is true (flag takes precedence)
173
- });
174
-
175
- it('omitting flag should fall back to config default', () => {
176
- const planCommand = createPlanCommand();
177
- planCommand.parseOptions([]);
178
- const opts = planCommand.opts();
179
- const resolved = opts.worktree ?? getWorktreeDefault();
180
-
181
- expect(opts.worktree).toBeUndefined(); // Flag not set
182
- expect(typeof resolved).toBe('boolean'); // Falls back to config (which is a boolean)
183
- expect(resolved).toBe(getWorktreeDefault()); // Final result matches config
184
- });
185
- });
186
- });