savepoint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/.prettierignore +4 -0
  3. package/.savepoint/Design.md +196 -0
  4. package/.savepoint/PRD.md +58 -0
  5. package/.savepoint/audit/E01-go-setup/proposals.md +166 -0
  6. package/.savepoint/audit/E01-go-setup/snapshot.md +71 -0
  7. package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +66 -0
  8. package/.savepoint/audit/E01-scaffolding/proposals/Design.md +210 -0
  9. package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +117 -0
  10. package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +101 -0
  11. package/.savepoint/audit/E01-scaffolding/snapshot.md +54 -0
  12. package/.savepoint/audit/E02-data-model/snapshot.md +128 -0
  13. package/.savepoint/audit/E02-data-readers/proposals.md +123 -0
  14. package/.savepoint/audit/E02-data-readers/snapshot.md +54 -0
  15. package/.savepoint/audit/E03-board-tui-core/proposals.md +146 -0
  16. package/.savepoint/audit/E03-board-tui-core/snapshot.md +57 -0
  17. package/.savepoint/audit/E03-cli-foundation/snapshot.md +106 -0
  18. package/.savepoint/audit/E04-board-components/proposals.md +118 -0
  19. package/.savepoint/audit/E04-board-components/snapshot.md +77 -0
  20. package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +115 -0
  21. package/.savepoint/audit/E05-init-command/snapshot.md +125 -0
  22. package/.savepoint/audit/E05-phase-transitions/proposals.md +83 -0
  23. package/.savepoint/audit/E05-phase-transitions/snapshot.md +36 -0
  24. package/.savepoint/audit/E06-tui-board/snapshot.md +64 -0
  25. package/.savepoint/audit/E07-audit-pipeline/snapshot.md +165 -0
  26. package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +65 -0
  27. package/.savepoint/config.yml +27 -0
  28. package/.savepoint/releases/v1/PRD.md +66 -0
  29. package/.savepoint/releases/v1/epics/E01-go-setup/Design.md +39 -0
  30. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +42 -0
  31. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T002-entrypoint.md +23 -0
  32. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T003-directory-structure.md +24 -0
  33. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T004-makefile.md +23 -0
  34. package/.savepoint/releases/v1/epics/E02-data-readers/Design.md +61 -0
  35. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T001-task-struct.md +29 -0
  36. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T002-frontmatter-parser.md +30 -0
  37. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T003-router-reader.md +29 -0
  38. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T004-config-reader.md +29 -0
  39. package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T005-discovery.md +30 -0
  40. package/.savepoint/releases/v1/epics/E03-board-tui-core/Design.md +38 -0
  41. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T001-model.md +29 -0
  42. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T002-update-loop.md +30 -0
  43. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T003-view.md +34 -0
  44. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T004-styles.md +29 -0
  45. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +42 -0
  46. package/.savepoint/releases/v1/epics/E04-board-components/Design.md +44 -0
  47. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T001-column.md +34 -0
  48. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +33 -0
  49. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T003-epic-panel.md +49 -0
  50. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T004-detail-overlay.md +40 -0
  51. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T005-release-dropdown.md +33 -0
  52. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +34 -0
  53. package/.savepoint/releases/v1/epics/E05-phase-transitions/Design.md +38 -0
  54. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T001-phase-stepping.md +29 -0
  55. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T002-gates.md +31 -0
  56. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T003-write-task.md +31 -0
  57. package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T004-write-router.md +31 -0
  58. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +42 -0
  59. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T001-color-system.md +39 -0
  60. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +52 -0
  61. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +52 -0
  62. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +53 -0
  63. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T005-restore-nav-hints.md +39 -0
  64. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +36 -0
  65. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +38 -0
  66. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +41 -0
  67. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +61 -0
  68. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/Design.md +39 -0
  69. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T001-archive-epics.md +20 -0
  70. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T002-rewrite-prd.md +22 -0
  71. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T003-create-epic-stubs.md +24 -0
  72. package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T004-update-router.md +22 -0
  73. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/Design.md +118 -0
  74. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/handoff.md +9 -0
  75. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T001-package-baseline.md +45 -0
  76. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T002-typescript-build.md +48 -0
  77. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T003-vitest-smoke.md +43 -0
  78. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T004-lint-format-gates.md +45 -0
  79. package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T005-scaffold-verification.md +40 -0
  80. package/.savepoint/releases/v1/epics/_archived/E02-data-model/Design.md +142 -0
  81. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T001-domain-ids-status.md +27 -0
  82. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T002-markdown-frontmatter-boundary.md +28 -0
  83. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T003-task-documents.md +29 -0
  84. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T004-release-epic-router-config-readers.md +30 -0
  85. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T005-dependency-validation.md +29 -0
  86. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T006-epic-task-set-reader.md +29 -0
  87. package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T007-quality-gates.md +31 -0
  88. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/Design.md +40 -0
  89. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T001-phase-types.md +27 -0
  90. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T002-phase-frontmatter.md +25 -0
  91. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T003-simplify-config.md +26 -0
  92. package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T004-simplify-router-domain.md +24 -0
  93. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/Design.md +122 -0
  94. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T001-argument-parser-contract.md +28 -0
  95. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T002-help-text-generation.md +28 -0
  96. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T003-terminal-environment-detection.md +27 -0
  97. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T004-command-stub-modules.md +29 -0
  98. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T005-cli-runner-dispatch.md +34 -0
  99. package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T006-entrypoint-quality-gates.md +32 -0
  100. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/Design.md +43 -0
  101. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T001-strip-args.md +26 -0
  102. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T002-strip-help.md +23 -0
  103. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T003-strip-run.md +23 -0
  104. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T004-delete-commands.md +24 -0
  105. package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T005-update-cli-tests.md +22 -0
  106. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/Design.md +48 -0
  107. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T001-board-data-phases.md +26 -0
  108. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T002-phase-rendering.md +28 -0
  109. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T003-detail-pane-phases.md +27 -0
  110. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T004-phase-transitions.md +42 -0
  111. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T005-phase-gates.md +24 -0
  112. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T006-phase-write-back.md +24 -0
  113. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T007-remove-audit-flow.md +27 -0
  114. package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T008-board-tests.md +25 -0
  115. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/Design.md +85 -0
  116. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T001-project-template-assets.md +17 -0
  117. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T002-release-and-prompt-assets.md +20 -0
  118. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T003-template-registry-renderer.md +22 -0
  119. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T004-template-integrity-tests.md +17 -0
  120. package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T005-template-closeout-quality-gates.md +16 -0
  121. package/.savepoint/releases/v1/epics/_archived/E05-init-command/Design.md +88 -0
  122. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T001-init-cli-contract.md +22 -0
  123. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T002-target-validation.md +23 -0
  124. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T003-scaffold-writer.md +24 -0
  125. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T004-magic-prompt-and-clipboard.md +23 -0
  126. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T005-dev-deps-install-option.md +24 -0
  127. package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T006-init-command-integration.md +28 -0
  128. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/Design.md +53 -0
  129. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T001-delete-dead-src.md +23 -0
  130. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T002-delete-dead-tests.md +26 -0
  131. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T003-delete-assets.md +25 -0
  132. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T004-clean-savepoint.md +28 -0
  133. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T005-rewrite-agents-md.md +28 -0
  134. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T006-clean-package-json.md +23 -0
  135. package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T007-verify.md +25 -0
  136. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/Design.md +104 -0
  137. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T001-board-command-data.md +23 -0
  138. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T002-board-view-state.md +24 -0
  139. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T003-transition-gates-and-writes.md +25 -0
  140. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T004-terminal-theme.md +23 -0
  141. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T005-ink-board-ui.md +26 -0
  142. package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T006-board-integration-audit-entry.md +24 -0
  143. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/Design.md +88 -0
  144. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T001-audit-cli-contract.md +23 -0
  145. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T002-quality-gate-runner.md +23 -0
  146. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T003-snapshot-and-prompt.md +23 -0
  147. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T004-audit-orchestration-router.md +27 -0
  148. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T005-proposal-validation-apply.md +25 -0
  149. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T006-audit-review-state.md +24 -0
  150. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T007-audit-review-ui.md +26 -0
  151. package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T008-audit-pipeline-integration.md +24 -0
  152. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/Design.md +103 -0
  153. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T001-acceptance-criteria-model.md +30 -0
  154. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T002-release-task-set-reader.md +33 -0
  155. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T003-board-data-and-plain-output.md +34 -0
  156. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T004-board-selection-state.md +33 -0
  157. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T005-ink-board-layout-cleanup.md +37 -0
  158. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T006-task-detail-popup.md +36 -0
  159. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T007-templates-acceptance-criteria.md +34 -0
  160. package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T008-board-workflow-integration.md +41 -0
  161. package/.savepoint/releases/v1/epics/_archived/E09-doctor-command/Design.md +70 -0
  162. package/.savepoint/releases/v1/epics/_archived/E10-docs-and-packaging/Design.md +68 -0
  163. package/.savepoint/releases/v1/epics/_archived/E11-release-validation/Design.md +68 -0
  164. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +26 -0
  165. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +35 -0
  166. package/.savepoint/router.md +136 -0
  167. package/.savepoint/visual-identity.md +124 -0
  168. package/AGENTS.md +141 -0
  169. package/CLAUDE.md +1 -0
  170. package/GEMINI.md +1 -0
  171. package/LICENSE +21 -0
  172. package/Makefile +13 -0
  173. package/README.md +78 -0
  174. package/agent-skills/ink-tui-design/SKILL.md +309 -0
  175. package/agent-skills/ink-tui-design/references/component-patterns.md +371 -0
  176. package/agent-skills/ink-tui-design/references/hooks-guide.md +436 -0
  177. package/agent-skills/ink-tui-design/references/ink-gotchas.md +330 -0
  178. package/agent-skills/ink-tui-design/references/testing-patterns.md +384 -0
  179. package/agent-skills/savepoint-audit/SKILL.md +35 -0
  180. package/agent-skills/savepoint-build-task/SKILL.md +39 -0
  181. package/agent-skills/savepoint-create-plan/SKILL.md +28 -0
  182. package/agent-skills/savepoint-create-task/SKILL.md +31 -0
  183. package/agent-skills/savepoint-draft-prd/SKILL.md +32 -0
  184. package/agent-skills/savepoint-system-design/SKILL.md +33 -0
  185. package/agent-skills/superpowers/brainstorming/SKILL.md +165 -0
  186. package/agent-skills/superpowers/brainstorming/visual-companion.md +304 -0
  187. package/agent-skills/superpowers/dispatching-parallel-agents/SKILL.md +193 -0
  188. package/agent-skills/superpowers/executing-plans/SKILL.md +77 -0
  189. package/agent-skills/superpowers/finishing-a-development-branch/SKILL.md +213 -0
  190. package/agent-skills/superpowers/receiving-code-review/SKILL.md +226 -0
  191. package/agent-skills/superpowers/requesting-code-review/SKILL.md +115 -0
  192. package/agent-skills/superpowers/requesting-code-review/code-reviewer.md +160 -0
  193. package/agent-skills/superpowers/subagent-driven-development/SKILL.md +292 -0
  194. package/agent-skills/superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +27 -0
  195. package/agent-skills/superpowers/subagent-driven-development/implementer-prompt.md +113 -0
  196. package/agent-skills/superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  197. package/agent-skills/superpowers/systematic-debugging/SKILL.md +305 -0
  198. package/agent-skills/superpowers/systematic-debugging/condition-based-waiting.md +122 -0
  199. package/agent-skills/superpowers/systematic-debugging/defense-in-depth.md +130 -0
  200. package/agent-skills/superpowers/systematic-debugging/root-cause-tracing.md +183 -0
  201. package/agent-skills/superpowers/test-driven-development/SKILL.md +389 -0
  202. package/agent-skills/superpowers/test-driven-development/testing-anti-patterns.md +317 -0
  203. package/agent-skills/superpowers/verification-before-completion/SKILL.md +147 -0
  204. package/agent-skills/superpowers/writing-plans/SKILL.md +159 -0
  205. package/agent-skills/superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
  206. package/assets/banner.png +0 -0
  207. package/assets/logo.png +0 -0
  208. package/assets/strawman.png +0 -0
  209. package/go.mod +33 -0
  210. package/go.sum +73 -0
  211. package/ink-cli-ui-design.zip +0 -0
  212. package/internal/board/board.go +121 -0
  213. package/internal/board/board_test.go +99 -0
  214. package/internal/board/card.go +72 -0
  215. package/internal/board/card_test.go +111 -0
  216. package/internal/board/column.go +61 -0
  217. package/internal/board/column_test.go +81 -0
  218. package/internal/board/detail.go +140 -0
  219. package/internal/board/detail_test.go +233 -0
  220. package/internal/board/epic_panel.go +69 -0
  221. package/internal/board/epic_panel_test.go +246 -0
  222. package/internal/board/help.go +40 -0
  223. package/internal/board/help_test.go +85 -0
  224. package/internal/board/layout.go +58 -0
  225. package/internal/board/layout_test.go +89 -0
  226. package/internal/board/model.go +151 -0
  227. package/internal/board/model_test.go +67 -0
  228. package/internal/board/release.go +42 -0
  229. package/internal/board/release_test.go +177 -0
  230. package/internal/board/transitions.go +88 -0
  231. package/internal/board/transitions_test.go +141 -0
  232. package/internal/board/update.go +155 -0
  233. package/internal/board/update_test.go +128 -0
  234. package/internal/board/view.go +190 -0
  235. package/internal/board/view_test.go +147 -0
  236. package/internal/data/config.go +87 -0
  237. package/internal/data/config_test.go +73 -0
  238. package/internal/data/discover.go +152 -0
  239. package/internal/data/discover_test.go +106 -0
  240. package/internal/data/errors.go +9 -0
  241. package/internal/data/lifecycle.go +37 -0
  242. package/internal/data/lifecycle_test.go +38 -0
  243. package/internal/data/parser.go +189 -0
  244. package/internal/data/parser_test.go +216 -0
  245. package/internal/data/router.go +52 -0
  246. package/internal/data/router_test.go +35 -0
  247. package/internal/data/task.go +46 -0
  248. package/internal/data/task_test.go +51 -0
  249. package/internal/data/write.go +144 -0
  250. package/internal/data/write_test.go +456 -0
  251. package/internal/styles/palette.go +47 -0
  252. package/internal/styles/styles.go +122 -0
  253. package/main.exe +0 -0
  254. package/main.go +11 -0
  255. package/package.json +25 -0
  256. package/savepoint +0 -0
  257. package/savepoint.exe +0 -0
  258. package/scripts/vitest-preload.cjs +95 -0
  259. package/templates/project/.savepoint/Design.md +47 -0
  260. package/templates/project/.savepoint/PRD.md +34 -0
  261. package/templates/project/.savepoint/config.yml +27 -0
  262. package/templates/project/.savepoint/router.md +152 -0
  263. package/templates/project/.savepoint/visual-identity.md +122 -0
  264. package/templates/project/AGENTS.md +130 -0
  265. package/templates/prompts/audit-reconciliation.prompt.md +67 -0
  266. package/templates/prompts/design.prompt.md +43 -0
  267. package/templates/prompts/epic-design.prompt.md +43 -0
  268. package/templates/prompts/magic-prompt.prompt.md +7 -0
  269. package/templates/prompts/prd.prompt.md +42 -0
  270. package/templates/prompts/task-breakdown.prompt.md +54 -0
  271. package/templates/prompts/task-building.prompt.md +38 -0
  272. package/templates/prompts/task-planning.prompt.md +53 -0
  273. package/templates/release/v1/PRD.md +37 -0
@@ -0,0 +1,40 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/opencode/savepoint/internal/styles"
7
+ )
8
+
9
+ const helpBorderPad = 4 // rounded border (2) + padding (2x1)
10
+
11
+ // RenderHelp renders the keyboard shortcut reference overlay.
12
+ func RenderHelp(width int) string {
13
+ inner := width - helpBorderPad
14
+ if inner < 4 {
15
+ inner = 4
16
+ }
17
+
18
+ lines := []string{
19
+ styles.ColumnTitleFocused.Render("KEYBOARD SHORTCUTS"),
20
+ strings.Repeat("─", inner),
21
+ helpRow("h / left", "previous column"),
22
+ helpRow("l / right", "next column"),
23
+ helpRow("enter", "open task detail / select item"),
24
+ helpRow("e", "open epic selector on narrow screens"),
25
+ helpRow("r", "open release selector"),
26
+ helpRow("up / k", "move selector up"),
27
+ helpRow("down / j", "move selector down"),
28
+ helpRow("?", "open help"),
29
+ helpRow("esc / q", "close overlay"),
30
+ helpRow("q / ctrl+c", "quit from board"),
31
+ "",
32
+ styles.CardMeta.Render("esc/q:close"),
33
+ }
34
+
35
+ return styles.DetailOverlay.Width(width).Render(strings.Join(lines, "\n"))
36
+ }
37
+
38
+ func helpRow(key, action string) string {
39
+ return styles.ColumnTitle.Render(key+": ") + styles.CardMeta.Render(action)
40
+ }
@@ -0,0 +1,85 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ tea "github.com/charmbracelet/bubbletea"
8
+ )
9
+
10
+ func TestRenderHelp_containsTitle(t *testing.T) {
11
+ got := RenderHelp(60)
12
+ if !strings.Contains(got, "KEYBOARD SHORTCUTS") {
13
+ t.Error("RenderHelp missing title")
14
+ }
15
+ }
16
+
17
+ func TestRenderHelp_containsShortcuts(t *testing.T) {
18
+ got := RenderHelp(60)
19
+ for _, shortcut := range []string{
20
+ "h / left",
21
+ "l / right",
22
+ "enter",
23
+ "e",
24
+ "r",
25
+ "up / k",
26
+ "down / j",
27
+ "?",
28
+ "esc / q",
29
+ "q / ctrl+c",
30
+ } {
31
+ if !strings.Contains(got, shortcut) {
32
+ t.Errorf("RenderHelp missing shortcut %q", shortcut)
33
+ }
34
+ }
35
+ }
36
+
37
+ func TestRenderHelp_containsCloseHint(t *testing.T) {
38
+ got := RenderHelp(60)
39
+ if !strings.Contains(got, "esc/q:close") {
40
+ t.Error("RenderHelp missing close hint")
41
+ }
42
+ }
43
+
44
+ func TestUpdate_questionMarkOpensHelpOverlay(t *testing.T) {
45
+ m := NewModel(nil, "v1", "E04")
46
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")})
47
+ updated := requireModel(t, got)
48
+ if updated.Overlay != OverlayHelp {
49
+ t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayHelp)
50
+ }
51
+ }
52
+
53
+ func TestUpdate_helpOverlayEscCloses(t *testing.T) {
54
+ m := NewModel(nil, "v1", "E04")
55
+ m.Overlay = OverlayHelp
56
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
57
+ updated := requireModel(t, got)
58
+ if updated.Overlay != OverlayNone {
59
+ t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
60
+ }
61
+ }
62
+
63
+ func TestUpdate_helpOverlayQCloses(t *testing.T) {
64
+ m := NewModel(nil, "v1", "E04")
65
+ m.Overlay = OverlayHelp
66
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")})
67
+ updated := requireModel(t, got)
68
+ if updated.Overlay != OverlayNone {
69
+ t.Errorf("Overlay = %q after q, want none", updated.Overlay)
70
+ }
71
+ }
72
+
73
+ func TestView_helpOverlayRendered(t *testing.T) {
74
+ m := NewModel(nil, "v1", "E04")
75
+ m.Width = 100
76
+ m.Height = 30
77
+ m.Overlay = OverlayHelp
78
+ got := m.View()
79
+ if !strings.Contains(got, "KEYBOARD SHORTCUTS") {
80
+ t.Error("View() with OverlayHelp missing help header")
81
+ }
82
+ if !strings.Contains(got, "q / ctrl+c") {
83
+ t.Error("View() with OverlayHelp missing quit shortcut")
84
+ }
85
+ }
@@ -0,0 +1,58 @@
1
+ package board
2
+
3
+ const (
4
+ colOverhead = 4 // rounded border (1) + padding (1) each side
5
+
6
+ minColWidth = 10
7
+
8
+ epicPanelWidth = 28
9
+ epicPanelOverhead = 4
10
+
11
+ boardFrameOverhead = 4 // rounded border (2) + padding (2×1)
12
+
13
+ breakpointWide = 120
14
+ breakpointNarrow = 80
15
+ )
16
+
17
+ // Layout describes board geometry for a given terminal size.
18
+ type Layout struct {
19
+ EpicPanelVisible bool
20
+ EpicPanelWidth int
21
+ ColCount int
22
+ ColWidths []int
23
+ }
24
+
25
+ // CalculateLayout returns the board layout for the given terminal dimensions.
26
+ //
27
+ // - >=120 cols: epic panel (28w) + 3 columns
28
+ // - 80–119 cols: 3 columns only
29
+ // - <80 cols: 1 column
30
+ func CalculateLayout(width, _ int) Layout {
31
+ inner := width - boardFrameOverhead
32
+ switch {
33
+ case width >= breakpointWide:
34
+ available := inner - (epicPanelWidth + epicPanelOverhead) - 3*colOverhead
35
+ cw := max(available/3, minColWidth)
36
+ return Layout{
37
+ EpicPanelVisible: true,
38
+ EpicPanelWidth: epicPanelWidth,
39
+ ColCount: 3,
40
+ ColWidths: []int{cw, cw, cw},
41
+ }
42
+ case width >= breakpointNarrow:
43
+ available := inner - 3*colOverhead
44
+ cw := max(available/3, minColWidth)
45
+ return Layout{
46
+ EpicPanelVisible: false,
47
+ ColCount: 3,
48
+ ColWidths: []int{cw, cw, cw},
49
+ }
50
+ default:
51
+ cw := max(inner-colOverhead, minColWidth)
52
+ return Layout{
53
+ EpicPanelVisible: false,
54
+ ColCount: 1,
55
+ ColWidths: []int{cw},
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,89 @@
1
+ package board
2
+
3
+ import "testing"
4
+
5
+ func TestCalculateLayout_wide(t *testing.T) {
6
+ l := CalculateLayout(120, 40)
7
+ if !l.EpicPanelVisible {
8
+ t.Error("expected epic panel visible at width=120")
9
+ }
10
+ if l.EpicPanelWidth != epicPanelWidth {
11
+ t.Errorf("EpicPanelWidth = %d, want %d", l.EpicPanelWidth, epicPanelWidth)
12
+ }
13
+ if l.ColCount != 3 {
14
+ t.Errorf("ColCount = %d, want 3", l.ColCount)
15
+ }
16
+ if len(l.ColWidths) != 3 {
17
+ t.Fatalf("len(ColWidths) = %d, want 3", len(l.ColWidths))
18
+ }
19
+ // inner = 120-4 = 116; available = 116-(28+4)-3*4 = 72; cw = 72/3 = 24
20
+ for i, w := range l.ColWidths {
21
+ if w != 24 {
22
+ t.Errorf("ColWidths[%d] = %d, want 24", i, w)
23
+ }
24
+ }
25
+ }
26
+
27
+ func TestCalculateLayout_medium(t *testing.T) {
28
+ l := CalculateLayout(100, 40)
29
+ if l.EpicPanelVisible {
30
+ t.Error("epic panel should be hidden at width=100")
31
+ }
32
+ if l.ColCount != 3 {
33
+ t.Errorf("ColCount = %d, want 3", l.ColCount)
34
+ }
35
+ if len(l.ColWidths) != 3 {
36
+ t.Fatalf("len(ColWidths) = %d, want 3", len(l.ColWidths))
37
+ }
38
+ for i, w := range l.ColWidths {
39
+ if w != 28 {
40
+ t.Errorf("ColWidths[%d] = %d, want 28", i, w)
41
+ }
42
+ }
43
+ }
44
+
45
+ func TestCalculateLayout_narrow(t *testing.T) {
46
+ l := CalculateLayout(60, 40)
47
+ if l.EpicPanelVisible {
48
+ t.Error("epic panel should be hidden at width=60")
49
+ }
50
+ if l.ColCount != 1 {
51
+ t.Errorf("ColCount = %d, want 1", l.ColCount)
52
+ }
53
+ // inner = 60 - 4 = 56; cw = 56 - 4 = 52
54
+ if l.ColWidths[0] != 52 {
55
+ t.Errorf("ColWidths[0] = %d, want 52", l.ColWidths[0])
56
+ }
57
+ }
58
+
59
+ func TestCalculateLayout_tinyWidth_floorsAtMinColWidth(t *testing.T) {
60
+ l := CalculateLayout(4, 40)
61
+ if l.ColCount != 1 {
62
+ t.Errorf("ColCount = %d, want 1", l.ColCount)
63
+ }
64
+ if l.ColWidths[0] != minColWidth {
65
+ t.Errorf("ColWidths[0] = %d, want %d (minColWidth floor)", l.ColWidths[0], minColWidth)
66
+ }
67
+ }
68
+
69
+ func TestCalculateLayout_breakpointBoundaries(t *testing.T) {
70
+ cases := []struct {
71
+ width int
72
+ wantColCount int
73
+ wantEpic bool
74
+ }{
75
+ {119, 3, false},
76
+ {120, 3, true},
77
+ {79, 1, false},
78
+ {80, 3, false},
79
+ }
80
+ for _, tc := range cases {
81
+ l := CalculateLayout(tc.width, 40)
82
+ if l.ColCount != tc.wantColCount {
83
+ t.Errorf("width=%d: ColCount = %d, want %d", tc.width, l.ColCount, tc.wantColCount)
84
+ }
85
+ if l.EpicPanelVisible != tc.wantEpic {
86
+ t.Errorf("width=%d: EpicPanelVisible = %v, want %v", tc.width, l.EpicPanelVisible, tc.wantEpic)
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,151 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+
7
+ tea "github.com/charmbracelet/bubbletea"
8
+ "github.com/opencode/savepoint/internal/data"
9
+ )
10
+
11
+ type OverlayType string
12
+
13
+ const (
14
+ OverlayNone OverlayType = ""
15
+ OverlayHelp OverlayType = "help"
16
+ OverlayEpic OverlayType = "epic"
17
+ OverlayRelease OverlayType = "release"
18
+ OverlayDetail OverlayType = "detail"
19
+ )
20
+
21
+ // Model holds all board state. Tasks are grouped by column for O(1) column access.
22
+ type Model struct {
23
+ AllTasks []data.Task
24
+ Tasks map[data.ColumnType][]data.Task
25
+ FocusedColumn data.ColumnType
26
+ FocusedTask int
27
+ SelectedEpic string
28
+ SelectedRelease string
29
+ Epics []string
30
+ EpicCursor int
31
+ Releases []string
32
+ ReleaseEpics map[string][]string
33
+ ReleaseCursor int
34
+ Overlay OverlayType
35
+ Width int
36
+ Height int
37
+ StatusMessage string
38
+ Root string
39
+ }
40
+
41
+ // NewModel groups tasks by column and returns an initialized Model.
42
+ func NewModel(tasks []data.Task, release, epic string) Model {
43
+ m := Model{
44
+ AllTasks: append([]data.Task(nil), tasks...),
45
+ FocusedColumn: data.ColumnPlanned,
46
+ FocusedTask: 0,
47
+ SelectedEpic: epic,
48
+ SelectedRelease: release,
49
+ Overlay: OverlayNone,
50
+ }
51
+ m.refreshTasks()
52
+ return m
53
+ }
54
+
55
+ func (m Model) Init() tea.Cmd {
56
+ return tea.Batch()
57
+ }
58
+
59
+ func groupedTasks(tasks []data.Task) map[data.ColumnType][]data.Task {
60
+ grouped := map[data.ColumnType][]data.Task{
61
+ data.ColumnPlanned: {},
62
+ data.ColumnInProgress: {},
63
+ data.ColumnDone: {},
64
+ }
65
+ for _, t := range tasks {
66
+ col := t.Column
67
+ if col == "" {
68
+ col = data.ColumnPlanned
69
+ }
70
+ grouped[col] = append(grouped[col], t)
71
+ }
72
+ return grouped
73
+ }
74
+
75
+ func (m *Model) refreshTasks() {
76
+ visible := make([]data.Task, 0, len(m.AllTasks))
77
+ for _, t := range m.AllTasks {
78
+ if m.SelectedRelease != "" && t.Release != "" && t.Release != m.SelectedRelease {
79
+ continue
80
+ }
81
+ if m.SelectedEpic != "" && t.Epic != "" && t.Epic != m.SelectedEpic {
82
+ continue
83
+ }
84
+ visible = append(visible, t)
85
+ }
86
+ m.Tasks = groupedTasks(visible)
87
+ m.clampFocusedTask()
88
+ }
89
+
90
+ func (m *Model) refreshEpicsForRelease() {
91
+ if len(m.ReleaseEpics) == 0 {
92
+ return
93
+ }
94
+
95
+ epics := m.ReleaseEpics[m.SelectedRelease]
96
+ m.Epics = append([]string(nil), epics...)
97
+ if len(m.Epics) == 0 {
98
+ m.SelectedEpic = ""
99
+ m.EpicCursor = 0
100
+ return
101
+ }
102
+
103
+ for _, epic := range m.Epics {
104
+ if epic == m.SelectedEpic {
105
+ m.EpicCursor = epicIndex(m.Epics, m.SelectedEpic)
106
+ return
107
+ }
108
+ }
109
+
110
+ m.SelectedEpic = m.Epics[0]
111
+ m.EpicCursor = 0
112
+ }
113
+
114
+ func (m *Model) clampFocusedTask() {
115
+ tasks := m.Tasks[m.FocusedColumn]
116
+ if len(tasks) == 0 {
117
+ m.FocusedTask = 0
118
+ return
119
+ }
120
+ if m.FocusedTask >= len(tasks) {
121
+ m.FocusedTask = len(tasks) - 1
122
+ }
123
+ if m.FocusedTask < 0 {
124
+ m.FocusedTask = 0
125
+ }
126
+ }
127
+
128
+ func (m *Model) writeRouterReleaseEpic() error {
129
+ routerPath := filepath.Join(m.Root, "router.md")
130
+
131
+ fi, err := os.Stat(routerPath)
132
+ if err != nil {
133
+ return err
134
+ }
135
+
136
+ content, err := os.ReadFile(routerPath)
137
+ if err != nil {
138
+ return err
139
+ }
140
+
141
+ r := data.NewRouterReader()
142
+ state, err := r.ReadState(string(content))
143
+ if err != nil {
144
+ return err
145
+ }
146
+
147
+ state.Epic = m.SelectedEpic
148
+ state.Release = m.SelectedRelease
149
+
150
+ return data.WriteRouterState(m.Root, state, fi.ModTime())
151
+ }
@@ -0,0 +1,67 @@
1
+ package board
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/opencode/savepoint/internal/data"
7
+ )
8
+
9
+ func TestNewModel_emptyTasks(t *testing.T) {
10
+ m := NewModel(nil, "v1", "E03")
11
+
12
+ if m.SelectedRelease != "v1" {
13
+ t.Errorf("SelectedRelease = %q, want %q", m.SelectedRelease, "v1")
14
+ }
15
+ if m.SelectedEpic != "E03" {
16
+ t.Errorf("SelectedEpic = %q, want %q", m.SelectedEpic, "E03")
17
+ }
18
+ if m.FocusedColumn != data.ColumnPlanned {
19
+ t.Errorf("FocusedColumn = %q, want %q", m.FocusedColumn, data.ColumnPlanned)
20
+ }
21
+ if m.FocusedTask != 0 {
22
+ t.Errorf("FocusedTask = %d, want 0", m.FocusedTask)
23
+ }
24
+ if m.Overlay != OverlayNone {
25
+ t.Errorf("Overlay = %q, want empty", m.Overlay)
26
+ }
27
+ for _, col := range []data.ColumnType{data.ColumnPlanned, data.ColumnInProgress, data.ColumnDone} {
28
+ if _, ok := m.Tasks[col]; !ok {
29
+ t.Errorf("Tasks missing column %q", col)
30
+ }
31
+ }
32
+ }
33
+
34
+ func TestNewModel_groupsByColumn(t *testing.T) {
35
+ tasks := []data.Task{
36
+ {ID: "T1", Column: data.ColumnPlanned},
37
+ {ID: "T2", Column: data.ColumnInProgress},
38
+ {ID: "T3", Column: data.ColumnDone},
39
+ {ID: "T4", Column: data.ColumnPlanned},
40
+ }
41
+ m := NewModel(tasks, "v1", "E03")
42
+
43
+ if got := len(m.Tasks[data.ColumnPlanned]); got != 2 {
44
+ t.Errorf("Planned count = %d, want 2", got)
45
+ }
46
+ if got := len(m.Tasks[data.ColumnInProgress]); got != 1 {
47
+ t.Errorf("InProgress count = %d, want 1", got)
48
+ }
49
+ if got := len(m.Tasks[data.ColumnDone]); got != 1 {
50
+ t.Errorf("Done count = %d, want 1", got)
51
+ }
52
+ }
53
+
54
+ func TestNewModel_emptyColumnDefaultsToPlanned(t *testing.T) {
55
+ tasks := []data.Task{{ID: "T1"}}
56
+ m := NewModel(tasks, "v1", "E03")
57
+
58
+ if got := len(m.Tasks[data.ColumnPlanned]); got != 1 {
59
+ t.Errorf("Planned count = %d, want 1 (empty column defaulted)", got)
60
+ }
61
+ }
62
+
63
+ func TestModel_Init(t *testing.T) {
64
+ m := NewModel(nil, "v1", "E03")
65
+ // Init must not panic and returns a valid Cmd (nil or batch).
66
+ _ = m.Init()
67
+ }
@@ -0,0 +1,42 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/opencode/savepoint/internal/styles"
7
+ )
8
+
9
+ const releaseActiveMarker = "►"
10
+
11
+ // RenderReleaseDropdown renders the release selection dropdown overlay.
12
+ func RenderReleaseDropdown(releases []string, cursor int, width int) string {
13
+ inner := width - epicPanelOverhead
14
+ if inner < 2 {
15
+ inner = 2
16
+ }
17
+
18
+ lines := []string{styles.ColumnTitleFocused.Render("SELECT RELEASE"), strings.Repeat("─", inner)}
19
+ for i, r := range releases {
20
+ label := truncate(r, inner-2)
21
+ if i == cursor {
22
+ lines = append(lines, styles.TaskItemFocused.Render(releaseActiveMarker+" "+label))
23
+ } else {
24
+ lines = append(lines, styles.TaskItem.Render(" "+label))
25
+ }
26
+ }
27
+ if len(releases) == 0 {
28
+ lines = append(lines, styles.TaskItem.Render("(none)"))
29
+ }
30
+ lines = append(lines, "", styles.CardMeta.Render("↑↓:nav enter:select esc:cancel"))
31
+ return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
32
+ }
33
+
34
+ // releaseIndex returns the index of selected in releases, or 0 if not found.
35
+ func releaseIndex(releases []string, selected string) int {
36
+ for i, r := range releases {
37
+ if r == selected {
38
+ return i
39
+ }
40
+ }
41
+ return 0
42
+ }
@@ -0,0 +1,177 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ tea "github.com/charmbracelet/bubbletea"
8
+ "github.com/opencode/savepoint/internal/data"
9
+ )
10
+
11
+ func TestRenderReleaseDropdown_showsReleases(t *testing.T) {
12
+ releases := []string{"v1", "v2", "v3"}
13
+ out := RenderReleaseDropdown(releases, 0, 40)
14
+ for _, r := range releases {
15
+ if !strings.Contains(out, r) {
16
+ t.Errorf("output missing release %q", r)
17
+ }
18
+ }
19
+ }
20
+
21
+ func TestRenderReleaseDropdown_marksCurrentCursor(t *testing.T) {
22
+ releases := []string{"v1", "v2"}
23
+ out := RenderReleaseDropdown(releases, 1, 40)
24
+ if !strings.Contains(out, releaseActiveMarker) {
25
+ t.Error("output missing active marker")
26
+ }
27
+ }
28
+
29
+ func TestRenderReleaseDropdown_emptyList(t *testing.T) {
30
+ out := RenderReleaseDropdown(nil, 0, 40)
31
+ if !strings.Contains(out, "(none)") {
32
+ t.Error("expected '(none)' for empty releases")
33
+ }
34
+ }
35
+
36
+ func TestRenderReleaseDropdown_hintsPresent(t *testing.T) {
37
+ out := RenderReleaseDropdown([]string{"v1"}, 0, 40)
38
+ if !strings.Contains(out, "esc:cancel") {
39
+ t.Error("expected hint text in dropdown")
40
+ }
41
+ }
42
+
43
+ func TestReleaseIndex_found(t *testing.T) {
44
+ releases := []string{"v1", "v2", "v3"}
45
+ if got := releaseIndex(releases, "v2"); got != 1 {
46
+ t.Errorf("releaseIndex = %d, want 1", got)
47
+ }
48
+ }
49
+
50
+ func TestReleaseIndex_notFound(t *testing.T) {
51
+ releases := []string{"v1", "v2"}
52
+ if got := releaseIndex(releases, "v9"); got != 0 {
53
+ t.Errorf("releaseIndex = %d, want 0", got)
54
+ }
55
+ }
56
+
57
+ func TestUpdate_rOpensReleaseOverlay(t *testing.T) {
58
+ m := NewModel(nil, "v1", "E03")
59
+ m.Releases = []string{"v1", "v2"}
60
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")})
61
+ updated := requireModel(t, got)
62
+ if updated.Overlay != OverlayRelease {
63
+ t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayRelease)
64
+ }
65
+ }
66
+
67
+ func TestUpdate_rSetsCursorToCurrentRelease(t *testing.T) {
68
+ m := NewModel(nil, "v2", "E03")
69
+ m.Releases = []string{"v1", "v2", "v3"}
70
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")})
71
+ updated := requireModel(t, got)
72
+ if updated.ReleaseCursor != 1 {
73
+ t.Errorf("ReleaseCursor = %d, want 1", updated.ReleaseCursor)
74
+ }
75
+ }
76
+
77
+ func TestUpdate_escClosesReleaseOverlay(t *testing.T) {
78
+ m := NewModel(nil, "v1", "E03")
79
+ m.Overlay = OverlayRelease
80
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
81
+ updated := requireModel(t, got)
82
+ if updated.Overlay != OverlayNone {
83
+ t.Errorf("Overlay = %q, want empty", updated.Overlay)
84
+ }
85
+ }
86
+
87
+ func TestUpdate_releaseNavDown(t *testing.T) {
88
+ m := NewModel(nil, "v1", "E03")
89
+ m.Releases = []string{"v1", "v2"}
90
+ m.Overlay = OverlayRelease
91
+ m.ReleaseCursor = 0
92
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
93
+ updated := requireModel(t, got)
94
+ if updated.ReleaseCursor != 1 {
95
+ t.Errorf("ReleaseCursor = %d, want 1", updated.ReleaseCursor)
96
+ }
97
+ }
98
+
99
+ func TestUpdate_releaseNavUp(t *testing.T) {
100
+ m := NewModel(nil, "v1", "E03")
101
+ m.Releases = []string{"v1", "v2"}
102
+ m.Overlay = OverlayRelease
103
+ m.ReleaseCursor = 1
104
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")})
105
+ updated := requireModel(t, got)
106
+ if updated.ReleaseCursor != 0 {
107
+ t.Errorf("ReleaseCursor = %d, want 0", updated.ReleaseCursor)
108
+ }
109
+ }
110
+
111
+ func TestUpdate_releaseEnterSelects(t *testing.T) {
112
+ tasks := []data.Task{
113
+ {ID: "T1", Epic: "E03", Release: "v1", Column: data.ColumnPlanned},
114
+ {ID: "T2", Epic: "E03", Release: "v2", Column: data.ColumnPlanned},
115
+ }
116
+ m := NewModel(tasks, "v1", "E03")
117
+ m.Releases = []string{"v1", "v2"}
118
+ m.Overlay = OverlayRelease
119
+ m.ReleaseCursor = 1
120
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
121
+ updated := requireModel(t, got)
122
+ if updated.SelectedRelease != "v2" {
123
+ t.Errorf("SelectedRelease = %q, want %q", updated.SelectedRelease, "v2")
124
+ }
125
+ if updated.Overlay != OverlayNone {
126
+ t.Errorf("Overlay = %q, want empty after selection", updated.Overlay)
127
+ }
128
+ if got := len(updated.Tasks[data.ColumnPlanned]); got != 1 {
129
+ t.Errorf("planned task count = %d, want 1 after release selection", got)
130
+ }
131
+ if updated.Tasks[data.ColumnPlanned][0].ID != "T2" {
132
+ t.Errorf("visible task = %q, want T2", updated.Tasks[data.ColumnPlanned][0].ID)
133
+ }
134
+ }
135
+
136
+ func TestUpdate_releaseEnterRefreshesEpics(t *testing.T) {
137
+ tasks := []data.Task{
138
+ {ID: "T1", Epic: "E01", Release: "v1", Column: data.ColumnPlanned},
139
+ {ID: "T2", Epic: "E03", Release: "v2", Column: data.ColumnPlanned},
140
+ }
141
+ m := NewModel(tasks, "v1", "E01")
142
+ m.Releases = []string{"v1", "v2"}
143
+ m.ReleaseEpics = map[string][]string{
144
+ "v1": []string{"E01"},
145
+ "v2": []string{"E03"},
146
+ }
147
+ m.Overlay = OverlayRelease
148
+ m.ReleaseCursor = 1
149
+
150
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
151
+ updated := requireModel(t, got)
152
+
153
+ if updated.SelectedEpic != "E03" {
154
+ t.Errorf("SelectedEpic = %q, want E03", updated.SelectedEpic)
155
+ }
156
+ if len(updated.Epics) != 1 || updated.Epics[0] != "E03" {
157
+ t.Errorf("Epics = %v, want [E03]", updated.Epics)
158
+ }
159
+ if got := len(updated.Tasks[data.ColumnPlanned]); got != 1 {
160
+ t.Fatalf("planned task count = %d, want 1", got)
161
+ }
162
+ if updated.Tasks[data.ColumnPlanned][0].ID != "T2" {
163
+ t.Errorf("visible task = %q, want T2", updated.Tasks[data.ColumnPlanned][0].ID)
164
+ }
165
+ }
166
+
167
+ func TestView_releaseDropdownKeepsBoardBehind(t *testing.T) {
168
+ m := NewModel(nil, "v1", "E03")
169
+ m.Width = 100
170
+ m.Height = 24
171
+ m.Overlay = OverlayRelease
172
+ m.Releases = []string{"v1", "v2"}
173
+ got := m.View()
174
+ if !strings.Contains(got, "S A V E P O I N T") {
175
+ t.Error("View() with OverlayRelease should keep board visible behind overlay")
176
+ }
177
+ }