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,88 @@
1
+ package board
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/opencode/savepoint/internal/data"
7
+ )
8
+
9
+ // Advance moves a task forward through the phase lifecycle.
10
+ func Advance(t *data.Task) {
11
+ switch t.Column {
12
+ case data.ColumnPlanned:
13
+ t.Column = data.ColumnInProgress
14
+ t.Stage = data.StageBuild
15
+ case data.ColumnInProgress:
16
+ switch t.Stage {
17
+ case data.StageBuild:
18
+ t.Stage = data.StageTest
19
+ case data.StageTest:
20
+ t.Stage = data.StageAudit
21
+ case data.StageAudit:
22
+ t.Column = data.ColumnDone
23
+ t.Stage = ""
24
+ }
25
+ }
26
+ }
27
+
28
+ // Retreat moves a task backward through the phase lifecycle.
29
+ func Retreat(t *data.Task) {
30
+ switch t.Column {
31
+ case data.ColumnDone:
32
+ t.Column = data.ColumnInProgress
33
+ t.Stage = data.StageAudit
34
+ case data.ColumnInProgress:
35
+ switch t.Stage {
36
+ case data.StageAudit:
37
+ t.Stage = data.StageTest
38
+ case data.StageTest:
39
+ t.Stage = data.StageBuild
40
+ case data.StageBuild:
41
+ t.Column = data.ColumnPlanned
42
+ t.Stage = ""
43
+ }
44
+ }
45
+ }
46
+
47
+ // CanAdvance checks whether a task is allowed to advance to its next phase.
48
+ // It validates phase adjacency and dependency completion.
49
+ // Returns (true, "") if allowed, or (false, reason) if blocked.
50
+ func CanAdvance(t *data.Task, allTasks []data.Task) (bool, string) {
51
+ switch t.Column {
52
+ case data.ColumnPlanned:
53
+ return true, ""
54
+ case data.ColumnInProgress:
55
+ switch t.Stage {
56
+ case data.StageBuild:
57
+ return true, ""
58
+ case data.StageTest:
59
+ return true, ""
60
+ case data.StageAudit:
61
+ for _, depID := range t.DependsOn {
62
+ dep := findTask(depID, allTasks)
63
+ if dep == nil {
64
+ return false, fmt.Sprintf("dependency %q not found", depID)
65
+ }
66
+ if dep.Column != data.ColumnDone {
67
+ return false, fmt.Sprintf("dependency %q is not done", depID)
68
+ }
69
+ }
70
+ return true, ""
71
+ default:
72
+ return false, fmt.Sprintf("unknown stage %q", t.Stage)
73
+ }
74
+ case data.ColumnDone:
75
+ return false, "task is already done"
76
+ default:
77
+ return false, fmt.Sprintf("unknown column %q", t.Column)
78
+ }
79
+ }
80
+
81
+ func findTask(id string, tasks []data.Task) *data.Task {
82
+ for i := range tasks {
83
+ if tasks[i].ID == id {
84
+ return &tasks[i]
85
+ }
86
+ }
87
+ return nil
88
+ }
@@ -0,0 +1,141 @@
1
+ package board
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/opencode/savepoint/internal/data"
7
+ )
8
+
9
+ func TestAdvance(t *testing.T) {
10
+ tests := []struct {
11
+ name string
12
+ initialCol data.ColumnType
13
+ initialSt data.ProgressStage
14
+ expectCol data.ColumnType
15
+ expectSt data.ProgressStage
16
+ }{
17
+ {"planned to in_progress/build", data.ColumnPlanned, "", data.ColumnInProgress, data.StageBuild},
18
+ {"in_progress/build to test", data.ColumnInProgress, data.StageBuild, data.ColumnInProgress, data.StageTest},
19
+ {"in_progress/test to audit", data.ColumnInProgress, data.StageTest, data.ColumnInProgress, data.StageAudit},
20
+ {"in_progress/audit to done", data.ColumnInProgress, data.StageAudit, data.ColumnDone, ""},
21
+ }
22
+
23
+ for _, tt := range tests {
24
+ t.Run(tt.name, func(t *testing.T) {
25
+ task := data.Task{Column: tt.initialCol, Stage: tt.initialSt}
26
+ Advance(&task)
27
+ if task.Column != tt.expectCol || task.Stage != tt.expectSt {
28
+ t.Errorf("Advance() = %v/%v, want %v/%v", task.Column, task.Stage, tt.expectCol, tt.expectSt)
29
+ }
30
+ })
31
+ }
32
+ }
33
+
34
+ func TestRetreat(t *testing.T) {
35
+ tests := []struct {
36
+ name string
37
+ initialCol data.ColumnType
38
+ initialSt data.ProgressStage
39
+ expectCol data.ColumnType
40
+ expectSt data.ProgressStage
41
+ }{
42
+ {"done to in_progress/audit", data.ColumnDone, "", data.ColumnInProgress, data.StageAudit},
43
+ {"in_progress/audit to test", data.ColumnInProgress, data.StageAudit, data.ColumnInProgress, data.StageTest},
44
+ {"in_progress/test to build", data.ColumnInProgress, data.StageTest, data.ColumnInProgress, data.StageBuild},
45
+ {"in_progress/build to planned", data.ColumnInProgress, data.StageBuild, data.ColumnPlanned, ""},
46
+ }
47
+
48
+ for _, tt := range tests {
49
+ t.Run(tt.name, func(t *testing.T) {
50
+ task := data.Task{Column: tt.initialCol, Stage: tt.initialSt}
51
+ Retreat(&task)
52
+ if task.Column != tt.expectCol || task.Stage != tt.expectSt {
53
+ t.Errorf("Retreat() = %v/%v, want %v/%v", task.Column, task.Stage, tt.expectCol, tt.expectSt)
54
+ }
55
+ })
56
+ }
57
+ }
58
+
59
+ func TestCanAdvance_plannedAlwaysAllowed(t *testing.T) {
60
+ task := data.Task{ID: "T1", Column: data.ColumnPlanned}
61
+ ok, reason := CanAdvance(&task, nil)
62
+ if !ok {
63
+ t.Errorf("CanAdvance(planned) = false %q, want true", reason)
64
+ }
65
+ }
66
+
67
+ func TestCanAdvance_buildAlwaysAllowed(t *testing.T) {
68
+ task := data.Task{ID: "T1", Column: data.ColumnInProgress, Stage: data.StageBuild}
69
+ ok, reason := CanAdvance(&task, nil)
70
+ if !ok {
71
+ t.Errorf("CanAdvance(build) = false %q, want true", reason)
72
+ }
73
+ }
74
+
75
+ func TestCanAdvance_testAlwaysAllowed(t *testing.T) {
76
+ task := data.Task{ID: "T1", Column: data.ColumnInProgress, Stage: data.StageTest}
77
+ ok, reason := CanAdvance(&task, nil)
78
+ if !ok {
79
+ t.Errorf("CanAdvance(test) = false %q, want true", reason)
80
+ }
81
+ }
82
+
83
+ func TestCanAdvance_auditDoneBlockedByDependency(t *testing.T) {
84
+ allTasks := []data.Task{
85
+ {ID: "T1", Column: data.ColumnInProgress, Stage: data.StageAudit, DependsOn: []string{"T2"}},
86
+ {ID: "T2", Column: data.ColumnInProgress, Stage: data.StageBuild},
87
+ }
88
+ ok, reason := CanAdvance(&allTasks[0], allTasks)
89
+ if ok {
90
+ t.Fatal("CanAdvance(audit with undep) = true, want false")
91
+ }
92
+ if reason == "" {
93
+ t.Fatal("expected non-empty reason string")
94
+ }
95
+ }
96
+
97
+ func TestCanAdvance_auditDoneAllowedWhenDepsDone(t *testing.T) {
98
+ allTasks := []data.Task{
99
+ {ID: "T1", Column: data.ColumnInProgress, Stage: data.StageAudit, DependsOn: []string{"T2"}},
100
+ {ID: "T2", Column: data.ColumnDone},
101
+ }
102
+ ok, reason := CanAdvance(&allTasks[0], allTasks)
103
+ if !ok {
104
+ t.Errorf("CanAdvance(audit with dep done) = false %q, want true", reason)
105
+ }
106
+ }
107
+
108
+ func TestCanAdvance_doneBlocked(t *testing.T) {
109
+ task := data.Task{ID: "T1", Column: data.ColumnDone}
110
+ ok, reason := CanAdvance(&task, nil)
111
+ if ok {
112
+ t.Fatal("CanAdvance(done) = true, want false")
113
+ }
114
+ if reason == "" {
115
+ t.Fatal("expected non-empty reason string")
116
+ }
117
+ }
118
+
119
+ func TestCanAdvance_unknownStageBlocked(t *testing.T) {
120
+ task := data.Task{ID: "T1", Column: data.ColumnInProgress, Stage: "invalid"}
121
+ ok, reason := CanAdvance(&task, nil)
122
+ if ok {
123
+ t.Fatal("CanAdvance(unknown stage) = true, want false")
124
+ }
125
+ if reason == "" {
126
+ t.Fatal("expected non-empty reason string")
127
+ }
128
+ }
129
+
130
+ func TestCanAdvance_auditDepsNotFoundBlocked(t *testing.T) {
131
+ allTasks := []data.Task{
132
+ {ID: "T1", Column: data.ColumnInProgress, Stage: data.StageAudit, DependsOn: []string{"T2"}},
133
+ }
134
+ ok, reason := CanAdvance(&allTasks[0], allTasks)
135
+ if ok {
136
+ t.Fatal("CanAdvance(audit missing dep) = true, want false")
137
+ }
138
+ if reason == "" {
139
+ t.Fatal("expected non-empty reason string")
140
+ }
141
+ }
@@ -0,0 +1,155 @@
1
+ package board
2
+
3
+ import (
4
+ tea "github.com/charmbracelet/bubbletea"
5
+ "github.com/opencode/savepoint/internal/data"
6
+ )
7
+
8
+ var columnOrder = []data.ColumnType{
9
+ data.ColumnPlanned,
10
+ data.ColumnInProgress,
11
+ data.ColumnDone,
12
+ }
13
+
14
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
15
+ switch msg := msg.(type) {
16
+ case tea.KeyMsg:
17
+ if m.Overlay != OverlayNone {
18
+ return m.updateOverlay(msg)
19
+ }
20
+ switch msg.String() {
21
+ case "q", "ctrl+c":
22
+ return m, tea.Quit
23
+ case "left", "h":
24
+ m.FocusedColumn = prevColumn(m.FocusedColumn)
25
+ m.FocusedTask = 0
26
+ m.StatusMessage = ""
27
+ case "right", "l":
28
+ m.FocusedColumn = nextColumn(m.FocusedColumn)
29
+ m.FocusedTask = 0
30
+ m.StatusMessage = ""
31
+ case "up", "k":
32
+ if m.FocusedTask > 0 {
33
+ m.FocusedTask--
34
+ }
35
+ m.StatusMessage = ""
36
+ case "down", "j":
37
+ if m.FocusedTask < len(m.Tasks[m.FocusedColumn])-1 {
38
+ m.FocusedTask++
39
+ }
40
+ m.StatusMessage = ""
41
+ case "enter":
42
+ tasks := m.Tasks[m.FocusedColumn]
43
+ if len(tasks) > 0 && m.FocusedTask < len(tasks) {
44
+ m.Overlay = OverlayDetail
45
+ }
46
+ m.StatusMessage = ""
47
+ case " ":
48
+ tasks := m.Tasks[m.FocusedColumn]
49
+ if len(tasks) > 0 && m.FocusedTask < len(tasks) {
50
+ task := tasks[m.FocusedTask]
51
+ if ok, reason := CanAdvance(&task, m.AllTasks); !ok {
52
+ m.StatusMessage = reason
53
+ } else {
54
+ m.StatusMessage = ""
55
+ for i, t := range m.AllTasks {
56
+ if t.ID == task.ID {
57
+ Advance(&m.AllTasks[i])
58
+ break
59
+ }
60
+ }
61
+ m.refreshTasks()
62
+ }
63
+ }
64
+ case "backspace":
65
+ tasks := m.Tasks[m.FocusedColumn]
66
+ if len(tasks) > 0 && m.FocusedTask < len(tasks) {
67
+ task := tasks[m.FocusedTask]
68
+ for i, t := range m.AllTasks {
69
+ if t.ID == task.ID {
70
+ Retreat(&m.AllTasks[i])
71
+ break
72
+ }
73
+ }
74
+ m.refreshTasks()
75
+ }
76
+ m.StatusMessage = ""
77
+ case "e":
78
+ m.Overlay = OverlayEpic
79
+ m.EpicCursor = epicIndex(m.Epics, m.SelectedEpic)
80
+ case "r":
81
+ m.Overlay = OverlayRelease
82
+ m.ReleaseCursor = releaseIndex(m.Releases, m.SelectedRelease)
83
+ case "?":
84
+ m.Overlay = OverlayHelp
85
+ }
86
+ case tea.WindowSizeMsg:
87
+ m.Width = msg.Width
88
+ m.Height = msg.Height
89
+ }
90
+ return m, nil
91
+ }
92
+
93
+ func (m Model) updateOverlay(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
94
+ switch msg.String() {
95
+ case "esc", "q":
96
+ m.Overlay = OverlayNone
97
+ case "up", "k":
98
+ if m.Overlay == OverlayEpic && m.EpicCursor > 0 {
99
+ m.EpicCursor--
100
+ }
101
+ if m.Overlay == OverlayRelease && m.ReleaseCursor > 0 {
102
+ m.ReleaseCursor--
103
+ }
104
+ case "down", "j":
105
+ if m.Overlay == OverlayEpic && len(m.Epics) > 0 && m.EpicCursor < len(m.Epics)-1 {
106
+ m.EpicCursor++
107
+ }
108
+ if m.Overlay == OverlayRelease && len(m.Releases) > 0 && m.ReleaseCursor < len(m.Releases)-1 {
109
+ m.ReleaseCursor++
110
+ }
111
+ case "enter":
112
+ if m.Overlay == OverlayEpic && len(m.Epics) > 0 {
113
+ m.SelectedEpic = m.Epics[m.EpicCursor]
114
+ m.FocusedTask = 0
115
+ m.refreshTasks()
116
+ m.Overlay = OverlayNone
117
+ if m.Root != "" {
118
+ if err := m.writeRouterReleaseEpic(); err != nil {
119
+ m.StatusMessage = err.Error()
120
+ }
121
+ }
122
+ }
123
+ if m.Overlay == OverlayRelease && len(m.Releases) > 0 {
124
+ m.SelectedRelease = m.Releases[m.ReleaseCursor]
125
+ m.refreshEpicsForRelease()
126
+ m.FocusedTask = 0
127
+ m.refreshTasks()
128
+ m.Overlay = OverlayNone
129
+ if m.Root != "" {
130
+ if err := m.writeRouterReleaseEpic(); err != nil {
131
+ m.StatusMessage = err.Error()
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return m, nil
137
+ }
138
+
139
+ func prevColumn(col data.ColumnType) data.ColumnType {
140
+ for i, c := range columnOrder {
141
+ if c == col {
142
+ return columnOrder[(i+len(columnOrder)-1)%len(columnOrder)]
143
+ }
144
+ }
145
+ return columnOrder[0]
146
+ }
147
+
148
+ func nextColumn(col data.ColumnType) data.ColumnType {
149
+ for i, c := range columnOrder {
150
+ if c == col {
151
+ return columnOrder[(i+1)%len(columnOrder)]
152
+ }
153
+ }
154
+ return columnOrder[0]
155
+ }
@@ -0,0 +1,128 @@
1
+ package board
2
+
3
+ import (
4
+ "testing"
5
+
6
+ tea "github.com/charmbracelet/bubbletea"
7
+ "github.com/opencode/savepoint/internal/data"
8
+ )
9
+
10
+ func requireModel(t *testing.T, got tea.Model) Model {
11
+ t.Helper()
12
+ m, ok := got.(Model)
13
+ if !ok {
14
+ t.Fatalf("updated model type = %T, want board.Model", got)
15
+ }
16
+ return m
17
+ }
18
+
19
+ func TestUpdate_qQuits(t *testing.T) {
20
+ m := NewModel(nil, "v1", "E03")
21
+ _, cmd := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")})
22
+ if cmd == nil {
23
+ t.Fatal("expected tea.Quit cmd, got nil")
24
+ }
25
+ }
26
+
27
+ func TestUpdate_ctrlCQuits(t *testing.T) {
28
+ m := NewModel(nil, "v1", "E03")
29
+ _, cmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
30
+ if cmd == nil {
31
+ t.Fatal("expected tea.Quit cmd, got nil")
32
+ }
33
+ }
34
+
35
+ func TestUpdate_windowSizeUpdatesModel(t *testing.T) {
36
+ m := NewModel(nil, "v1", "E03")
37
+ got, cmd := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
38
+ updated := requireModel(t, got)
39
+ if cmd != nil {
40
+ t.Errorf("expected nil cmd for window resize, got %v", cmd)
41
+ }
42
+ if updated.Width != 120 {
43
+ t.Errorf("Width = %d, want 120", updated.Width)
44
+ }
45
+ if updated.Height != 40 {
46
+ t.Errorf("Height = %d, want 40", updated.Height)
47
+ }
48
+ }
49
+
50
+ func TestUpdate_rightMovesColumn(t *testing.T) {
51
+ m := NewModel(nil, "v1", "E03")
52
+ // FocusedColumn starts at Planned
53
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
54
+ updated := requireModel(t, got)
55
+ if updated.FocusedColumn != data.ColumnInProgress {
56
+ t.Errorf("FocusedColumn = %q, want %q", updated.FocusedColumn, data.ColumnInProgress)
57
+ }
58
+ }
59
+
60
+ func TestUpdate_leftWrapsAround(t *testing.T) {
61
+ m := NewModel(nil, "v1", "E03")
62
+ // Planned -> left -> Done (wrap)
63
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("h")})
64
+ updated := requireModel(t, got)
65
+ if updated.FocusedColumn != data.ColumnDone {
66
+ t.Errorf("FocusedColumn = %q, want %q", updated.FocusedColumn, data.ColumnDone)
67
+ }
68
+ }
69
+
70
+ func TestUpdate_rightWrapsAround(t *testing.T) {
71
+ m := NewModel(nil, "v1", "E03")
72
+ m.FocusedColumn = data.ColumnDone
73
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")})
74
+ updated := requireModel(t, got)
75
+ if updated.FocusedColumn != data.ColumnPlanned {
76
+ t.Errorf("FocusedColumn = %q, want %q", updated.FocusedColumn, data.ColumnPlanned)
77
+ }
78
+ }
79
+
80
+ func TestUpdate_downMovesTaskFocus(t *testing.T) {
81
+ tasks := []data.Task{
82
+ {ID: "T1", Column: data.ColumnPlanned},
83
+ {ID: "T2", Column: data.ColumnPlanned},
84
+ }
85
+ m := NewModel(tasks, "v1", "E03")
86
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
87
+ updated := requireModel(t, got)
88
+ if updated.FocusedTask != 1 {
89
+ t.Errorf("FocusedTask = %d, want 1", updated.FocusedTask)
90
+ }
91
+ }
92
+
93
+ func TestUpdate_upMovesTaskFocus(t *testing.T) {
94
+ tasks := []data.Task{
95
+ {ID: "T1", Column: data.ColumnPlanned},
96
+ {ID: "T2", Column: data.ColumnPlanned},
97
+ }
98
+ m := NewModel(tasks, "v1", "E03")
99
+ m.FocusedTask = 1
100
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")})
101
+ updated := requireModel(t, got)
102
+ if updated.FocusedTask != 0 {
103
+ t.Errorf("FocusedTask = %d, want 0", updated.FocusedTask)
104
+ }
105
+ }
106
+
107
+ func TestUpdate_taskFocusClampedAtEnd(t *testing.T) {
108
+ tasks := []data.Task{{ID: "T1", Column: data.ColumnPlanned}}
109
+ m := NewModel(tasks, "v1", "E03")
110
+ got, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
111
+ updated := requireModel(t, got)
112
+ if updated.FocusedTask != 0 {
113
+ t.Errorf("FocusedTask = %d, want 0", updated.FocusedTask)
114
+ }
115
+ }
116
+
117
+ func TestUpdate_unknownMsgNoOp(t *testing.T) {
118
+ m := NewModel(nil, "v1", "E03")
119
+ m.Width = 80
120
+ got, cmd := m.Update(nil)
121
+ updated := requireModel(t, got)
122
+ if cmd != nil {
123
+ t.Errorf("expected nil cmd, got %v", cmd)
124
+ }
125
+ if updated.Width != 80 {
126
+ t.Errorf("Width changed unexpectedly: %d", updated.Width)
127
+ }
128
+ }
@@ -0,0 +1,190 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/charmbracelet/lipgloss"
7
+ xansi "github.com/charmbracelet/x/ansi"
8
+ "github.com/opencode/savepoint/internal/data"
9
+ "github.com/opencode/savepoint/internal/styles"
10
+ )
11
+
12
+ const defaultTermH = 24
13
+
14
+ const defaultTermW = 80
15
+
16
+ func (m Model) View() string {
17
+ w := m.Width
18
+ if w == 0 {
19
+ w = defaultTermW
20
+ }
21
+
22
+ layout := CalculateLayout(w, m.Height)
23
+ icon := styles.HeaderIcon.Render("▣")
24
+ text := styles.HeaderText.Render("S A V E P O I N T")
25
+ header := styles.HeaderFrame.Width(w).Render(icon + " " + text)
26
+ board := m.renderBoard(layout)
27
+ footer := m.renderFooter(w)
28
+ base := lipgloss.JoinVertical(lipgloss.Left, header, board, footer)
29
+
30
+ h := m.Height
31
+ if h == 0 {
32
+ h = defaultTermH
33
+ }
34
+
35
+ if m.Overlay == OverlayEpic {
36
+ overlay := RenderEpicDropdown(m.Epics, m.EpicCursor, min(40, w))
37
+ return overlayOnBase(dimLines(base), overlay, w, h)
38
+ }
39
+
40
+ if m.Overlay == OverlayRelease {
41
+ overlay := RenderReleaseDropdown(m.Releases, m.ReleaseCursor, min(40, w))
42
+ return overlayOnBase(dimLines(base), overlay, w, h)
43
+ }
44
+
45
+ if m.Overlay == OverlayHelp {
46
+ help := RenderHelp(overlayWidth(w))
47
+ return overlayOnBase(dimLines(base), help, w, h)
48
+ }
49
+
50
+ if m.Overlay == OverlayDetail {
51
+ task, ok := m.focusedTask()
52
+ if !ok {
53
+ return base
54
+ }
55
+ ow := overlayWidth(w)
56
+ detail := RenderDetail(task, ow)
57
+ return overlayOnBase(dimLines(base), detail, w, h)
58
+ }
59
+
60
+ return base
61
+ }
62
+
63
+ func (m Model) focusedTask() (data.Task, bool) {
64
+ tasks := m.Tasks[m.FocusedColumn]
65
+ if len(tasks) == 0 || m.FocusedTask >= len(tasks) {
66
+ return data.Task{}, false
67
+ }
68
+ return tasks[m.FocusedTask], true
69
+ }
70
+
71
+ func overlayWidth(termW int) int {
72
+ ow := termW - 4
73
+ if ow > 80 {
74
+ ow = 80
75
+ }
76
+ if ow < 20 {
77
+ ow = 20
78
+ }
79
+ return ow
80
+ }
81
+
82
+ // dimLines applies faint ANSI styling to each line individually.
83
+ func dimLines(s string) string {
84
+ dim := lipgloss.NewStyle().Faint(true)
85
+ lines := strings.Split(s, "\n")
86
+ for i, l := range lines {
87
+ lines[i] = dim.Render(l)
88
+ }
89
+ return strings.Join(lines, "\n")
90
+ }
91
+
92
+ // overlayOnBase places overlay centered on base, preserving base lines outside
93
+ // the overlay area and replacing the left portion of intersecting lines.
94
+ func overlayOnBase(base, overlay string, termW, termH int) string {
95
+ baseLines := strings.Split(base, "\n")
96
+ overlayLines := strings.Split(overlay, "\n")
97
+
98
+ overlayH := len(overlayLines)
99
+ overlayW := 0
100
+ for _, l := range overlayLines {
101
+ if lw := lipgloss.Width(l); lw > overlayW {
102
+ overlayW = lw
103
+ }
104
+ }
105
+
106
+ startY := (termH - overlayH) / 2
107
+ if startY < 0 {
108
+ startY = 0
109
+ }
110
+ startX := (termW - overlayW) / 2
111
+ if startX < 0 {
112
+ startX = 0
113
+ }
114
+
115
+ for len(baseLines) < termH {
116
+ baseLines = append(baseLines, "")
117
+ }
118
+
119
+ result := make([]string, len(baseLines))
120
+ for i, line := range baseLines {
121
+ oi := i - startY
122
+ if oi >= 0 && oi < overlayH {
123
+ left := xansi.Truncate(line, startX, "")
124
+ leftW := lipgloss.Width(left)
125
+ if leftW < startX {
126
+ left += strings.Repeat(" ", startX-leftW)
127
+ }
128
+ result[i] = left + overlayLines[oi]
129
+ } else {
130
+ result[i] = line
131
+ }
132
+ }
133
+ return strings.Join(result, "\n")
134
+ }
135
+
136
+ func (m Model) renderBoard(layout Layout) string {
137
+ cols := m.renderColumns(layout)
138
+ var content string
139
+ if layout.EpicPanelVisible {
140
+ epic := m.renderEpicPanel(layout.EpicPanelWidth)
141
+ content = lipgloss.JoinHorizontal(lipgloss.Top, epic, cols)
142
+ } else {
143
+ content = cols
144
+ }
145
+ return styles.BoardFrame.Width(m.Width).Render(content)
146
+ }
147
+
148
+ func (m Model) renderColumns(layout Layout) string {
149
+ if layout.ColCount == 1 {
150
+ return m.renderColumn(m.FocusedColumn, layout.ColWidths[0])
151
+ }
152
+ allCols := []data.ColumnType{data.ColumnPlanned, data.ColumnInProgress, data.ColumnDone}
153
+ rendered := make([]string, len(allCols))
154
+ for i, col := range allCols {
155
+ rendered[i] = m.renderColumn(col, layout.ColWidths[i])
156
+ }
157
+ return lipgloss.JoinHorizontal(lipgloss.Top, rendered...)
158
+ }
159
+
160
+ func (m Model) renderEpicPanel(w int) string {
161
+ return RenderEpicSidebar(m.Epics, m.SelectedEpic, w)
162
+ }
163
+
164
+ func (m Model) renderColumn(col data.ColumnType, colW int) string {
165
+ focused := m.FocusedColumn == col
166
+ return RenderColumn(m.Tasks[col], col, colW, m.FocusedTask, focused)
167
+ }
168
+
169
+ func (m Model) renderFooter(termW int) string {
170
+ phase := footerLine(termW,
171
+ styles.FooterPhasePlan.Render("PLAN")+
172
+ styles.FooterDivider.Render(" │ ")+
173
+ styles.FooterPhaseBuild.Render("BUILD")+
174
+ styles.FooterDivider.Render(" │ ")+
175
+ styles.FooterPhaseAudit.Render("AUDIT"),
176
+ )
177
+ hints := footerLine(termW, styles.FooterHints.Render("←/→:nav E:epic R:release ?:help q:quit"))
178
+ spacer := footerLine(termW, "")
179
+ return lipgloss.JoinVertical(lipgloss.Center, phase, spacer, hints)
180
+ }
181
+
182
+ func footerLine(termW int, content string) string {
183
+ if termW <= 0 {
184
+ termW = defaultTermW
185
+ }
186
+ if lipgloss.Width(content) > termW {
187
+ content = xansi.Truncate(content, termW, "")
188
+ }
189
+ return lipgloss.NewStyle().Width(termW).Align(lipgloss.Center).Render(content)
190
+ }