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,741 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import {
5
+ getNextProjectNumber,
6
+ formatProjectNumber,
7
+ listProjects,
8
+ extractProjectNumber,
9
+ extractProjectName,
10
+ extractTaskNameFromPlanFile,
11
+ resolveProjectIdentifier,
12
+ resolveProjectIdentifierWithDetails,
13
+ getDecisionsPath,
14
+ encodeBase36,
15
+ decodeBase36,
16
+ isBase36Prefix,
17
+ parseProjectPrefix,
18
+ } from '../../src/utils/paths.js';
19
+
20
+ describe('Paths', () => {
21
+ let tempDir: string;
22
+
23
+ beforeEach(() => {
24
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-test-'));
25
+ });
26
+
27
+ afterEach(() => {
28
+ fs.rmSync(tempDir, { recursive: true, force: true });
29
+ });
30
+
31
+ describe('getNextProjectNumber', () => {
32
+ it('should return 1 for empty directory', () => {
33
+ expect(getNextProjectNumber(tempDir)).toBe(1);
34
+ });
35
+
36
+ it('should return 1 for non-existent directory', () => {
37
+ expect(getNextProjectNumber('/non/existent/path')).toBe(1);
38
+ });
39
+
40
+ it('should return next number after existing projects', () => {
41
+ fs.mkdirSync(path.join(tempDir, '01-first'));
42
+ fs.mkdirSync(path.join(tempDir, '02-second'));
43
+
44
+ expect(getNextProjectNumber(tempDir)).toBe(3);
45
+ });
46
+
47
+ it('should handle gaps in numbering', () => {
48
+ fs.mkdirSync(path.join(tempDir, '01-first'));
49
+ fs.mkdirSync(path.join(tempDir, '05-fifth'));
50
+
51
+ expect(getNextProjectNumber(tempDir)).toBe(6);
52
+ });
53
+ });
54
+
55
+ describe('formatProjectNumber', () => {
56
+ it('should pad single digits to 3 digits', () => {
57
+ expect(formatProjectNumber(1)).toBe('001');
58
+ expect(formatProjectNumber(9)).toBe('009');
59
+ });
60
+
61
+ it('should pad double digits to 3 digits', () => {
62
+ expect(formatProjectNumber(10)).toBe('010');
63
+ expect(formatProjectNumber(99)).toBe('099');
64
+ });
65
+
66
+ it('should not pad triple digits', () => {
67
+ expect(formatProjectNumber(100)).toBe('100');
68
+ expect(formatProjectNumber(999)).toBe('999');
69
+ });
70
+ });
71
+
72
+ describe('listProjects', () => {
73
+ it('should list projects in order', () => {
74
+ fs.mkdirSync(path.join(tempDir, '02-second'));
75
+ fs.mkdirSync(path.join(tempDir, '01-first'));
76
+ fs.mkdirSync(path.join(tempDir, '03-third'));
77
+
78
+ const projects = listProjects(tempDir);
79
+ expect(projects).toHaveLength(3);
80
+ expect(projects[0]?.name).toBe('first');
81
+ expect(projects[1]?.name).toBe('second');
82
+ expect(projects[2]?.name).toBe('third');
83
+ });
84
+
85
+ it('should ignore non-project directories', () => {
86
+ fs.mkdirSync(path.join(tempDir, '01-valid'));
87
+ fs.mkdirSync(path.join(tempDir, 'not-a-project')); // 'not' is valid base36, but project name must follow hyphen
88
+ fs.mkdirSync(path.join(tempDir, 'random')); // No hyphen, not a project
89
+ fs.mkdirSync(path.join(tempDir, 'ab')); // Too short prefix
90
+
91
+ const projects = listProjects(tempDir);
92
+ // 'not-a-project' is now recognized as base36 project (not = 18741)
93
+ expect(projects).toHaveLength(2);
94
+ expect(projects[0]?.name).toBe('valid');
95
+ expect(projects[0]?.number).toBe(1);
96
+ expect(projects[1]?.name).toBe('a-project');
97
+ expect(projects[1]?.number).toBe(18741); // 'not' in base36
98
+ });
99
+
100
+ it('should return empty array for non-existent directory', () => {
101
+ const projects = listProjects('/non/existent/path');
102
+ expect(projects).toEqual([]);
103
+ });
104
+ });
105
+
106
+ describe('extractProjectNumber', () => {
107
+ it('should extract 3-digit project number from path', () => {
108
+ expect(extractProjectNumber('/Users/foo/RAF/001-my-project')).toBe('001');
109
+ expect(extractProjectNumber('/RAF/123-another-project')).toBe('123');
110
+ });
111
+
112
+ it('should extract 2-digit project number from path', () => {
113
+ expect(extractProjectNumber('/RAF/01-first')).toBe('01');
114
+ expect(extractProjectNumber('/RAF/99-last')).toBe('99');
115
+ });
116
+
117
+ it('should return null for invalid paths', () => {
118
+ expect(extractProjectNumber('/RAF/my-project')).toBeNull(); // 'my' is only 2 chars
119
+ expect(extractProjectNumber('')).toBeNull();
120
+ // 'not' is a valid base36 prefix, so not-numbered returns 'not'
121
+ expect(extractProjectNumber('/RAF/not-numbered')).toBe('not');
122
+ });
123
+
124
+ it('should handle path with trailing slash', () => {
125
+ expect(extractProjectNumber('/RAF/001-my-project/')).toBe('001');
126
+ });
127
+
128
+ it('should only match valid prefixes at the start of folder name', () => {
129
+ // 'abc' is a valid base36 prefix, so abc-001-project returns 'abc'
130
+ expect(extractProjectNumber('/RAF/abc-001-project')).toBe('abc');
131
+ // 'project' is too long for a valid prefix (must be 2-3 digits or 3 char base36)
132
+ expect(extractProjectNumber('/RAF/project-001')).toBeNull();
133
+ });
134
+ });
135
+
136
+ describe('extractProjectName', () => {
137
+ it('should extract project name from 3-digit numbered path', () => {
138
+ expect(extractProjectName('/Users/foo/RAF/001-my-project')).toBe('my-project');
139
+ expect(extractProjectName('/RAF/123-another-project')).toBe('another-project');
140
+ });
141
+
142
+ it('should extract project name from 2-digit numbered path', () => {
143
+ expect(extractProjectName('/RAF/01-first')).toBe('first');
144
+ expect(extractProjectName('/RAF/99-last')).toBe('last');
145
+ });
146
+
147
+ it('should return null for invalid paths', () => {
148
+ expect(extractProjectName('/RAF/my-project')).toBeNull(); // 'my' is only 2 chars
149
+ expect(extractProjectName('')).toBeNull();
150
+ // 'not' is a valid base36 prefix, so not-numbered extracts 'numbered' as name
151
+ expect(extractProjectName('/RAF/not-numbered')).toBe('numbered');
152
+ });
153
+
154
+ it('should handle path with trailing slash', () => {
155
+ expect(extractProjectName('/RAF/001-my-project/')).toBe('my-project');
156
+ });
157
+
158
+ it('should only match valid prefixes at the start of folder name', () => {
159
+ // 'abc' is a valid base36 prefix, so abc-001-project extracts '001-project' as name
160
+ expect(extractProjectName('/RAF/abc-001-project')).toBe('001-project');
161
+ // 'project' is too long for a valid prefix
162
+ expect(extractProjectName('/RAF/project-001')).toBeNull();
163
+ });
164
+
165
+ it('should handle project names with hyphens', () => {
166
+ expect(extractProjectName('/RAF/001-my-complex-project-name')).toBe('my-complex-project-name');
167
+ });
168
+
169
+ it('should handle project names with numbers', () => {
170
+ expect(extractProjectName('/RAF/001-project-v2')).toBe('project-v2');
171
+ expect(extractProjectName('/RAF/001-123-test')).toBe('123-test');
172
+ });
173
+ });
174
+
175
+ describe('extractTaskNameFromPlanFile', () => {
176
+ it('should extract task name from 3-digit numbered plan file', () => {
177
+ expect(extractTaskNameFromPlanFile('001-fix-login-bug.md')).toBe('fix-login-bug');
178
+ expect(extractTaskNameFromPlanFile('123-add-feature.md')).toBe('add-feature');
179
+ });
180
+
181
+ it('should extract task name from 2-digit numbered plan file', () => {
182
+ expect(extractTaskNameFromPlanFile('01-first-task.md')).toBe('first-task');
183
+ expect(extractTaskNameFromPlanFile('99-last-task.md')).toBe('last-task');
184
+ });
185
+
186
+ it('should return null for invalid filenames', () => {
187
+ expect(extractTaskNameFromPlanFile('my-task.md')).toBeNull();
188
+ expect(extractTaskNameFromPlanFile('not-numbered.md')).toBeNull();
189
+ expect(extractTaskNameFromPlanFile('')).toBeNull();
190
+ });
191
+
192
+ it('should handle task names with hyphens', () => {
193
+ expect(extractTaskNameFromPlanFile('001-my-complex-task-name.md')).toBe('my-complex-task-name');
194
+ });
195
+
196
+ it('should handle task names with numbers', () => {
197
+ expect(extractTaskNameFromPlanFile('001-task-v2.md')).toBe('task-v2');
198
+ expect(extractTaskNameFromPlanFile('001-123-test.md')).toBe('123-test');
199
+ });
200
+
201
+ it('should handle full paths', () => {
202
+ expect(extractTaskNameFromPlanFile('/path/to/plans/002-fix-login-bug.md')).toBe('fix-login-bug');
203
+ });
204
+
205
+ it('should handle files without .md extension', () => {
206
+ expect(extractTaskNameFromPlanFile('001-task-name')).toBe('task-name');
207
+ });
208
+ });
209
+
210
+ describe('resolveProjectIdentifier', () => {
211
+ it('should resolve project by exact number (3 digits)', () => {
212
+ fs.mkdirSync(path.join(tempDir, '003-my-project'));
213
+ const result = resolveProjectIdentifier(tempDir, '003');
214
+ expect(result).toBe(path.join(tempDir, '003-my-project'));
215
+ });
216
+
217
+ it('should resolve project by number without leading zeros', () => {
218
+ fs.mkdirSync(path.join(tempDir, '003-my-project'));
219
+ const result = resolveProjectIdentifier(tempDir, '3');
220
+ expect(result).toBe(path.join(tempDir, '003-my-project'));
221
+ });
222
+
223
+ it('should resolve project by 2-digit number', () => {
224
+ fs.mkdirSync(path.join(tempDir, '42-my-project'));
225
+ const result = resolveProjectIdentifier(tempDir, '42');
226
+ expect(result).toBe(path.join(tempDir, '42-my-project'));
227
+ });
228
+
229
+ it('should resolve project by name', () => {
230
+ fs.mkdirSync(path.join(tempDir, '005-my-awesome-project'));
231
+ const result = resolveProjectIdentifier(tempDir, 'my-awesome-project');
232
+ expect(result).toBe(path.join(tempDir, '005-my-awesome-project'));
233
+ });
234
+
235
+ it('should return null for non-existent project number', () => {
236
+ fs.mkdirSync(path.join(tempDir, '001-first'));
237
+ const result = resolveProjectIdentifier(tempDir, '999');
238
+ expect(result).toBeNull();
239
+ });
240
+
241
+ it('should return null for non-existent project name', () => {
242
+ fs.mkdirSync(path.join(tempDir, '001-first'));
243
+ const result = resolveProjectIdentifier(tempDir, 'non-existent');
244
+ expect(result).toBeNull();
245
+ });
246
+
247
+ it('should return null for non-existent directory', () => {
248
+ const result = resolveProjectIdentifier('/non/existent/path', '001');
249
+ expect(result).toBeNull();
250
+ });
251
+
252
+ it('should handle multiple projects and find correct one by number', () => {
253
+ fs.mkdirSync(path.join(tempDir, '001-first'));
254
+ fs.mkdirSync(path.join(tempDir, '002-second'));
255
+ fs.mkdirSync(path.join(tempDir, '003-third'));
256
+
257
+ expect(resolveProjectIdentifier(tempDir, '1')).toBe(path.join(tempDir, '001-first'));
258
+ expect(resolveProjectIdentifier(tempDir, '2')).toBe(path.join(tempDir, '002-second'));
259
+ expect(resolveProjectIdentifier(tempDir, '003')).toBe(path.join(tempDir, '003-third'));
260
+ });
261
+
262
+ it('should handle multiple projects and find correct one by name', () => {
263
+ fs.mkdirSync(path.join(tempDir, '001-first'));
264
+ fs.mkdirSync(path.join(tempDir, '002-second'));
265
+ fs.mkdirSync(path.join(tempDir, '003-third'));
266
+
267
+ expect(resolveProjectIdentifier(tempDir, 'first')).toBe(path.join(tempDir, '001-first'));
268
+ expect(resolveProjectIdentifier(tempDir, 'second')).toBe(path.join(tempDir, '002-second'));
269
+ expect(resolveProjectIdentifier(tempDir, 'third')).toBe(path.join(tempDir, '003-third'));
270
+ });
271
+
272
+ it('should not resolve partial name match', () => {
273
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
274
+ expect(resolveProjectIdentifier(tempDir, 'my')).toBeNull();
275
+ expect(resolveProjectIdentifier(tempDir, 'project')).toBeNull();
276
+ expect(resolveProjectIdentifier(tempDir, 'my-proj')).toBeNull();
277
+ });
278
+
279
+ it('should prefer number match when identifier is numeric', () => {
280
+ fs.mkdirSync(path.join(tempDir, '005-project'));
281
+ // Numeric identifier "5" should match project number 5
282
+ const result = resolveProjectIdentifier(tempDir, '5');
283
+ expect(result).toBe(path.join(tempDir, '005-project'));
284
+ });
285
+ });
286
+
287
+ describe('getDecisionsPath', () => {
288
+ it('should return decisions.md at project root', () => {
289
+ const projectPath = '/Users/foo/RAF/001-my-project';
290
+ expect(getDecisionsPath(projectPath)).toBe(path.join(projectPath, 'decisions.md'));
291
+ });
292
+ });
293
+
294
+ // Base36 project numbering tests
295
+ describe('encodeBase36', () => {
296
+ it('should encode 1000 as a00', () => {
297
+ expect(encodeBase36(1000)).toBe('a00');
298
+ });
299
+
300
+ it('should encode 1001 as a01', () => {
301
+ expect(encodeBase36(1001)).toBe('a01');
302
+ });
303
+
304
+ it('should encode 1035 as a0z', () => {
305
+ expect(encodeBase36(1035)).toBe('a0z');
306
+ });
307
+
308
+ it('should encode 1036 as a10', () => {
309
+ expect(encodeBase36(1036)).toBe('a10');
310
+ });
311
+
312
+ it('should encode 2296 as b00 (1000 + 1296)', () => {
313
+ // 1296 = 36 * 36, so b00 = 1000 + 1296 = 2296
314
+ expect(encodeBase36(2296)).toBe('b00');
315
+ });
316
+
317
+ it('should encode larger numbers correctly', () => {
318
+ // c00 = 1000 + 2*1296 = 1000 + 2592 = 3592
319
+ expect(encodeBase36(3592)).toBe('c00');
320
+ });
321
+
322
+ it('should throw for numbers less than 1000', () => {
323
+ expect(() => encodeBase36(999)).toThrow();
324
+ expect(() => encodeBase36(0)).toThrow();
325
+ expect(() => encodeBase36(500)).toThrow();
326
+ });
327
+
328
+ it('should handle boundary between digits', () => {
329
+ // a0z = 1035, a10 = 1036
330
+ expect(encodeBase36(1035)).toBe('a0z');
331
+ expect(encodeBase36(1036)).toBe('a10');
332
+ // azz = 1000 + 35*36 + 35 = 1000 + 1260 + 35 = 2295
333
+ expect(encodeBase36(2295)).toBe('azz');
334
+ });
335
+ });
336
+
337
+ describe('decodeBase36', () => {
338
+ it('should decode a00 as 1000', () => {
339
+ expect(decodeBase36('a00')).toBe(1000);
340
+ });
341
+
342
+ it('should decode a01 as 1001', () => {
343
+ expect(decodeBase36('a01')).toBe(1001);
344
+ });
345
+
346
+ it('should decode a0z as 1035', () => {
347
+ expect(decodeBase36('a0z')).toBe(1035);
348
+ });
349
+
350
+ it('should decode a10 as 1036', () => {
351
+ expect(decodeBase36('a10')).toBe(1036);
352
+ });
353
+
354
+ it('should decode b00 as 2296', () => {
355
+ expect(decodeBase36('b00')).toBe(2296);
356
+ });
357
+
358
+ it('should return null for invalid format', () => {
359
+ expect(decodeBase36('001')).toBeNull(); // Starts with digit
360
+ expect(decodeBase36('1a0')).toBeNull(); // Starts with digit
361
+ expect(decodeBase36('ab')).toBeNull(); // Too short
362
+ expect(decodeBase36('abcd')).toBeNull(); // Too long
363
+ expect(decodeBase36('')).toBeNull(); // Empty
364
+ });
365
+
366
+ it('should handle uppercase input', () => {
367
+ expect(decodeBase36('A00')).toBe(1000);
368
+ expect(decodeBase36('B00')).toBe(2296);
369
+ });
370
+
371
+ it('should be inverse of encodeBase36', () => {
372
+ for (const num of [1000, 1001, 1035, 1036, 2000, 2296, 5000, 10000]) {
373
+ expect(decodeBase36(encodeBase36(num))).toBe(num);
374
+ }
375
+ });
376
+ });
377
+
378
+ describe('isBase36Prefix', () => {
379
+ it('should return true for valid base36 prefixes', () => {
380
+ expect(isBase36Prefix('a00')).toBe(true);
381
+ expect(isBase36Prefix('a01')).toBe(true);
382
+ expect(isBase36Prefix('azz')).toBe(true);
383
+ expect(isBase36Prefix('b00')).toBe(true);
384
+ expect(isBase36Prefix('z99')).toBe(true);
385
+ expect(isBase36Prefix('zzz')).toBe(true);
386
+ });
387
+
388
+ it('should return false for numeric prefixes', () => {
389
+ expect(isBase36Prefix('001')).toBe(false);
390
+ expect(isBase36Prefix('999')).toBe(false);
391
+ expect(isBase36Prefix('123')).toBe(false);
392
+ });
393
+
394
+ it('should return false for invalid formats', () => {
395
+ expect(isBase36Prefix('ab')).toBe(false); // Too short
396
+ expect(isBase36Prefix('abcd')).toBe(false); // Too long
397
+ expect(isBase36Prefix('1ab')).toBe(false); // Starts with digit
398
+ expect(isBase36Prefix('')).toBe(false); // Empty
399
+ });
400
+
401
+ it('should handle uppercase', () => {
402
+ expect(isBase36Prefix('A00')).toBe(true);
403
+ expect(isBase36Prefix('ABC')).toBe(true);
404
+ });
405
+ });
406
+
407
+ describe('parseProjectPrefix', () => {
408
+ it('should parse numeric prefixes', () => {
409
+ expect(parseProjectPrefix('001')).toBe(1);
410
+ expect(parseProjectPrefix('123')).toBe(123);
411
+ expect(parseProjectPrefix('999')).toBe(999);
412
+ expect(parseProjectPrefix('01')).toBe(1);
413
+ expect(parseProjectPrefix('99')).toBe(99);
414
+ });
415
+
416
+ it('should parse base36 prefixes', () => {
417
+ expect(parseProjectPrefix('a00')).toBe(1000);
418
+ expect(parseProjectPrefix('a01')).toBe(1001);
419
+ expect(parseProjectPrefix('b00')).toBe(2296);
420
+ });
421
+
422
+ it('should return null for invalid prefixes', () => {
423
+ expect(parseProjectPrefix('abc-')).toBeNull();
424
+ expect(parseProjectPrefix('1')).toBeNull(); // Too short
425
+ expect(parseProjectPrefix('1234')).toBeNull(); // Too long for numeric
426
+ expect(parseProjectPrefix('')).toBeNull();
427
+ });
428
+ });
429
+
430
+ describe('formatProjectNumber (base36)', () => {
431
+ it('should use numeric format for numbers 1-999', () => {
432
+ expect(formatProjectNumber(1)).toBe('001');
433
+ expect(formatProjectNumber(999)).toBe('999');
434
+ expect(formatProjectNumber(500)).toBe('500');
435
+ });
436
+
437
+ it('should use base36 format for numbers >= 1000', () => {
438
+ expect(formatProjectNumber(1000)).toBe('a00');
439
+ expect(formatProjectNumber(1001)).toBe('a01');
440
+ expect(formatProjectNumber(2296)).toBe('b00');
441
+ });
442
+
443
+ it('should correctly format transition from 999 to 1000', () => {
444
+ expect(formatProjectNumber(999)).toBe('999');
445
+ expect(formatProjectNumber(1000)).toBe('a00');
446
+ });
447
+ });
448
+
449
+ describe('extractProjectNumber (base36)', () => {
450
+ it('should extract base36 project number from path', () => {
451
+ expect(extractProjectNumber('/RAF/a00-my-project')).toBe('a00');
452
+ expect(extractProjectNumber('/RAF/a01-another-project')).toBe('a01');
453
+ expect(extractProjectNumber('/RAF/b00-third-project')).toBe('b00');
454
+ });
455
+
456
+ it('should handle uppercase base36 prefixes', () => {
457
+ expect(extractProjectNumber('/RAF/A00-my-project')).toBe('a00');
458
+ });
459
+
460
+ it('should prefer numeric format when ambiguous', () => {
461
+ // Numeric should be matched first
462
+ expect(extractProjectNumber('/RAF/001-my-project')).toBe('001');
463
+ });
464
+ });
465
+
466
+ describe('extractProjectName (base36)', () => {
467
+ it('should extract project name from base36 numbered path', () => {
468
+ expect(extractProjectName('/RAF/a00-my-project')).toBe('my-project');
469
+ expect(extractProjectName('/RAF/b00-another-project')).toBe('another-project');
470
+ });
471
+
472
+ it('should handle uppercase base36 prefixes', () => {
473
+ expect(extractProjectName('/RAF/A00-my-project')).toBe('my-project');
474
+ });
475
+ });
476
+
477
+ describe('getNextProjectNumber (base36)', () => {
478
+ it('should return next number after base36 projects', () => {
479
+ fs.mkdirSync(path.join(tempDir, 'a00-project1000'));
480
+ expect(getNextProjectNumber(tempDir)).toBe(1001);
481
+ });
482
+
483
+ it('should handle mixed numeric and base36 projects', () => {
484
+ fs.mkdirSync(path.join(tempDir, '999-last-numeric'));
485
+ fs.mkdirSync(path.join(tempDir, 'a00-first-base36'));
486
+ expect(getNextProjectNumber(tempDir)).toBe(1001);
487
+ });
488
+
489
+ it('should find max across mixed formats', () => {
490
+ fs.mkdirSync(path.join(tempDir, '001-first'));
491
+ fs.mkdirSync(path.join(tempDir, 'a05-middle'));
492
+ fs.mkdirSync(path.join(tempDir, '500-later'));
493
+ // a05 = 1005, which is the max
494
+ expect(getNextProjectNumber(tempDir)).toBe(1006);
495
+ });
496
+ });
497
+
498
+ describe('listProjects (base36)', () => {
499
+ it('should list base36 projects with correct number', () => {
500
+ fs.mkdirSync(path.join(tempDir, 'a00-project1000'));
501
+ const projects = listProjects(tempDir);
502
+ expect(projects).toHaveLength(1);
503
+ expect(projects[0]?.number).toBe(1000);
504
+ expect(projects[0]?.name).toBe('project1000');
505
+ });
506
+
507
+ it('should sort mixed numeric and base36 projects correctly', () => {
508
+ fs.mkdirSync(path.join(tempDir, 'a00-project1000'));
509
+ fs.mkdirSync(path.join(tempDir, '001-first'));
510
+ fs.mkdirSync(path.join(tempDir, '999-last-numeric'));
511
+
512
+ const projects = listProjects(tempDir);
513
+ expect(projects).toHaveLength(3);
514
+ expect(projects[0]?.number).toBe(1);
515
+ expect(projects[1]?.number).toBe(999);
516
+ expect(projects[2]?.number).toBe(1000);
517
+ });
518
+ });
519
+
520
+ describe('resolveProjectIdentifier (base36)', () => {
521
+ it('should resolve base36 project by prefix', () => {
522
+ fs.mkdirSync(path.join(tempDir, 'a00-my-project'));
523
+ const result = resolveProjectIdentifier(tempDir, 'a00');
524
+ expect(result).toBe(path.join(tempDir, 'a00-my-project'));
525
+ });
526
+
527
+ it('should resolve base36 project by numeric value', () => {
528
+ fs.mkdirSync(path.join(tempDir, 'a00-my-project'));
529
+ const result = resolveProjectIdentifier(tempDir, '1000');
530
+ expect(result).toBe(path.join(tempDir, 'a00-my-project'));
531
+ });
532
+
533
+ it('should resolve base36 project by name', () => {
534
+ fs.mkdirSync(path.join(tempDir, 'a00-my-project'));
535
+ const result = resolveProjectIdentifier(tempDir, 'my-project');
536
+ expect(result).toBe(path.join(tempDir, 'a00-my-project'));
537
+ });
538
+
539
+ it('should handle mixed projects correctly', () => {
540
+ fs.mkdirSync(path.join(tempDir, '001-first'));
541
+ fs.mkdirSync(path.join(tempDir, 'a00-second'));
542
+
543
+ expect(resolveProjectIdentifier(tempDir, '1')).toBe(path.join(tempDir, '001-first'));
544
+ expect(resolveProjectIdentifier(tempDir, '1000')).toBe(path.join(tempDir, 'a00-second'));
545
+ expect(resolveProjectIdentifier(tempDir, 'a00')).toBe(path.join(tempDir, 'a00-second'));
546
+ expect(resolveProjectIdentifier(tempDir, 'first')).toBe(path.join(tempDir, '001-first'));
547
+ expect(resolveProjectIdentifier(tempDir, 'second')).toBe(path.join(tempDir, 'a00-second'));
548
+ });
549
+ });
550
+
551
+ describe('resolveProjectIdentifier (full folder name)', () => {
552
+ it('should resolve project by full numeric folder name', () => {
553
+ fs.mkdirSync(path.join(tempDir, '001-fix-stuff'));
554
+ const result = resolveProjectIdentifier(tempDir, '001-fix-stuff');
555
+ expect(result).toBe(path.join(tempDir, '001-fix-stuff'));
556
+ });
557
+
558
+ it('should resolve project by full base36 folder name', () => {
559
+ fs.mkdirSync(path.join(tempDir, 'a01-important-project'));
560
+ const result = resolveProjectIdentifier(tempDir, 'a01-important-project');
561
+ expect(result).toBe(path.join(tempDir, 'a01-important-project'));
562
+ });
563
+
564
+ it('should resolve project with hyphens in name', () => {
565
+ fs.mkdirSync(path.join(tempDir, '001-my-cool-project'));
566
+ const result = resolveProjectIdentifier(tempDir, '001-my-cool-project');
567
+ expect(result).toBe(path.join(tempDir, '001-my-cool-project'));
568
+ });
569
+
570
+ it('should return null for wrong prefix with correct name format', () => {
571
+ fs.mkdirSync(path.join(tempDir, '001-correct-name'));
572
+ // Full folder format but wrong prefix - should NOT match
573
+ const result = resolveProjectIdentifier(tempDir, '002-correct-name');
574
+ expect(result).toBeNull();
575
+ });
576
+
577
+ it('should return null for correct prefix with wrong name format', () => {
578
+ fs.mkdirSync(path.join(tempDir, '001-correct-name'));
579
+ // Full folder format but wrong name - should NOT match
580
+ const result = resolveProjectIdentifier(tempDir, '001-wrong-name');
581
+ expect(result).toBeNull();
582
+ });
583
+
584
+ it('should handle case-insensitive folder matching', () => {
585
+ fs.mkdirSync(path.join(tempDir, 'A01-My-Project'));
586
+ // Lowercase identifier should still match
587
+ const result = resolveProjectIdentifier(tempDir, 'a01-my-project');
588
+ expect(result).toBe(path.join(tempDir, 'A01-My-Project'));
589
+ });
590
+
591
+ it('should resolve 2-digit prefix full folder names', () => {
592
+ fs.mkdirSync(path.join(tempDir, '01-first-project'));
593
+ const result = resolveProjectIdentifier(tempDir, '01-first-project');
594
+ expect(result).toBe(path.join(tempDir, '01-first-project'));
595
+ });
596
+
597
+ it('should still resolve by name alone after full folder check', () => {
598
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
599
+ // Name-only should still work via fallback
600
+ const result = resolveProjectIdentifier(tempDir, 'my-project');
601
+ expect(result).toBe(path.join(tempDir, '001-my-project'));
602
+ });
603
+
604
+ it('should still resolve by number alone after full folder check', () => {
605
+ fs.mkdirSync(path.join(tempDir, '003-my-project'));
606
+ // Number-only should still work via fallback
607
+ const result = resolveProjectIdentifier(tempDir, '3');
608
+ expect(result).toBe(path.join(tempDir, '003-my-project'));
609
+ });
610
+
611
+ it('should prefer exact full folder match over name-only match', () => {
612
+ // Create two projects where one's name is a full folder format
613
+ fs.mkdirSync(path.join(tempDir, '001-project'));
614
+ fs.mkdirSync(path.join(tempDir, '002-001-project')); // Name portion is "001-project"
615
+
616
+ // "001-project" as full folder name should match the first project
617
+ const result = resolveProjectIdentifier(tempDir, '001-project');
618
+ expect(result).toBe(path.join(tempDir, '001-project'));
619
+ });
620
+ });
621
+
622
+ describe('resolveProjectIdentifier (case-insensitive name matching)', () => {
623
+ it('should match project name case-insensitively', () => {
624
+ fs.mkdirSync(path.join(tempDir, '001-fix-double-summary-headers'));
625
+
626
+ // All these should match
627
+ expect(resolveProjectIdentifier(tempDir, 'fix-double-summary-headers')).toBe(
628
+ path.join(tempDir, '001-fix-double-summary-headers')
629
+ );
630
+ expect(resolveProjectIdentifier(tempDir, 'Fix-Double-Summary-Headers')).toBe(
631
+ path.join(tempDir, '001-fix-double-summary-headers')
632
+ );
633
+ expect(resolveProjectIdentifier(tempDir, 'FIX-DOUBLE-SUMMARY-HEADERS')).toBe(
634
+ path.join(tempDir, '001-fix-double-summary-headers')
635
+ );
636
+ });
637
+
638
+ it('should match mixed case project name', () => {
639
+ fs.mkdirSync(path.join(tempDir, '001-MyProject'));
640
+
641
+ expect(resolveProjectIdentifier(tempDir, 'myproject')).toBe(
642
+ path.join(tempDir, '001-MyProject')
643
+ );
644
+ expect(resolveProjectIdentifier(tempDir, 'MYPROJECT')).toBe(
645
+ path.join(tempDir, '001-MyProject')
646
+ );
647
+ expect(resolveProjectIdentifier(tempDir, 'MyProject')).toBe(
648
+ path.join(tempDir, '001-MyProject')
649
+ );
650
+ });
651
+ });
652
+
653
+ describe('resolveProjectIdentifierWithDetails', () => {
654
+ it('should return path for unique name match', () => {
655
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
656
+
657
+ const result = resolveProjectIdentifierWithDetails(tempDir, 'my-project');
658
+ expect(result.path).toBe(path.join(tempDir, '001-my-project'));
659
+ expect(result.error).toBeUndefined();
660
+ expect(result.matches).toBeUndefined();
661
+ });
662
+
663
+ it('should return ambiguous error for multiple projects with same name', () => {
664
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
665
+ fs.mkdirSync(path.join(tempDir, '002-my-project'));
666
+
667
+ const result = resolveProjectIdentifierWithDetails(tempDir, 'my-project');
668
+ expect(result.path).toBeNull();
669
+ expect(result.error).toBe('ambiguous');
670
+ expect(result.matches).toHaveLength(2);
671
+ expect(result.matches?.[0]?.folder).toBe('001-my-project');
672
+ expect(result.matches?.[1]?.folder).toBe('002-my-project');
673
+ });
674
+
675
+ it('should return ambiguous error for case-insensitive duplicate names', () => {
676
+ fs.mkdirSync(path.join(tempDir, '001-MyProject'));
677
+ fs.mkdirSync(path.join(tempDir, '002-myproject'));
678
+
679
+ const result = resolveProjectIdentifierWithDetails(tempDir, 'myproject');
680
+ expect(result.path).toBeNull();
681
+ expect(result.error).toBe('ambiguous');
682
+ expect(result.matches).toHaveLength(2);
683
+ });
684
+
685
+ it('should return not_found error for non-existent project', () => {
686
+ fs.mkdirSync(path.join(tempDir, '001-existing-project'));
687
+
688
+ const result = resolveProjectIdentifierWithDetails(tempDir, 'non-existent');
689
+ expect(result.path).toBeNull();
690
+ expect(result.error).toBe('not_found');
691
+ expect(result.matches).toBeUndefined();
692
+ });
693
+
694
+ it('should return not_found error for non-existent directory', () => {
695
+ const result = resolveProjectIdentifierWithDetails('/non/existent/path', 'project');
696
+ expect(result.path).toBeNull();
697
+ expect(result.error).toBe('not_found');
698
+ });
699
+
700
+ it('should resolve by number even with duplicate names', () => {
701
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
702
+ fs.mkdirSync(path.join(tempDir, '002-my-project'));
703
+
704
+ // By number should still work unambiguously
705
+ const result1 = resolveProjectIdentifierWithDetails(tempDir, '1');
706
+ expect(result1.path).toBe(path.join(tempDir, '001-my-project'));
707
+ expect(result1.error).toBeUndefined();
708
+
709
+ const result2 = resolveProjectIdentifierWithDetails(tempDir, '2');
710
+ expect(result2.path).toBe(path.join(tempDir, '002-my-project'));
711
+ expect(result2.error).toBeUndefined();
712
+ });
713
+
714
+ it('should resolve by full folder name even with duplicate names', () => {
715
+ fs.mkdirSync(path.join(tempDir, '001-my-project'));
716
+ fs.mkdirSync(path.join(tempDir, '002-my-project'));
717
+
718
+ // Full folder name should work unambiguously
719
+ const result1 = resolveProjectIdentifierWithDetails(tempDir, '001-my-project');
720
+ expect(result1.path).toBe(path.join(tempDir, '001-my-project'));
721
+ expect(result1.error).toBeUndefined();
722
+
723
+ const result2 = resolveProjectIdentifierWithDetails(tempDir, '002-my-project');
724
+ expect(result2.path).toBe(path.join(tempDir, '002-my-project'));
725
+ expect(result2.error).toBeUndefined();
726
+ });
727
+
728
+ it('should sort matches by project number', () => {
729
+ fs.mkdirSync(path.join(tempDir, '005-duplicate'));
730
+ fs.mkdirSync(path.join(tempDir, '001-duplicate'));
731
+ fs.mkdirSync(path.join(tempDir, '003-duplicate'));
732
+
733
+ const result = resolveProjectIdentifierWithDetails(tempDir, 'duplicate');
734
+ expect(result.error).toBe('ambiguous');
735
+ expect(result.matches).toHaveLength(3);
736
+ expect(result.matches?.[0]?.number).toBe(1);
737
+ expect(result.matches?.[1]?.number).toBe(3);
738
+ expect(result.matches?.[2]?.number).toBe(5);
739
+ });
740
+ });
741
+ });