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
@@ -23,6 +23,7 @@ func RenderHelp(width int) string {
23
23
  helpRow("enter", "open task detail / select item"),
24
24
  helpRow("e", "open epic selector on narrow screens"),
25
25
  helpRow("r", "open release selector"),
26
+ helpRow("p", "mark focused task as priority"),
26
27
  helpRow("up / k", "move selector up"),
27
28
  helpRow("down / j", "move selector down"),
28
29
  helpRow("?", "open help"),
@@ -22,6 +22,7 @@ func TestRenderHelp_containsShortcuts(t *testing.T) {
22
22
  "enter",
23
23
  "e",
24
24
  "r",
25
+ "p",
25
26
  "up / k",
26
27
  "down / j",
27
28
  "?",
@@ -0,0 +1,266 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+
10
+ tea "github.com/charmbracelet/bubbletea"
11
+ "github.com/opencode/savepoint/internal/data"
12
+ "github.com/opencode/savepoint/internal/testutil"
13
+ )
14
+
15
+ // writeTaskWithBody creates a task file with a body section to verify content preservation.
16
+ func writeTaskWithBody(t *testing.T, root, release, epic, taskSlug string, column data.ColumnType, body string) string {
17
+ t.Helper()
18
+ tf := testutil.TaskFixture{
19
+ Slug: taskSlug,
20
+ Release: release,
21
+ Status: string(column),
22
+ Objective: "Test task",
23
+ Body: body,
24
+ }
25
+ if column == data.ColumnInProgress {
26
+ tf.Phase = "build"
27
+ }
28
+ testutil.WriteTask(t, root, release, epic, tf)
29
+ return filepath.Join(root, "releases", release, "epics", epic, "tasks", taskSlug+".md")
30
+ }
31
+
32
+ // TestBoardPipeline_endToEnd loads a real project from disk and renders the full board view.
33
+ func TestBoardPipeline_endToEnd(t *testing.T) {
34
+ projectRoot := t.TempDir()
35
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
36
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1", "E01-init", "", "test")
37
+ writeTask(t, savepointRoot, "v1", "E01-init", "T001-scaffold", data.ColumnPlanned)
38
+ writeTask(t, savepointRoot, "v1", "E01-init", "T002-validate", data.ColumnInProgress)
39
+ writeTask(t, savepointRoot, "v1", "E01-init", "T003-done-task", data.ColumnDone)
40
+
41
+ model, err := newProjectModel(projectRoot, "", "")
42
+ if err != nil {
43
+ t.Fatalf("newProjectModel: %v", err)
44
+ }
45
+ if model.Watcher != nil {
46
+ t.Cleanup(func() { model.Watcher.Close() })
47
+ }
48
+
49
+ model.Width = 120
50
+ model.Height = 40
51
+ view := model.View()
52
+
53
+ for _, want := range []string{"PLANNED", "IN PROGRESS", "DONE", "T001", "T002", "T003"} {
54
+ if !strings.Contains(view, want) {
55
+ t.Errorf("board view missing %q", want)
56
+ }
57
+ }
58
+ }
59
+
60
+ // TestRunPlainOutput_endToEnd calls runPlainOutput against a real temp project root.
61
+ func TestRunPlainOutput_endToEnd(t *testing.T) {
62
+ projectRoot := t.TempDir()
63
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
64
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1", "E01-init", "", "test")
65
+ writeTask(t, savepointRoot, "v1", "E01-init", "T001-scaffold", data.ColumnPlanned)
66
+ writeTask(t, savepointRoot, "v1", "E01-init", "T002-validate", data.ColumnDone)
67
+
68
+ model, err := newProjectModel(projectRoot, "", "")
69
+ if err != nil {
70
+ t.Fatalf("newProjectModel: %v", err)
71
+ }
72
+ if model.Watcher != nil {
73
+ t.Cleanup(func() { model.Watcher.Close() })
74
+ }
75
+
76
+ out := RenderPlainTable(model)
77
+
78
+ if !strings.Contains(out, plainNonTTYWarning) {
79
+ t.Error("plain output missing non-TTY warning")
80
+ }
81
+ for _, want := range []string{"PLANNED", "DONE", "T001-scaffold", "T002-validate"} {
82
+ if !strings.Contains(out, want) {
83
+ t.Errorf("plain output missing %q", want)
84
+ }
85
+ }
86
+ }
87
+
88
+ // TestStatusWrite_preservesTaskBody advances a task via space key and verifies the body text is unchanged.
89
+ func TestStatusWrite_preservesTaskBody(t *testing.T) {
90
+ root := t.TempDir()
91
+ body := "## Acceptance Criteria\n\n- [ ] thing one\n- [ ] thing two\n"
92
+ path := writeTaskWithBody(t, root, "v1", "E01-init", "T001-scaffold", data.ColumnPlanned, body)
93
+
94
+ fi, err := os.Stat(path)
95
+ if err != nil {
96
+ t.Fatal(err)
97
+ }
98
+ task := data.Task{
99
+ ID: "E01-init/T001-scaffold",
100
+ Column: data.ColumnPlanned,
101
+ Path: path,
102
+ Mtime: fi.ModTime(),
103
+ }
104
+
105
+ m := NewModel([]data.Task{task}, "v1", "E01-init")
106
+ m.FocusedColumn = data.ColumnPlanned
107
+
108
+ got, cmd := m.Update(tea.KeyMsg{Type: tea.KeySpace})
109
+ msg := cmd()
110
+ got2, _ := got.Update(msg)
111
+ updated := requireModel(t, got2)
112
+
113
+ if updated.AllTasks[0].Column != data.ColumnInProgress {
114
+ t.Errorf("Column = %q, want in_progress", updated.AllTasks[0].Column)
115
+ }
116
+
117
+ raw, err := os.ReadFile(path)
118
+ if err != nil {
119
+ t.Fatal(err)
120
+ }
121
+ if !strings.Contains(string(raw), body) {
122
+ t.Errorf("task body was altered after status write; got:\n%s", raw)
123
+ }
124
+ }
125
+
126
+ // TestMtimeConflict_directDetection verifies WriteTaskStatus returns ErrMtimeConflict on mtime mismatch.
127
+ func TestMtimeConflict_directDetection(t *testing.T) {
128
+ dir := t.TempDir()
129
+ path := filepath.Join(dir, "T001.md")
130
+ content := "---\nid: E01/T001\nstatus: planned\nphase: build\n---\n\n# Task\n"
131
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
132
+ t.Fatal(err)
133
+ }
134
+
135
+ task := &data.Task{
136
+ ID: "E01/T001",
137
+ Column: data.ColumnInProgress,
138
+ Stage: data.StageBuild,
139
+ }
140
+ staleTime := time.Now().Add(-time.Hour)
141
+ err := data.WriteTaskStatus(path, task, staleTime)
142
+ if err != data.ErrMtimeConflict {
143
+ t.Errorf("WriteTaskStatus with stale mtime = %v, want ErrMtimeConflict", err)
144
+ }
145
+ }
146
+
147
+ // TestMtimeConflict_boardWarns verifies the board surfaces an mtime conflict instead of overwriting external edits.
148
+ func TestMtimeConflict_boardWarns(t *testing.T) {
149
+ path := filepath.Join(t.TempDir(), "T001.md")
150
+ content := "---\nid: E01/T001\nstatus: in_progress\nphase: build\n---\n\n# Task\n"
151
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
152
+ t.Fatal(err)
153
+ }
154
+ fi, err := os.Stat(path)
155
+ if err != nil {
156
+ t.Fatal(err)
157
+ }
158
+
159
+ task := data.Task{
160
+ ID: "E01/T001",
161
+ Column: data.ColumnInProgress,
162
+ Stage: data.StageBuild,
163
+ Path: path,
164
+ Mtime: fi.ModTime().Add(-time.Minute), // intentionally stale
165
+ }
166
+ m := NewModel([]data.Task{task}, "v1", "E01")
167
+ m.FocusedColumn = data.ColumnInProgress
168
+
169
+ got, cmd := m.Update(tea.KeyMsg{Type: tea.KeySpace})
170
+ msg := cmd()
171
+ got2, _ := got.Update(msg)
172
+ updated := requireModel(t, got2)
173
+
174
+ if !strings.Contains(updated.StatusMessage, "mtime conflict") {
175
+ t.Errorf("StatusMessage = %q, want mtime conflict warning", updated.StatusMessage)
176
+ }
177
+
178
+ raw, err := os.ReadFile(path)
179
+ if err != nil {
180
+ t.Fatal(err)
181
+ }
182
+ if !strings.Contains(string(raw), "phase: build") {
183
+ t.Errorf("task file was overwritten despite mtime conflict:\n%s", raw)
184
+ }
185
+ }
186
+
187
+ // TestReleaseFilter_showsOnlyMatchingRelease verifies the --release flag filters tasks.
188
+ func TestReleaseFilter_showsOnlyMatchingRelease(t *testing.T) {
189
+ projectRoot := t.TempDir()
190
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
191
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1", "E01-init", "", "test")
192
+ writeTask(t, savepointRoot, "v1", "E01-init", "T001-v1-task", data.ColumnPlanned)
193
+ writeTask(t, savepointRoot, "v2", "E01-init", "T001-v2-task", data.ColumnPlanned)
194
+
195
+ model, err := newProjectModel(projectRoot, "v2", "")
196
+ if err != nil {
197
+ t.Fatalf("newProjectModel: %v", err)
198
+ }
199
+ if model.Watcher != nil {
200
+ t.Cleanup(func() { model.Watcher.Close() })
201
+ }
202
+
203
+ if model.SelectedRelease != "v2" {
204
+ t.Errorf("SelectedRelease = %q, want v2", model.SelectedRelease)
205
+ }
206
+ planned := model.Tasks[data.ColumnPlanned]
207
+ for _, task := range planned {
208
+ if task.Release != "v2" {
209
+ t.Errorf("task %q has release %q, want v2 only", task.ID, task.Release)
210
+ }
211
+ }
212
+ }
213
+
214
+ // TestEpicFilter_showsOnlyMatchingEpic verifies the --epic flag filters tasks.
215
+ func TestEpicFilter_showsOnlyMatchingEpic(t *testing.T) {
216
+ projectRoot := t.TempDir()
217
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
218
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1", "E01-init", "", "test")
219
+ writeTask(t, savepointRoot, "v1", "E01-init", "T001-alpha", data.ColumnPlanned)
220
+ writeTask(t, savepointRoot, "v1", "E02-build", "T001-beta", data.ColumnPlanned)
221
+
222
+ model, err := newProjectModel(projectRoot, "v1", "E02-build")
223
+ if err != nil {
224
+ t.Fatalf("newProjectModel: %v", err)
225
+ }
226
+ if model.Watcher != nil {
227
+ t.Cleanup(func() { model.Watcher.Close() })
228
+ }
229
+
230
+ if model.SelectedEpic != "E02-build" {
231
+ t.Errorf("SelectedEpic = %q, want E02-build", model.SelectedEpic)
232
+ }
233
+ planned := model.Tasks[data.ColumnPlanned]
234
+ for _, task := range planned {
235
+ if task.Epic != "E02-build" {
236
+ t.Errorf("task %q has epic %q, want E02-build only", task.ID, task.Epic)
237
+ }
238
+ }
239
+ }
240
+
241
+ // TestDetailPane_opensOnEnter verifies enter key opens the detail overlay.
242
+ func TestDetailPane_opensOnEnter(t *testing.T) {
243
+ tasks := []data.Task{{ID: "E01/T001", Title: "Scaffold init", Column: data.ColumnPlanned}}
244
+ m := NewModel(tasks, "v1", "E01")
245
+ m.FocusedColumn = data.ColumnPlanned
246
+
247
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
248
+ updated := requireModel(t, got)
249
+
250
+ if updated.Overlay != OverlayDetail {
251
+ t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayDetail)
252
+ }
253
+ }
254
+
255
+ // TestDetailPane_escClosesOverlay verifies esc dismisses the detail overlay.
256
+ func TestDetailPane_escClosesOverlay(t *testing.T) {
257
+ m := NewModel(nil, "v1", "E01")
258
+ m.Overlay = OverlayDetail
259
+
260
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
261
+ updated := requireModel(t, got)
262
+
263
+ if updated.Overlay != OverlayNone {
264
+ t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
265
+ }
266
+ }
@@ -0,0 +1,65 @@
1
+ package board
2
+
3
+ import "github.com/opencode/savepoint/internal/data"
4
+
5
+ // taskDiscoverer provides project traversal for board loading.
6
+ type taskDiscoverer interface {
7
+ FindSavepointRoot(start string) (string, error)
8
+ ListReleases(root string) ([]data.ReleaseInfo, error)
9
+ ListEpics(root, release string) ([]data.EpicInfo, error)
10
+ ListTasks(root, release, epic string) ([]data.TaskInfo, error)
11
+ }
12
+
13
+ // taskParser parses Savepoint frontmatter and task files for board loading.
14
+ type taskParser interface {
15
+ ParseFrontmatter(content string) (map[string]any, error)
16
+ ParseTaskFile(path string, content string) (*data.Task, error)
17
+ }
18
+
19
+ // configReader reads board display configuration.
20
+ type configReader interface {
21
+ Read(path string) (*data.Config, error)
22
+ }
23
+
24
+ // routerReader parses router state from router.md content.
25
+ type routerReader interface {
26
+ ReadState(content string) (*data.RouterState, error)
27
+ }
28
+
29
+ // ModelDependencies contains board data-access dependencies.
30
+ type ModelDependencies struct {
31
+ Discoverer taskDiscoverer
32
+ Parser taskParser
33
+ ConfigReader configReader
34
+ RouterReader routerReader
35
+ }
36
+
37
+ func defaultModelDependencies() ModelDependencies {
38
+ return ModelDependencies{
39
+ Discoverer: data.NewDiscover(),
40
+ Parser: data.NewParser(),
41
+ ConfigReader: data.NewConfigReader(),
42
+ RouterReader: data.NewRouterReader(),
43
+ }
44
+ }
45
+
46
+ func modelDependencies(overrides []ModelDependencies) ModelDependencies {
47
+ deps := defaultModelDependencies()
48
+ if len(overrides) == 0 {
49
+ return deps
50
+ }
51
+ override := overrides[0]
52
+ if override.Discoverer != nil {
53
+ deps.Discoverer = override.Discoverer
54
+ }
55
+ if override.Parser != nil {
56
+ deps.Parser = override.Parser
57
+ }
58
+ if override.ConfigReader != nil {
59
+ deps.ConfigReader = override.ConfigReader
60
+ }
61
+ if override.RouterReader != nil {
62
+ deps.RouterReader = override.RouterReader
63
+ }
64
+ return deps
65
+ }
@@ -0,0 +1,114 @@
1
+ package board
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/opencode/savepoint/internal/data"
8
+ "github.com/opencode/savepoint/internal/testutil"
9
+ )
10
+
11
+ type stubBoardDiscoverer struct {
12
+ root string
13
+ releases []data.ReleaseInfo
14
+ epics map[string][]data.EpicInfo
15
+ tasks map[string][]data.TaskInfo
16
+ findCalls int
17
+ }
18
+
19
+ func (d *stubBoardDiscoverer) FindSavepointRoot(start string) (string, error) {
20
+ d.findCalls++
21
+ return d.root, nil
22
+ }
23
+
24
+ func (d *stubBoardDiscoverer) ListReleases(root string) ([]data.ReleaseInfo, error) {
25
+ return d.releases, nil
26
+ }
27
+
28
+ func (d *stubBoardDiscoverer) ListEpics(root, release string) ([]data.EpicInfo, error) {
29
+ return d.epics[release], nil
30
+ }
31
+
32
+ func (d *stubBoardDiscoverer) ListTasks(root, release, epic string) ([]data.TaskInfo, error) {
33
+ return d.tasks[release+"/"+epic], nil
34
+ }
35
+
36
+ type countingBoardParser struct {
37
+ parser *data.Parser
38
+ frontmatterCalls int
39
+ taskFileCalls int
40
+ }
41
+
42
+ func (p *countingBoardParser) ParseFrontmatter(content string) (map[string]any, error) {
43
+ p.frontmatterCalls++
44
+ return p.parser.ParseFrontmatter(content)
45
+ }
46
+
47
+ func (p *countingBoardParser) ParseTaskFile(path string, content string) (*data.Task, error) {
48
+ p.taskFileCalls++
49
+ return p.parser.ParseTaskFile(path, content)
50
+ }
51
+
52
+ type stubBoardRouterReader struct {
53
+ state *data.RouterState
54
+ calls int
55
+ }
56
+
57
+ func (r *stubBoardRouterReader) ReadState(content string) (*data.RouterState, error) {
58
+ r.calls++
59
+ return r.state, nil
60
+ }
61
+
62
+ func TestNewProjectModelUsesInjectedInterfaces(t *testing.T) {
63
+ projectRoot := t.TempDir()
64
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
65
+ epicPath := filepath.Join(savepointRoot, "releases", "v9", "epics", "E01-mock")
66
+ taskPath := filepath.Join(epicPath, "tasks", "T001-mock.md")
67
+
68
+ testutil.WriteFile(t, filepath.Join(savepointRoot, "router.md"), "# router")
69
+ testutil.WriteFile(t, filepath.Join(epicPath, "E01-Detail.md"), "---\ntype: epic-design\nstatus: planned\n---\n\n# Epic\n")
70
+ testutil.WriteFile(t, taskPath, "---\nid: E01-mock/T001-mock\nstatus: planned\nobjective: Mock task\ndepends_on: []\n---\n\n# Task\n")
71
+
72
+ discoverer := &stubBoardDiscoverer{
73
+ root: savepointRoot,
74
+ releases: []data.ReleaseInfo{{
75
+ ID: "v9",
76
+ Path: filepath.Join(savepointRoot, "releases", "v9"),
77
+ }},
78
+ epics: map[string][]data.EpicInfo{
79
+ "v9": {{ID: "E01-mock", Path: epicPath}},
80
+ },
81
+ tasks: map[string][]data.TaskInfo{
82
+ "v9/E01-mock": {{ID: "T001-mock", Path: taskPath}},
83
+ },
84
+ }
85
+ parser := &countingBoardParser{parser: data.NewParser()}
86
+ router := &stubBoardRouterReader{state: &data.RouterState{
87
+ State: "task-building",
88
+ Release: "v9",
89
+ Epic: "E01-mock",
90
+ Task: "E01-mock/T001-mock",
91
+ }}
92
+
93
+ model, err := newProjectModelWithDependencies(projectRoot, "", "", ModelDependencies{
94
+ Discoverer: discoverer,
95
+ Parser: parser,
96
+ RouterReader: router,
97
+ })
98
+ if err != nil {
99
+ t.Fatalf("newProjectModelWithDependencies() error = %v", err)
100
+ }
101
+
102
+ if discoverer.findCalls != 1 {
103
+ t.Fatalf("FindSavepointRoot calls = %d, want 1", discoverer.findCalls)
104
+ }
105
+ if router.calls != 1 {
106
+ t.Fatalf("ReadState calls = %d, want 1", router.calls)
107
+ }
108
+ if parser.frontmatterCalls != 1 || parser.taskFileCalls != 1 {
109
+ t.Fatalf("parser calls = frontmatter:%d task:%d, want 1 each", parser.frontmatterCalls, parser.taskFileCalls)
110
+ }
111
+ if got := model.Tasks[data.ColumnPlanned][0].ID; got != "E01-mock/T001-mock" {
112
+ t.Fatalf("loaded task = %q, want injected task", got)
113
+ }
114
+ }
@@ -0,0 +1,93 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "time"
7
+
8
+ tea "github.com/charmbracelet/bubbletea"
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ func writeRouterTaskCmd(root string, task data.Task, reader routerReader) tea.Cmd {
13
+ return func() tea.Msg {
14
+ routerPath := filepath.Join(root, "router.md")
15
+ fi, err := os.Stat(routerPath)
16
+ if err != nil {
17
+ return errorMsg{message: err.Error()}
18
+ }
19
+ content, err := os.ReadFile(routerPath)
20
+ if err != nil {
21
+ return errorMsg{message: err.Error()}
22
+ }
23
+ state, err := reader.ReadState(string(content))
24
+ if err != nil {
25
+ return errorMsg{message: err.Error()}
26
+ }
27
+ state.Release = task.Release
28
+ state.Epic = task.Epic
29
+ state.State = "task-building"
30
+ state.Task = task.ID
31
+ state.NextAction = "Build " + task.ID + "."
32
+ if err := data.WriteRouterState(root, state, fi.ModTime()); err != nil {
33
+ return errorMsg{message: err.Error()}
34
+ }
35
+ message := "Router set to " + task.Release + " " + task.Epic + "/" + shortID(task.ID)
36
+ return routerWriteMsg{message: message, state: state, taskID: task.ID}
37
+ }
38
+ }
39
+
40
+ func writeRouterReleaseEpicCmd(root, selectedEpic, selectedRelease string, reader routerReader) tea.Cmd {
41
+ return func() tea.Msg {
42
+ routerPath := filepath.Join(root, "router.md")
43
+ fi, err := os.Stat(routerPath)
44
+ if err != nil {
45
+ return errorMsg{message: err.Error()}
46
+ }
47
+ content, err := os.ReadFile(routerPath)
48
+ if err != nil {
49
+ return errorMsg{message: err.Error()}
50
+ }
51
+ state, err := reader.ReadState(string(content))
52
+ if err != nil {
53
+ return errorMsg{message: err.Error()}
54
+ }
55
+ state.Epic = shortID(selectedEpic)
56
+ state.Release = selectedRelease
57
+ if err := data.WriteRouterState(root, state, fi.ModTime()); err != nil {
58
+ return errorMsg{message: err.Error()}
59
+ }
60
+ return routerWriteMsg{state: state}
61
+ }
62
+ }
63
+
64
+ func writeTaskStatusCmd(orig, next data.Task, expectedMtime time.Time, prefix string) tea.Cmd {
65
+ return func() tea.Msg {
66
+ if err := data.WriteTaskStatus(next.Path, &next, expectedMtime); err != nil {
67
+ return errorMsg{message: taskWriteErrorMessage(err)}
68
+ }
69
+ fi, err := os.Stat(next.Path)
70
+ if err != nil {
71
+ return errorMsg{message: err.Error()}
72
+ }
73
+ next.Mtime = fi.ModTime()
74
+ return taskWriteMsg{prefix: prefix, next: next}
75
+ }
76
+ }
77
+
78
+ func readEpicDetailCmd(epicDir, shortIDStr string) tea.Cmd {
79
+ return func() tea.Msg {
80
+ content := readEpicDetailFile(epicDir, shortIDStr)
81
+ return epicDetailMsg{content: content}
82
+ }
83
+ }
84
+
85
+ func readEpicAuditCmd(epicDir, shortIDStr string) tea.Cmd {
86
+ return func() tea.Msg {
87
+ raw, err := os.ReadFile(filepath.Join(epicDir, shortIDStr+"-Audit.md"))
88
+ if err != nil {
89
+ return auditContentMsg{content: "(no audit available)"}
90
+ }
91
+ return auditContentMsg{content: string(raw)}
92
+ }
93
+ }
@@ -3,7 +3,8 @@ package board
3
3
  const (
4
4
  colOverhead = 4 // rounded border (1) + padding (1) each side
5
5
 
6
- minColWidth = 10
6
+ minColWidth = 10
7
+ minContentHeight = 5
7
8
 
8
9
  epicPanelWidth = 28
9
10
  epicPanelOverhead = 4
@@ -20,6 +21,7 @@ type Layout struct {
20
21
  EpicPanelWidth int
21
22
  ColCount int
22
23
  ColWidths []int
24
+ ContentHeight int
23
25
  }
24
26
 
25
27
  // CalculateLayout returns the board layout for the given terminal dimensions.
@@ -27,7 +29,12 @@ type Layout struct {
27
29
  // - >=120 cols: epic panel (28w) + 3 columns
28
30
  // - 80–119 cols: 3 columns only
29
31
  // - <80 cols: 1 column
30
- func CalculateLayout(width, _ int) Layout {
32
+ func CalculateLayout(width, height int) Layout {
33
+ return CalculateLayoutWithChrome(width, height, 0)
34
+ }
35
+
36
+ func CalculateLayoutWithChrome(width, height, extraHeaderLines int) Layout {
37
+ contentHeight := max(height-10-extraHeaderLines, minContentHeight)
31
38
  inner := width - boardFrameOverhead
32
39
  switch {
33
40
  case width >= breakpointWide:
@@ -38,6 +45,7 @@ func CalculateLayout(width, _ int) Layout {
38
45
  EpicPanelWidth: epicPanelWidth,
39
46
  ColCount: 3,
40
47
  ColWidths: []int{cw, cw, cw},
48
+ ContentHeight: contentHeight,
41
49
  }
42
50
  case width >= breakpointNarrow:
43
51
  available := inner - 3*colOverhead
@@ -46,6 +54,7 @@ func CalculateLayout(width, _ int) Layout {
46
54
  EpicPanelVisible: false,
47
55
  ColCount: 3,
48
56
  ColWidths: []int{cw, cw, cw},
57
+ ContentHeight: contentHeight,
49
58
  }
50
59
  default:
51
60
  cw := max(inner-colOverhead, minColWidth)
@@ -53,6 +62,7 @@ func CalculateLayout(width, _ int) Layout {
53
62
  EpicPanelVisible: false,
54
63
  ColCount: 1,
55
64
  ColWidths: []int{cw},
65
+ ContentHeight: contentHeight,
56
66
  }
57
67
  }
58
68
  }
@@ -22,6 +22,9 @@ func TestCalculateLayout_wide(t *testing.T) {
22
22
  t.Errorf("ColWidths[%d] = %d, want 24", i, w)
23
23
  }
24
24
  }
25
+ if l.ContentHeight != 30 {
26
+ t.Errorf("ContentHeight = %d, want 30", l.ContentHeight)
27
+ }
25
28
  }
26
29
 
27
30
  func TestCalculateLayout_medium(t *testing.T) {
@@ -66,6 +69,20 @@ func TestCalculateLayout_tinyWidth_floorsAtMinColWidth(t *testing.T) {
66
69
  }
67
70
  }
68
71
 
72
+ func TestCalculateLayout_tinyHeight_floorsAtMinContentHeight(t *testing.T) {
73
+ l := CalculateLayout(100, 8)
74
+ if l.ContentHeight != minContentHeight {
75
+ t.Errorf("ContentHeight = %d, want %d", l.ContentHeight, minContentHeight)
76
+ }
77
+ }
78
+
79
+ func TestCalculateLayoutWithChrome_accountsForExtraHeaderLine(t *testing.T) {
80
+ l := CalculateLayoutWithChrome(120, 40, 1)
81
+ if l.ContentHeight != 29 {
82
+ t.Errorf("ContentHeight = %d, want 29", l.ContentHeight)
83
+ }
84
+ }
85
+
69
86
  func TestCalculateLayout_breakpointBoundaries(t *testing.T) {
70
87
  cases := []struct {
71
88
  width int