rafcode 1.0.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 (370) hide show
  1. package/.claude/settings.local.json +32 -0
  2. package/CLAUDE.md +187 -0
  3. package/LICENSE +21 -0
  4. package/RAF/001-raf-task-improvements/input.md +9 -0
  5. package/RAF/001-raf-task-improvements/outcomes/001-add-decisions-folder.md +21 -0
  6. package/RAF/001-raf-task-improvements/outcomes/002-fix-write-error-on-shutdown.md +22 -0
  7. package/RAF/001-raf-task-improvements/outcomes/003-stash-changes-on-failure.md +34 -0
  8. package/RAF/001-raf-task-improvements/outcomes/004-add-project-name-to-commits.md +28 -0
  9. package/RAF/001-raf-task-improvements/outcomes/005-add-running-time-display.md +36 -0
  10. package/RAF/001-raf-task-improvements/outcomes/006-add-task-name-to-logs.md +22 -0
  11. package/RAF/001-raf-task-improvements/outcomes/007-show-model-at-task-start.md +52 -0
  12. package/RAF/001-raf-task-improvements/outcomes/009-remove-editor-placeholder-text.md +20 -0
  13. package/RAF/001-raf-task-improvements/outcomes/SUMMARY.md +83 -0
  14. package/RAF/001-raf-task-improvements/plans/001-add-decisions-folder.md +38 -0
  15. package/RAF/001-raf-task-improvements/plans/002-fix-write-error-on-shutdown.md +33 -0
  16. package/RAF/001-raf-task-improvements/plans/003-stash-changes-on-failure.md +37 -0
  17. package/RAF/001-raf-task-improvements/plans/004-add-project-name-to-commits.md +34 -0
  18. package/RAF/001-raf-task-improvements/plans/005-add-running-time-display.md +39 -0
  19. package/RAF/001-raf-task-improvements/plans/006-add-task-name-to-logs.md +37 -0
  20. package/RAF/001-raf-task-improvements/plans/009-remove-editor-placeholder-text.md +34 -0
  21. package/RAF/002-raf-task-improvements-execution/decisions/DECISIONS.md +13 -0
  22. package/RAF/002-raf-task-improvements-execution/input.md +3 -0
  23. package/RAF/002-raf-task-improvements-execution/outcomes/001-commit-show-model-at-task-start.md +17 -0
  24. package/RAF/002-raf-task-improvements-execution/outcomes/002-delete-skipped-plan.md +23 -0
  25. package/RAF/002-raf-task-improvements-execution/outcomes/SUMMARY.md +32 -0
  26. package/RAF/002-raf-task-improvements-execution/plans/001-commit-show-model-at-task-start.md +37 -0
  27. package/RAF/002-raf-task-improvements-execution/plans/002-delete-skipped-plan.md +23 -0
  28. package/RAF/003-multi-project-execution/decisions/DECISIONS.md +68 -0
  29. package/RAF/003-multi-project-execution/input.md +6 -0
  30. package/RAF/003-multi-project-execution/outcomes/001-remove-state-json.md +52 -0
  31. package/RAF/003-multi-project-execution/outcomes/002-update-raf-status.md +50 -0
  32. package/RAF/003-multi-project-execution/outcomes/003-simplify-git-logic.md +35 -0
  33. package/RAF/003-multi-project-execution/outcomes/004-auto-commit-planning.md +43 -0
  34. package/RAF/003-multi-project-execution/outcomes/005-rerun-failed-tasks.md +43 -0
  35. package/RAF/003-multi-project-execution/outcomes/006-multi-project-execution.md +42 -0
  36. package/RAF/003-multi-project-execution/outcomes/007-verify-timeout.md +54 -0
  37. package/RAF/003-multi-project-execution/outcomes/008-move-decisions-file.md +38 -0
  38. package/RAF/003-multi-project-execution/outcomes/SUMMARY.md +79 -0
  39. package/RAF/003-multi-project-execution/plans/001-remove-state-json.md +71 -0
  40. package/RAF/003-multi-project-execution/plans/002-update-raf-status.md +65 -0
  41. package/RAF/003-multi-project-execution/plans/003-simplify-git-logic.md +74 -0
  42. package/RAF/003-multi-project-execution/plans/004-auto-commit-planning.md +57 -0
  43. package/RAF/003-multi-project-execution/plans/005-rerun-failed-tasks.md +69 -0
  44. package/RAF/003-multi-project-execution/plans/006-multi-project-execution.md +81 -0
  45. package/RAF/003-multi-project-execution/plans/007-verify-timeout.md +63 -0
  46. package/RAF/003-multi-project-execution/plans/008-move-decisions-file.md +78 -0
  47. package/RAF/004-task-naming-optimization/decisions.md +22 -0
  48. package/RAF/004-task-naming-optimization/input.md +6 -0
  49. package/RAF/004-task-naming-optimization/outcomes/001-remove-summary-file.md +17 -0
  50. package/RAF/004-task-naming-optimization/outcomes/002-base36-project-numbering.md +32 -0
  51. package/RAF/004-task-naming-optimization/outcomes/003-improve-haiku-prompt.md +20 -0
  52. package/RAF/004-task-naming-optimization/outcomes/SUMMARY.md +28 -0
  53. package/RAF/004-task-naming-optimization/plans/001-remove-summary-file.md +34 -0
  54. package/RAF/004-task-naming-optimization/plans/002-base36-project-numbering.md +56 -0
  55. package/RAF/004-task-naming-optimization/plans/003-improve-haiku-prompt.md +50 -0
  56. package/RAF/005-task-naming-improvements/decisions.md +60 -0
  57. package/RAF/005-task-naming-improvements/input.md +2 -0
  58. package/RAF/005-task-naming-improvements/outcomes/001-enhance-identifier-resolution.md +42 -0
  59. package/RAF/005-task-naming-improvements/outcomes/002-add-identifier-support-to-status.md +38 -0
  60. package/RAF/005-task-naming-improvements/outcomes/003-update-do-for-full-folder-names.md +44 -0
  61. package/RAF/005-task-naming-improvements/outcomes/004-implement-amend-flag-for-plan.md +55 -0
  62. package/RAF/005-task-naming-improvements/outcomes/005-commit-outcomes-on-complete.md +47 -0
  63. package/RAF/005-task-naming-improvements/outcomes/006-update-execution-prompt-commit-schema.md +40 -0
  64. package/RAF/005-task-naming-improvements/outcomes/007-allow-pending-task-amendments.md +38 -0
  65. package/RAF/005-task-naming-improvements/outcomes/008-fix-timeout-label.md +24 -0
  66. package/RAF/005-task-naming-improvements/plans/001-enhance-identifier-resolution.md +46 -0
  67. package/RAF/005-task-naming-improvements/plans/002-add-identifier-support-to-status.md +36 -0
  68. package/RAF/005-task-naming-improvements/plans/003-update-do-for-full-folder-names.md +38 -0
  69. package/RAF/005-task-naming-improvements/plans/004-implement-amend-flag-for-plan.md +67 -0
  70. package/RAF/005-task-naming-improvements/plans/005-commit-outcomes-on-complete.md +86 -0
  71. package/RAF/005-task-naming-improvements/plans/006-update-execution-prompt-commit-schema.md +60 -0
  72. package/RAF/005-task-naming-improvements/plans/007-allow-pending-task-amendments.md +60 -0
  73. package/RAF/005-task-naming-improvements/plans/008-fix-timeout-label.md +31 -0
  74. package/RAF/006-fix-double-summary-headers/decisions.md +28 -0
  75. package/RAF/006-fix-double-summary-headers/input.md +3 -0
  76. package/RAF/006-fix-double-summary-headers/outcomes/001-fix-double-summary-headers.md +29 -0
  77. package/RAF/006-fix-double-summary-headers/outcomes/002-update-readme-for-npm.md +31 -0
  78. package/RAF/006-fix-double-summary-headers/outcomes/003-npm-publish-instructions.md +30 -0
  79. package/RAF/006-fix-double-summary-headers/outcomes/004-flexible-project-lookup.md +47 -0
  80. package/RAF/006-fix-double-summary-headers/plans/001-fix-double-summary-headers.md +42 -0
  81. package/RAF/006-fix-double-summary-headers/plans/002-update-readme-for-npm.md +44 -0
  82. package/RAF/006-fix-double-summary-headers/plans/003-npm-publish-instructions.md +45 -0
  83. package/RAF/006-fix-double-summary-headers/plans/004-flexible-project-lookup.md +40 -0
  84. package/RAF/007-improve-outcome-format/decisions.md +28 -0
  85. package/RAF/007-improve-outcome-format/input.md +2 -0
  86. package/RAF/007-improve-outcome-format/outcomes/001-update-execution-prompt.md +10 -0
  87. package/RAF/007-improve-outcome-format/outcomes/002-update-state-derivation.md +17 -0
  88. package/RAF/007-improve-outcome-format/outcomes/003-update-do-command-outcome-handling.md +16 -0
  89. package/RAF/007-improve-outcome-format/outcomes/004-implement-failure-analysis.md +16 -0
  90. package/RAF/007-improve-outcome-format/outcomes/005-update-documentation.md +15 -0
  91. package/RAF/007-improve-outcome-format/plans/001-update-execution-prompt.md +36 -0
  92. package/RAF/007-improve-outcome-format/plans/002-update-state-derivation.md +35 -0
  93. package/RAF/007-improve-outcome-format/plans/003-update-do-command-outcome-handling.md +37 -0
  94. package/RAF/007-improve-outcome-format/plans/004-implement-failure-analysis.md +44 -0
  95. package/RAF/007-improve-outcome-format/plans/005-update-documentation.md +33 -0
  96. package/RAF/008-beautiful-do/decisions.md +31 -0
  97. package/RAF/008-beautiful-do/input.md +1 -0
  98. package/RAF/008-beautiful-do/outcomes/001-terminal-symbols.md +55 -0
  99. package/RAF/008-beautiful-do/outcomes/002-refactor-do-output.md +95 -0
  100. package/RAF/008-beautiful-do/outcomes/003-refactor-status-output.md +71 -0
  101. package/RAF/008-beautiful-do/outcomes/004-simplify-logger.md +53 -0
  102. package/RAF/008-beautiful-do/outcomes/005-add-tests.md +41 -0
  103. package/RAF/008-beautiful-do/plans/001-terminal-symbols.md +41 -0
  104. package/RAF/008-beautiful-do/plans/002-refactor-do-output.md +44 -0
  105. package/RAF/008-beautiful-do/plans/003-refactor-status-output.md +37 -0
  106. package/RAF/008-beautiful-do/plans/004-simplify-logger.md +32 -0
  107. package/RAF/008-beautiful-do/plans/005-add-tests.md +40 -0
  108. package/RAF/009-system-promt-ammend/decisions.md +13 -0
  109. package/RAF/009-system-promt-ammend/input.md +9 -0
  110. package/RAF/009-system-promt-ammend/outcomes/001-model-override.md +79 -0
  111. package/RAF/009-system-promt-ammend/outcomes/002-system-prompt-append.md +51 -0
  112. package/RAF/009-system-promt-ammend/outcomes/003-retry-context.md +60 -0
  113. package/RAF/009-system-promt-ammend/plans/001-model-override.md +61 -0
  114. package/RAF/009-system-promt-ammend/plans/002-system-prompt-append.md +56 -0
  115. package/RAF/009-system-promt-ammend/plans/003-retry-context.md +76 -0
  116. package/RAF/010-outcome-marker-fallback/decisions.md +19 -0
  117. package/RAF/010-outcome-marker-fallback/input.md +1 -0
  118. package/RAF/010-outcome-marker-fallback/outcomes/001-outcome-file-marker-fallback.md +35 -0
  119. package/RAF/010-outcome-marker-fallback/outcomes/002-creative-project-naming.md +47 -0
  120. package/RAF/010-outcome-marker-fallback/plans/001-outcome-file-marker-fallback.md +58 -0
  121. package/RAF/010-outcome-marker-fallback/plans/002-creative-project-naming.md +68 -0
  122. package/RAF/011-do-task-in-commit/decisions.md +22 -0
  123. package/RAF/011-do-task-in-commit/input.md +1 -0
  124. package/RAF/011-do-task-in-commit/outcomes/001-update-execution-prompt.md +54 -0
  125. package/RAF/011-do-task-in-commit/outcomes/002-update-tests.md +61 -0
  126. package/RAF/011-do-task-in-commit/outcomes/003-update-documentation.md +51 -0
  127. package/RAF/011-do-task-in-commit/plans/001-update-execution-prompt.md +46 -0
  128. package/RAF/011-do-task-in-commit/plans/002-update-tests.md +51 -0
  129. package/RAF/011-do-task-in-commit/plans/003-update-documentation.md +45 -0
  130. package/RAF/012-name-picker-buffet/decisions.md +40 -0
  131. package/RAF/012-name-picker-buffet/input.md +6 -0
  132. package/RAF/012-name-picker-buffet/outcomes/001-name-picker-for-raf-plan.md +49 -0
  133. package/RAF/012-name-picker-buffet/outcomes/002-interactive-project-picker-for-raf-do.md +49 -0
  134. package/RAF/012-name-picker-buffet/outcomes/003-raf-status-truncation.md +55 -0
  135. package/RAF/012-name-picker-buffet/outcomes/004-failure-reason-details.md +65 -0
  136. package/RAF/012-name-picker-buffet/outcomes/005-remove-raf-commits.md +57 -0
  137. package/RAF/012-name-picker-buffet/outcomes/006-update-execution-prompt-for-commits.md +47 -0
  138. package/RAF/012-name-picker-buffet/outcomes/007-fix-plan-mode-user-prompt.md +83 -0
  139. package/RAF/012-name-picker-buffet/outcomes/008-add-auto-flag-for-plan-mode.md +77 -0
  140. package/RAF/012-name-picker-buffet/plans/001-name-picker-for-raf-plan.md +47 -0
  141. package/RAF/012-name-picker-buffet/plans/002-interactive-project-picker-for-raf-do.md +43 -0
  142. package/RAF/012-name-picker-buffet/plans/003-raf-status-truncation.md +36 -0
  143. package/RAF/012-name-picker-buffet/plans/004-failure-reason-details.md +46 -0
  144. package/RAF/012-name-picker-buffet/plans/005-remove-raf-commits.md +42 -0
  145. package/RAF/012-name-picker-buffet/plans/006-update-execution-prompt-for-commits.md +47 -0
  146. package/RAF/012-name-picker-buffet/plans/007-fix-plan-mode-user-prompt.md +55 -0
  147. package/RAF/012-name-picker-buffet/plans/008-add-auto-flag-for-plan-mode.md +49 -0
  148. package/RAF/013-dependencies-watchdog/decisions.md +37 -0
  149. package/RAF/013-dependencies-watchdog/input.md +1 -0
  150. package/RAF/013-dependencies-watchdog/outcomes/001-define-dependency-syntax.md +56 -0
  151. package/RAF/013-dependencies-watchdog/outcomes/002-update-planning-prompts.md +60 -0
  152. package/RAF/013-dependencies-watchdog/outcomes/003-parse-dependencies-update-state.md +81 -0
  153. package/RAF/013-dependencies-watchdog/outcomes/004-implement-dependency-checking-in-do.md +116 -0
  154. package/RAF/013-dependencies-watchdog/outcomes/005-update-execution-prompts.md +75 -0
  155. package/RAF/013-dependencies-watchdog/outcomes/006-add-tests.md +100 -0
  156. package/RAF/013-dependencies-watchdog/outcomes/007-add-act-alias.md +46 -0
  157. package/RAF/013-dependencies-watchdog/outcomes/008-add-exit-message.md +52 -0
  158. package/RAF/013-dependencies-watchdog/plans/001-define-dependency-syntax.md +32 -0
  159. package/RAF/013-dependencies-watchdog/plans/002-update-planning-prompts.md +38 -0
  160. package/RAF/013-dependencies-watchdog/plans/003-parse-dependencies-update-state.md +46 -0
  161. package/RAF/013-dependencies-watchdog/plans/004-implement-dependency-checking-in-do.md +48 -0
  162. package/RAF/013-dependencies-watchdog/plans/005-update-execution-prompts.md +44 -0
  163. package/RAF/013-dependencies-watchdog/plans/006-add-tests.md +54 -0
  164. package/RAF/013-dependencies-watchdog/plans/007-add-act-alias.md +26 -0
  165. package/RAF/013-dependencies-watchdog/plans/008-add-exit-message.md +31 -0
  166. package/RAF/014-watchdog/decisions.md +16 -0
  167. package/RAF/014-watchdog/input.md +2 -0
  168. package/RAF/014-watchdog/outcomes/001-amend-flag-position.md +50 -0
  169. package/RAF/014-watchdog/outcomes/002-details-only-on-failure.md +58 -0
  170. package/RAF/014-watchdog/plans/001-amend-flag-position.md +34 -0
  171. package/RAF/014-watchdog/plans/002-details-only-on-failure.md +46 -0
  172. package/RAF/015-name-lottery/decisions.md +14 -0
  173. package/RAF/015-name-lottery/input.md +3 -0
  174. package/RAF/015-name-lottery/outcomes/001-auto-pick-project-name.md +31 -0
  175. package/RAF/015-name-lottery/outcomes/002-mention-plan-files-in-commit.md +23 -0
  176. package/RAF/015-name-lottery/outcomes/003-fix-input-md-in-amend-flow.md +44 -0
  177. package/RAF/015-name-lottery/plans/001-auto-pick-project-name.md +38 -0
  178. package/RAF/015-name-lottery/plans/002-mention-plan-files-in-commit.md +32 -0
  179. package/RAF/015-name-lottery/plans/003-fix-input-md-in-amend-flow.md +44 -0
  180. package/README.md +116 -0
  181. package/dist/commands/do.d.ts +12 -0
  182. package/dist/commands/do.d.ts.map +1 -0
  183. package/dist/commands/do.js +684 -0
  184. package/dist/commands/do.js.map +1 -0
  185. package/dist/commands/plan.d.ts +3 -0
  186. package/dist/commands/plan.d.ts.map +1 -0
  187. package/dist/commands/plan.js +345 -0
  188. package/dist/commands/plan.js.map +1 -0
  189. package/dist/commands/status.d.ts +3 -0
  190. package/dist/commands/status.d.ts.map +1 -0
  191. package/dist/commands/status.js +117 -0
  192. package/dist/commands/status.js.map +1 -0
  193. package/dist/core/claude-runner.d.ts +78 -0
  194. package/dist/core/claude-runner.d.ts.map +1 -0
  195. package/dist/core/claude-runner.js +297 -0
  196. package/dist/core/claude-runner.js.map +1 -0
  197. package/dist/core/editor.d.ts +10 -0
  198. package/dist/core/editor.d.ts.map +1 -0
  199. package/dist/core/editor.js +77 -0
  200. package/dist/core/editor.js.map +1 -0
  201. package/dist/core/failure-analyzer.d.ts +28 -0
  202. package/dist/core/failure-analyzer.d.ts.map +1 -0
  203. package/dist/core/failure-analyzer.js +305 -0
  204. package/dist/core/failure-analyzer.js.map +1 -0
  205. package/dist/core/git.d.ts +42 -0
  206. package/dist/core/git.d.ts.map +1 -0
  207. package/dist/core/git.js +148 -0
  208. package/dist/core/git.js.map +1 -0
  209. package/dist/core/project-manager.d.ts +72 -0
  210. package/dist/core/project-manager.d.ts.map +1 -0
  211. package/dist/core/project-manager.js +193 -0
  212. package/dist/core/project-manager.js.map +1 -0
  213. package/dist/core/retry-handler.d.ts +19 -0
  214. package/dist/core/retry-handler.d.ts.map +1 -0
  215. package/dist/core/retry-handler.js +51 -0
  216. package/dist/core/retry-handler.js.map +1 -0
  217. package/dist/core/shutdown-handler.d.ts +30 -0
  218. package/dist/core/shutdown-handler.d.ts.map +1 -0
  219. package/dist/core/shutdown-handler.js +79 -0
  220. package/dist/core/shutdown-handler.js.map +1 -0
  221. package/dist/core/state-derivation.d.ts +82 -0
  222. package/dist/core/state-derivation.d.ts.map +1 -0
  223. package/dist/core/state-derivation.js +271 -0
  224. package/dist/core/state-derivation.js.map +1 -0
  225. package/dist/core/state-manager.d.ts +54 -0
  226. package/dist/core/state-manager.d.ts.map +1 -0
  227. package/dist/core/state-manager.js +198 -0
  228. package/dist/core/state-manager.js.map +1 -0
  229. package/dist/index.d.ts +3 -0
  230. package/dist/index.d.ts.map +1 -0
  231. package/dist/index.js +16 -0
  232. package/dist/index.js.map +1 -0
  233. package/dist/parsers/output-parser.d.ts +19 -0
  234. package/dist/parsers/output-parser.d.ts.map +1 -0
  235. package/dist/parsers/output-parser.js +137 -0
  236. package/dist/parsers/output-parser.js.map +1 -0
  237. package/dist/prompts/amend.d.ts +20 -0
  238. package/dist/prompts/amend.d.ts.map +1 -0
  239. package/dist/prompts/amend.js +166 -0
  240. package/dist/prompts/amend.js.map +1 -0
  241. package/dist/prompts/execution.d.ts +30 -0
  242. package/dist/prompts/execution.d.ts.map +1 -0
  243. package/dist/prompts/execution.js +179 -0
  244. package/dist/prompts/execution.js.map +1 -0
  245. package/dist/prompts/planning.d.ts +15 -0
  246. package/dist/prompts/planning.d.ts.map +1 -0
  247. package/dist/prompts/planning.js +163 -0
  248. package/dist/prompts/planning.js.map +1 -0
  249. package/dist/types/config.d.ts +26 -0
  250. package/dist/types/config.d.ts.map +1 -0
  251. package/dist/types/config.js +7 -0
  252. package/dist/types/config.js.map +1 -0
  253. package/dist/types/state.d.ts +33 -0
  254. package/dist/types/state.d.ts.map +1 -0
  255. package/dist/types/state.js +28 -0
  256. package/dist/types/state.js.map +1 -0
  257. package/dist/ui/name-picker-subprocess.d.ts +11 -0
  258. package/dist/ui/name-picker-subprocess.d.ts.map +1 -0
  259. package/dist/ui/name-picker-subprocess.js +83 -0
  260. package/dist/ui/name-picker-subprocess.js.map +1 -0
  261. package/dist/ui/name-picker.d.ts +19 -0
  262. package/dist/ui/name-picker.d.ts.map +1 -0
  263. package/dist/ui/name-picker.js +173 -0
  264. package/dist/ui/name-picker.js.map +1 -0
  265. package/dist/ui/project-picker.d.ts +27 -0
  266. package/dist/ui/project-picker.d.ts.map +1 -0
  267. package/dist/ui/project-picker.js +58 -0
  268. package/dist/ui/project-picker.js.map +1 -0
  269. package/dist/utils/config.d.ts +24 -0
  270. package/dist/utils/config.d.ts.map +1 -0
  271. package/dist/utils/config.js +63 -0
  272. package/dist/utils/config.js.map +1 -0
  273. package/dist/utils/logger.d.ts +32 -0
  274. package/dist/utils/logger.d.ts.map +1 -0
  275. package/dist/utils/logger.js +60 -0
  276. package/dist/utils/logger.js.map +1 -0
  277. package/dist/utils/name-generator.d.ts +20 -0
  278. package/dist/utils/name-generator.d.ts.map +1 -0
  279. package/dist/utils/name-generator.js +183 -0
  280. package/dist/utils/name-generator.js.map +1 -0
  281. package/dist/utils/paths.d.ts +132 -0
  282. package/dist/utils/paths.d.ts.map +1 -0
  283. package/dist/utils/paths.js +412 -0
  284. package/dist/utils/paths.js.map +1 -0
  285. package/dist/utils/status-line.d.ts +14 -0
  286. package/dist/utils/status-line.d.ts.map +1 -0
  287. package/dist/utils/status-line.js +36 -0
  288. package/dist/utils/status-line.js.map +1 -0
  289. package/dist/utils/terminal-symbols.d.ts +50 -0
  290. package/dist/utils/terminal-symbols.d.ts.map +1 -0
  291. package/dist/utils/terminal-symbols.js +97 -0
  292. package/dist/utils/terminal-symbols.js.map +1 -0
  293. package/dist/utils/timer.d.ts +17 -0
  294. package/dist/utils/timer.d.ts.map +1 -0
  295. package/dist/utils/timer.js +56 -0
  296. package/dist/utils/timer.js.map +1 -0
  297. package/dist/utils/validation.d.ts +17 -0
  298. package/dist/utils/validation.d.ts.map +1 -0
  299. package/dist/utils/validation.js +106 -0
  300. package/dist/utils/validation.js.map +1 -0
  301. package/dist/utils/version.d.ts +2 -0
  302. package/dist/utils/version.d.ts.map +1 -0
  303. package/dist/utils/version.js +12 -0
  304. package/dist/utils/version.js.map +1 -0
  305. package/jest.config.ts +30 -0
  306. package/package.json +55 -0
  307. package/src/commands/do.ts +829 -0
  308. package/src/commands/plan.ts +422 -0
  309. package/src/commands/status.ts +146 -0
  310. package/src/core/claude-runner.ts +374 -0
  311. package/src/core/editor.ts +85 -0
  312. package/src/core/failure-analyzer.ts +372 -0
  313. package/src/core/git.ts +166 -0
  314. package/src/core/project-manager.ts +243 -0
  315. package/src/core/retry-handler.ts +72 -0
  316. package/src/core/shutdown-handler.ts +93 -0
  317. package/src/core/state-derivation.ts +343 -0
  318. package/src/index.ts +20 -0
  319. package/src/parsers/output-parser.ts +164 -0
  320. package/src/prompts/amend.ts +194 -0
  321. package/src/prompts/execution.ts +223 -0
  322. package/src/prompts/planning.ts +175 -0
  323. package/src/types/config.ts +35 -0
  324. package/src/ui/name-picker-subprocess.ts +96 -0
  325. package/src/ui/name-picker.ts +198 -0
  326. package/src/ui/project-picker.ts +80 -0
  327. package/src/utils/config.ts +69 -0
  328. package/src/utils/logger.ts +81 -0
  329. package/src/utils/name-generator.ts +211 -0
  330. package/src/utils/paths.ts +497 -0
  331. package/src/utils/status-line.ts +45 -0
  332. package/src/utils/terminal-symbols.ts +124 -0
  333. package/src/utils/timer.ts +64 -0
  334. package/src/utils/validation.ts +132 -0
  335. package/src/utils/version.ts +12 -0
  336. package/tests/unit/claude-runner-interactive.test.ts +343 -0
  337. package/tests/unit/claude-runner.test.ts +629 -0
  338. package/tests/unit/command-output.test.ts +295 -0
  339. package/tests/unit/config.test.ts +72 -0
  340. package/tests/unit/dependency-integration.test.ts +559 -0
  341. package/tests/unit/do-blocked-tasks.test.ts +323 -0
  342. package/tests/unit/do-command.test.ts +198 -0
  343. package/tests/unit/do-multiproject.test.ts +270 -0
  344. package/tests/unit/do-rerun.test.ts +270 -0
  345. package/tests/unit/execution-prompt.test.ts +406 -0
  346. package/tests/unit/failure-analyzer.test.ts +276 -0
  347. package/tests/unit/failure-history.test.ts +143 -0
  348. package/tests/unit/git-stash.test.ts +138 -0
  349. package/tests/unit/git.test.ts +80 -0
  350. package/tests/unit/logger.test.ts +132 -0
  351. package/tests/unit/name-generator.test.ts +283 -0
  352. package/tests/unit/name-picker.test.ts +179 -0
  353. package/tests/unit/outcome-content.test.ts +166 -0
  354. package/tests/unit/output-parser.test.ts +178 -0
  355. package/tests/unit/paths.test.ts +741 -0
  356. package/tests/unit/plan-command-amend-flag.test.ts +115 -0
  357. package/tests/unit/plan-command-amend-input.test.ts +156 -0
  358. package/tests/unit/plan-command-auto-flag.test.ts +112 -0
  359. package/tests/unit/plan-command.test.ts +580 -0
  360. package/tests/unit/planning-prompt.test.ts +137 -0
  361. package/tests/unit/project-manager.test.ts +265 -0
  362. package/tests/unit/project-picker.test.ts +338 -0
  363. package/tests/unit/retry-handler.test.ts +89 -0
  364. package/tests/unit/state-derivation.test.ts +714 -0
  365. package/tests/unit/status-command.test.ts +271 -0
  366. package/tests/unit/status-line.test.ts +92 -0
  367. package/tests/unit/terminal-symbols.test.ts +214 -0
  368. package/tests/unit/timer.test.ts +102 -0
  369. package/tests/unit/validation.test.ts +118 -0
  370. package/tsconfig.json +26 -0
@@ -0,0 +1,271 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { resolveProjectIdentifier, extractProjectName } from '../../src/utils/paths.js';
5
+ import { discoverProjects } from '../../src/core/state-derivation.js';
6
+
7
+ describe('Status Command - Identifier Support', () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(() => {
11
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-status-test-'));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe('Identifier Resolution for Status', () => {
19
+ it('should resolve project by numeric ID', () => {
20
+ fs.mkdirSync(path.join(tempDir, '003-fix-bug'));
21
+ const result = resolveProjectIdentifier(tempDir, '3');
22
+ expect(result).toBe(path.join(tempDir, '003-fix-bug'));
23
+ });
24
+
25
+ it('should resolve project by zero-padded numeric ID', () => {
26
+ fs.mkdirSync(path.join(tempDir, '003-fix-bug'));
27
+ const result = resolveProjectIdentifier(tempDir, '003');
28
+ expect(result).toBe(path.join(tempDir, '003-fix-bug'));
29
+ });
30
+
31
+ it('should resolve project by base36 ID', () => {
32
+ fs.mkdirSync(path.join(tempDir, 'a00-large-project'));
33
+ const result = resolveProjectIdentifier(tempDir, 'a00');
34
+ expect(result).toBe(path.join(tempDir, 'a00-large-project'));
35
+ });
36
+
37
+ it('should resolve project by name', () => {
38
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
39
+ const result = resolveProjectIdentifier(tempDir, 'my-project');
40
+ expect(result).toBe(path.join(tempDir, '001-my-project'));
41
+ });
42
+
43
+ it('should resolve project by full numeric folder name', () => {
44
+ fs.mkdirSync(path.join(tempDir, '001-fix-stuff'));
45
+ const result = resolveProjectIdentifier(tempDir, '001-fix-stuff');
46
+ expect(result).toBe(path.join(tempDir, '001-fix-stuff'));
47
+ });
48
+
49
+ it('should resolve project by full base36 folder name', () => {
50
+ fs.mkdirSync(path.join(tempDir, 'a00-important'));
51
+ const result = resolveProjectIdentifier(tempDir, 'a00-important');
52
+ expect(result).toBe(path.join(tempDir, 'a00-important'));
53
+ });
54
+
55
+ it('should return null for invalid identifier', () => {
56
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
57
+ const result = resolveProjectIdentifier(tempDir, 'non-existent');
58
+ expect(result).toBeNull();
59
+ });
60
+
61
+ it('should handle case-insensitive folder matching for full folder names', () => {
62
+ fs.mkdirSync(path.join(tempDir, 'A01-My-Project'));
63
+ const result = resolveProjectIdentifier(tempDir, 'a01-my-project');
64
+ expect(result).toBe(path.join(tempDir, 'A01-My-Project'));
65
+ });
66
+ });
67
+
68
+ describe('Project Name Extraction', () => {
69
+ it('should extract project name from numeric folder', () => {
70
+ const projectPath = path.join(tempDir, '001-my-project');
71
+ const name = extractProjectName(projectPath);
72
+ expect(name).toBe('my-project');
73
+ });
74
+
75
+ it('should extract project name from base36 folder', () => {
76
+ const projectPath = path.join(tempDir, 'a00-my-project');
77
+ const name = extractProjectName(projectPath);
78
+ expect(name).toBe('my-project');
79
+ });
80
+ });
81
+
82
+ describe('All Identifier Formats Work', () => {
83
+ beforeEach(() => {
84
+ // Create a numeric project
85
+ fs.mkdirSync(path.join(tempDir, '003-numeric-project'));
86
+ // Create a base36 project
87
+ fs.mkdirSync(path.join(tempDir, 'a01-base36-project'));
88
+ });
89
+
90
+ it('should work with number without leading zeros', () => {
91
+ expect(resolveProjectIdentifier(tempDir, '3')).toBe(path.join(tempDir, '003-numeric-project'));
92
+ });
93
+
94
+ it('should work with number with leading zeros', () => {
95
+ expect(resolveProjectIdentifier(tempDir, '003')).toBe(path.join(tempDir, '003-numeric-project'));
96
+ });
97
+
98
+ it('should work with base36 prefix', () => {
99
+ expect(resolveProjectIdentifier(tempDir, 'a01')).toBe(path.join(tempDir, 'a01-base36-project'));
100
+ });
101
+
102
+ it('should work with project name for numeric project', () => {
103
+ expect(resolveProjectIdentifier(tempDir, 'numeric-project')).toBe(path.join(tempDir, '003-numeric-project'));
104
+ });
105
+
106
+ it('should work with project name for base36 project', () => {
107
+ expect(resolveProjectIdentifier(tempDir, 'base36-project')).toBe(path.join(tempDir, 'a01-base36-project'));
108
+ });
109
+
110
+ it('should work with full numeric folder name', () => {
111
+ expect(resolveProjectIdentifier(tempDir, '003-numeric-project')).toBe(path.join(tempDir, '003-numeric-project'));
112
+ });
113
+
114
+ it('should work with full base36 folder name', () => {
115
+ expect(resolveProjectIdentifier(tempDir, 'a01-base36-project')).toBe(path.join(tempDir, 'a01-base36-project'));
116
+ });
117
+
118
+ it('should work with numeric value of base36 project', () => {
119
+ // a01 = 1001 in base36 encoding
120
+ expect(resolveProjectIdentifier(tempDir, '1001')).toBe(path.join(tempDir, 'a01-base36-project'));
121
+ });
122
+ });
123
+
124
+ describe('Error Cases', () => {
125
+ it('should return null for non-existent numeric ID', () => {
126
+ fs.mkdirSync(path.join(tempDir, '001-project'));
127
+ expect(resolveProjectIdentifier(tempDir, '999')).toBeNull();
128
+ });
129
+
130
+ it('should return null for non-existent base36 ID', () => {
131
+ fs.mkdirSync(path.join(tempDir, 'a00-project'));
132
+ expect(resolveProjectIdentifier(tempDir, 'b00')).toBeNull();
133
+ });
134
+
135
+ it('should return null for non-existent project name', () => {
136
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
137
+ expect(resolveProjectIdentifier(tempDir, 'other-project')).toBeNull();
138
+ });
139
+
140
+ it('should return null for non-existent full folder name', () => {
141
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
142
+ expect(resolveProjectIdentifier(tempDir, '002-my-project')).toBeNull();
143
+ });
144
+
145
+ it('should return null for empty RAF directory', () => {
146
+ expect(resolveProjectIdentifier(tempDir, '001')).toBeNull();
147
+ });
148
+
149
+ it('should return null for non-existent RAF directory', () => {
150
+ expect(resolveProjectIdentifier('/non/existent/path', '001')).toBeNull();
151
+ });
152
+ });
153
+ });
154
+
155
+ describe('Status Command - Truncation Behavior', () => {
156
+ let tempDir: string;
157
+ const MAX_DISPLAYED_PROJECTS = 10;
158
+
159
+ beforeEach(() => {
160
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-truncation-test-'));
161
+ });
162
+
163
+ afterEach(() => {
164
+ fs.rmSync(tempDir, { recursive: true, force: true });
165
+ });
166
+
167
+ function createProject(number: number, name: string): void {
168
+ const projectNumber = String(number).padStart(3, '0');
169
+ fs.mkdirSync(path.join(tempDir, `${projectNumber}-${name}`));
170
+ }
171
+
172
+ describe('Project List Truncation Logic', () => {
173
+ it('should return all projects when count is less than max', () => {
174
+ createProject(1, 'project-a');
175
+ createProject(2, 'project-b');
176
+ createProject(3, 'project-c');
177
+
178
+ const projects = discoverProjects(tempDir);
179
+ expect(projects.length).toBe(3);
180
+ expect(projects.length).toBeLessThanOrEqual(MAX_DISPLAYED_PROJECTS);
181
+ });
182
+
183
+ it('should return all projects when count equals max', () => {
184
+ for (let i = 1; i <= MAX_DISPLAYED_PROJECTS; i++) {
185
+ createProject(i, `project-${i}`);
186
+ }
187
+
188
+ const projects = discoverProjects(tempDir);
189
+ expect(projects.length).toBe(MAX_DISPLAYED_PROJECTS);
190
+ });
191
+
192
+ it('should have more than max projects for truncation testing', () => {
193
+ for (let i = 1; i <= 15; i++) {
194
+ createProject(i, `project-${i}`);
195
+ }
196
+
197
+ const allProjects = discoverProjects(tempDir);
198
+ expect(allProjects.length).toBe(15);
199
+ expect(allProjects.length).toBeGreaterThan(MAX_DISPLAYED_PROJECTS);
200
+
201
+ // Verify truncation would yield correct hidden count
202
+ const hiddenCount = allProjects.length - MAX_DISPLAYED_PROJECTS;
203
+ expect(hiddenCount).toBe(5);
204
+ });
205
+
206
+ it('should slice to get last N projects (sorted by number ascending)', () => {
207
+ for (let i = 1; i <= 15; i++) {
208
+ createProject(i, `project-${i}`);
209
+ }
210
+
211
+ const allProjects = discoverProjects(tempDir);
212
+ const displayedProjects = allProjects.slice(-MAX_DISPLAYED_PROJECTS);
213
+
214
+ // Should have projects 6-15 (last 10)
215
+ expect(displayedProjects.length).toBe(MAX_DISPLAYED_PROJECTS);
216
+ expect(displayedProjects[0].number).toBe(6);
217
+ expect(displayedProjects[9].number).toBe(15);
218
+ });
219
+
220
+ it('should keep projects in ascending order after slicing', () => {
221
+ for (let i = 1; i <= 12; i++) {
222
+ createProject(i, `project-${i}`);
223
+ }
224
+
225
+ const allProjects = discoverProjects(tempDir);
226
+ const displayedProjects = allProjects.slice(-MAX_DISPLAYED_PROJECTS);
227
+
228
+ // Verify ascending order
229
+ for (let i = 1; i < displayedProjects.length; i++) {
230
+ expect(displayedProjects[i].number).toBeGreaterThan(displayedProjects[i - 1].number);
231
+ }
232
+ });
233
+
234
+ it('should calculate correct hidden count', () => {
235
+ // Test with 25 projects
236
+ for (let i = 1; i <= 25; i++) {
237
+ createProject(i, `project-${i}`);
238
+ }
239
+
240
+ const allProjects = discoverProjects(tempDir);
241
+ const hiddenCount = Math.max(0, allProjects.length - MAX_DISPLAYED_PROJECTS);
242
+
243
+ expect(hiddenCount).toBe(15);
244
+ });
245
+
246
+ it('should have zero hidden count when projects are at or below max', () => {
247
+ for (let i = 1; i <= 5; i++) {
248
+ createProject(i, `project-${i}`);
249
+ }
250
+
251
+ const allProjects = discoverProjects(tempDir);
252
+ const hiddenCount = Math.max(0, allProjects.length - MAX_DISPLAYED_PROJECTS);
253
+
254
+ expect(hiddenCount).toBe(0);
255
+ });
256
+
257
+ it('should handle exactly max + 1 projects', () => {
258
+ for (let i = 1; i <= MAX_DISPLAYED_PROJECTS + 1; i++) {
259
+ createProject(i, `project-${i}`);
260
+ }
261
+
262
+ const allProjects = discoverProjects(tempDir);
263
+ const hiddenCount = Math.max(0, allProjects.length - MAX_DISPLAYED_PROJECTS);
264
+ const displayedProjects = allProjects.slice(-MAX_DISPLAYED_PROJECTS);
265
+
266
+ expect(hiddenCount).toBe(1);
267
+ expect(displayedProjects.length).toBe(MAX_DISPLAYED_PROJECTS);
268
+ expect(displayedProjects[0].number).toBe(2);
269
+ });
270
+ });
271
+ });
@@ -0,0 +1,92 @@
1
+ import { jest } from '@jest/globals';
2
+ import { createStatusLine } from '../../src/utils/status-line.js';
3
+
4
+ describe('StatusLine', () => {
5
+ let originalIsTTY: boolean | undefined;
6
+ let originalWrite: typeof process.stdout.write;
7
+ let writeOutput: string[];
8
+
9
+ beforeEach(() => {
10
+ originalIsTTY = process.stdout.isTTY;
11
+ originalWrite = process.stdout.write;
12
+ writeOutput = [];
13
+
14
+ // Mock stdout.write to capture output
15
+ process.stdout.write = jest.fn((data: string | Uint8Array) => {
16
+ writeOutput.push(data.toString());
17
+ return true;
18
+ }) as typeof process.stdout.write;
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.stdout.isTTY = originalIsTTY;
23
+ process.stdout.write = originalWrite;
24
+ });
25
+
26
+ describe('when stdout is TTY', () => {
27
+ beforeEach(() => {
28
+ process.stdout.isTTY = true;
29
+ });
30
+
31
+ it('should write text with carriage return prefix', () => {
32
+ const statusLine = createStatusLine();
33
+ statusLine.update('Hello');
34
+
35
+ expect(writeOutput).toHaveLength(1);
36
+ expect(writeOutput[0]).toContain('\r');
37
+ expect(writeOutput[0]).toContain('Hello');
38
+ });
39
+
40
+ it('should clear previous content before writing new', () => {
41
+ const statusLine = createStatusLine();
42
+ statusLine.update('Short');
43
+ writeOutput = [];
44
+
45
+ statusLine.update('A');
46
+ // Should clear 5 chars (length of 'Short') before writing 'A'
47
+ expect(writeOutput[0]).toContain(' ');
48
+ expect(writeOutput[0]).toContain('A');
49
+ });
50
+
51
+ it('should clear line completely when clear() is called', () => {
52
+ const statusLine = createStatusLine();
53
+ statusLine.update('Test');
54
+ writeOutput = [];
55
+
56
+ statusLine.clear();
57
+ // Should clear 4 chars and reset
58
+ expect(writeOutput[0]).toContain('\r');
59
+ });
60
+
61
+ it('should not write again after clear() until update() is called', () => {
62
+ const statusLine = createStatusLine();
63
+ statusLine.update('Test');
64
+ statusLine.clear();
65
+ writeOutput = [];
66
+
67
+ statusLine.clear();
68
+ expect(writeOutput).toHaveLength(0);
69
+ });
70
+ });
71
+
72
+ describe('when stdout is not TTY', () => {
73
+ beforeEach(() => {
74
+ process.stdout.isTTY = false;
75
+ });
76
+
77
+ it('should not write anything on update', () => {
78
+ const statusLine = createStatusLine();
79
+ statusLine.update('Hello');
80
+
81
+ expect(writeOutput).toHaveLength(0);
82
+ });
83
+
84
+ it('should not write anything on clear', () => {
85
+ const statusLine = createStatusLine();
86
+ statusLine.update('Hello');
87
+ statusLine.clear();
88
+
89
+ expect(writeOutput).toHaveLength(0);
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,214 @@
1
+ import {
2
+ SYMBOLS,
3
+ formatTaskProgress,
4
+ formatProjectHeader,
5
+ formatSummary,
6
+ formatProgressBar,
7
+ TaskStatus,
8
+ } from '../../src/utils/terminal-symbols.js';
9
+
10
+ describe('Terminal Symbols', () => {
11
+ describe('SYMBOLS', () => {
12
+ it('should have all required symbols', () => {
13
+ expect(SYMBOLS.running).toBe('●');
14
+ expect(SYMBOLS.completed).toBe('✓');
15
+ expect(SYMBOLS.failed).toBe('✗');
16
+ expect(SYMBOLS.pending).toBe('○');
17
+ expect(SYMBOLS.blocked).toBe('⊘');
18
+ expect(SYMBOLS.project).toBe('▶');
19
+ });
20
+
21
+ it('should be a const object with correct types', () => {
22
+ // TypeScript's 'as const' provides compile-time immutability
23
+ // Verify all expected keys exist
24
+ expect(Object.keys(SYMBOLS)).toEqual(['running', 'completed', 'failed', 'pending', 'blocked', 'project']);
25
+ });
26
+ });
27
+
28
+ describe('formatTaskProgress', () => {
29
+ it('should format a running task with elapsed time', () => {
30
+ const result = formatTaskProgress(1, 5, 'running', 'auth-login', 83000);
31
+ expect(result).toBe('● auth-login 1m 23s');
32
+ });
33
+
34
+ it('should format a running task without elapsed time', () => {
35
+ const result = formatTaskProgress(1, 5, 'running', 'auth-login');
36
+ expect(result).toBe('● auth-login 1/5');
37
+ });
38
+
39
+ it('should format a completed task without elapsed time', () => {
40
+ const result = formatTaskProgress(3, 5, 'completed', 'setup-db');
41
+ expect(result).toBe('✓ setup-db 3/5');
42
+ });
43
+
44
+ it('should format a completed task with elapsed time', () => {
45
+ const result = formatTaskProgress(3, 5, 'completed', 'setup-db', 154000);
46
+ expect(result).toBe('✓ setup-db 2m 34s');
47
+ });
48
+
49
+ it('should format a failed task without elapsed time', () => {
50
+ const result = formatTaskProgress(2, 5, 'failed', 'deploy');
51
+ expect(result).toBe('✗ deploy 2/5');
52
+ });
53
+
54
+ it('should format a failed task with elapsed time', () => {
55
+ const result = formatTaskProgress(2, 5, 'failed', 'deploy', 45000);
56
+ expect(result).toBe('✗ deploy 45s');
57
+ });
58
+
59
+ it('should format a pending task', () => {
60
+ const result = formatTaskProgress(4, 5, 'pending', 'cleanup');
61
+ expect(result).toBe('○ cleanup 4/5');
62
+ });
63
+
64
+ it('should truncate long task names', () => {
65
+ const longName = 'this-is-a-very-long-task-name-that-should-be-truncated-for-display';
66
+ const result = formatTaskProgress(1, 1, 'running', longName);
67
+ expect(result).toContain('…');
68
+ expect(result.length).toBeLessThan(60);
69
+ });
70
+
71
+ it('should handle empty task name', () => {
72
+ const result = formatTaskProgress(1, 1, 'pending', '');
73
+ expect(result).toBe('○ task 1/1');
74
+ });
75
+
76
+ it('should handle zero elapsed time for running task', () => {
77
+ const result = formatTaskProgress(1, 1, 'running', 'test', 0);
78
+ expect(result).toBe('● test 0s');
79
+ });
80
+
81
+ it('should format a blocked task', () => {
82
+ const result = formatTaskProgress(2, 5, 'blocked', 'depends-on-failed');
83
+ expect(result).toBe('⊘ depends-on-failed 2/5');
84
+ });
85
+ });
86
+
87
+ describe('formatProjectHeader', () => {
88
+ it('should format project header with multiple tasks', () => {
89
+ const result = formatProjectHeader('my-project', 5);
90
+ expect(result).toBe('▶ my-project (5 tasks)');
91
+ });
92
+
93
+ it('should format project header with single task', () => {
94
+ const result = formatProjectHeader('small-project', 1);
95
+ expect(result).toBe('▶ small-project (1 task)');
96
+ });
97
+
98
+ it('should format project header with zero tasks', () => {
99
+ const result = formatProjectHeader('empty-project', 0);
100
+ expect(result).toBe('▶ empty-project (0 tasks)');
101
+ });
102
+
103
+ it('should truncate long project names', () => {
104
+ const longName = 'this-is-a-very-very-long-project-name-that-exceeds-fifty-chars';
105
+ const result = formatProjectHeader(longName, 3);
106
+ expect(result).toContain('…');
107
+ expect(result.length).toBeLessThan(70);
108
+ });
109
+
110
+ it('should handle empty project name', () => {
111
+ const result = formatProjectHeader('', 5);
112
+ expect(result).toBe('▶ project (5 tasks)');
113
+ });
114
+ });
115
+
116
+ describe('formatSummary', () => {
117
+ it('should format all completed with elapsed time', () => {
118
+ const result = formatSummary(5, 0, 0, 754000);
119
+ expect(result).toBe('✓ 5/5 completed in 12m 34s');
120
+ });
121
+
122
+ it('should format all completed without elapsed time', () => {
123
+ const result = formatSummary(5, 0, 0);
124
+ expect(result).toBe('✓ 5/5 completed');
125
+ });
126
+
127
+ it('should format with failures', () => {
128
+ const result = formatSummary(3, 2, 0);
129
+ expect(result).toBe('✗ 3/5 (2 failed)');
130
+ });
131
+
132
+ it('should format single failure', () => {
133
+ const result = formatSummary(4, 1, 0);
134
+ expect(result).toBe('✗ 4/5 (1 failed)');
135
+ });
136
+
137
+ it('should format with pending tasks', () => {
138
+ const result = formatSummary(3, 0, 2);
139
+ expect(result).toBe('✓ 3/5 completed');
140
+ });
141
+
142
+ it('should format with mixed status', () => {
143
+ const result = formatSummary(2, 1, 2);
144
+ expect(result).toBe('✗ 2/5 (1 failed)');
145
+ });
146
+
147
+ it('should handle zero total tasks', () => {
148
+ const result = formatSummary(0, 0, 0);
149
+ expect(result).toBe('○ no tasks');
150
+ });
151
+
152
+ it('should ignore elapsed time when there are failures', () => {
153
+ const result = formatSummary(3, 2, 0, 60000);
154
+ expect(result).toBe('✗ 3/5 (2 failed)');
155
+ });
156
+
157
+ it('should format with blocked tasks only', () => {
158
+ const result = formatSummary(3, 0, 0, undefined, 2);
159
+ expect(result).toBe('✗ 3/5 (2 blocked)');
160
+ });
161
+
162
+ it('should format single blocked task', () => {
163
+ const result = formatSummary(4, 0, 0, undefined, 1);
164
+ expect(result).toBe('✗ 4/5 (1 blocked)');
165
+ });
166
+
167
+ it('should format with failures and blocked tasks', () => {
168
+ const result = formatSummary(2, 1, 0, undefined, 2);
169
+ expect(result).toBe('✗ 2/5 (1 failed, 2 blocked)');
170
+ });
171
+
172
+ it('should include blocked in total count', () => {
173
+ const result = formatSummary(2, 1, 1, undefined, 1);
174
+ expect(result).toBe('✗ 2/5 (1 failed, 1 blocked)');
175
+ });
176
+ });
177
+
178
+ describe('formatProgressBar', () => {
179
+ it('should format a sequence of task statuses', () => {
180
+ const tasks: TaskStatus[] = ['completed', 'completed', 'running', 'pending', 'pending'];
181
+ const result = formatProgressBar(tasks);
182
+ expect(result).toBe('✓✓●○○');
183
+ });
184
+
185
+ it('should format all completed', () => {
186
+ const tasks: TaskStatus[] = ['completed', 'completed', 'completed'];
187
+ const result = formatProgressBar(tasks);
188
+ expect(result).toBe('✓✓✓');
189
+ });
190
+
191
+ it('should format with failures', () => {
192
+ const tasks: TaskStatus[] = ['completed', 'failed', 'pending'];
193
+ const result = formatProgressBar(tasks);
194
+ expect(result).toBe('✓✗○');
195
+ });
196
+
197
+ it('should format single task', () => {
198
+ const tasks: TaskStatus[] = ['running'];
199
+ const result = formatProgressBar(tasks);
200
+ expect(result).toBe('●');
201
+ });
202
+
203
+ it('should handle empty array', () => {
204
+ const result = formatProgressBar([]);
205
+ expect(result).toBe('');
206
+ });
207
+
208
+ it('should format blocked tasks', () => {
209
+ const tasks: TaskStatus[] = ['completed', 'failed', 'blocked', 'pending'];
210
+ const result = formatProgressBar(tasks);
211
+ expect(result).toBe('✓✗⊘○');
212
+ });
213
+ });
214
+ });
@@ -0,0 +1,102 @@
1
+ import { jest } from '@jest/globals';
2
+ import { formatElapsedTime, createTaskTimer } from '../../src/utils/timer.js';
3
+
4
+ describe('Timer', () => {
5
+ describe('formatElapsedTime', () => {
6
+ it('should format seconds only for less than a minute', () => {
7
+ expect(formatElapsedTime(0)).toBe('0s');
8
+ expect(formatElapsedTime(1000)).toBe('1s');
9
+ expect(formatElapsedTime(30000)).toBe('30s');
10
+ expect(formatElapsedTime(59000)).toBe('59s');
11
+ });
12
+
13
+ it('should format minutes and seconds for less than an hour', () => {
14
+ expect(formatElapsedTime(60000)).toBe('1m 0s');
15
+ expect(formatElapsedTime(90000)).toBe('1m 30s');
16
+ expect(formatElapsedTime(150000)).toBe('2m 30s');
17
+ expect(formatElapsedTime(3599000)).toBe('59m 59s');
18
+ });
19
+
20
+ it('should format hours and minutes for an hour or more', () => {
21
+ expect(formatElapsedTime(3600000)).toBe('1h 0m');
22
+ expect(formatElapsedTime(3660000)).toBe('1h 1m');
23
+ expect(formatElapsedTime(7200000)).toBe('2h 0m');
24
+ expect(formatElapsedTime(7290000)).toBe('2h 1m');
25
+ expect(formatElapsedTime(86400000)).toBe('24h 0m');
26
+ });
27
+
28
+ it('should round down to whole seconds', () => {
29
+ expect(formatElapsedTime(1500)).toBe('1s');
30
+ expect(formatElapsedTime(1999)).toBe('1s');
31
+ });
32
+ });
33
+
34
+ describe('createTaskTimer', () => {
35
+ beforeEach(() => {
36
+ jest.useFakeTimers();
37
+ });
38
+
39
+ afterEach(() => {
40
+ jest.useRealTimers();
41
+ });
42
+
43
+ it('should track elapsed time', () => {
44
+ const timer = createTaskTimer();
45
+ timer.start();
46
+
47
+ jest.advanceTimersByTime(5000);
48
+ expect(timer.getElapsed()).toBeGreaterThanOrEqual(5000);
49
+
50
+ const elapsed = timer.stop();
51
+ expect(elapsed).toBeGreaterThanOrEqual(5000);
52
+ });
53
+
54
+ it('should return 0 when stopped before starting', () => {
55
+ const timer = createTaskTimer();
56
+ expect(timer.stop()).toBe(0);
57
+ });
58
+
59
+ it('should return 0 for getElapsed when not started', () => {
60
+ const timer = createTaskTimer();
61
+ expect(timer.getElapsed()).toBe(0);
62
+ });
63
+
64
+ it('should call onTick callback immediately and every second', () => {
65
+ const onTick = jest.fn();
66
+ const timer = createTaskTimer(onTick);
67
+
68
+ timer.start();
69
+ expect(onTick).toHaveBeenCalledWith(0);
70
+
71
+ jest.advanceTimersByTime(1000);
72
+ expect(onTick).toHaveBeenCalledTimes(2);
73
+
74
+ jest.advanceTimersByTime(2000);
75
+ expect(onTick).toHaveBeenCalledTimes(4);
76
+
77
+ timer.stop();
78
+ });
79
+
80
+ it('should stop calling onTick after stop()', () => {
81
+ const onTick = jest.fn();
82
+ const timer = createTaskTimer(onTick);
83
+
84
+ timer.start();
85
+ jest.advanceTimersByTime(2000);
86
+ const callCount = onTick.mock.calls.length;
87
+
88
+ timer.stop();
89
+ jest.advanceTimersByTime(5000);
90
+
91
+ expect(onTick.mock.calls.length).toBe(callCount);
92
+ });
93
+
94
+ it('should not call onTick when not provided', () => {
95
+ const timer = createTaskTimer();
96
+ timer.start();
97
+ jest.advanceTimersByTime(5000);
98
+ timer.stop();
99
+ // No assertion needed - just checking it doesn't throw
100
+ });
101
+ });
102
+ });