savepoint 1.0.1 → 1.0.3

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 (278) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +52 -46
  4. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
  5. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
  6. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
  7. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
  8. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/{Design.md → E06-Detail.md} +5 -3
  9. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
  10. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
  11. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
  12. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +2 -0
  13. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/{Design.md → E01-Detail.md} +9 -1
  14. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/{T007-next-activity-header.md → T001-next-activity-header.md} +13 -12
  15. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +9 -9
  16. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +2 -2
  17. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +13 -12
  18. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +14 -13
  19. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +25 -15
  20. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
  21. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md +124 -0
  22. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/{Design.md → E02-Detail.md} +12 -3
  23. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +11 -8
  24. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +12 -7
  25. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +9 -5
  26. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +30 -9
  27. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  28. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +45 -0
  29. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
  30. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
  31. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
  32. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
  33. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
  34. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Audit.md +167 -0
  35. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
  36. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
  37. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
  38. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
  39. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  40. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +54 -0
  41. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +45 -0
  42. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +40 -0
  43. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +47 -0
  44. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +98 -0
  45. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +33 -0
  46. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +62 -0
  47. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  48. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  49. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  50. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  51. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  52. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  53. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  54. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  55. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  56. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  57. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  58. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  59. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  60. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  61. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  62. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  63. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  64. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  65. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  66. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  67. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  68. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  69. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  70. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  71. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  72. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  73. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  74. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  75. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  76. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  77. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  78. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  79. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  80. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  81. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  83. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  84. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  85. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  86. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  87. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  88. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  89. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  90. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  91. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  92. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  93. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  94. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  95. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  96. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  97. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  98. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  99. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  100. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  101. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  102. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  103. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  104. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  105. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  106. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  107. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  108. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  109. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  110. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  111. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  112. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  113. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  114. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  115. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  116. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  117. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  118. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  119. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  120. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  121. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  122. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  123. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  124. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  125. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  126. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  127. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  128. package/.savepoint/releases/v1.1/v1.1-PRD.md +139 -0
  129. package/.savepoint/router.md +29 -108
  130. package/AGENTS.md +69 -111
  131. package/Makefile +19 -3
  132. package/README.md +6 -6
  133. package/agent-skills/savepoint-audit/SKILL.md +87 -35
  134. package/agent-skills/savepoint-build-task/SKILL.md +9 -4
  135. package/agent-skills/savepoint-create-plan/SKILL.md +10 -5
  136. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  137. package/agent-skills/savepoint-draft-prd/SKILL.md +8 -3
  138. package/agent-skills/savepoint-system-design/SKILL.md +8 -3
  139. package/agent_skills_test.go +91 -0
  140. package/cmd/board.go +59 -0
  141. package/cmd/board_test.go +137 -0
  142. package/cmd/doctor.go +53 -0
  143. package/cmd/doctor_test.go +146 -0
  144. package/cmd/init.go +63 -0
  145. package/cmd/init_test.go +104 -0
  146. package/internal/board/board.go +69 -49
  147. package/internal/board/board_test.go +83 -67
  148. package/internal/board/card.go +71 -20
  149. package/internal/board/card_test.go +141 -12
  150. package/internal/board/column.go +77 -11
  151. package/internal/board/column_test.go +63 -13
  152. package/internal/board/detail.go +107 -72
  153. package/internal/board/detail_test.go +117 -26
  154. package/internal/board/epic_panel.go +211 -18
  155. package/internal/board/epic_panel_test.go +637 -14
  156. package/internal/board/help.go +1 -0
  157. package/internal/board/help_test.go +1 -0
  158. package/internal/board/integration_test.go +266 -0
  159. package/internal/board/interfaces.go +65 -0
  160. package/internal/board/interfaces_test.go +114 -0
  161. package/internal/board/io.go +93 -0
  162. package/internal/board/layout.go +12 -2
  163. package/internal/board/layout_test.go +17 -0
  164. package/internal/board/model.go +130 -52
  165. package/internal/board/plain.go +88 -0
  166. package/internal/board/plain_test.go +117 -0
  167. package/internal/board/release.go +1 -9
  168. package/internal/board/release_test.go +6 -6
  169. package/internal/board/render_policy_test.go +77 -0
  170. package/internal/board/status.go +23 -0
  171. package/internal/board/theme.go +24 -0
  172. package/internal/board/theme_test.go +31 -0
  173. package/internal/board/transitions.go +113 -88
  174. package/internal/board/transitions_test.go +164 -141
  175. package/internal/board/tui.go +32 -0
  176. package/internal/board/update.go +472 -94
  177. package/internal/board/update_test.go +447 -0
  178. package/internal/board/util.go +76 -0
  179. package/internal/board/view.go +139 -22
  180. package/internal/board/view_test.go +171 -3
  181. package/internal/board/watch.go +57 -9
  182. package/internal/buildtool/main.go +211 -0
  183. package/internal/buildtool/main_test.go +46 -0
  184. package/internal/data/config.go +17 -3
  185. package/internal/data/config_test.go +49 -0
  186. package/internal/data/discover.go +26 -0
  187. package/internal/data/discover_test.go +34 -10
  188. package/internal/data/errors.go +4 -0
  189. package/internal/data/lifecycle.go +13 -6
  190. package/internal/data/lifecycle_test.go +14 -11
  191. package/internal/data/parser.go +29 -6
  192. package/internal/data/parser_test.go +66 -7
  193. package/internal/data/task.go +1 -0
  194. package/internal/data/write.go +85 -11
  195. package/internal/data/write_test.go +167 -0
  196. package/internal/doctor/checks.go +567 -0
  197. package/internal/doctor/checks_test.go +716 -0
  198. package/internal/doctor/gates.go +193 -0
  199. package/internal/doctor/gates_test.go +166 -0
  200. package/internal/doctor/interfaces.go +64 -0
  201. package/internal/doctor/interfaces_test.go +104 -0
  202. package/internal/doctor/repairs.go +80 -0
  203. package/internal/doctor/repairs_test.go +81 -0
  204. package/internal/doctor/report.go +157 -0
  205. package/internal/doctor/report_test.go +89 -0
  206. package/internal/init/clipboard.go +146 -0
  207. package/internal/init/clipboard_test.go +74 -0
  208. package/internal/init/install.go +16 -0
  209. package/internal/init/integration_test.go +197 -0
  210. package/internal/init/prompt.go +14 -0
  211. package/internal/init/prompt_test.go +77 -0
  212. package/internal/init/scaffold.go +59 -0
  213. package/internal/init/scaffold_test.go +179 -0
  214. package/internal/init/template_freshness_test.go +56 -0
  215. package/internal/init/validate.go +85 -0
  216. package/internal/init/validate_test.go +141 -0
  217. package/internal/init/write.go +73 -0
  218. package/internal/init/write_test.go +91 -0
  219. package/internal/styles/palette.go +3 -3
  220. package/internal/styles/styles.go +39 -12
  221. package/internal/styles/styles_test.go +133 -0
  222. package/internal/testutil/fixture.go +113 -0
  223. package/internal/testutil/fs.go +26 -0
  224. package/main.go +107 -1
  225. package/package.json +2 -2
  226. package/project-audit/audit_report_glm_5.1.md +411 -0
  227. package/project-audit/audit_report_opus_4.6 +406 -0
  228. package/project-audit/consolidated-audit-report.md +456 -0
  229. package/savepoint +0 -0
  230. package/templates/project/.savepoint/Design.md +2 -2
  231. package/templates/project/.savepoint/router.md +15 -14
  232. package/templates/project/AGENTS.md +56 -98
  233. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  234. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  235. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  236. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  237. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  238. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  239. package/templates/prompts/audit-reconciliation.prompt.md +35 -30
  240. package/templates/prompts/design.prompt.md +3 -1
  241. package/templates/prompts/epic-design.prompt.md +3 -3
  242. package/templates/prompts/task-breakdown.prompt.md +1 -1
  243. package/templates/prompts/task-building.prompt.md +1 -1
  244. package/templates/prompts/task-planning.prompt.md +1 -1
  245. package/.savepoint/audit/E01-go-setup/proposals.md +0 -166
  246. package/.savepoint/audit/E01-go-setup/snapshot.md +0 -71
  247. package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +0 -66
  248. package/.savepoint/audit/E01-scaffolding/proposals/Design.md +0 -210
  249. package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +0 -117
  250. package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +0 -101
  251. package/.savepoint/audit/E01-scaffolding/snapshot.md +0 -54
  252. package/.savepoint/audit/E02-data-model/snapshot.md +0 -128
  253. package/.savepoint/audit/E02-data-readers/proposals.md +0 -123
  254. package/.savepoint/audit/E02-data-readers/snapshot.md +0 -54
  255. package/.savepoint/audit/E03-board-tui-core/proposals.md +0 -146
  256. package/.savepoint/audit/E03-board-tui-core/snapshot.md +0 -57
  257. package/.savepoint/audit/E03-cli-foundation/snapshot.md +0 -106
  258. package/.savepoint/audit/E04-board-components/proposals.md +0 -118
  259. package/.savepoint/audit/E04-board-components/snapshot.md +0 -77
  260. package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +0 -115
  261. package/.savepoint/audit/E05-init-command/snapshot.md +0 -125
  262. package/.savepoint/audit/E05-phase-transitions/proposals.md +0 -83
  263. package/.savepoint/audit/E05-phase-transitions/snapshot.md +0 -36
  264. package/.savepoint/audit/E06-atari-noir-layout/proposals.md +0 -130
  265. package/.savepoint/audit/E06-atari-noir-layout/snapshot.md +0 -84
  266. package/.savepoint/audit/E06-tui-board/snapshot.md +0 -64
  267. package/.savepoint/audit/E07-audit-pipeline/snapshot.md +0 -165
  268. package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +0 -65
  269. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -36
  270. package/ink-cli-ui-design.zip +0 -0
  271. package/main.exe +0 -0
  272. package/savepoint.exe +0 -0
  273. /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
  274. /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
  275. /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
  276. /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
  277. /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
  278. /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
@@ -1,6 +1,7 @@
1
1
  package board
2
2
 
3
3
  import (
4
+ "fmt"
4
5
  "strings"
5
6
 
6
7
  "github.com/charmbracelet/lipgloss"
@@ -19,21 +20,34 @@ func (m Model) View() string {
19
20
  w = defaultTermW
20
21
  }
21
22
  m.Width = w
23
+ h := m.Height
24
+ if h == 0 {
25
+ h = defaultTermH
26
+ }
27
+ m.Height = h
22
28
 
23
- layout := CalculateLayout(w, m.Height)
24
- icon := styles.HeaderIcon.Render("▣")
25
- text := styles.HeaderText.Render("S A V E P O I N T")
26
- header := styles.HeaderFrame.Width(w).Render(icon + " " + text)
29
+ header := m.renderHeader(w)
30
+ nextActivity := m.renderNextActivityLine(w)
31
+ extra := extraHeaderLines(nextActivity)
32
+ layout := CalculateLayoutWithChrome(w, h, extra)
27
33
  topDivider := dividerLine(w)
28
34
  board := m.renderBoard(layout)
35
+ boardBudget := h - 8 - extra
36
+ if boardBudget < 0 {
37
+ boardBudget = 0
38
+ }
39
+ boardLines := strings.Split(board, "\n")
40
+ if len(boardLines) > boardBudget {
41
+ board = strings.Join(boardLines[:boardBudget], "\n")
42
+ }
29
43
  bottomDivider := dividerLine(w)
30
44
  footer := m.renderFooter(w)
31
- base := lipgloss.JoinVertical(lipgloss.Left, header, topDivider, board, bottomDivider, footer)
32
-
33
- h := m.Height
34
- if h == 0 {
35
- h = defaultTermH
45
+ sections := []string{header}
46
+ if nextActivity != "" {
47
+ sections = append(sections, nextActivity)
36
48
  }
49
+ sections = append(sections, topDivider, board, bottomDivider, footer)
50
+ base := lipgloss.JoinVertical(lipgloss.Left, sections...)
37
51
 
38
52
  if m.Overlay == OverlayEpic {
39
53
  overlay := RenderEpicDropdown(m.Epics, m.EpicCursor, min(40, w))
@@ -56,13 +70,101 @@ func (m Model) View() string {
56
70
  return base
57
71
  }
58
72
  ow := overlayWidth(w)
59
- detail := RenderDetail(task, ow, m.RouterTask)
73
+ detail := RenderDetail(task, ow, m.RouterState, detailMaxHeight(h), m.DetailOffset)
74
+ return overlayOnBase(dimLines(base), detail, w, h)
75
+ }
76
+
77
+ if m.Overlay == OverlayEpicDetail {
78
+ ow := overlayWidth(w)
79
+ epicSlug := m.epicDetailEpic()
80
+ var detail string
81
+ if m.EpicDetailTab == 1 {
82
+ detail = RenderEpicAuditTab(epicSlug, m.EpicAuditContent, ow, detailMaxHeight(h), m.EpicDetailOffset, m.EpicDetailTab)
83
+ } else {
84
+ detail = RenderEpicDetail(epicSlug, m.EpicDetailContent, ow, detailMaxHeight(h), m.EpicDetailOffset, m.EpicDetailTab)
85
+ }
60
86
  return overlayOnBase(dimLines(base), detail, w, h)
61
87
  }
62
88
 
63
89
  return base
64
90
  }
65
91
 
92
+ func (m Model) renderHeader(w int) string {
93
+ icon := styles.HeaderIcon.Render("▣")
94
+ text := styles.HeaderText.Render("S A V E P O I N T")
95
+ left := icon + " " + text
96
+ return styles.HeaderFrame.Width(w).Render(left)
97
+ }
98
+
99
+ func extraHeaderLines(line string) int {
100
+ if line == "" {
101
+ return 0
102
+ }
103
+ return 1
104
+ }
105
+
106
+ func (m Model) renderNextActivityLine(w int) string {
107
+ if w <= 0 {
108
+ w = defaultTermW
109
+ }
110
+ return renderNextActivityLine(m.RouterState, w)
111
+ }
112
+
113
+ func renderNextActivityLine(state *data.RouterState, w int) string {
114
+ tag, style, ok := nextActivityPhase(state)
115
+ if !ok || strings.TrimSpace(state.NextAction) == "" {
116
+ return ""
117
+ }
118
+
119
+ content := style.Render(tag+":") + " " + state.NextAction
120
+ if lipgloss.Width(content) > w {
121
+ content = xansi.Truncate(content, w, "…")
122
+ }
123
+ return styles.RootLine.Width(w).Render(content)
124
+ }
125
+
126
+ func nextActivityPhase(state *data.RouterState) (string, lipgloss.Style, bool) {
127
+ if state == nil {
128
+ return "", lipgloss.Style{}, false
129
+ }
130
+ switch state.State {
131
+ case "task-building":
132
+ return "BUILD", styles.FooterPhaseBuild, true
133
+ case "audit-pending":
134
+ return "AUDIT", styles.FooterPhaseAudit, true
135
+ case "pre-implementation", "epic-design", "epic-task-breakdown":
136
+ return "PLAN", styles.FooterPhasePlan, true
137
+ default:
138
+ return "", lipgloss.Style{}, false
139
+ }
140
+ }
141
+
142
+ // FormatNextActivity formats a compact activity string from router state.
143
+ // Returns empty string when state is nil. Result is capped at 20 visible chars.
144
+ func FormatNextActivity(state *data.RouterState) string {
145
+ if state == nil {
146
+ return ""
147
+ }
148
+ var s string
149
+ switch state.State {
150
+ case "task-building":
151
+ s = fmt.Sprintf("Build %s %s/%s", state.Release, shortID(state.Epic), shortID(state.Task))
152
+ case "audit-pending":
153
+ s = fmt.Sprintf("Audit %s", shortID(state.Epic))
154
+ case "epic-design":
155
+ s = fmt.Sprintf("Design %s", shortID(state.Epic))
156
+ case "epic-task-breakdown":
157
+ s = fmt.Sprintf("Plan %s", shortID(state.Epic))
158
+ case "pre-implementation":
159
+ s = fmt.Sprintf("Planning %s", state.Release)
160
+ default:
161
+ s = state.State
162
+ }
163
+ return xansi.Truncate(s, 20, "…")
164
+ }
165
+
166
+
167
+
66
168
  func (m Model) focusedTask() (data.Task, bool) {
67
169
  tasks := m.Tasks[m.FocusedColumn]
68
170
  if len(tasks) == 0 || m.FocusedTask >= len(tasks) {
@@ -140,7 +242,7 @@ func (m Model) renderBoard(layout Layout) string {
140
242
  cols := m.renderColumns(layout)
141
243
  var content string
142
244
  if layout.EpicPanelVisible {
143
- epic := m.renderEpicPanel(layout.EpicPanelWidth)
245
+ epic := m.renderEpicPanel(layout.EpicPanelWidth, layout.ContentHeight)
144
246
  content = lipgloss.JoinHorizontal(lipgloss.Top, epic, cols)
145
247
  } else {
146
248
  content = cols
@@ -150,23 +252,34 @@ func (m Model) renderBoard(layout Layout) string {
150
252
 
151
253
  func (m Model) renderColumns(layout Layout) string {
152
254
  if layout.ColCount == 1 {
153
- return m.renderColumn(m.FocusedColumn, layout.ColWidths[0])
255
+ return m.renderColumn(m.FocusedColumn, layout.ColWidths[0], layout.ContentHeight)
154
256
  }
155
257
  allCols := []data.ColumnType{data.ColumnPlanned, data.ColumnInProgress, data.ColumnDone}
156
258
  rendered := make([]string, len(allCols))
157
259
  for i, col := range allCols {
158
- rendered[i] = m.renderColumn(col, layout.ColWidths[i])
260
+ rendered[i] = m.renderColumn(col, layout.ColWidths[i], layout.ContentHeight)
159
261
  }
160
262
  return lipgloss.JoinHorizontal(lipgloss.Top, rendered...)
161
263
  }
162
264
 
163
- func (m Model) renderEpicPanel(w int) string {
164
- return RenderEpicSidebar(m.Epics, m.SelectedEpic, w)
265
+ func (m Model) renderEpicPanel(w int, maxHeight int) string {
266
+ return RenderEpicSidebar(m.Epics, m.SelectedEpic, w, m.EpicPanelFocus, m.EpicPanelCursor, m.EpicStatus, maxHeight)
267
+ }
268
+
269
+ func (m Model) renderColumn(col data.ColumnType, colW, maxHeight int) string {
270
+ focused := !m.EpicPanelFocus && m.FocusedColumn == col
271
+ return RenderColumn(m.Tasks[col], col, colW, maxHeight, m.ColumnOffsets[col], m.FocusedTask, focused, m.RouterState)
165
272
  }
166
273
 
167
- func (m Model) renderColumn(col data.ColumnType, colW int) string {
168
- focused := m.FocusedColumn == col
169
- return RenderColumn(m.Tasks[col], col, colW, m.FocusedTask, focused, m.RouterTask)
274
+ func detailMaxHeight(termH int) int {
275
+ if termH <= 0 {
276
+ termH = defaultTermH
277
+ }
278
+ h := termH * 7 / 10
279
+ if h < 6 {
280
+ h = 6
281
+ }
282
+ return h
170
283
  }
171
284
 
172
285
  func (m Model) renderFooter(termW int) string {
@@ -177,9 +290,13 @@ func (m Model) renderFooter(termW int) string {
177
290
  styles.FooterDivider.Render(" │ ")+
178
291
  styles.FooterPhaseAudit.Render("AUDIT"),
179
292
  )
180
- hints := footerLine(termW, styles.FooterHints.Render("←/→:nav E:epic R:release ?:help q:quit"))
181
- spacer := footerLine(termW, "")
182
- return lipgloss.JoinVertical(lipgloss.Center, phase, spacer, hints)
293
+ hints := footerLine(termW, styles.FooterHints.Render("←/→:nav p: Priority R:release ?:help q:quit"))
294
+ status := ""
295
+ if m.StatusMessage != "" {
296
+ status = styles.StatusBar.Render(m.StatusMessage)
297
+ }
298
+ statusLine := footerLine(termW, status)
299
+ return lipgloss.JoinVertical(lipgloss.Center, phase, statusLine, hints)
183
300
  }
184
301
 
185
302
  func dividerLine(termW int) string {
@@ -196,5 +313,5 @@ func footerLine(termW int, content string) string {
196
313
  if lipgloss.Width(content) > termW {
197
314
  content = xansi.Truncate(content, termW, "")
198
315
  }
199
- return lipgloss.NewStyle().Width(termW).Align(lipgloss.Center).Render(content)
316
+ return styles.RootLine.Width(termW).Align(lipgloss.Center).Render(content)
200
317
  }
@@ -66,7 +66,7 @@ func TestView_containsFooterHints(t *testing.T) {
66
66
  m := NewModel(nil, "v1", "E03")
67
67
  footer := m.renderFooter(80)
68
68
 
69
- if !strings.Contains(footer, "←/→:nav E:epic R:release ?:help q:quit") {
69
+ if !strings.Contains(footer, "←/→:nav p: Priority R:release ?:help q:quit") {
70
70
  t.Fatal("renderFooter() missing navigation hints")
71
71
  }
72
72
 
@@ -74,8 +74,8 @@ func TestView_containsFooterHints(t *testing.T) {
74
74
  if len(lines) != 3 {
75
75
  t.Fatalf("renderFooter() returned %d lines, want 3", len(lines))
76
76
  }
77
- if strings.TrimSpace(lines[1]) != "" {
78
- t.Fatalf("renderFooter() spacer line = %q, want blank", lines[1])
77
+ if strings.TrimSpace(plainTerminal(lines[1])) != "" {
78
+ t.Fatalf("renderFooter() status line = %q, want blank", lines[1])
79
79
  }
80
80
  for i, line := range lines {
81
81
  if got := lipgloss.Width(line); got > 80 {
@@ -84,6 +84,16 @@ func TestView_containsFooterHints(t *testing.T) {
84
84
  }
85
85
  }
86
86
 
87
+ func TestView_footerRendersStatusMessage(t *testing.T) {
88
+ m := NewModel(nil, "v1", "E03")
89
+ m.StatusMessage = "Router set to v1.1 E05-tasking-permissions/T004"
90
+ footer := plainTerminal(m.renderFooter(80))
91
+
92
+ if !strings.Contains(footer, "Router set to v1.1 E05-tasking-permissions/T004") {
93
+ t.Fatal("renderFooter() missing status message")
94
+ }
95
+ }
96
+
87
97
  func TestView_containsBottomDivider(t *testing.T) {
88
98
  m := NewModel(nil, "v1", "E03")
89
99
  m.Width = 120
@@ -133,6 +143,164 @@ func TestView_wideShowsEpicPanel(t *testing.T) {
133
143
  }
134
144
  }
135
145
 
146
+ func TestFormatNextActivity_nil(t *testing.T) {
147
+ if got := FormatNextActivity(nil); got != "" {
148
+ t.Errorf("FormatNextActivity(nil) = %q, want empty", got)
149
+ }
150
+ }
151
+
152
+ func TestFormatNextActivity_states(t *testing.T) {
153
+ cases := []struct {
154
+ state *data.RouterState
155
+ want string
156
+ }{
157
+ {&data.RouterState{State: "task-building", Task: "T010", Epic: "E06", Release: "v1"}, "Build v1 E06/T010"},
158
+ {&data.RouterState{State: "audit-pending", Epic: "E06"}, "Audit E06"},
159
+ {&data.RouterState{State: "epic-design", Epic: "E06"}, "Design E06"},
160
+ {&data.RouterState{State: "epic-task-breakdown", Epic: "E06"}, "Plan E06"},
161
+ {&data.RouterState{State: "pre-implementation", Release: "v1"}, "Planning v1"},
162
+ }
163
+ for _, c := range cases {
164
+ got := FormatNextActivity(c.state)
165
+ if got != c.want {
166
+ t.Errorf("FormatNextActivity(%q) = %q, want %q", c.state.State, got, c.want)
167
+ }
168
+ }
169
+ }
170
+
171
+ func TestFormatNextActivity_truncation(t *testing.T) {
172
+ state := &data.RouterState{State: "task-building", Task: "T001", Epic: "E01-very-long-epic-name", Release: "v1.1"}
173
+ got := FormatNextActivity(state)
174
+ if lipgloss.Width(got) > 20 {
175
+ t.Errorf("FormatNextActivity truncation: width %d > 20, got %q", lipgloss.Width(got), got)
176
+ }
177
+ }
178
+
179
+ func TestFormatNextActivity_taskBuildingKeepsMinorReleaseVisible(t *testing.T) {
180
+ state := &data.RouterState{State: "task-building", Task: "T001", Epic: "E03", Release: "v1.1"}
181
+ got := FormatNextActivity(state)
182
+ if got != "Build v1.1 E03/T001" {
183
+ t.Errorf("FormatNextActivity() = %q, want Build v1.1 E03/T001", got)
184
+ }
185
+ }
186
+
187
+ func TestView_headerShowsNextActivity(t *testing.T) {
188
+ m := NewModel(nil, "v1", "E03")
189
+ m.Width = 120
190
+ m.RouterState = &data.RouterState{State: "audit-pending", NextAction: "Audit E06"}
191
+ got := m.View()
192
+ if !strings.Contains(got, "AUDIT:") {
193
+ t.Error("View() missing Next Activity phase tag")
194
+ }
195
+ if !strings.Contains(got, "Audit E06") {
196
+ t.Error("View() missing activity text below header")
197
+ }
198
+ if strings.Contains(got, "Next Activity:") {
199
+ t.Error("View() should not render Next Activity inside the header")
200
+ }
201
+ }
202
+
203
+ func TestView_nextActivityLineImmediatelyBelowHeader(t *testing.T) {
204
+ m := NewModel(nil, "v1", "E03")
205
+ m.Width = 120
206
+ m.RouterState = &data.RouterState{State: "task-building", NextAction: "Build T010 (E06) v1"}
207
+ got := m.View()
208
+
209
+ lines := strings.Split(got, "\n")
210
+ headerIndex := -1
211
+ activityIndex := -1
212
+ dividerIndex := -1
213
+ for i, line := range lines {
214
+ if strings.Contains(line, "S A V E P O I N T") {
215
+ headerIndex = i
216
+ }
217
+ if strings.Contains(line, "BUILD:") && strings.Contains(line, "Build T010 (E06) v1") {
218
+ activityIndex = i
219
+ }
220
+ if dividerIndex == -1 && strings.Contains(line, strings.Repeat("─", 120)) {
221
+ dividerIndex = i
222
+ }
223
+ }
224
+ if headerIndex == -1 || activityIndex == -1 || dividerIndex == -1 {
225
+ t.Fatalf("View() missing expected header/activity/divider lines: header=%d activity=%d divider=%d", headerIndex, activityIndex, dividerIndex)
226
+ }
227
+ if !(headerIndex < activityIndex && activityIndex < dividerIndex) {
228
+ t.Fatalf("Next Activity line order invalid: header=%d activity=%d divider=%d", headerIndex, activityIndex, dividerIndex)
229
+ }
230
+ }
231
+
232
+ func TestView_headerNoActivityWhenNilState(t *testing.T) {
233
+ m := NewModel(nil, "v1", "E03")
234
+ m.Width = 120
235
+ m.RouterState = nil
236
+ got := m.View()
237
+ if strings.Contains(got, "Next Activity:") || strings.Contains(got, "BUILD:") || strings.Contains(got, "PLAN:") || strings.Contains(got, "AUDIT:") {
238
+ t.Error("View() should not show Next Activity line when RouterState is nil")
239
+ }
240
+ }
241
+
242
+ func TestView_headerNarrowWidth(t *testing.T) {
243
+ m := NewModel(nil, "v1", "E03")
244
+ m.Width = 40
245
+ m.RouterState = &data.RouterState{State: "audit-pending", NextAction: "Audit E06"}
246
+ got := m.View()
247
+ // Should not panic and header text should still be present
248
+ if !strings.Contains(got, "S A V E P O I N T") {
249
+ t.Error("View() at narrow width missing header text")
250
+ }
251
+ }
252
+
253
+ func TestRenderNextActivityLine_phaseMapping(t *testing.T) {
254
+ cases := []struct {
255
+ name string
256
+ state *data.RouterState
257
+ tag string
258
+ }{
259
+ {"build", &data.RouterState{State: "task-building", NextAction: "Build T010 (E06) v1"}, "BUILD:"},
260
+ {"audit", &data.RouterState{State: "audit-pending", NextAction: "Audit E03"}, "AUDIT:"},
261
+ {"pre implementation", &data.RouterState{State: "pre-implementation", NextAction: "Plan v1.1"}, "PLAN:"},
262
+ {"epic design", &data.RouterState{State: "epic-design", NextAction: "Design E03"}, "PLAN:"},
263
+ {"task breakdown", &data.RouterState{State: "epic-task-breakdown", NextAction: "Break down E03"}, "PLAN:"},
264
+ }
265
+ for _, tc := range cases {
266
+ t.Run(tc.name, func(t *testing.T) {
267
+ got := renderNextActivityLine(tc.state, 80)
268
+ if !strings.Contains(got, tc.tag) {
269
+ t.Fatalf("renderNextActivityLine() missing phase tag %q in %q", tc.tag, got)
270
+ }
271
+ if !strings.Contains(got, tc.state.NextAction) {
272
+ t.Fatalf("renderNextActivityLine() missing next_action %q in %q", tc.state.NextAction, got)
273
+ }
274
+ })
275
+ }
276
+ }
277
+
278
+ func TestRenderNextActivityLine_hiddenStates(t *testing.T) {
279
+ cases := []*data.RouterState{
280
+ nil,
281
+ {State: "idle", NextAction: "Wait"},
282
+ {State: "task-building", NextAction: ""},
283
+ }
284
+ for _, state := range cases {
285
+ if got := renderNextActivityLine(state, 80); got != "" {
286
+ t.Fatalf("renderNextActivityLine(%v) = %q, want empty", state, got)
287
+ }
288
+ }
289
+ }
290
+
291
+ func TestRenderNextActivityLine_truncatesAtNarrowWidth(t *testing.T) {
292
+ got := renderNextActivityLine(&data.RouterState{
293
+ State: "pre-implementation",
294
+ NextAction: "Build T010 (E06) v1 with a very long follow-up activity",
295
+ }, 18)
296
+ if lipgloss.Width(got) > 18 {
297
+ t.Fatalf("renderNextActivityLine() width = %d, want <= 18; got %q", lipgloss.Width(got), got)
298
+ }
299
+ if !strings.Contains(got, "PLAN:") || !strings.Contains(got, "…") {
300
+ t.Fatalf("renderNextActivityLine() = %q, want PLAN tag and ellipsis", got)
301
+ }
302
+ }
303
+
136
304
  func TestView_narrowShowsSingleColumn(t *testing.T) {
137
305
  m := NewModel(nil, "v1", "E03")
138
306
  m.Width = 60
@@ -11,26 +11,58 @@ import (
11
11
  )
12
12
 
13
13
  type fileChangeMsg struct{}
14
- type reloadMsg struct{ tasks []data.Task }
14
+ type reloadMsg struct {
15
+ tasks []data.Task
16
+ releases []string
17
+ releaseEpics map[string][]string
18
+ epicStatuses map[string]string
19
+ routerState *data.RouterState
20
+ }
21
+
22
+ type routerWriteMsg struct {
23
+ message string
24
+ state *data.RouterState
25
+ taskID string
26
+ }
27
+
28
+ type taskWriteMsg struct {
29
+ prefix string
30
+ next data.Task
31
+ err error
32
+ }
33
+
34
+ type epicDetailMsg struct {
35
+ content string
36
+ }
37
+
38
+ type auditContentMsg struct {
39
+ content string
40
+ }
41
+
42
+ type errorMsg struct {
43
+ message string
44
+ }
15
45
 
16
46
  // watchFiles blocks until a file event arrives, debounces for 100ms, emits fileChangeMsg.
17
47
  func watchFiles(w *fsnotify.Watcher) tea.Cmd {
18
48
  return func() tea.Msg {
19
49
  for {
20
50
  select {
21
- case _, ok := <-w.Events:
51
+ case event, ok := <-w.Events:
22
52
  if !ok {
23
53
  return nil
24
54
  }
55
+ watchCreatedDir(w, event)
25
56
  timer := time.NewTimer(100 * time.Millisecond)
26
57
  drain:
27
58
  for {
28
59
  select {
29
- case _, ok := <-w.Events:
60
+ case event, ok := <-w.Events:
30
61
  if !ok {
31
62
  timer.Stop()
32
63
  return nil
33
64
  }
65
+ watchCreatedDir(w, event)
34
66
  case <-timer.C:
35
67
  break drain
36
68
  }
@@ -45,23 +77,28 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
45
77
  }
46
78
  }
47
79
 
48
- func reloadTasks(root string) tea.Cmd {
80
+ func reloadTasks(root string, deps ModelDependencies) tea.Cmd {
49
81
  return func() tea.Msg {
50
- tasks, err := loadAllTasks(root)
82
+ tasks, releases, releaseEpics, epicStatuses, err := loadBoardData(root, deps.Discoverer, deps.Parser)
51
83
  if err != nil {
52
- return nil
84
+ return errorMsg{message: "reload failed: " + err.Error()}
53
85
  }
54
- return reloadMsg{tasks: tasks}
86
+ routerState, _ := readRouterState(root, deps.RouterReader)
87
+ return reloadMsg{tasks: tasks, releases: releases, releaseEpics: releaseEpics, epicStatuses: epicStatuses, routerState: routerState}
55
88
  }
56
89
  }
57
90
 
58
- // newWatcher watches the releases directory by walking all subdirs (fsnotify v1.10 has no recursive opt).
91
+ // newWatcher watches the savepoint root (for router.md) and all releases subdirs.
59
92
  func newWatcher(root string) (*fsnotify.Watcher, error) {
60
93
  w, err := fsnotify.NewWatcher()
61
94
  if err != nil {
62
95
  return nil, err
63
96
  }
64
- releasesPath := filepath.Join(root, ".savepoint", "releases")
97
+ if err := w.Add(root); err != nil {
98
+ w.Close()
99
+ return nil, err
100
+ }
101
+ releasesPath := filepath.Join(root, "releases")
65
102
  if err := addDirsRecursive(w, releasesPath); err != nil {
66
103
  w.Close()
67
104
  return nil, err
@@ -69,6 +106,17 @@ func newWatcher(root string) (*fsnotify.Watcher, error) {
69
106
  return w, nil
70
107
  }
71
108
 
109
+ func watchCreatedDir(w *fsnotify.Watcher, event fsnotify.Event) {
110
+ if !event.Has(fsnotify.Create) {
111
+ return
112
+ }
113
+ info, err := os.Stat(event.Name)
114
+ if err != nil || !info.IsDir() {
115
+ return
116
+ }
117
+ _ = addDirsRecursive(w, event.Name)
118
+ }
119
+
72
120
  func addDirsRecursive(w *fsnotify.Watcher, root string) error {
73
121
  return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
74
122
  if err != nil {