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
package/go.mod ADDED
@@ -0,0 +1,33 @@
1
+ module github.com/opencode/savepoint
2
+
3
+ go 1.26.2
4
+
5
+ require github.com/charmbracelet/bubbletea v1.3.10
6
+
7
+ require (
8
+ github.com/atotto/clipboard v0.1.4 // indirect
9
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
10
+ github.com/charmbracelet/bubbles v1.0.0 // indirect
11
+ github.com/charmbracelet/colorprofile v0.4.1 // indirect
12
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
13
+ github.com/charmbracelet/x/ansi v0.11.6 // indirect
14
+ github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
15
+ github.com/charmbracelet/x/term v0.2.2 // indirect
16
+ github.com/clipperhouse/displaywidth v0.9.0 // indirect
17
+ github.com/clipperhouse/stringish v0.1.1 // indirect
18
+ github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
19
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
20
+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
21
+ github.com/mattn/go-isatty v0.0.20 // indirect
22
+ github.com/mattn/go-localereader v0.0.1 // indirect
23
+ github.com/mattn/go-runewidth v0.0.19 // indirect
24
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
25
+ github.com/muesli/cancelreader v0.2.2 // indirect
26
+ github.com/muesli/termenv v0.16.0 // indirect
27
+ github.com/rivo/uniseg v0.4.7 // indirect
28
+ github.com/sahilm/fuzzy v0.1.1 // indirect
29
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
30
+ golang.org/x/sys v0.38.0 // indirect
31
+ golang.org/x/text v0.3.8 // indirect
32
+ gopkg.in/yaml.v3 v3.0.1 // indirect
33
+ )
package/go.sum ADDED
@@ -0,0 +1,73 @@
1
+ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2
+ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5
+ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
6
+ github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
7
+ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
8
+ github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
9
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
10
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
11
+ github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
12
+ github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
13
+ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
14
+ github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
15
+ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
16
+ github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
17
+ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
18
+ github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
19
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
20
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
21
+ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
22
+ github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
23
+ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
24
+ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
25
+ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
26
+ github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
27
+ github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
28
+ github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
29
+ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
30
+ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
31
+ github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
32
+ github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
33
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
34
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
35
+ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
36
+ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
37
+ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
38
+ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
39
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
40
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
41
+ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
42
+ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
43
+ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
44
+ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
45
+ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
46
+ github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
47
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
48
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
49
+ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
50
+ github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
51
+ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
52
+ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
53
+ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
54
+ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
55
+ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
56
+ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
57
+ github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
58
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
59
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
60
+ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
61
+ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
62
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
63
+ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
64
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
65
+ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
66
+ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
67
+ golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
68
+ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
69
+ golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
70
+ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
71
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
72
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
73
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Binary file
@@ -0,0 +1,121 @@
1
+ package board
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+
8
+ tea "github.com/charmbracelet/bubbletea"
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ func Run() error {
13
+ model, err := newProjectModel(".")
14
+ if err != nil {
15
+ return err
16
+ }
17
+
18
+ p := tea.NewProgram(model, tea.WithAltScreen())
19
+ if _, err := p.Run(); err != nil {
20
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
21
+ return err
22
+ }
23
+ return nil
24
+ }
25
+
26
+ func newProgramModel() Model {
27
+ return NewModel(nil, "v1", "E03-board-tui-core")
28
+ }
29
+
30
+ func newProjectModel(start string) (Model, error) {
31
+ d := data.NewDiscover()
32
+ root, err := d.FindSavepointRoot(start)
33
+ if err != nil {
34
+ return Model{}, err
35
+ }
36
+
37
+ routerState, err := readRouterState(root)
38
+ if err != nil {
39
+ return Model{}, err
40
+ }
41
+
42
+ releases, err := d.ListReleases(root)
43
+ if err != nil {
44
+ return Model{}, err
45
+ }
46
+
47
+ releaseIDs := make([]string, 0, len(releases))
48
+ releaseEpics := make(map[string][]string, len(releases))
49
+ tasks := []data.Task{}
50
+
51
+ for _, release := range releases {
52
+ releaseIDs = append(releaseIDs, release.ID)
53
+ epics, err := d.ListEpics(root, release.ID)
54
+ if err != nil {
55
+ return Model{}, err
56
+ }
57
+ for _, epic := range epics {
58
+ releaseEpics[release.ID] = append(releaseEpics[release.ID], epic.ID)
59
+ epicTasks, err := loadEpicTasks(d, root, release.ID, epic.ID)
60
+ if err != nil {
61
+ return Model{}, err
62
+ }
63
+ tasks = append(tasks, epicTasks...)
64
+ }
65
+ }
66
+
67
+ release := firstKnown(routerState.Release, releaseIDs)
68
+ epic := firstKnown(routerState.Epic, releaseEpics[release])
69
+
70
+ model := NewModel(tasks, release, epic)
71
+ model.Root = root
72
+ model.Releases = releaseIDs
73
+ model.ReleaseEpics = releaseEpics
74
+ model.refreshEpicsForRelease()
75
+ model.refreshTasks()
76
+
77
+ return model, nil
78
+ }
79
+
80
+ func readRouterState(root string) (*data.RouterState, error) {
81
+ content, err := os.ReadFile(filepath.Join(root, "router.md"))
82
+ if err != nil {
83
+ return nil, err
84
+ }
85
+
86
+ return data.NewRouterReader().ReadState(string(content))
87
+ }
88
+
89
+ func loadEpicTasks(d *data.Discover, root, release, epic string) ([]data.Task, error) {
90
+ taskInfos, err := d.ListTasks(root, release, epic)
91
+ if err != nil {
92
+ return nil, err
93
+ }
94
+
95
+ parser := data.NewParser()
96
+ tasks := make([]data.Task, 0, len(taskInfos))
97
+ for _, taskInfo := range taskInfos {
98
+ content, err := os.ReadFile(taskInfo.Path)
99
+ if err != nil {
100
+ return nil, err
101
+ }
102
+ task, err := parser.ParseTaskFile(taskInfo.Path, string(content))
103
+ if err != nil {
104
+ return nil, err
105
+ }
106
+ tasks = append(tasks, *task)
107
+ }
108
+ return tasks, nil
109
+ }
110
+
111
+ func firstKnown(preferred string, values []string) string {
112
+ for _, value := range values {
113
+ if value == preferred {
114
+ return preferred
115
+ }
116
+ }
117
+ if len(values) == 0 {
118
+ return ""
119
+ }
120
+ return values[0]
121
+ }
@@ -0,0 +1,99 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ func TestNewProgramModelUsesBoardCore(t *testing.T) {
13
+ m := newProgramModel()
14
+ m.Width = 100
15
+
16
+ got := m.View()
17
+ if strings.Contains(got, "Welcome to Savepoint") {
18
+ t.Fatal("program model still renders placeholder welcome screen")
19
+ }
20
+ for _, title := range []string{"PLANNED", "IN PROGRESS", "DONE"} {
21
+ if !strings.Contains(got, title) {
22
+ t.Fatalf("program model view missing board column %q", title)
23
+ }
24
+ }
25
+ }
26
+
27
+ func TestNewProjectModelLoadsReleasesEpicsAndTasks(t *testing.T) {
28
+ projectRoot := t.TempDir()
29
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
30
+ writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
31
+
32
+ ## Current state
33
+
34
+ `+"```"+`yaml
35
+ state: task-building
36
+ release: v2
37
+ epic: E03-live
38
+ task: ""
39
+ next_action: "test"
40
+ `+"```"+`
41
+ `)
42
+ writeTask(t, savepointRoot, "v1", "E01-old", "T001-old", data.ColumnPlanned)
43
+ writeTask(t, savepointRoot, "v2", "E03-live", "T001-live", data.ColumnInProgress)
44
+
45
+ model, err := newProjectModel(projectRoot)
46
+ if err != nil {
47
+ t.Fatalf("newProjectModel() error = %v", err)
48
+ }
49
+
50
+ if model.Root != savepointRoot {
51
+ t.Errorf("Root = %q, want %q", model.Root, savepointRoot)
52
+ }
53
+ if model.SelectedRelease != "v2" {
54
+ t.Errorf("SelectedRelease = %q, want v2", model.SelectedRelease)
55
+ }
56
+ if model.SelectedEpic != "E03-live" {
57
+ t.Errorf("SelectedEpic = %q, want E03-live", model.SelectedEpic)
58
+ }
59
+ if len(model.Releases) != 2 {
60
+ t.Errorf("Releases = %v, want two releases", model.Releases)
61
+ }
62
+ if len(model.Epics) != 1 || model.Epics[0] != "E03-live" {
63
+ t.Errorf("Epics = %v, want [E03-live]", model.Epics)
64
+ }
65
+ tasks := model.Tasks[data.ColumnInProgress]
66
+ if len(tasks) != 1 || tasks[0].ID != "E03-live/T001-live" {
67
+ t.Errorf("visible in-progress tasks = %v, want E03-live/T001-live", tasks)
68
+ }
69
+ }
70
+
71
+ func writeTask(t *testing.T, root, release, epic, task string, column data.ColumnType) {
72
+ t.Helper()
73
+ path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
74
+ phase := ""
75
+ if column == data.ColumnInProgress {
76
+ phase = "phase: build\n"
77
+ }
78
+ content := `---
79
+ id: ` + epic + `/` + task + `
80
+ release: ` + release + `
81
+ status: ` + string(column) + `
82
+ ` + phase + `objective: "Test task"
83
+ depends_on: []
84
+ ---
85
+
86
+ # Test task
87
+ `
88
+ writeFile(t, path, content)
89
+ }
90
+
91
+ func writeFile(t *testing.T, path, content string) {
92
+ t.Helper()
93
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
97
+ t.Fatal(err)
98
+ }
99
+ }
@@ -0,0 +1,72 @@
1
+ package board
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/opencode/savepoint/internal/data"
8
+ "github.com/opencode/savepoint/internal/styles"
9
+ )
10
+
11
+ const (
12
+ glyphBuild = "▣"
13
+ glyphTest = "◇"
14
+ glyphAudit = "◆"
15
+
16
+ cardOverhead = 4 // border (2) + padding (2×1)
17
+ )
18
+
19
+ // RenderCard renders a task card with phase glyph, truncated ID+title, and focus styling.
20
+ func RenderCard(t data.Task, width int, focused bool) string {
21
+ inner := width - cardOverhead
22
+ if inner < 2 {
23
+ inner = 2
24
+ }
25
+
26
+ glyph := phaseGlyphStyled(t.Stage)
27
+ // glyph is 1 rune + 1 space prefix; leave room for "▣ "
28
+ idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), inner-2))
29
+ titleLine := styles.CardMeta.Render(truncate(t.Title, inner))
30
+
31
+ content := idLine + "\n" + titleLine
32
+
33
+ if focused {
34
+ return styles.CardFocused.Width(width).Render(content)
35
+ }
36
+ return styles.Card.Width(width).Render(content)
37
+ }
38
+
39
+ func phaseGlyphStyled(stage data.ProgressStage) string {
40
+ switch stage {
41
+ case data.StageTest:
42
+ return styles.GlyphTest.Render(glyphTest)
43
+ case data.StageAudit:
44
+ return styles.GlyphAudit.Render(glyphAudit)
45
+ default:
46
+ return styles.GlyphBuild.Render(glyphBuild)
47
+ }
48
+ }
49
+
50
+ // shortID strips the epic prefix and slug from a task ID.
51
+ // "E06-atari-noir-layout/T004-component-refinement" → "T004"
52
+ func shortID(id string) string {
53
+ if idx := strings.LastIndex(id, "/"); idx >= 0 {
54
+ id = id[idx+1:]
55
+ }
56
+ if idx := strings.Index(id, "-"); idx >= 0 {
57
+ id = id[:idx]
58
+ }
59
+ return id
60
+ }
61
+
62
+ // truncate clips s to max runes, appending "…" if clipped.
63
+ func truncate(s string, max int) string {
64
+ runes := []rune(s)
65
+ if len(runes) <= max {
66
+ return s
67
+ }
68
+ if max <= 1 {
69
+ return "…"
70
+ }
71
+ return string(runes[:max-1]) + "…"
72
+ }
@@ -0,0 +1,111 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ "github.com/opencode/savepoint/internal/data"
8
+ )
9
+
10
+ func TestRenderCard_containsID(t *testing.T) {
11
+ task := data.Task{ID: "E04/T002", Title: "Build card", Stage: data.StageBuild}
12
+ got := RenderCard(task, 30, false)
13
+ if !strings.Contains(got, "T002") {
14
+ t.Error("RenderCard missing short task ID")
15
+ }
16
+ }
17
+
18
+ func TestRenderCard_containsTitle(t *testing.T) {
19
+ task := data.Task{ID: "T1", Title: "My title", Stage: data.StageBuild}
20
+ got := RenderCard(task, 30, false)
21
+ if !strings.Contains(got, "My title") {
22
+ t.Error("RenderCard missing task title")
23
+ }
24
+ }
25
+
26
+ func TestRenderCard_containsBuildGlyph(t *testing.T) {
27
+ task := data.Task{ID: "T1", Stage: data.StageBuild}
28
+ got := RenderCard(task, 30, false)
29
+ if !strings.Contains(got, glyphBuild) {
30
+ t.Errorf("RenderCard missing build glyph %q", glyphBuild)
31
+ }
32
+ }
33
+
34
+ func TestRenderCard_containsTestGlyph(t *testing.T) {
35
+ task := data.Task{ID: "T1", Stage: data.StageTest}
36
+ got := RenderCard(task, 30, false)
37
+ if !strings.Contains(got, glyphTest) {
38
+ t.Errorf("RenderCard missing test glyph %q", glyphTest)
39
+ }
40
+ }
41
+
42
+ func TestRenderCard_containsAuditGlyph(t *testing.T) {
43
+ task := data.Task{ID: "T1", Stage: data.StageAudit}
44
+ got := RenderCard(task, 30, false)
45
+ if !strings.Contains(got, glyphAudit) {
46
+ t.Errorf("RenderCard missing audit glyph %q", glyphAudit)
47
+ }
48
+ }
49
+
50
+ func TestRenderCard_focusedDoesNotPanic(t *testing.T) {
51
+ task := data.Task{ID: "T1", Title: "hello", Stage: data.StageBuild}
52
+ got := RenderCard(task, 30, true)
53
+ if got == "" {
54
+ t.Error("RenderCard focused returned empty string")
55
+ }
56
+ }
57
+
58
+ func TestRenderCard_titleTruncated(t *testing.T) {
59
+ long := "This is a very long title that should be truncated for sure"
60
+ task := data.Task{ID: "T1", Title: long, Stage: data.StageBuild}
61
+ got := RenderCard(task, 20, false)
62
+ if strings.Contains(got, long) {
63
+ t.Error("RenderCard should truncate long title")
64
+ }
65
+ if !strings.Contains(got, "…") {
66
+ t.Error("RenderCard should include ellipsis when title truncated")
67
+ }
68
+ }
69
+
70
+ func TestRenderCard_idTruncated(t *testing.T) {
71
+ long := "E04-board-components/T999-very-long-id"
72
+ task := data.Task{ID: long, Stage: data.StageBuild}
73
+ got := RenderCard(task, 20, false)
74
+ if strings.Contains(got, long) {
75
+ t.Error("RenderCard should truncate long ID")
76
+ }
77
+ }
78
+
79
+ func TestTruncate_shortString(t *testing.T) {
80
+ if truncate("hi", 10) != "hi" {
81
+ t.Error("truncate should not clip short string")
82
+ }
83
+ }
84
+
85
+ func TestTruncate_exactLength(t *testing.T) {
86
+ if truncate("hello", 5) != "hello" {
87
+ t.Error("truncate should not clip string at exact max")
88
+ }
89
+ }
90
+
91
+ func TestTruncate_clipsWithEllipsis(t *testing.T) {
92
+ got := truncate("hello", 4)
93
+ if got != "hel…" {
94
+ t.Errorf("truncate got %q, want %q", got, "hel…")
95
+ }
96
+ }
97
+
98
+ func TestTruncate_maxOne(t *testing.T) {
99
+ got := truncate("hello", 1)
100
+ if got != "…" {
101
+ t.Errorf("truncate(max=1) got %q, want %q", got, "…")
102
+ }
103
+ }
104
+
105
+ func TestRenderCard_defaultStageUsesBuildGlyph(t *testing.T) {
106
+ task := data.Task{ID: "T1", Stage: ""}
107
+ got := RenderCard(task, 30, false)
108
+ if !strings.Contains(got, glyphBuild) {
109
+ t.Error("RenderCard with empty stage should use build glyph")
110
+ }
111
+ }
@@ -0,0 +1,61 @@
1
+ package board
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/opencode/savepoint/internal/data"
8
+ "github.com/opencode/savepoint/internal/styles"
9
+ )
10
+
11
+ // RenderColumn renders a board column: header with label+count, task list, bordered container.
12
+ func RenderColumn(tasks []data.Task, col data.ColumnType, width, focusedTask int, focused bool) string {
13
+ inner := width - colOverhead
14
+ if inner < minColWidth {
15
+ inner = minColWidth
16
+ }
17
+
18
+ title := columnTitle(col)
19
+ header := fmt.Sprintf("%s (%d)", title, len(tasks))
20
+ if focused {
21
+ header = styles.ColumnTitleFocused.Render(header)
22
+ } else {
23
+ header = styles.ColumnTitle.Render(header)
24
+ }
25
+
26
+ lines := []string{header, strings.Repeat("─", inner)}
27
+ if len(tasks) == 0 {
28
+ lines = append(lines, styles.TaskItem.Render("(empty)"))
29
+ } else {
30
+ for i, t := range tasks {
31
+ lines = append(lines, RenderCard(t, inner, focused && i == focusedTask))
32
+ }
33
+ }
34
+
35
+ content := strings.Join(lines, "\n")
36
+ st := styles.Column.Width(width)
37
+ if focused {
38
+ st = styles.ColumnFocused.Width(width)
39
+ }
40
+ return st.Render(content)
41
+ }
42
+
43
+ func columnTitle(col data.ColumnType) string {
44
+ switch col {
45
+ case data.ColumnPlanned:
46
+ return "PLANNED"
47
+ case data.ColumnInProgress:
48
+ return "IN PROGRESS"
49
+ case data.ColumnDone:
50
+ return "DONE"
51
+ default:
52
+ return strings.ToUpper(string(col))
53
+ }
54
+ }
55
+
56
+ func taskLabel(t data.Task) string {
57
+ if t.Title == "" {
58
+ return t.ID
59
+ }
60
+ return fmt.Sprintf("%s %s", t.ID, t.Title)
61
+ }
@@ -0,0 +1,81 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ "github.com/opencode/savepoint/internal/data"
8
+ )
9
+
10
+ func TestRenderColumn_headerContainsLabel(t *testing.T) {
11
+ got := RenderColumn(nil, data.ColumnPlanned, 30, 0, false)
12
+ if !strings.Contains(got, "PLANNED") {
13
+ t.Error("RenderColumn missing PLANNED label")
14
+ }
15
+ }
16
+
17
+ func TestRenderColumn_headerContainsCount(t *testing.T) {
18
+ tasks := []data.Task{{ID: "T1", Title: "Task one", Column: data.ColumnPlanned}}
19
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false)
20
+ if !strings.Contains(got, "(1)") {
21
+ t.Error("RenderColumn missing task count")
22
+ }
23
+ }
24
+
25
+ func TestRenderColumn_emptyShowsPlaceholder(t *testing.T) {
26
+ got := RenderColumn(nil, data.ColumnDone, 30, 0, false)
27
+ if !strings.Contains(got, "(empty)") {
28
+ t.Error("RenderColumn missing (empty) for empty column")
29
+ }
30
+ }
31
+
32
+ func TestRenderColumn_focusedDoesNotPanic(t *testing.T) {
33
+ tasks := []data.Task{{ID: "T1", Column: data.ColumnInProgress}}
34
+ got := RenderColumn(tasks, data.ColumnInProgress, 30, 0, true)
35
+ if got == "" {
36
+ t.Error("RenderColumn returned empty string for focused column")
37
+ }
38
+ }
39
+
40
+ func TestRenderColumn_allColumnTitles(t *testing.T) {
41
+ cases := []struct {
42
+ col data.ColumnType
43
+ label string
44
+ }{
45
+ {data.ColumnPlanned, "PLANNED"},
46
+ {data.ColumnInProgress, "IN PROGRESS"},
47
+ {data.ColumnDone, "DONE"},
48
+ }
49
+ for _, tc := range cases {
50
+ got := RenderColumn(nil, tc.col, 30, 0, false)
51
+ if !strings.Contains(got, tc.label) {
52
+ t.Errorf("RenderColumn missing label %q for col %q", tc.label, tc.col)
53
+ }
54
+ }
55
+ }
56
+
57
+ func TestRenderColumn_taskTitleRendered(t *testing.T) {
58
+ tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned}}
59
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false)
60
+ if !strings.Contains(got, "Build it") {
61
+ t.Error("RenderColumn missing task title")
62
+ }
63
+ }
64
+
65
+ func TestRenderColumn_rendersTaskCards(t *testing.T) {
66
+ tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned, Stage: data.StageAudit}}
67
+ got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, true)
68
+ if !strings.Contains(got, glyphAudit) {
69
+ t.Error("RenderColumn should render task phase glyph from card")
70
+ }
71
+ if !strings.Contains(got, "╭") {
72
+ t.Error("RenderColumn should render bordered card")
73
+ }
74
+ }
75
+
76
+ func TestRenderColumn_emptyCountIsZero(t *testing.T) {
77
+ got := RenderColumn(nil, data.ColumnPlanned, 30, 0, false)
78
+ if !strings.Contains(got, "(0)") {
79
+ t.Error("RenderColumn missing (0) count for empty column")
80
+ }
81
+ }