savepoint 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +52 -46
  4. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
  5. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
  6. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
  7. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
  8. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/{Design.md → E06-Detail.md} +5 -3
  9. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
  10. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
  11. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
  12. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +2 -0
  13. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/{Design.md → E01-Detail.md} +9 -1
  14. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/{T007-next-activity-header.md → T001-next-activity-header.md} +13 -12
  15. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +9 -9
  16. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +2 -2
  17. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +13 -12
  18. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +14 -13
  19. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +25 -15
  20. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
  21. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md +124 -0
  22. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/{Design.md → E02-Detail.md} +12 -3
  23. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +11 -8
  24. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +12 -7
  25. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +9 -5
  26. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +30 -9
  27. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  28. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +45 -0
  29. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
  30. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
  31. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
  32. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
  33. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
  34. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Audit.md +167 -0
  35. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
  36. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
  37. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
  38. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
  39. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  40. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +54 -0
  41. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +45 -0
  42. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +40 -0
  43. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +47 -0
  44. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +98 -0
  45. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +33 -0
  46. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +62 -0
  47. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  48. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  49. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  50. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  51. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  52. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  53. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  54. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  55. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  56. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  57. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  58. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  59. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  60. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  61. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  62. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  63. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  64. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  65. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  66. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  67. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  68. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  69. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  70. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  71. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  72. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  73. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  74. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  75. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  76. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  77. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  78. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  79. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  80. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  81. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  83. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  84. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  85. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  86. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  87. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  88. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  89. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  90. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  91. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  92. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  93. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  94. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  95. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  96. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  97. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  98. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  99. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  100. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  101. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  102. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  103. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  104. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  105. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  106. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  107. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  108. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  109. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  110. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  111. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  112. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  113. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  114. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  115. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  116. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  117. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  118. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  119. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  120. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  121. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  122. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  123. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  124. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  125. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  126. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  127. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  128. package/.savepoint/releases/v1.1/v1.1-PRD.md +139 -0
  129. package/.savepoint/router.md +29 -108
  130. package/AGENTS.md +69 -111
  131. package/Makefile +19 -3
  132. package/README.md +6 -6
  133. package/agent-skills/savepoint-audit/SKILL.md +87 -35
  134. package/agent-skills/savepoint-build-task/SKILL.md +9 -4
  135. package/agent-skills/savepoint-create-plan/SKILL.md +10 -5
  136. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  137. package/agent-skills/savepoint-draft-prd/SKILL.md +8 -3
  138. package/agent-skills/savepoint-system-design/SKILL.md +8 -3
  139. package/agent_skills_test.go +91 -0
  140. package/cmd/board.go +59 -0
  141. package/cmd/board_test.go +137 -0
  142. package/cmd/doctor.go +53 -0
  143. package/cmd/doctor_test.go +146 -0
  144. package/cmd/init.go +63 -0
  145. package/cmd/init_test.go +104 -0
  146. package/internal/board/board.go +69 -49
  147. package/internal/board/board_test.go +83 -67
  148. package/internal/board/card.go +71 -20
  149. package/internal/board/card_test.go +141 -12
  150. package/internal/board/column.go +77 -11
  151. package/internal/board/column_test.go +63 -13
  152. package/internal/board/detail.go +107 -72
  153. package/internal/board/detail_test.go +117 -26
  154. package/internal/board/epic_panel.go +211 -18
  155. package/internal/board/epic_panel_test.go +637 -14
  156. package/internal/board/help.go +1 -0
  157. package/internal/board/help_test.go +1 -0
  158. package/internal/board/integration_test.go +266 -0
  159. package/internal/board/interfaces.go +65 -0
  160. package/internal/board/interfaces_test.go +114 -0
  161. package/internal/board/io.go +93 -0
  162. package/internal/board/layout.go +12 -2
  163. package/internal/board/layout_test.go +17 -0
  164. package/internal/board/model.go +130 -52
  165. package/internal/board/plain.go +88 -0
  166. package/internal/board/plain_test.go +117 -0
  167. package/internal/board/release.go +1 -9
  168. package/internal/board/release_test.go +6 -6
  169. package/internal/board/render_policy_test.go +77 -0
  170. package/internal/board/status.go +23 -0
  171. package/internal/board/theme.go +24 -0
  172. package/internal/board/theme_test.go +31 -0
  173. package/internal/board/transitions.go +113 -88
  174. package/internal/board/transitions_test.go +164 -141
  175. package/internal/board/tui.go +32 -0
  176. package/internal/board/update.go +472 -94
  177. package/internal/board/update_test.go +447 -0
  178. package/internal/board/util.go +76 -0
  179. package/internal/board/view.go +139 -22
  180. package/internal/board/view_test.go +171 -3
  181. package/internal/board/watch.go +57 -9
  182. package/internal/buildtool/main.go +211 -0
  183. package/internal/buildtool/main_test.go +46 -0
  184. package/internal/data/config.go +17 -3
  185. package/internal/data/config_test.go +49 -0
  186. package/internal/data/discover.go +26 -0
  187. package/internal/data/discover_test.go +34 -10
  188. package/internal/data/errors.go +4 -0
  189. package/internal/data/lifecycle.go +13 -6
  190. package/internal/data/lifecycle_test.go +14 -11
  191. package/internal/data/parser.go +29 -6
  192. package/internal/data/parser_test.go +66 -7
  193. package/internal/data/task.go +1 -0
  194. package/internal/data/write.go +85 -11
  195. package/internal/data/write_test.go +167 -0
  196. package/internal/doctor/checks.go +567 -0
  197. package/internal/doctor/checks_test.go +716 -0
  198. package/internal/doctor/gates.go +193 -0
  199. package/internal/doctor/gates_test.go +166 -0
  200. package/internal/doctor/interfaces.go +64 -0
  201. package/internal/doctor/interfaces_test.go +104 -0
  202. package/internal/doctor/repairs.go +80 -0
  203. package/internal/doctor/repairs_test.go +81 -0
  204. package/internal/doctor/report.go +157 -0
  205. package/internal/doctor/report_test.go +89 -0
  206. package/internal/init/clipboard.go +146 -0
  207. package/internal/init/clipboard_test.go +74 -0
  208. package/internal/init/install.go +16 -0
  209. package/internal/init/integration_test.go +197 -0
  210. package/internal/init/prompt.go +14 -0
  211. package/internal/init/prompt_test.go +77 -0
  212. package/internal/init/scaffold.go +59 -0
  213. package/internal/init/scaffold_test.go +179 -0
  214. package/internal/init/template_freshness_test.go +56 -0
  215. package/internal/init/validate.go +85 -0
  216. package/internal/init/validate_test.go +141 -0
  217. package/internal/init/write.go +73 -0
  218. package/internal/init/write_test.go +91 -0
  219. package/internal/styles/palette.go +3 -3
  220. package/internal/styles/styles.go +39 -12
  221. package/internal/styles/styles_test.go +133 -0
  222. package/internal/testutil/fixture.go +113 -0
  223. package/internal/testutil/fs.go +26 -0
  224. package/main.go +107 -1
  225. package/package.json +2 -2
  226. package/project-audit/audit_report_glm_5.1.md +411 -0
  227. package/project-audit/audit_report_opus_4.6 +406 -0
  228. package/project-audit/consolidated-audit-report.md +456 -0
  229. package/savepoint +0 -0
  230. package/templates/project/.savepoint/Design.md +2 -2
  231. package/templates/project/.savepoint/router.md +15 -14
  232. package/templates/project/AGENTS.md +56 -98
  233. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  234. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  235. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  236. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  237. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  238. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  239. package/templates/prompts/audit-reconciliation.prompt.md +35 -30
  240. package/templates/prompts/design.prompt.md +3 -1
  241. package/templates/prompts/epic-design.prompt.md +3 -3
  242. package/templates/prompts/task-breakdown.prompt.md +1 -1
  243. package/templates/prompts/task-building.prompt.md +1 -1
  244. package/templates/prompts/task-planning.prompt.md +1 -1
  245. package/.savepoint/audit/E01-go-setup/proposals.md +0 -166
  246. package/.savepoint/audit/E01-go-setup/snapshot.md +0 -71
  247. package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +0 -66
  248. package/.savepoint/audit/E01-scaffolding/proposals/Design.md +0 -210
  249. package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +0 -117
  250. package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +0 -101
  251. package/.savepoint/audit/E01-scaffolding/snapshot.md +0 -54
  252. package/.savepoint/audit/E02-data-model/snapshot.md +0 -128
  253. package/.savepoint/audit/E02-data-readers/proposals.md +0 -123
  254. package/.savepoint/audit/E02-data-readers/snapshot.md +0 -54
  255. package/.savepoint/audit/E03-board-tui-core/proposals.md +0 -146
  256. package/.savepoint/audit/E03-board-tui-core/snapshot.md +0 -57
  257. package/.savepoint/audit/E03-cli-foundation/snapshot.md +0 -106
  258. package/.savepoint/audit/E04-board-components/proposals.md +0 -118
  259. package/.savepoint/audit/E04-board-components/snapshot.md +0 -77
  260. package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +0 -115
  261. package/.savepoint/audit/E05-init-command/snapshot.md +0 -125
  262. package/.savepoint/audit/E05-phase-transitions/proposals.md +0 -83
  263. package/.savepoint/audit/E05-phase-transitions/snapshot.md +0 -36
  264. package/.savepoint/audit/E06-atari-noir-layout/proposals.md +0 -130
  265. package/.savepoint/audit/E06-atari-noir-layout/snapshot.md +0 -84
  266. package/.savepoint/audit/E06-tui-board/snapshot.md +0 -64
  267. package/.savepoint/audit/E07-audit-pipeline/snapshot.md +0 -165
  268. package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +0 -65
  269. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -36
  270. package/ink-cli-ui-design.zip +0 -0
  271. package/main.exe +0 -0
  272. package/savepoint.exe +0 -0
  273. /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
  274. /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
  275. /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
  276. /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
  277. /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
  278. /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
@@ -5,122 +5,137 @@ import (
5
5
  "os"
6
6
  "path/filepath"
7
7
 
8
- tea "github.com/charmbracelet/bubbletea"
8
+ xterm "github.com/charmbracelet/x/term"
9
9
  "github.com/opencode/savepoint/internal/data"
10
10
  )
11
11
 
12
12
  func Run() error {
13
- model, err := newProjectModel(".")
14
- if err != nil {
15
- return err
13
+ return RunWithFilters("", "")
14
+ }
15
+
16
+ func RunWithFilters(release, epic string) error {
17
+ if !xterm.IsTerminal(os.Stdout.Fd()) {
18
+ return runPlainOutput(release, epic)
16
19
  }
20
+ return RunTUI(release, epic)
21
+ }
17
22
 
18
- p := tea.NewProgram(model, tea.WithAltScreen())
19
- if _, err := p.Run(); err != nil {
20
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
23
+ func runPlainOutput(release, epic string) error {
24
+ model, err := newProjectModel(".", release, epic)
25
+ if err != nil {
21
26
  return err
22
27
  }
28
+ fmt.Print(RenderPlainTable(model))
23
29
  return nil
24
30
  }
25
31
 
26
- func newProgramModel() Model {
27
- return NewModel(nil, "v1", "E03-board-tui-core")
32
+ func newProjectModel(start, releaseFilter, epicFilter string) (Model, error) {
33
+ return newProjectModelWithDependencies(start, releaseFilter, epicFilter, defaultModelDependencies())
28
34
  }
29
35
 
30
- func newProjectModel(start string) (Model, error) {
31
- d := data.NewDiscover()
32
- root, err := d.FindSavepointRoot(start)
36
+ func newProjectModelWithDependencies(start, releaseFilter, epicFilter string, deps ModelDependencies) (Model, error) {
37
+ deps = modelDependencies([]ModelDependencies{deps})
38
+
39
+ root, err := deps.Discoverer.FindSavepointRoot(start)
33
40
  if err != nil {
34
41
  return Model{}, err
35
42
  }
36
43
 
37
- routerState, err := readRouterState(root)
44
+ routerState, err := readRouterState(root, deps.RouterReader)
38
45
  if err != nil {
39
46
  return Model{}, err
40
47
  }
41
48
 
42
- releases, err := d.ListReleases(root)
49
+ tasks, releaseIDs, releaseEpics, epicStatuses, err := loadBoardData(root, deps.Discoverer, deps.Parser)
43
50
  if err != nil {
44
51
  return Model{}, err
45
52
  }
46
53
 
47
- releaseIDs := make([]string, 0, len(releases))
48
- releaseEpics := make(map[string][]string, len(releases))
49
-
50
- for _, release := range releases {
51
- releaseIDs = append(releaseIDs, release.ID)
52
- epics, err := d.ListEpics(root, release.ID)
53
- if err != nil {
54
- return Model{}, err
55
- }
56
- for _, epic := range epics {
57
- releaseEpics[release.ID] = append(releaseEpics[release.ID], epic.ID)
58
- }
54
+ preferredRelease := routerState.Release
55
+ if releaseFilter != "" {
56
+ preferredRelease = releaseFilter
59
57
  }
60
-
61
- tasks, err := loadAllTasks(root)
62
- if err != nil {
63
- return Model{}, err
58
+ preferredEpic := routerState.Epic
59
+ if epicFilter != "" {
60
+ preferredEpic = epicFilter
64
61
  }
65
62
 
66
- release := firstKnown(routerState.Release, releaseIDs)
67
- epic := firstKnown(routerState.Epic, releaseEpics[release])
63
+ release := firstKnown(preferredRelease, releaseIDs)
64
+ epic := firstKnown(preferredEpic, releaseEpics[release])
68
65
 
69
- model := NewModel(tasks, release, epic)
66
+ model := NewModel(tasks, release, epic, deps)
70
67
  model.Root = root
71
68
  model.RouterTask = routerState.Task
69
+ model.RouterState = routerState
72
70
  model.Releases = releaseIDs
73
71
  model.ReleaseEpics = releaseEpics
72
+ model.EpicStatus = epicStatuses
74
73
  model.refreshEpicsForRelease()
75
74
  model.refreshTasks()
76
75
 
77
76
  watcher, err := newWatcher(root)
78
- if err == nil {
79
- model.Watcher = watcher
77
+ if err != nil {
78
+ return Model{}, err
80
79
  }
80
+ model.Watcher = watcher
81
81
 
82
82
  return model, nil
83
83
  }
84
84
 
85
- func loadAllTasks(root string) ([]data.Task, error) {
86
- d := data.NewDiscover()
87
- releases, err := d.ListReleases(root)
85
+ func loadBoardData(root string, discoverer taskDiscoverer, parser taskParser) ([]data.Task, []string, map[string][]string, map[string]string, error) {
86
+ releases, err := discoverer.ListReleases(root)
88
87
  if err != nil {
89
- return nil, err
88
+ return nil, nil, nil, nil, err
90
89
  }
90
+
91
+ releaseIDs := make([]string, 0, len(releases))
92
+ releaseEpics := make(map[string][]string, len(releases))
91
93
  var tasks []data.Task
94
+ epicStatuses := make(map[string]string)
95
+
92
96
  for _, release := range releases {
93
- epics, err := d.ListEpics(root, release.ID)
97
+ releaseIDs = append(releaseIDs, release.ID)
98
+ epics, err := discoverer.ListEpics(root, release.ID)
94
99
  if err != nil {
95
- return nil, err
100
+ return nil, nil, nil, nil, err
96
101
  }
97
102
  for _, epic := range epics {
98
- epicTasks, err := loadEpicTasks(d, root, release.ID, epic.ID)
103
+ releaseEpics[release.ID] = append(releaseEpics[release.ID], epic.ID)
104
+ epicTasks, err := loadEpicTasks(discoverer, parser, root, release.ID, epic.ID)
99
105
  if err != nil {
100
- return nil, err
106
+ return nil, nil, nil, nil, err
101
107
  }
102
108
  tasks = append(tasks, epicTasks...)
109
+
110
+ detailPath := filepath.Join(epic.Path, shortID(epic.ID)+"-Detail.md")
111
+ if raw, err := os.ReadFile(detailPath); err == nil {
112
+ if fm, err := parser.ParseFrontmatter(string(raw)); err == nil {
113
+ if status, ok := fm["status"].(string); ok {
114
+ epicStatuses[epic.ID] = status
115
+ }
116
+ }
117
+ }
103
118
  }
104
119
  }
105
- return tasks, nil
120
+
121
+ return tasks, releaseIDs, releaseEpics, epicStatuses, nil
106
122
  }
107
123
 
108
- func readRouterState(root string) (*data.RouterState, error) {
124
+ func readRouterState(root string, reader routerReader) (*data.RouterState, error) {
109
125
  content, err := os.ReadFile(filepath.Join(root, "router.md"))
110
126
  if err != nil {
111
127
  return nil, err
112
128
  }
113
129
 
114
- return data.NewRouterReader().ReadState(string(content))
130
+ return reader.ReadState(string(content))
115
131
  }
116
132
 
117
- func loadEpicTasks(d *data.Discover, root, release, epic string) ([]data.Task, error) {
118
- taskInfos, err := d.ListTasks(root, release, epic)
133
+ func loadEpicTasks(discoverer taskDiscoverer, parser taskParser, root, release, epic string) ([]data.Task, error) {
134
+ taskInfos, err := discoverer.ListTasks(root, release, epic)
119
135
  if err != nil {
120
136
  return nil, err
121
137
  }
122
138
 
123
- parser := data.NewParser()
124
139
  tasks := make([]data.Task, 0, len(taskInfos))
125
140
  for _, taskInfo := range taskInfos {
126
141
  content, err := os.ReadFile(taskInfo.Path)
@@ -150,6 +165,11 @@ func firstKnown(preferred string, values []string) string {
150
165
  return preferred
151
166
  }
152
167
  }
168
+ for _, value := range values {
169
+ if shortID(value) == shortID(preferred) {
170
+ return value
171
+ }
172
+ }
153
173
  if len(values) == 0 {
154
174
  return ""
155
175
  }
@@ -1,16 +1,16 @@
1
1
  package board
2
2
 
3
3
  import (
4
- "os"
5
4
  "path/filepath"
6
5
  "strings"
7
6
  "testing"
8
7
 
9
8
  "github.com/opencode/savepoint/internal/data"
9
+ "github.com/opencode/savepoint/internal/testutil"
10
10
  )
11
11
 
12
- func TestNewProgramModelUsesBoardCore(t *testing.T) {
13
- m := newProgramModel()
12
+ func TestNewModelUsesBoardCore(t *testing.T) {
13
+ m := NewModel(nil, "v1", "E03-board-tui-core")
14
14
  m.Width = 100
15
15
 
16
16
  got := m.View()
@@ -27,22 +27,11 @@ func TestNewProgramModelUsesBoardCore(t *testing.T) {
27
27
  func TestNewProjectModelLoadsReleasesEpicsAndTasks(t *testing.T) {
28
28
  projectRoot := t.TempDir()
29
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
- `)
30
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v2", "E03-live", "", "test")
42
31
  writeTask(t, savepointRoot, "v1", "E01-old", "T001-old", data.ColumnPlanned)
43
32
  writeTask(t, savepointRoot, "v2", "E03-live", "T001-live", data.ColumnInProgress)
44
33
 
45
- model, err := newProjectModel(projectRoot)
34
+ model, err := newProjectModel(projectRoot, "", "")
46
35
  if err != nil {
47
36
  t.Fatalf("newProjectModel() error = %v", err)
48
37
  }
@@ -66,26 +55,19 @@ next_action: "test"
66
55
  if len(tasks) != 1 || tasks[0].ID != "E03-live/T001-live" {
67
56
  t.Errorf("visible in-progress tasks = %v, want E03-live/T001-live", tasks)
68
57
  }
58
+ if model.Watcher == nil {
59
+ t.Fatal("Watcher is nil, want auto-refresh watcher")
60
+ }
61
+ t.Cleanup(func() { model.Watcher.Close() })
69
62
  }
70
63
 
71
64
  func TestNewProjectModelUsesPathReleaseForTaskWithoutReleaseFrontmatter(t *testing.T) {
72
65
  projectRoot := t.TempDir()
73
66
  savepointRoot := filepath.Join(projectRoot, ".savepoint")
74
- writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
75
-
76
- ## Current state
77
-
78
- `+"```"+`yaml
79
- state: task-building
80
- release: v1.1
81
- epic: E01-tui-optimisation
82
- task: E01-tui-optimisation/T001-border-resize-fix
83
- next_action: "test"
84
- `+"```"+`
85
- `)
67
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1.1", "E01-tui-optimisation", "E01-tui-optimisation/T001-border-resize-fix", "test")
86
68
  writeTaskWithoutRelease(t, savepointRoot, "v1.1", "E01-tui-optimisation", "T001-border-resize-fix", data.ColumnInProgress)
87
69
 
88
- model, err := newProjectModel(projectRoot)
70
+ model, err := newProjectModel(projectRoot, "", "")
89
71
  if err != nil {
90
72
  t.Fatalf("newProjectModel() error = %v", err)
91
73
  }
@@ -100,53 +82,87 @@ next_action: "test"
100
82
  if tasks[0].Release != "v1.1" {
101
83
  t.Errorf("Task.Release = %q, want v1.1", tasks[0].Release)
102
84
  }
85
+ if model.Watcher != nil {
86
+ t.Cleanup(func() { model.Watcher.Close() })
87
+ }
103
88
  }
104
89
 
105
- func writeTask(t *testing.T, root, release, epic, task string, column data.ColumnType) {
106
- t.Helper()
107
- path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
108
- phase := ""
109
- if column == data.ColumnInProgress {
110
- phase = "phase: build\n"
111
- }
112
- content := `---
113
- id: ` + epic + `/` + task + `
114
- release: ` + release + `
115
- status: ` + string(column) + `
116
- ` + phase + `objective: "Test task"
117
- depends_on: []
118
- ---
119
-
120
- # Test task
121
- `
122
- writeFile(t, path, content)
90
+ func TestNewProjectModelResolvesShortRouterEpicToFullEpicID(t *testing.T) {
91
+ projectRoot := t.TempDir()
92
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
93
+ testutil.WriteRouter(t, savepointRoot, "task-building", "v1.1", "E03", "T001", "Build v1.1 E03/T001")
94
+ writeTask(t, savepointRoot, "v1.1", "E01-tui-optimisation", "T007-column-focus-border-stability", data.ColumnInProgress)
95
+ writeTask(t, savepointRoot, "v1.1", "E03-ui-visual-refinement", "T001-border-resize-fix", data.ColumnInProgress)
96
+
97
+ model, err := newProjectModel(projectRoot, "", "")
98
+ if err != nil {
99
+ t.Fatalf("newProjectModel() error = %v", err)
100
+ }
101
+
102
+ if model.SelectedEpic != "E03-ui-visual-refinement" {
103
+ t.Errorf("SelectedEpic = %q, want E03-ui-visual-refinement", model.SelectedEpic)
104
+ }
105
+ tasks := model.Tasks[data.ColumnInProgress]
106
+ if len(tasks) != 1 || tasks[0].ID != "E03-ui-visual-refinement/T001-border-resize-fix" {
107
+ t.Errorf("visible in-progress tasks = %v, want E03-ui-visual-refinement/T001-border-resize-fix", tasks)
108
+ }
109
+ if model.Watcher != nil {
110
+ t.Cleanup(func() { model.Watcher.Close() })
111
+ }
123
112
  }
124
113
 
125
- func writeTaskWithoutRelease(t *testing.T, root, release, epic, task string, column data.ColumnType) {
114
+ func TestUpdateReloadMsgRefreshesReleaseEpicIndex(t *testing.T) {
115
+ m := NewModel(nil, "v1", "E01-old")
116
+ m.Releases = []string{"v1"}
117
+ m.ReleaseEpics = map[string][]string{"v1": []string{"E01-old"}}
118
+
119
+ task := data.Task{
120
+ ID: "E02-new/T001-new",
121
+ Release: "v1",
122
+ Epic: "E02-new",
123
+ Column: data.ColumnPlanned,
124
+ }
125
+ got, _ := m.Update(reloadMsg{
126
+ tasks: []data.Task{task},
127
+ releases: []string{"v1"},
128
+ releaseEpics: map[string][]string{"v1": []string{"E02-new"}},
129
+ })
130
+ updated := requireModel(t, got)
131
+
132
+ if updated.SelectedEpic != "E02-new" {
133
+ t.Errorf("SelectedEpic = %q, want E02-new", updated.SelectedEpic)
134
+ }
135
+ if len(updated.Epics) != 1 || updated.Epics[0] != "E02-new" {
136
+ t.Errorf("Epics = %v, want [E02-new]", updated.Epics)
137
+ }
138
+ if len(updated.Tasks[data.ColumnPlanned]) != 1 {
139
+ t.Errorf("planned tasks = %v, want reloaded task visible", updated.Tasks[data.ColumnPlanned])
140
+ }
141
+ }
142
+
143
+ func writeTask(t *testing.T, root, release, epic, taskSlug string, column data.ColumnType) {
126
144
  t.Helper()
127
- path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
128
- phase := ""
145
+ tf := testutil.TaskFixture{
146
+ Slug: taskSlug,
147
+ Release: release,
148
+ Status: string(column),
149
+ Objective: "Test task",
150
+ }
129
151
  if column == data.ColumnInProgress {
130
- phase = "phase: build\n"
131
- }
132
- content := `---
133
- id: ` + epic + `/` + task + `
134
- status: ` + string(column) + `
135
- ` + phase + `objective: "Test task"
136
- depends_on: []
137
- ---
138
-
139
- # Test task
140
- `
141
- writeFile(t, path, content)
152
+ tf.Phase = "build"
153
+ }
154
+ testutil.WriteTask(t, root, release, epic, tf)
142
155
  }
143
156
 
144
- func writeFile(t *testing.T, path, content string) {
157
+ func writeTaskWithoutRelease(t *testing.T, root, release, epic, taskSlug string, column data.ColumnType) {
145
158
  t.Helper()
146
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
147
- t.Fatal(err)
159
+ tf := testutil.TaskFixture{
160
+ Slug: taskSlug,
161
+ Status: string(column),
162
+ Objective: "Test task",
148
163
  }
149
- if err := os.WriteFile(path, []byte(content), 0644); err != nil {
150
- t.Fatal(err)
164
+ if column == data.ColumnInProgress {
165
+ tf.Phase = "build"
151
166
  }
167
+ testutil.WriteTask(t, root, release, epic, tf)
152
168
  }
@@ -4,6 +4,7 @@ import (
4
4
  "fmt"
5
5
  "strings"
6
6
 
7
+ "github.com/charmbracelet/lipgloss"
7
8
  "github.com/opencode/savepoint/internal/data"
8
9
  "github.com/opencode/savepoint/internal/styles"
9
10
  )
@@ -17,21 +18,27 @@ const (
17
18
  )
18
19
 
19
20
  // RenderCard renders a task card with phase glyph, truncated ID+title, and focus styling.
20
- // When routerTaskID matches t.ID, a green priority glyph replaces the phase glyph.
21
- func RenderCard(t data.Task, width int, focused bool, routerTaskID string) string {
21
+ // When router state matches t's release/epic/task, a green priority glyph replaces the phase glyph.
22
+ func RenderCard(t data.Task, width int, focused bool, routerState *data.RouterState) string {
22
23
  inner := width - cardOverhead
23
24
  if inner < 2 {
24
25
  inner = 2
25
26
  }
26
27
 
27
- var glyph string
28
- if routerTaskID != "" && t.ID == routerTaskID {
29
- glyph = styles.TagDone.Render(glyphBuild)
30
- } else {
31
- glyph = phaseGlyphStyled(t.Stage)
28
+ glyph := taskGlyph(t, routerState)
29
+ phase := taskPhaseText(t)
30
+ idWidth := inner - 2
31
+ if phase != "" {
32
+ idWidth -= lipgloss.Width(phase) + 1
33
+ }
34
+ if idWidth < 1 {
35
+ idWidth = 1
36
+ }
37
+
38
+ idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), idWidth))
39
+ if phase != "" && lipgloss.Width(idLine)+1+lipgloss.Width(phase) <= inner {
40
+ idLine += " " + phase
32
41
  }
33
- // glyph is 1 rune + 1 space prefix; leave room for "▣ "
34
- idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), inner-2))
35
42
  titleLine := styles.CardMeta.Render(strings.Join(WrapText(t.Title, inner), "\n"))
36
43
 
37
44
  content := idLine + "\n" + titleLine
@@ -42,6 +49,33 @@ func RenderCard(t data.Task, width int, focused bool, routerTaskID string) strin
42
49
  return styles.Card.Width(width).Render(content)
43
50
  }
44
51
 
52
+ func taskGlyph(t data.Task, routerState *data.RouterState) string {
53
+ if t.Column == data.ColumnInProgress {
54
+ return phaseGlyphStyled(t.Stage)
55
+ }
56
+ if t.Column == data.ColumnDone {
57
+ return styles.GlyphBuild.Render(glyphBuild)
58
+ }
59
+ if isRouterPriority(t, routerState) {
60
+ return styles.TagDone.Render(glyphBuild)
61
+ }
62
+ if t.Status != "" {
63
+ return statusGlyph(t.Status)
64
+ }
65
+ return phaseGlyphStyled(t.Stage)
66
+ }
67
+
68
+ func taskPhaseText(t data.Task) string {
69
+ switch t.Column {
70
+ case data.ColumnInProgress:
71
+ return styles.CardMeta.Render(strings.ToUpper(phaseLabel(t.Stage)))
72
+ case data.ColumnDone:
73
+ return styles.CardMeta.Render("DONE")
74
+ default:
75
+ return ""
76
+ }
77
+ }
78
+
45
79
  func phaseGlyphStyled(stage data.ProgressStage) string {
46
80
  switch stage {
47
81
  case data.StageTest:
@@ -53,6 +87,33 @@ func phaseGlyphStyled(stage data.ProgressStage) string {
53
87
  }
54
88
  }
55
89
 
90
+ func isRouterPriority(t data.Task, state *data.RouterState) bool {
91
+ if state == nil || state.Task == "" {
92
+ return false
93
+ }
94
+ if shortID(t.ID) != shortID(state.Task) {
95
+ return false
96
+ }
97
+ if state.Release != "" && t.Release != "" && t.Release != state.Release {
98
+ return false
99
+ }
100
+ routerEpic := state.Epic
101
+ if routerEpic == "" {
102
+ routerEpic = taskEpic(state.Task)
103
+ }
104
+ if routerEpic != "" && t.Epic != "" && shortID(t.Epic) != shortID(routerEpic) {
105
+ return false
106
+ }
107
+ return true
108
+ }
109
+
110
+ func taskEpic(taskID string) string {
111
+ if idx := strings.LastIndex(taskID, "/"); idx >= 0 {
112
+ return taskID[:idx]
113
+ }
114
+ return ""
115
+ }
116
+
56
117
  // shortID strips the epic prefix and slug from a task ID.
57
118
  // "E06-atari-noir-layout/T004-component-refinement" → "T004"
58
119
  func shortID(id string) string {
@@ -65,14 +126,4 @@ func shortID(id string) string {
65
126
  return id
66
127
  }
67
128
 
68
- // truncate clips s to max runes, appending "…" if clipped.
69
- func truncate(s string, max int) string {
70
- runes := []rune(s)
71
- if len(runes) <= max {
72
- return s
73
- }
74
- if max <= 1 {
75
- return "…"
76
- }
77
- return string(runes[:max-1]) + "…"
78
- }
129
+