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,140 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/opencode/savepoint/internal/data"
7
+ "github.com/opencode/savepoint/internal/styles"
8
+ )
9
+
10
+ const detailBorderPad = 4 // rounded border (2) + padding (2×1)
11
+
12
+ // RenderDetail renders a task detail overlay panel at the given display width.
13
+ func RenderDetail(t data.Task, overlayW int) string {
14
+ inner := overlayW - detailBorderPad
15
+ if inner < 4 {
16
+ inner = 4
17
+ }
18
+
19
+ lines := []string{
20
+ styles.ColumnTitleFocused.Render("TASK DETAIL"),
21
+ strings.Repeat("─", inner),
22
+ }
23
+ lines = append(lines,
24
+ detailRow("ID", t.ID, inner),
25
+ detailRow("Title", t.Title, inner),
26
+ detailRow("Epic", t.Epic, inner),
27
+ detailRow("Release", t.Release, inner),
28
+ detailRow("Status", string(t.Column), inner),
29
+ detailRow("Phase", phaseLabel(t.Stage), inner),
30
+ )
31
+
32
+ if t.Description != "" {
33
+ lines = append(lines,
34
+ "",
35
+ styles.ColumnTitle.Render("Description:"),
36
+ )
37
+ for _, line := range wrapText(t.Description, inner) {
38
+ lines = append(lines, styles.CardMeta.Render(line))
39
+ }
40
+ }
41
+
42
+ if len(t.Acceptance) > 0 {
43
+ lines = append(lines, "", styles.ColumnTitle.Render("Acceptance Criteria:"))
44
+ for _, a := range t.Acceptance {
45
+ for _, line := range wrapText(a, inner-2) {
46
+ lines = append(lines, styles.CardMeta.Render(" • "+line))
47
+ }
48
+ }
49
+ }
50
+
51
+ if len(t.Checklist) > 0 {
52
+ lines = append(lines, "", styles.ColumnTitle.Render("Implementation Plan:"))
53
+ for _, item := range t.Checklist {
54
+ for _, line := range wrapText(item, inner-2) {
55
+ lines = append(lines, styles.CardMeta.Render(" □ "+line))
56
+ }
57
+ }
58
+ }
59
+
60
+ lines = append(lines, "", styles.CardMeta.Render("esc:close"))
61
+
62
+ return styles.DetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
63
+ }
64
+
65
+ func detailRow(label, value string, width int) string {
66
+ prefix := label + ": "
67
+ wrapped := wrapText(value, width-len(prefix))
68
+ if len(wrapped) == 0 {
69
+ wrapped = []string{""}
70
+ }
71
+ lines := make([]string, 0, len(wrapped))
72
+ for i, line := range wrapped {
73
+ if i == 0 {
74
+ lines = append(lines, styles.ColumnTitle.Render(prefix)+styles.CardMeta.Render(line))
75
+ continue
76
+ }
77
+ lines = append(lines, strings.Repeat(" ", len(prefix))+styles.CardMeta.Render(line))
78
+ }
79
+ return strings.Join(lines, "\n")
80
+ }
81
+
82
+ func phaseLabel(s data.ProgressStage) string {
83
+ switch s {
84
+ case data.StageTest:
85
+ return "test"
86
+ case data.StageAudit:
87
+ return "audit"
88
+ default:
89
+ return "build"
90
+ }
91
+ }
92
+
93
+ func wrapText(s string, width int) []string {
94
+ if width < 4 {
95
+ width = 4
96
+ }
97
+ words := strings.Fields(s)
98
+ if len(words) == 0 {
99
+ return nil
100
+ }
101
+ lines := []string{}
102
+ current := ""
103
+ for _, word := range words {
104
+ if len([]rune(word)) > width {
105
+ if current != "" {
106
+ lines = append(lines, current)
107
+ current = ""
108
+ }
109
+ lines = append(lines, splitLongWord(word, width)...)
110
+ continue
111
+ }
112
+ if current == "" {
113
+ current = word
114
+ continue
115
+ }
116
+ if len([]rune(current))+1+len([]rune(word)) <= width {
117
+ current += " " + word
118
+ continue
119
+ }
120
+ lines = append(lines, current)
121
+ current = word
122
+ }
123
+ if current != "" {
124
+ lines = append(lines, current)
125
+ }
126
+ return lines
127
+ }
128
+
129
+ func splitLongWord(word string, width int) []string {
130
+ runes := []rune(word)
131
+ lines := []string{}
132
+ for len(runes) > width {
133
+ lines = append(lines, string(runes[:width]))
134
+ runes = runes[width:]
135
+ }
136
+ if len(runes) > 0 {
137
+ lines = append(lines, string(runes))
138
+ }
139
+ return lines
140
+ }
@@ -0,0 +1,233 @@
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 sampleTask() data.Task {
12
+ return data.Task{
13
+ ID: "E04/T001",
14
+ Title: "My Task",
15
+ Epic: "E04-board-components",
16
+ Release: "v1",
17
+ Column: data.ColumnInProgress,
18
+ Stage: data.StageBuild,
19
+ }
20
+ }
21
+
22
+ func TestRenderDetail_containsID(t *testing.T) {
23
+ got := RenderDetail(sampleTask(), 60)
24
+ if !strings.Contains(got, "E04/T001") {
25
+ t.Error("RenderDetail missing task ID")
26
+ }
27
+ }
28
+
29
+ func TestRenderDetail_containsTitle(t *testing.T) {
30
+ got := RenderDetail(sampleTask(), 60)
31
+ if !strings.Contains(got, "My Task") {
32
+ t.Error("RenderDetail missing task title")
33
+ }
34
+ }
35
+
36
+ func TestRenderDetail_containsEpic(t *testing.T) {
37
+ got := RenderDetail(sampleTask(), 60)
38
+ if !strings.Contains(got, "E04-board-components") {
39
+ t.Error("RenderDetail missing epic")
40
+ }
41
+ }
42
+
43
+ func TestRenderDetail_containsRelease(t *testing.T) {
44
+ got := RenderDetail(sampleTask(), 60)
45
+ if !strings.Contains(got, "v1") {
46
+ t.Error("RenderDetail missing release")
47
+ }
48
+ }
49
+
50
+ func TestRenderDetail_containsStatus(t *testing.T) {
51
+ got := RenderDetail(sampleTask(), 60)
52
+ if !strings.Contains(got, "in_progress") {
53
+ t.Error("RenderDetail missing status")
54
+ }
55
+ }
56
+
57
+ func TestRenderDetail_containsPhase(t *testing.T) {
58
+ got := RenderDetail(sampleTask(), 60)
59
+ if !strings.Contains(got, "build") {
60
+ t.Error("RenderDetail missing phase")
61
+ }
62
+ }
63
+
64
+ func TestRenderDetail_containsEscHint(t *testing.T) {
65
+ got := RenderDetail(sampleTask(), 60)
66
+ if !strings.Contains(got, "esc") {
67
+ t.Error("RenderDetail missing esc:close hint")
68
+ }
69
+ }
70
+
71
+ func TestRenderDetail_containsDescription(t *testing.T) {
72
+ tk := sampleTask()
73
+ tk.Description = "some description text"
74
+ got := RenderDetail(tk, 60)
75
+ if !strings.Contains(got, "some description text") {
76
+ t.Error("RenderDetail missing description text")
77
+ }
78
+ }
79
+
80
+ func TestRenderDetail_noDescriptionSectionWhenEmpty(t *testing.T) {
81
+ got := RenderDetail(sampleTask(), 60)
82
+ if strings.Contains(got, "Description:") {
83
+ t.Error("RenderDetail should not show Description section when empty")
84
+ }
85
+ }
86
+
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
+ func TestRenderDetail_containsChecklist(t *testing.T) {
100
+ tk := sampleTask()
101
+ tk.Checklist = []string{"first implementation item", "second implementation item"}
102
+ got := RenderDetail(tk, 60)
103
+ if !strings.Contains(got, "Implementation Plan:") {
104
+ t.Error("RenderDetail missing implementation plan heading")
105
+ }
106
+ if !strings.Contains(got, "first implementation item") {
107
+ t.Error("RenderDetail missing first checklist item")
108
+ }
109
+ if !strings.Contains(got, "second implementation item") {
110
+ t.Error("RenderDetail missing second checklist item")
111
+ }
112
+ }
113
+
114
+ func TestRenderDetail_wrapsLongDescription(t *testing.T) {
115
+ tk := sampleTask()
116
+ tk.Description = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda"
117
+ got := RenderDetail(tk, 30)
118
+ if strings.Contains(got, tk.Description) {
119
+ t.Error("RenderDetail should wrap long description text")
120
+ }
121
+ if !strings.Contains(got, "alpha beta") || !strings.Contains(got, "lambda") {
122
+ t.Error("RenderDetail should preserve wrapped description words")
123
+ }
124
+ }
125
+
126
+ func TestRenderDetail_noAcceptanceSectionWhenEmpty(t *testing.T) {
127
+ got := RenderDetail(sampleTask(), 60)
128
+ if strings.Contains(got, "Acceptance Criteria:") {
129
+ t.Error("RenderDetail should not show Acceptance section when empty")
130
+ }
131
+ }
132
+
133
+ func TestPhaseLabel_build(t *testing.T) {
134
+ if got := phaseLabel(data.StageBuild); got != "build" {
135
+ t.Errorf("phaseLabel(StageBuild) = %q, want %q", got, "build")
136
+ }
137
+ }
138
+
139
+ func TestPhaseLabel_test(t *testing.T) {
140
+ if got := phaseLabel(data.StageTest); got != "test" {
141
+ t.Errorf("phaseLabel(StageTest) = %q, want %q", got, "test")
142
+ }
143
+ }
144
+
145
+ func TestPhaseLabel_audit(t *testing.T) {
146
+ if got := phaseLabel(data.StageAudit); got != "audit" {
147
+ t.Errorf("phaseLabel(StageAudit) = %q, want %q", got, "audit")
148
+ }
149
+ }
150
+
151
+ func TestPhaseLabel_default(t *testing.T) {
152
+ if got := phaseLabel(""); got != "build" {
153
+ t.Errorf("phaseLabel(%q) = %q, want %q", "", got, "build")
154
+ }
155
+ }
156
+
157
+ func TestUpdate_enterOpensDetailOverlay(t *testing.T) {
158
+ tasks := []data.Task{sampleTask()}
159
+ m := NewModel(tasks, "v1", "E04-board-components")
160
+ m.FocusedColumn = data.ColumnInProgress
161
+ m.FocusedTask = 0
162
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
163
+ updated := requireModel(t, got)
164
+ if updated.Overlay != OverlayDetail {
165
+ t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayDetail)
166
+ }
167
+ }
168
+
169
+ func TestUpdate_enterNoOpWhenNoTasks(t *testing.T) {
170
+ m := NewModel(nil, "v1", "E04-board-components")
171
+ m.FocusedColumn = data.ColumnPlanned
172
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
173
+ updated := requireModel(t, got)
174
+ if updated.Overlay != OverlayNone {
175
+ t.Errorf("Overlay = %q, want none when column has no tasks", updated.Overlay)
176
+ }
177
+ }
178
+
179
+ func TestUpdate_detailOverlayEscCloses(t *testing.T) {
180
+ m := NewModel(nil, "v1", "E04-board-components")
181
+ m.Overlay = OverlayDetail
182
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
183
+ updated := requireModel(t, got)
184
+ if updated.Overlay != OverlayNone {
185
+ t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
186
+ }
187
+ }
188
+
189
+ func TestUpdate_detailOverlayBlocksColumnNav(t *testing.T) {
190
+ m := NewModel(nil, "v1", "E04-board-components")
191
+ m.Overlay = OverlayDetail
192
+ m.FocusedColumn = data.ColumnPlanned
193
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
194
+ updated := requireModel(t, got)
195
+ if updated.FocusedColumn != data.ColumnPlanned {
196
+ t.Error("column nav should be blocked when detail overlay is open")
197
+ }
198
+ }
199
+
200
+ func TestView_detailOverlayRendered(t *testing.T) {
201
+ tasks := []data.Task{sampleTask()}
202
+ m := NewModel(tasks, "v1", "E04-board-components")
203
+ m.Width = 100
204
+ m.Height = 30
205
+ m.FocusedColumn = data.ColumnInProgress
206
+ m.FocusedTask = 0
207
+ m.Overlay = OverlayDetail
208
+ got := m.View()
209
+ if !strings.Contains(got, "TASK DETAIL") {
210
+ t.Error("View() with OverlayDetail missing TASK DETAIL header")
211
+ }
212
+ if !strings.Contains(got, "E04/T001") {
213
+ t.Error("View() with OverlayDetail missing task ID")
214
+ }
215
+ }
216
+
217
+ func TestOverlayWidth_clampMax(t *testing.T) {
218
+ if got := overlayWidth(120); got != 80 {
219
+ t.Errorf("overlayWidth(120) = %d, want 80", got)
220
+ }
221
+ }
222
+
223
+ func TestOverlayWidth_termMinus4(t *testing.T) {
224
+ if got := overlayWidth(60); got != 56 {
225
+ t.Errorf("overlayWidth(60) = %d, want 56", got)
226
+ }
227
+ }
228
+
229
+ func TestOverlayWidth_clampMin(t *testing.T) {
230
+ if got := overlayWidth(10); got != 20 {
231
+ t.Errorf("overlayWidth(10) = %d, want 20", got)
232
+ }
233
+ }
@@ -0,0 +1,69 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/opencode/savepoint/internal/styles"
7
+ )
8
+
9
+ const epicActiveMarker = "►"
10
+
11
+ // RenderEpicSidebar renders the fixed left sidebar listing epics with active indicator.
12
+ // If epics is empty and selected is non-empty, selected is shown as the sole entry.
13
+ func RenderEpicSidebar(epics []string, selected string, width int) string {
14
+ inner := width - epicPanelOverhead
15
+ if inner < 2 {
16
+ inner = 2
17
+ }
18
+ list := epics
19
+ if len(list) == 0 && selected != "" {
20
+ list = []string{selected}
21
+ }
22
+
23
+ lines := []string{styles.ColumnTitle.Render("EPICS"), strings.Repeat("─", inner)}
24
+ for _, e := range list {
25
+ label := truncate(e, inner-2)
26
+ if e == selected {
27
+ lines = append(lines, styles.TaskItemFocused.Render(epicActiveMarker+" "+label))
28
+ } else {
29
+ lines = append(lines, styles.TaskItem.Render(" "+label))
30
+ }
31
+ }
32
+ if len(list) == 0 {
33
+ lines = append(lines, styles.TaskItem.Render("(none)"))
34
+ }
35
+ return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
36
+ }
37
+
38
+ // RenderEpicDropdown renders the epic selection dropdown overlay.
39
+ func RenderEpicDropdown(epics []string, cursor int, width int) string {
40
+ inner := width - epicPanelOverhead
41
+ if inner < 2 {
42
+ inner = 2
43
+ }
44
+
45
+ lines := []string{styles.ColumnTitleFocused.Render("SELECT EPIC"), strings.Repeat("─", inner)}
46
+ for i, e := range epics {
47
+ label := truncate(e, inner-2)
48
+ if i == cursor {
49
+ lines = append(lines, styles.TaskItemFocused.Render(epicActiveMarker+" "+label))
50
+ } else {
51
+ lines = append(lines, styles.TaskItem.Render(" "+label))
52
+ }
53
+ }
54
+ if len(epics) == 0 {
55
+ lines = append(lines, styles.TaskItem.Render("(none)"))
56
+ }
57
+ lines = append(lines, "", styles.CardMeta.Render("↑↓:nav enter:select esc:cancel"))
58
+ return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
59
+ }
60
+
61
+ // epicIndex returns the index of selected in epics, or 0 if not found.
62
+ func epicIndex(epics []string, selected string) int {
63
+ for i, e := range epics {
64
+ if e == selected {
65
+ return i
66
+ }
67
+ }
68
+ return 0
69
+ }
@@ -0,0 +1,246 @@
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 TestRenderEpicSidebar_containsEpicsHeader(t *testing.T) {
12
+ got := RenderEpicSidebar([]string{"E01", "E02"}, "E01", 28)
13
+ if !strings.Contains(got, "EPICS") {
14
+ t.Error("RenderEpicSidebar missing EPICS header")
15
+ }
16
+ }
17
+
18
+ func TestRenderEpicSidebar_activeEpicMarked(t *testing.T) {
19
+ got := RenderEpicSidebar([]string{"E01", "E02"}, "E01", 28)
20
+ if !strings.Contains(got, epicActiveMarker) {
21
+ t.Errorf("RenderEpicSidebar missing active marker %q", epicActiveMarker)
22
+ }
23
+ }
24
+
25
+ func TestRenderEpicSidebar_allEpicsPresent(t *testing.T) {
26
+ epics := []string{"E01-foo", "E02-bar", "E03-baz"}
27
+ got := RenderEpicSidebar(epics, "E01-foo", 32)
28
+ for _, e := range epics {
29
+ if !strings.Contains(got, e) {
30
+ t.Errorf("RenderEpicSidebar missing epic %q", e)
31
+ }
32
+ }
33
+ }
34
+
35
+ func TestRenderEpicSidebar_emptyEpicsFallback(t *testing.T) {
36
+ got := RenderEpicSidebar(nil, "E03", 28)
37
+ if !strings.Contains(got, "E03") {
38
+ t.Error("RenderEpicSidebar with empty list should show selected epic")
39
+ }
40
+ }
41
+
42
+ func TestRenderEpicSidebar_emptyBothShowsNone(t *testing.T) {
43
+ got := RenderEpicSidebar(nil, "", 28)
44
+ if !strings.Contains(got, "(none)") {
45
+ t.Error("RenderEpicSidebar with no epics and no selected should show (none)")
46
+ }
47
+ }
48
+
49
+ func TestRenderEpicDropdown_containsHeader(t *testing.T) {
50
+ got := RenderEpicDropdown([]string{"E01", "E02"}, 0, 32)
51
+ if !strings.Contains(got, "SELECT EPIC") {
52
+ t.Error("RenderEpicDropdown missing SELECT EPIC header")
53
+ }
54
+ }
55
+
56
+ func TestRenderEpicDropdown_cursorMarked(t *testing.T) {
57
+ got := RenderEpicDropdown([]string{"E01", "E02"}, 1, 32)
58
+ if !strings.Contains(got, epicActiveMarker) {
59
+ t.Errorf("RenderEpicDropdown missing cursor marker %q", epicActiveMarker)
60
+ }
61
+ }
62
+
63
+ func TestRenderEpicDropdown_containsHint(t *testing.T) {
64
+ got := RenderEpicDropdown([]string{"E01"}, 0, 32)
65
+ if !strings.Contains(got, "esc") {
66
+ t.Error("RenderEpicDropdown missing esc hint")
67
+ }
68
+ }
69
+
70
+ func TestRenderEpicDropdown_emptyShowsNone(t *testing.T) {
71
+ got := RenderEpicDropdown(nil, 0, 32)
72
+ if !strings.Contains(got, "(none)") {
73
+ t.Error("RenderEpicDropdown with no epics should show (none)")
74
+ }
75
+ }
76
+
77
+ func TestEpicIndex_found(t *testing.T) {
78
+ epics := []string{"E01", "E02", "E03"}
79
+ if got := epicIndex(epics, "E02"); got != 1 {
80
+ t.Errorf("epicIndex = %d, want 1", got)
81
+ }
82
+ }
83
+
84
+ func TestEpicIndex_notFound(t *testing.T) {
85
+ if got := epicIndex([]string{"E01"}, "E99"); got != 0 {
86
+ t.Errorf("epicIndex not-found = %d, want 0", got)
87
+ }
88
+ }
89
+
90
+ func TestEpicIndex_empty(t *testing.T) {
91
+ if got := epicIndex(nil, "E01"); got != 0 {
92
+ t.Errorf("epicIndex empty = %d, want 0", got)
93
+ }
94
+ }
95
+
96
+ // Update integration tests for epic dropdown
97
+
98
+ func TestUpdate_eKeyOpensDropdownNarrow(t *testing.T) {
99
+ m := NewModel(nil, "v1", "E03")
100
+ m.Width = 80 // narrow: < 120
101
+ m.Epics = []string{"E01", "E03"}
102
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("e")})
103
+ updated := requireModel(t, got)
104
+ if updated.Overlay != OverlayEpic {
105
+ t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayEpic)
106
+ }
107
+ }
108
+
109
+ func TestUpdate_eKeyOpensDropdownWide(t *testing.T) {
110
+ m := NewModel(nil, "v1", "E03")
111
+ m.Width = 120
112
+ m.Epics = []string{"E01", "E03"}
113
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("e")})
114
+ updated := requireModel(t, got)
115
+ if updated.Overlay != OverlayEpic {
116
+ t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayEpic)
117
+ }
118
+ }
119
+
120
+ func TestUpdate_epicDropdownEscCloses(t *testing.T) {
121
+ m := NewModel(nil, "v1", "E03")
122
+ m.Overlay = OverlayEpic
123
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
124
+ updated := requireModel(t, got)
125
+ if updated.Overlay != OverlayNone {
126
+ t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
127
+ }
128
+ }
129
+
130
+ func TestUpdate_epicDropdownDownMovesCursor(t *testing.T) {
131
+ m := NewModel(nil, "v1", "E01")
132
+ m.Overlay = OverlayEpic
133
+ m.Epics = []string{"E01", "E02", "E03"}
134
+ m.EpicCursor = 0
135
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
136
+ updated := requireModel(t, got)
137
+ if updated.EpicCursor != 1 {
138
+ t.Errorf("EpicCursor = %d, want 1", updated.EpicCursor)
139
+ }
140
+ }
141
+
142
+ func TestUpdate_epicDropdownUpMovesCursor(t *testing.T) {
143
+ m := NewModel(nil, "v1", "E02")
144
+ m.Overlay = OverlayEpic
145
+ m.Epics = []string{"E01", "E02", "E03"}
146
+ m.EpicCursor = 2
147
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyUp})
148
+ updated := requireModel(t, got)
149
+ if updated.EpicCursor != 1 {
150
+ t.Errorf("EpicCursor = %d, want 1", updated.EpicCursor)
151
+ }
152
+ }
153
+
154
+ func TestUpdate_epicDropdownEnterSelectsEpic(t *testing.T) {
155
+ tasks := []data.Task{
156
+ {ID: "T1", Epic: "E01", Release: "v1", Column: data.ColumnPlanned},
157
+ {ID: "T3", Epic: "E03", Release: "v1", Column: data.ColumnPlanned},
158
+ }
159
+ m := NewModel(tasks, "v1", "E01")
160
+ m.Overlay = OverlayEpic
161
+ m.Epics = []string{"E01", "E02", "E03"}
162
+ m.EpicCursor = 2
163
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
164
+ updated := requireModel(t, got)
165
+ if updated.SelectedEpic != "E03" {
166
+ t.Errorf("SelectedEpic = %q, want %q", updated.SelectedEpic, "E03")
167
+ }
168
+ if updated.Overlay != OverlayNone {
169
+ t.Errorf("Overlay = %q after enter, want none", updated.Overlay)
170
+ }
171
+ if got := len(updated.Tasks[data.ColumnPlanned]); got != 1 {
172
+ t.Errorf("planned task count = %d, want 1 after epic selection", got)
173
+ }
174
+ if updated.Tasks[data.ColumnPlanned][0].ID != "T3" {
175
+ t.Errorf("visible task = %q, want T3", updated.Tasks[data.ColumnPlanned][0].ID)
176
+ }
177
+ }
178
+
179
+ func TestUpdate_epicDropdownDownClampedAtEnd(t *testing.T) {
180
+ m := NewModel(nil, "v1", "E03")
181
+ m.Overlay = OverlayEpic
182
+ m.Epics = []string{"E01", "E02"}
183
+ m.EpicCursor = 1
184
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
185
+ updated := requireModel(t, got)
186
+ if updated.EpicCursor != 1 {
187
+ t.Errorf("EpicCursor = %d, want 1 (clamped)", updated.EpicCursor)
188
+ }
189
+ }
190
+
191
+ func TestUpdate_epicDropdownUpClampedAtStart(t *testing.T) {
192
+ m := NewModel(nil, "v1", "E01")
193
+ m.Overlay = OverlayEpic
194
+ m.Epics = []string{"E01", "E02"}
195
+ m.EpicCursor = 0
196
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyUp})
197
+ updated := requireModel(t, got)
198
+ if updated.EpicCursor != 0 {
199
+ t.Errorf("EpicCursor = %d, want 0 (clamped)", updated.EpicCursor)
200
+ }
201
+ }
202
+
203
+ func TestUpdate_overlayBlocksColumnNav(t *testing.T) {
204
+ m := NewModel(nil, "v1", "E01")
205
+ m.Overlay = OverlayEpic
206
+ m.FocusedColumn = "planned"
207
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
208
+ updated := requireModel(t, got)
209
+ if updated.FocusedColumn != "planned" {
210
+ t.Error("column nav should be blocked when overlay is open")
211
+ }
212
+ }
213
+
214
+ func TestView_epicDropdownOverlayRendered(t *testing.T) {
215
+ m := NewModel(nil, "v1", "E01")
216
+ m.Width = 80
217
+ m.Height = 24
218
+ m.Overlay = OverlayEpic
219
+ m.Epics = []string{"E01", "E02"}
220
+ got := m.View()
221
+ if !strings.Contains(got, "SELECT EPIC") {
222
+ t.Error("View() with OverlayEpic missing SELECT EPIC")
223
+ }
224
+ }
225
+
226
+ func TestView_epicDropdownKeepsBoardBehind(t *testing.T) {
227
+ m := NewModel(nil, "v1", "E01")
228
+ m.Width = 100
229
+ m.Height = 24
230
+ m.Overlay = OverlayEpic
231
+ m.Epics = []string{"E01", "E02"}
232
+ got := m.View()
233
+ if !strings.Contains(got, "S A V E P O I N T") {
234
+ t.Error("View() with OverlayEpic should keep board visible behind overlay")
235
+ }
236
+ }
237
+
238
+ func TestView_epicSidebarOnWide(t *testing.T) {
239
+ m := NewModel(nil, "v1", "E03")
240
+ m.Width = 120
241
+ m.Epics = []string{"E01", "E03"}
242
+ got := m.View()
243
+ if !strings.Contains(got, "EPICS") {
244
+ t.Error("View() at width>=120 missing EPICS header in sidebar")
245
+ }
246
+ }