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
@@ -3,15 +3,17 @@ package board
3
3
  import (
4
4
  "strings"
5
5
 
6
+ "github.com/charmbracelet/lipgloss"
6
7
  "github.com/opencode/savepoint/internal/data"
7
8
  "github.com/opencode/savepoint/internal/styles"
8
9
  )
9
10
 
10
- const detailBorderPad = 4 // rounded border (2) + padding (2×1)
11
+ const detailBorderPad = 4 // rounded border (2) + padding (2×1)
12
+ const detailVerticalOverhead = 4 // overlay border (2) + fixed title/header rows (2)
11
13
 
12
14
  // RenderDetail renders a task detail overlay panel at the given display width.
13
- // When routerTaskID matches t.ID, a "(router priority)" label is shown.
14
- func RenderDetail(t data.Task, overlayW int, routerTaskID string) string {
15
+ // When router state matches t's release/epic/task, a "(router priority)" label is shown.
16
+ func RenderDetail(t data.Task, overlayW int, routerState *data.RouterState, maxHeight, offset int) string {
15
17
  inner := overlayW - detailBorderPad
16
18
  if inner < 4 {
17
19
  inner = 4
@@ -21,57 +23,137 @@ func RenderDetail(t data.Task, overlayW int, routerTaskID string) string {
21
23
  styles.ColumnTitleFocused.Render("TASK DETAIL"),
22
24
  strings.Repeat("─", inner),
23
25
  }
24
- lines = append(lines,
26
+ body := []string{
25
27
  detailRow("ID", t.ID, inner),
26
28
  detailRow("Title", t.Title, inner),
27
29
  detailRow("Epic", t.Epic, inner),
28
30
  detailRow("Release", t.Release, inner),
29
31
  detailRow("Status", string(t.Column), inner),
30
32
  detailRow("Phase", phaseLabel(t.Stage), inner),
31
- )
33
+ }
32
34
 
33
35
  if t.Description != "" {
34
- lines = append(lines,
36
+ body = append(body,
35
37
  "",
36
38
  styles.ColumnTitle.Render("Description:"),
37
39
  )
38
40
  for _, line := range WrapText(t.Description, inner) {
39
- lines = append(lines, styles.CardMeta.Render(line))
40
- }
41
- }
42
-
43
- if len(t.Acceptance) > 0 {
44
- lines = append(lines, "", styles.ColumnTitle.Render("Acceptance Criteria:"), "")
45
- for _, a := range t.Acceptance {
46
- for _, line := range WrapText(a, inner-2) {
47
- lines = append(lines, styles.CardMeta.Render(" • "+line))
48
- }
41
+ body = append(body, styles.CardMeta.Render(line))
49
42
  }
50
43
  }
51
44
 
52
45
  if len(t.Checklist) > 0 {
53
- lines = append(lines, "", styles.ColumnTitle.Render("Implementation Plan:"), "")
46
+ body = append(body, "", styles.ColumnTitle.Render("Implementation Plan:"), "")
54
47
  for _, item := range t.Checklist {
55
- glyph := " "
48
+ glyph := "[ ] "
56
49
  style := styles.CardMeta
57
50
  if item.Done {
58
- glyph = " "
51
+ glyph = "[x] "
59
52
  style = styles.TagDone
60
53
  }
61
- for _, line := range WrapText(item.Text, inner-2) {
62
- lines = append(lines, style.Render(" "+glyph+line))
63
- }
54
+ body = append(body, renderChecklistSentences(item.Text, glyph, inner, style)...)
64
55
  }
65
56
  }
66
57
 
67
- if routerTaskID != "" && t.ID == routerTaskID {
68
- lines = append(lines, "", styles.TagDone.Render("(router priority)"))
58
+ if t.Column != data.ColumnDone && isRouterPriority(t, routerState) {
59
+ body = append(body, "", styles.TagDone.Render("(router priority)"))
69
60
  }
70
- lines = append(lines, "", styles.CardMeta.Render("esc:close"))
61
+ body = append(body, "", styles.CardMeta.Render("esc:close"))
62
+ lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead, offset)...)
71
63
 
72
64
  return styles.DetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
73
65
  }
74
66
 
67
+ func renderChecklistSentences(text, glyph string, width int, style lipgloss.Style) []string {
68
+ textWidth := width - len(glyph)
69
+ if textWidth < 4 {
70
+ textWidth = 4
71
+ }
72
+
73
+ lines := []string{}
74
+ continuationIndent := strings.Repeat(" ", len(glyph))
75
+ for _, sentence := range splitChecklistSentences(text) {
76
+ wrapped := WrapText(sentence, textWidth)
77
+ for i, line := range wrapped {
78
+ if i == 0 {
79
+ lines = append(lines, style.Render(glyph+line))
80
+ continue
81
+ }
82
+ lines = append(lines, style.Render(continuationIndent+line))
83
+ }
84
+ }
85
+ return lines
86
+ }
87
+
88
+ func splitChecklistSentences(text string) []string {
89
+ fields := strings.Fields(text)
90
+ if len(fields) == 0 {
91
+ return nil
92
+ }
93
+ normalized := strings.Join(fields, " ")
94
+
95
+ sentences := []string{}
96
+ start := 0
97
+ for i, r := range normalized {
98
+ if r != '.' && r != '!' && r != '?' {
99
+ continue
100
+ }
101
+ end := i + len(string(r))
102
+ if end < len(normalized) && normalized[end] != ' ' {
103
+ continue
104
+ }
105
+ sentence := strings.TrimSpace(normalized[start:end])
106
+ if sentence != "" {
107
+ sentences = append(sentences, sentence)
108
+ }
109
+ start = end
110
+ }
111
+ if tail := strings.TrimSpace(normalized[start:]); tail != "" {
112
+ sentences = append(sentences, tail)
113
+ }
114
+ return sentences
115
+ }
116
+
117
+ func visibleDetailLines(lines []string, maxBodyHeight, offset int) []string {
118
+ total := len(lines)
119
+ if maxBodyHeight <= 0 || total <= maxBodyHeight {
120
+ return lines
121
+ }
122
+ offset = clampDetailOffset(offset, total)
123
+ available := maxBodyHeight
124
+ if offset > 0 {
125
+ available--
126
+ }
127
+ if available < 1 {
128
+ available = 1
129
+ }
130
+ end := min(offset+available, total)
131
+ if end < total && available > 1 {
132
+ available--
133
+ end = min(offset+available, total)
134
+ }
135
+
136
+ visible := make([]string, 0, available+2)
137
+ if offset > 0 {
138
+ visible = append(visible, renderScrollIndicator("↑", offset, "above"))
139
+ }
140
+ visible = append(visible, lines[offset:end]...)
141
+ if end < total {
142
+ visible = append(visible, renderScrollIndicator("↓", total-end, "more"))
143
+ }
144
+ return visible
145
+ }
146
+
147
+ func clampDetailOffset(offset, total int) int {
148
+ if offset < 0 || total <= 0 {
149
+ return 0
150
+ }
151
+ if offset >= total {
152
+ return total - 1
153
+ }
154
+ return offset
155
+ }
156
+
75
157
  func detailRow(label, value string, width int) string {
76
158
  prefix := label + ": "
77
159
  wrapped := WrapText(value, width-len(prefix))
@@ -100,51 +182,4 @@ func phaseLabel(s data.ProgressStage) string {
100
182
  }
101
183
  }
102
184
 
103
- func WrapText(s string, width int) []string {
104
- if width < 4 {
105
- width = 4
106
- }
107
- words := strings.Fields(s)
108
- if len(words) == 0 {
109
- return nil
110
- }
111
- lines := []string{}
112
- current := ""
113
- for _, word := range words {
114
- if len([]rune(word)) > width {
115
- if current != "" {
116
- lines = append(lines, current)
117
- current = ""
118
- }
119
- lines = append(lines, SplitLongWord(word, width)...)
120
- continue
121
- }
122
- if current == "" {
123
- current = word
124
- continue
125
- }
126
- if len([]rune(current))+1+len([]rune(word)) <= width {
127
- current += " " + word
128
- continue
129
- }
130
- lines = append(lines, current)
131
- current = word
132
- }
133
- if current != "" {
134
- lines = append(lines, current)
135
- }
136
- return lines
137
- }
138
185
 
139
- func SplitLongWord(word string, width int) []string {
140
- runes := []rune(word)
141
- lines := []string{}
142
- for len(runes) > width {
143
- lines = append(lines, string(runes[:width]))
144
- runes = runes[width:]
145
- }
146
- if len(runes) > 0 {
147
- lines = append(lines, string(runes))
148
- }
149
- return lines
150
- }
@@ -1,6 +1,7 @@
1
1
  package board
2
2
 
3
3
  import (
4
+ "regexp"
4
5
  "strings"
5
6
  "testing"
6
7
 
@@ -8,6 +9,12 @@ import (
8
9
  "github.com/opencode/savepoint/internal/data"
9
10
  )
10
11
 
12
+ var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
13
+
14
+ func plainTerminal(s string) string {
15
+ return ansiPattern.ReplaceAllString(s, "")
16
+ }
17
+
11
18
  func sampleTask() data.Task {
12
19
  return data.Task{
13
20
  ID: "E04/T001",
@@ -20,49 +27,49 @@ func sampleTask() data.Task {
20
27
  }
21
28
 
22
29
  func TestRenderDetail_containsID(t *testing.T) {
23
- got := RenderDetail(sampleTask(), 60, "")
30
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
24
31
  if !strings.Contains(got, "E04/T001") {
25
32
  t.Error("RenderDetail missing task ID")
26
33
  }
27
34
  }
28
35
 
29
36
  func TestRenderDetail_containsTitle(t *testing.T) {
30
- got := RenderDetail(sampleTask(), 60, "")
37
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
31
38
  if !strings.Contains(got, "My Task") {
32
39
  t.Error("RenderDetail missing task title")
33
40
  }
34
41
  }
35
42
 
36
43
  func TestRenderDetail_containsEpic(t *testing.T) {
37
- got := RenderDetail(sampleTask(), 60, "")
44
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
38
45
  if !strings.Contains(got, "E04-board-components") {
39
46
  t.Error("RenderDetail missing epic")
40
47
  }
41
48
  }
42
49
 
43
50
  func TestRenderDetail_containsRelease(t *testing.T) {
44
- got := RenderDetail(sampleTask(), 60, "")
51
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
45
52
  if !strings.Contains(got, "v1") {
46
53
  t.Error("RenderDetail missing release")
47
54
  }
48
55
  }
49
56
 
50
57
  func TestRenderDetail_containsStatus(t *testing.T) {
51
- got := RenderDetail(sampleTask(), 60, "")
58
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
52
59
  if !strings.Contains(got, "in_progress") {
53
60
  t.Error("RenderDetail missing status")
54
61
  }
55
62
  }
56
63
 
57
64
  func TestRenderDetail_containsPhase(t *testing.T) {
58
- got := RenderDetail(sampleTask(), 60, "")
65
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
59
66
  if !strings.Contains(got, "build") {
60
67
  t.Error("RenderDetail missing phase")
61
68
  }
62
69
  }
63
70
 
64
71
  func TestRenderDetail_containsEscHint(t *testing.T) {
65
- got := RenderDetail(sampleTask(), 60, "")
72
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
66
73
  if !strings.Contains(got, "esc") {
67
74
  t.Error("RenderDetail missing esc:close hint")
68
75
  }
@@ -71,35 +78,23 @@ func TestRenderDetail_containsEscHint(t *testing.T) {
71
78
  func TestRenderDetail_containsDescription(t *testing.T) {
72
79
  tk := sampleTask()
73
80
  tk.Description = "some description text"
74
- got := RenderDetail(tk, 60, "")
81
+ got := RenderDetail(tk, 60, nil, 0, 0)
75
82
  if !strings.Contains(got, "some description text") {
76
83
  t.Error("RenderDetail missing description text")
77
84
  }
78
85
  }
79
86
 
80
87
  func TestRenderDetail_noDescriptionSectionWhenEmpty(t *testing.T) {
81
- got := RenderDetail(sampleTask(), 60, "")
88
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
82
89
  if strings.Contains(got, "Description:") {
83
90
  t.Error("RenderDetail should not show Description section when empty")
84
91
  }
85
92
  }
86
93
 
87
- func TestRenderDetail_containsAcceptanceCriteria(t *testing.T) {
88
- tk := sampleTask()
89
- tk.Acceptance = []string{"criterion one", "criterion two"}
90
- got := RenderDetail(tk, 60, "")
91
- if !strings.Contains(got, "criterion one") {
92
- t.Error("RenderDetail missing first acceptance criterion")
93
- }
94
- if !strings.Contains(got, "criterion two") {
95
- t.Error("RenderDetail missing second acceptance criterion")
96
- }
97
- }
98
-
99
94
  func TestRenderDetail_containsChecklist(t *testing.T) {
100
95
  tk := sampleTask()
101
96
  tk.Checklist = []data.CheckItem{{Text: "first implementation item"}, {Text: "second implementation item", Done: true}}
102
- got := RenderDetail(tk, 60, "")
97
+ got := RenderDetail(tk, 60, nil, 0, 0)
103
98
  if !strings.Contains(got, "Implementation Plan:") {
104
99
  t.Error("RenderDetail missing implementation plan heading")
105
100
  }
@@ -111,10 +106,70 @@ func TestRenderDetail_containsChecklist(t *testing.T) {
111
106
  }
112
107
  }
113
108
 
109
+ func TestRenderDetail_checklistSingleSentenceGetsOneCheckbox(t *testing.T) {
110
+ tk := sampleTask()
111
+ tk.Checklist = []data.CheckItem{{Text: "single sentence task"}}
112
+
113
+ got := plainTerminal(RenderDetail(tk, 60, nil, 0, 0))
114
+
115
+ if count := strings.Count(got, "[ ]"); count != 1 {
116
+ t.Fatalf("RenderDetail checkbox count = %d, want 1\n%s", count, got)
117
+ }
118
+ if strings.Contains(got, "[x]") {
119
+ t.Fatal("RenderDetail should not render checked marker for unchecked item")
120
+ }
121
+ }
122
+
123
+ func TestRenderDetail_checklistMultiSentenceGetsOneCheckboxPerSentence(t *testing.T) {
124
+ tk := sampleTask()
125
+ tk.Checklist = []data.CheckItem{{Text: "First sentence. Second sentence! Third sentence?"}}
126
+
127
+ got := plainTerminal(RenderDetail(tk, 60, nil, 0, 0))
128
+
129
+ if count := strings.Count(got, "[ ]"); count != 3 {
130
+ t.Fatalf("RenderDetail checkbox count = %d, want 3\n%s", count, got)
131
+ }
132
+ for _, want := range []string{"[ ] First sentence.", "[ ] Second sentence!", "[ ] Third sentence?"} {
133
+ if !strings.Contains(got, want) {
134
+ t.Fatalf("RenderDetail missing sentence checkbox line %q\n%s", want, got)
135
+ }
136
+ }
137
+ }
138
+
139
+ func TestRenderDetail_checklistHardWrappedSentenceDoesNotDuplicateCheckbox(t *testing.T) {
140
+ tk := sampleTask()
141
+ tk.Checklist = []data.CheckItem{{
142
+ Text: "This sentence is intentionally long enough to wrap inside a narrow detail overlay while remaining one semantic sentence.",
143
+ }}
144
+
145
+ got := plainTerminal(RenderDetail(tk, 34, nil, 0, 0))
146
+
147
+ if count := strings.Count(got, "[ ]"); count != 1 {
148
+ t.Fatalf("RenderDetail checkbox count = %d, want 1\n%s", count, got)
149
+ }
150
+ if !strings.Contains(got, " intentionally long enough") {
151
+ t.Fatalf("RenderDetail continuation line should align under checkbox text\n%s", got)
152
+ }
153
+ }
154
+
155
+ func TestRenderDetail_checklistCheckedSentenceUsesCheckedMarker(t *testing.T) {
156
+ tk := sampleTask()
157
+ tk.Checklist = []data.CheckItem{{Text: "already done. still done.", Done: true}}
158
+
159
+ got := plainTerminal(RenderDetail(tk, 60, nil, 0, 0))
160
+
161
+ if count := strings.Count(got, "[x]"); count != 2 {
162
+ t.Fatalf("RenderDetail checked checkbox count = %d, want 2\n%s", count, got)
163
+ }
164
+ if strings.Contains(got, "[ ]") {
165
+ t.Fatal("RenderDetail should not render unchecked marker for checked item")
166
+ }
167
+ }
168
+
114
169
  func TestRenderDetail_wrapsLongDescription(t *testing.T) {
115
170
  tk := sampleTask()
116
171
  tk.Description = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda"
117
- got := RenderDetail(tk, 30, "")
172
+ got := RenderDetail(tk, 30, nil, 0, 0)
118
173
  if strings.Contains(got, tk.Description) {
119
174
  t.Error("RenderDetail should wrap long description text")
120
175
  }
@@ -124,7 +179,7 @@ func TestRenderDetail_wrapsLongDescription(t *testing.T) {
124
179
  }
125
180
 
126
181
  func TestRenderDetail_noAcceptanceSectionWhenEmpty(t *testing.T) {
127
- got := RenderDetail(sampleTask(), 60, "")
182
+ got := RenderDetail(sampleTask(), 60, nil, 0, 0)
128
183
  if strings.Contains(got, "Acceptance Criteria:") {
129
184
  t.Error("RenderDetail should not show Acceptance section when empty")
130
185
  }
@@ -216,7 +271,8 @@ func TestView_detailOverlayRendered(t *testing.T) {
216
271
 
217
272
  func TestRenderDetail_routerPriorityLabel(t *testing.T) {
218
273
  task := sampleTask()
219
- got := RenderDetail(task, 60, task.ID)
274
+ router := &data.RouterState{Release: task.Release, Epic: task.Epic, Task: task.ID}
275
+ got := RenderDetail(task, 60, router, 0, 0)
220
276
  if !strings.Contains(got, "(router priority)") {
221
277
  t.Error("RenderDetail missing router priority label for matching task")
222
278
  }
@@ -224,12 +280,47 @@ func TestRenderDetail_routerPriorityLabel(t *testing.T) {
224
280
 
225
281
  func TestRenderDetail_noRouterPriorityLabelWhenNoMatch(t *testing.T) {
226
282
  task := sampleTask()
227
- got := RenderDetail(task, 60, "other-id")
283
+ router := &data.RouterState{Release: task.Release, Epic: task.Epic, Task: "other-id"}
284
+ got := RenderDetail(task, 60, router, 0, 0)
228
285
  if strings.Contains(got, "(router priority)") {
229
286
  t.Error("RenderDetail should not show router priority label for non-matching task")
230
287
  }
231
288
  }
232
289
 
290
+ func TestRenderDetail_viewportShowsScrollIndicators(t *testing.T) {
291
+ task := sampleTask()
292
+ task.Description = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron"
293
+
294
+ got := RenderDetail(task, 32, nil, 8, 2)
295
+
296
+ if !strings.Contains(got, "↑ 2 above") {
297
+ t.Error("RenderDetail missing above indicator")
298
+ }
299
+ if !strings.Contains(got, "↓") || !strings.Contains(got, "more") {
300
+ t.Error("RenderDetail missing more indicator")
301
+ }
302
+ if strings.Contains(got, "ID:") {
303
+ t.Error("RenderDetail should not render body lines above viewport")
304
+ }
305
+ }
306
+
307
+ func TestUpdate_detailOverlayScrollsWithJK(t *testing.T) {
308
+ m := NewModel([]data.Task{sampleTask()}, "v1", "E04-board-components")
309
+ m.Overlay = OverlayDetail
310
+
311
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
312
+ updated := requireModel(t, got)
313
+ if updated.DetailOffset != 1 {
314
+ t.Errorf("DetailOffset after j = %d, want 1", updated.DetailOffset)
315
+ }
316
+
317
+ got, _ = updated.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")})
318
+ updated = requireModel(t, got)
319
+ if updated.DetailOffset != 0 {
320
+ t.Errorf("DetailOffset after k = %d, want 0", updated.DetailOffset)
321
+ }
322
+ }
323
+
233
324
  func TestOverlayWidth_clampMax(t *testing.T) {
234
325
  if got := overlayWidth(120); got != 80 {
235
326
  t.Errorf("overlayWidth(120) = %d, want 80", got)