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
@@ -0,0 +1,211 @@
1
+ package main
2
+
3
+ import (
4
+ "archive/tar"
5
+ "compress/gzip"
6
+ "errors"
7
+ "flag"
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ "os/exec"
12
+ "strings"
13
+ "path/filepath"
14
+ "runtime"
15
+ )
16
+
17
+ type target struct {
18
+ os string
19
+ arch string
20
+ }
21
+
22
+ var targets = []target{
23
+ {os: "linux", arch: "amd64"},
24
+ {os: "linux", arch: "arm64"},
25
+ {os: "darwin", arch: "amd64"},
26
+ {os: "darwin", arch: "arm64"},
27
+ }
28
+
29
+ var versionOverride string
30
+
31
+ func main() {
32
+ if err := run(os.Args[1:]); err != nil {
33
+ fmt.Fprintln(os.Stderr, err)
34
+ os.Exit(1)
35
+ }
36
+ }
37
+
38
+ func run(args []string) error {
39
+ flags := flag.NewFlagSet("buildtool", flag.ContinueOnError)
40
+ flags.StringVar(&versionOverride, "version", "", "version to inject into the binary")
41
+ if err := flags.Parse(args); err != nil {
42
+ return err
43
+ }
44
+ if flags.NArg() != 1 {
45
+ return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-all|dist|smoke-test>")
46
+ }
47
+
48
+ switch flags.Arg(0) {
49
+ case "build":
50
+ return buildLocal()
51
+ case "clean":
52
+ return clean()
53
+ case "build-linux":
54
+ return buildMatching("linux")
55
+ case "build-darwin":
56
+ return buildMatching("darwin")
57
+ case "build-all":
58
+ return buildAll()
59
+ case "dist":
60
+ return dist()
61
+ case "smoke-test":
62
+ return smokeTest()
63
+ default:
64
+ return fmt.Errorf("unknown build target %q", flags.Arg(0))
65
+ }
66
+ }
67
+
68
+ func buildLocal() error {
69
+ return runGoBuild(localExecutable(), runtime.GOOS, runtime.GOARCH)
70
+ }
71
+
72
+ func clean() error {
73
+ for _, path := range []string{"savepoint", "savepoint.exe", "dist"} {
74
+ if err := os.RemoveAll(path); err != nil {
75
+ return fmt.Errorf("clean %s: %w", path, err)
76
+ }
77
+ }
78
+ return nil
79
+ }
80
+
81
+ func buildMatching(goos string) error {
82
+ for _, target := range targets {
83
+ if target.os != goos {
84
+ continue
85
+ }
86
+ if err := buildTarget(target); err != nil {
87
+ return err
88
+ }
89
+ }
90
+ return nil
91
+ }
92
+
93
+ func buildAll() error {
94
+ for _, target := range targets {
95
+ if err := buildTarget(target); err != nil {
96
+ return err
97
+ }
98
+ }
99
+ return nil
100
+ }
101
+
102
+ func buildTarget(target target) error {
103
+ output := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
104
+ return runGoBuild(output, target.os, target.arch)
105
+ }
106
+
107
+ func runGoBuild(output, goos, goarch string) error {
108
+ if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil && filepath.Dir(output) != "." {
109
+ return fmt.Errorf("create output dir: %w", err)
110
+ }
111
+
112
+ cmd := exec.Command("go", "build", "-ldflags", "-X main.version="+version(), "-o", output, "main.go")
113
+ cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
114
+ cmd.Stdout = os.Stdout
115
+ cmd.Stderr = os.Stderr
116
+ if err := cmd.Run(); err != nil {
117
+ return fmt.Errorf("build %s/%s: %w", goos, goarch, err)
118
+ }
119
+ return nil
120
+ }
121
+
122
+ func dist() error {
123
+ if err := buildAll(); err != nil {
124
+ return err
125
+ }
126
+ for _, target := range targets {
127
+ name := "savepoint-" + version() + "-" + target.os + "-" + target.arch + ".tar.gz"
128
+ source := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
129
+ archive := filepath.Join("dist", name)
130
+ if err := writeTarGz(archive, source, "savepoint"); err != nil {
131
+ return err
132
+ }
133
+ }
134
+ return nil
135
+ }
136
+
137
+ func writeTarGz(archivePath, sourcePath, archiveName string) error {
138
+ source, err := os.Open(sourcePath)
139
+ if err != nil {
140
+ return fmt.Errorf("open artifact source: %w", err)
141
+ }
142
+ defer source.Close()
143
+
144
+ info, err := source.Stat()
145
+ if err != nil {
146
+ return fmt.Errorf("stat artifact source: %w", err)
147
+ }
148
+
149
+ archive, err := os.Create(archivePath)
150
+ if err != nil {
151
+ return fmt.Errorf("create archive: %w", err)
152
+ }
153
+ defer archive.Close()
154
+
155
+ gzipWriter := gzip.NewWriter(archive)
156
+ defer gzipWriter.Close()
157
+
158
+ tarWriter := tar.NewWriter(gzipWriter)
159
+ defer tarWriter.Close()
160
+
161
+ header, err := tar.FileInfoHeader(info, "")
162
+ if err != nil {
163
+ return fmt.Errorf("create archive header: %w", err)
164
+ }
165
+ header.Name = archiveName
166
+ if err := tarWriter.WriteHeader(header); err != nil {
167
+ return fmt.Errorf("write archive header: %w", err)
168
+ }
169
+ if _, err := io.Copy(tarWriter, source); err != nil {
170
+ return fmt.Errorf("write archive content: %w", err)
171
+ }
172
+ return nil
173
+ }
174
+
175
+ func smokeTest() error {
176
+ if err := buildLocal(); err != nil {
177
+ return err
178
+ }
179
+ cmd := exec.Command("."+string(os.PathSeparator)+localExecutable(), "--version")
180
+ cmd.Stdout = os.Stdout
181
+ cmd.Stderr = os.Stderr
182
+ if err := cmd.Run(); err != nil {
183
+ return fmt.Errorf("smoke test: %w", err)
184
+ }
185
+ fmt.Println("smoke test passed")
186
+ return nil
187
+ }
188
+
189
+ func version() string {
190
+ if versionOverride != "" {
191
+ return versionOverride
192
+ }
193
+ if value := os.Getenv("VERSION"); value != "" {
194
+ return value
195
+ }
196
+
197
+ cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
198
+ output, err := cmd.Output()
199
+ if err == nil && len(output) > 0 {
200
+ return strings.TrimSpace(string(output))
201
+ }
202
+ return "v0.0.0"
203
+ }
204
+
205
+ func localExecutable() string {
206
+ if runtime.GOOS == "windows" {
207
+ return "savepoint.exe"
208
+ }
209
+ return "savepoint"
210
+ }
211
+
@@ -0,0 +1,46 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "runtime"
6
+ "testing"
7
+ )
8
+
9
+ func TestVersion_override(t *testing.T) {
10
+ versionOverride = "v1.2.3"
11
+ defer func() { versionOverride = "" }()
12
+ if got := version(); got != "v1.2.3" {
13
+ t.Errorf("version() = %q, want %q", got, "v1.2.3")
14
+ }
15
+ }
16
+
17
+ func TestVersion_env(t *testing.T) {
18
+ versionOverride = ""
19
+ os.Setenv("VERSION", "v2.0.0-env")
20
+ defer os.Unsetenv("VERSION")
21
+ if got := version(); got != "v2.0.0-env" {
22
+ t.Errorf("version() = %q, want %q", got, "v2.0.0-env")
23
+ }
24
+ }
25
+
26
+ func TestVersion_fallback(t *testing.T) {
27
+ versionOverride = ""
28
+ os.Unsetenv("VERSION")
29
+ got := version()
30
+ if got == "" {
31
+ t.Error("version() returned empty string")
32
+ }
33
+ }
34
+
35
+ func TestLocalExecutable(t *testing.T) {
36
+ got := localExecutable()
37
+ if runtime.GOOS == "windows" {
38
+ if got != "savepoint.exe" {
39
+ t.Errorf("localExecutable() = %q, want %q", got, "savepoint.exe")
40
+ }
41
+ } else {
42
+ if got != "savepoint" {
43
+ t.Errorf("localExecutable() = %q, want %q", got, "savepoint")
44
+ }
45
+ }
46
+ }
@@ -16,8 +16,17 @@ type Theme struct {
16
16
  Accents map[string]string `yaml:"accents"`
17
17
  }
18
18
 
19
+ type QualityGates struct {
20
+ Lint *string `yaml:"lint"`
21
+ Typecheck *string `yaml:"typecheck"`
22
+ Test *string `yaml:"test"`
23
+ BlockOnFailure bool `yaml:"block_on_failure"`
24
+ Timeout string `yaml:"gate_timeout"`
25
+ }
26
+
19
27
  type Config struct {
20
- Theme Theme `yaml:"theme"`
28
+ Theme Theme `yaml:"theme"`
29
+ QualityGates QualityGates `yaml:"quality_gates"`
21
30
  }
22
31
 
23
32
  var defaultTheme = Theme{
@@ -80,8 +89,13 @@ func fillThemeDefaults(theme Theme) Theme {
80
89
  if theme.Text == "" {
81
90
  theme.Text = defaultTheme.Text
82
91
  }
83
- if len(theme.Accents) == 0 {
84
- theme.Accents = defaultTheme.Accents
92
+ if theme.Accents == nil {
93
+ theme.Accents = make(map[string]string)
94
+ }
95
+ for k, v := range defaultTheme.Accents {
96
+ if _, ok := theme.Accents[k]; !ok {
97
+ theme.Accents[k] = v
98
+ }
85
99
  }
86
100
  return theme
87
101
  }
@@ -53,6 +53,55 @@ func TestConfigReaderRead(t *testing.T) {
53
53
  }
54
54
  }
55
55
 
56
+ func TestFillThemeDefaults_PartialAccents(t *testing.T) {
57
+ theme := Theme{
58
+ BG: "#000000",
59
+ Accents: map[string]string{"planned": "#ff0000"},
60
+ }
61
+ result := fillThemeDefaults(theme)
62
+ if result.Accents["planned"] != "#ff0000" {
63
+ t.Errorf("Accents[planned] = %v, want #ff0000 (user value preserved)", result.Accents["planned"])
64
+ }
65
+ if result.Accents["in_progress"] != defaultTheme.Accents["in_progress"] {
66
+ t.Errorf("Accents[in_progress] = %v, want default %v", result.Accents["in_progress"], defaultTheme.Accents["in_progress"])
67
+ }
68
+ if result.Accents["done"] != defaultTheme.Accents["done"] {
69
+ t.Errorf("Accents[done] = %v, want default %v", result.Accents["done"], defaultTheme.Accents["done"])
70
+ }
71
+ if result.Accents["blocked"] != defaultTheme.Accents["blocked"] {
72
+ t.Errorf("Accents[blocked] = %v, want default %v", result.Accents["blocked"], defaultTheme.Accents["blocked"])
73
+ }
74
+ if result.Accents["epic"] != defaultTheme.Accents["epic"] {
75
+ t.Errorf("Accents[epic] = %v, want default %v", result.Accents["epic"], defaultTheme.Accents["epic"])
76
+ }
77
+ }
78
+
79
+ func TestFillThemeDefaults_NilAccents(t *testing.T) {
80
+ theme := Theme{
81
+ BG: "#000000",
82
+ Accents: nil,
83
+ }
84
+ result := fillThemeDefaults(theme)
85
+ for k, v := range defaultTheme.Accents {
86
+ if result.Accents[k] != v {
87
+ t.Errorf("Accents[%s] = %v, want default %v", k, result.Accents[k], v)
88
+ }
89
+ }
90
+ }
91
+
92
+ func TestFillThemeDefaults_EmptyAccents(t *testing.T) {
93
+ theme := Theme{
94
+ BG: "#000000",
95
+ Accents: map[string]string{},
96
+ }
97
+ result := fillThemeDefaults(theme)
98
+ for k, v := range defaultTheme.Accents {
99
+ if result.Accents[k] != v {
100
+ t.Errorf("Accents[%s] = %v, want default %v", k, result.Accents[k], v)
101
+ }
102
+ }
103
+ }
104
+
56
105
  func TestConfigReaderMalformedYAML(t *testing.T) {
57
106
  tmpfile, err := os.CreateTemp("", "config-*.yml")
58
107
  if err != nil {
@@ -85,6 +85,32 @@ func (d *Discover) ListReleases(root string) ([]ReleaseInfo, error) {
85
85
  return releases, nil
86
86
  }
87
87
 
88
+ // ListRootDirs returns sorted child directory names directly under root.
89
+ func (d *Discover) ListRootDirs(root string) ([]string, error) {
90
+ info, err := os.Stat(root)
91
+ if err != nil {
92
+ return nil, err
93
+ }
94
+ if !info.IsDir() {
95
+ return nil, fmt.Errorf("%s is not a directory", root)
96
+ }
97
+
98
+ entries, err := os.ReadDir(root)
99
+ if err != nil {
100
+ return nil, err
101
+ }
102
+
103
+ var dirs []string
104
+ for _, entry := range entries {
105
+ if entry.IsDir() {
106
+ dirs = append(dirs, entry.Name())
107
+ }
108
+ }
109
+
110
+ sort.Strings(dirs)
111
+ return dirs, nil
112
+ }
113
+
88
114
  func (d *Discover) ListEpics(root, release string) ([]EpicInfo, error) {
89
115
  epicsPath := filepath.Join(root, "releases", release, "epics")
90
116
  info, err := os.Stat(epicsPath)
@@ -1,18 +1,17 @@
1
1
  package data
2
2
 
3
3
  import (
4
- "os"
5
4
  "path/filepath"
6
5
  "testing"
6
+
7
+ "github.com/opencode/savepoint/internal/testutil"
7
8
  )
8
9
 
9
10
  func TestFindSavepointRoot(t *testing.T) {
10
11
  d := NewDiscover()
11
12
  savepointRoot := createDiscoveryFixture(t)
12
13
  start := filepath.Join(filepath.Dir(savepointRoot), "nested", "child")
13
- if err := os.MkdirAll(start, 0755); err != nil {
14
- t.Fatal(err)
15
- }
14
+ testutil.MkdirAll(t, start)
16
15
 
17
16
  root, err := d.FindSavepointRoot(start)
18
17
  if err != nil {
@@ -40,6 +39,35 @@ func TestListReleases(t *testing.T) {
40
39
  }
41
40
  }
42
41
 
42
+ func TestListRootDirs(t *testing.T) {
43
+ d := NewDiscover()
44
+ root := t.TempDir()
45
+ testutil.MkdirAll(t, filepath.Join(root, "beta"))
46
+ testutil.MkdirAll(t, filepath.Join(root, "alpha"))
47
+ testutil.WriteFile(t, filepath.Join(root, "notes.txt"), "test")
48
+
49
+ dirs, err := d.ListRootDirs(root)
50
+ if err != nil {
51
+ t.Fatalf("ListRootDirs() error = %v", err)
52
+ }
53
+
54
+ if len(dirs) != 2 || dirs[0] != "alpha" || dirs[1] != "beta" {
55
+ t.Fatalf("ListRootDirs() = %v, want [alpha beta]", dirs)
56
+ }
57
+ }
58
+
59
+ func TestListRootDirsRejectsFile(t *testing.T) {
60
+ d := NewDiscover()
61
+ root := t.TempDir()
62
+ path := filepath.Join(root, "not-dir")
63
+ testutil.WriteFile(t, path, "test")
64
+
65
+ _, err := d.ListRootDirs(path)
66
+ if err == nil {
67
+ t.Fatal("ListRootDirs() error = nil, want not directory error")
68
+ }
69
+ }
70
+
43
71
  func TestListEpics(t *testing.T) {
44
72
  d := NewDiscover()
45
73
  root := createDiscoveryFixture(t)
@@ -86,9 +114,7 @@ func createDiscoveryFixture(t *testing.T) string {
86
114
  filepath.Join(savepointRoot, "releases", "v2", "epics"),
87
115
  }
88
116
  for _, path := range paths {
89
- if err := os.MkdirAll(path, 0755); err != nil {
90
- t.Fatal(err)
91
- }
117
+ testutil.MkdirAll(t, path)
92
118
  }
93
119
 
94
120
  files := []string{
@@ -97,9 +123,7 @@ func createDiscoveryFixture(t *testing.T) string {
97
123
  filepath.Join(savepointRoot, "releases", "v1", "epics", "E02-data-readers", "tasks", "notes.txt"),
98
124
  }
99
125
  for _, file := range files {
100
- if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
101
- t.Fatal(err)
102
- }
126
+ testutil.WriteFile(t, file, "test")
103
127
  }
104
128
 
105
129
  return savepointRoot
@@ -6,4 +6,8 @@ var (
6
6
  ErrNoFrontmatter = errors.New("no frontmatter found")
7
7
  ErrNoClosingFrontmatter = errors.New("no closing frontmatter delimiter found")
8
8
  ErrSavepointDirectoryMissing = errors.New(".savepoint directory not found")
9
+ ErrInvalidStatus = errors.New("invalid router state")
10
+ ErrMissingFrontmatter = errors.New("missing or invalid frontmatter")
11
+ ErrConfigNotFound = errors.New("configuration file not found")
12
+ ErrStructureProblem = errors.New("project structure problem")
9
13
  )
@@ -2,17 +2,24 @@ package data
2
2
 
3
3
  import "fmt"
4
4
 
5
- func ValidateTaskLifecycle(task Task) error {
5
+ func ValidateTaskLifecycle(task *Task) error {
6
6
  if !IsCanonicalColumn(task.Column) {
7
- return fmt.Errorf("invalid task status %q: use planned, in_progress, or done", task.Column)
7
+ return fmt.Errorf("invalid status %q: use planned, in_progress, or done. Add 'status: planned' or 'status: in_progress' to task frontmatter", task.Column)
8
8
  }
9
9
 
10
- if task.Column != ColumnInProgress && task.Stage != "" {
11
- return fmt.Errorf("phase %q is only valid when status is in_progress", task.Stage)
10
+ if task.Column == ColumnInProgress {
11
+ if task.Stage == "" {
12
+ task.Stage = StageBuild
13
+ return nil
14
+ }
15
+ if !IsCanonicalStage(task.Stage) {
16
+ return fmt.Errorf("invalid phase %q: use build, test, or audit. Add 'phase: build' to task frontmatter", task.Stage)
17
+ }
18
+ return nil
12
19
  }
13
20
 
14
- if task.Column == ColumnInProgress && !IsCanonicalStage(task.Stage) {
15
- return fmt.Errorf("invalid in_progress phase %q: use build, test, or audit", task.Stage)
21
+ if task.Stage != "" {
22
+ return fmt.Errorf("phase field %q is only valid when status is in_progress. Remove 'phase' or change status to in_progress", task.Stage)
16
23
  }
17
24
 
18
25
  return nil
@@ -4,35 +4,38 @@ import "testing"
4
4
 
5
5
  func TestValidateTaskLifecycle_allowsPlannedWithoutPhase(t *testing.T) {
6
6
  task := Task{Column: ColumnPlanned}
7
- if err := ValidateTaskLifecycle(task); err != nil {
7
+ if err := ValidateTaskLifecycle(&task); err != nil {
8
8
  t.Fatalf("ValidateTaskLifecycle() error = %v", err)
9
9
  }
10
10
  }
11
11
 
12
+ func TestValidateTaskLifecycle_defaultsInProgressWithoutPhase(t *testing.T) {
13
+ task := Task{Column: ColumnInProgress}
14
+ if err := ValidateTaskLifecycle(&task); err != nil {
15
+ t.Fatalf("ValidateTaskLifecycle() error = %v", err)
16
+ }
17
+ if task.Stage != StageBuild {
18
+ t.Fatalf("Task.Stage = %q, want %q", task.Stage, StageBuild)
19
+ }
20
+ }
21
+
12
22
  func TestValidateTaskLifecycle_allowsInProgressWithPhase(t *testing.T) {
13
23
  task := Task{Column: ColumnInProgress, Stage: StageAudit}
14
- if err := ValidateTaskLifecycle(task); err != nil {
24
+ if err := ValidateTaskLifecycle(&task); err != nil {
15
25
  t.Fatalf("ValidateTaskLifecycle() error = %v", err)
16
26
  }
17
27
  }
18
28
 
19
29
  func TestValidateTaskLifecycle_rejectsUnknownStatus(t *testing.T) {
20
30
  task := Task{Column: "review"}
21
- if err := ValidateTaskLifecycle(task); err == nil {
31
+ if err := ValidateTaskLifecycle(&task); err == nil {
22
32
  t.Fatal("ValidateTaskLifecycle() expected unknown status error")
23
33
  }
24
34
  }
25
35
 
26
36
  func TestValidateTaskLifecycle_rejectsPhaseOutsideInProgress(t *testing.T) {
27
37
  task := Task{Column: ColumnPlanned, Stage: StageBuild}
28
- if err := ValidateTaskLifecycle(task); err == nil {
38
+ if err := ValidateTaskLifecycle(&task); err == nil {
29
39
  t.Fatal("ValidateTaskLifecycle() expected phase/status error")
30
40
  }
31
41
  }
32
-
33
- func TestValidateTaskLifecycle_rejectsInProgressWithoutCanonicalPhase(t *testing.T) {
34
- task := Task{Column: ColumnInProgress}
35
- if err := ValidateTaskLifecycle(task); err == nil {
36
- t.Fatal("ValidateTaskLifecycle() expected missing phase error")
37
- }
38
- }
@@ -46,7 +46,7 @@ func (p *Parser) ParseTaskFile(path string, content string) (*Task, error) {
46
46
  Epic: firstNonEmpty(fields.Epic, extractEpicFromID(fields.ID)),
47
47
  Release: firstNonEmpty(fields.Release, "v1"),
48
48
  Column: normalizeColumn(rawColumn),
49
- Stage: firstStage(fields.Stage, fields.Phase),
49
+ Stage: firstStage(fields.Phase, fields.Stage),
50
50
  Priority: fields.Priority,
51
51
  Points: fields.Points,
52
52
  Tags: fields.Tags,
@@ -61,6 +61,10 @@ func (p *Parser) ParseTaskFile(path string, content string) (*Task, error) {
61
61
  return nil, fmt.Errorf("parse error for %s: %w", path, err)
62
62
  }
63
63
 
64
+ if task.Column == ColumnInProgress && task.Stage == "" {
65
+ task.Stage = StageBuild
66
+ }
67
+
64
68
  return task, nil
65
69
  }
66
70
 
@@ -84,8 +88,13 @@ type taskFrontmatter struct {
84
88
  Progress Progress `yaml:"progress"`
85
89
  }
86
90
 
91
+ // normalizeLineEndings replaces Windows line endings with Unix line endings.
92
+ func normalizeLineEndings(s string) string {
93
+ return strings.ReplaceAll(s, "\r\n", "\n")
94
+ }
95
+
87
96
  func extractFrontmatter(content string) (string, error) {
88
- normalized := strings.ReplaceAll(content, "\r\n", "\n")
97
+ normalized := normalizeLineEndings(content)
89
98
  if !strings.HasPrefix(normalized, "---\n") {
90
99
  return "", ErrNoFrontmatter
91
100
  }
@@ -139,9 +148,15 @@ const legacyTodoColumn ColumnType = "todo"
139
148
 
140
149
  func validateParsedTaskLifecycle(rawColumn ColumnType, task Task) error {
141
150
  if rawColumn != "" && rawColumn != legacyTodoColumn && !IsCanonicalColumn(rawColumn) {
142
- return fmt.Errorf("invalid task status %q: use planned, in_progress, or done", rawColumn)
151
+ return fmt.Errorf("invalid task status %q: use planned, in_progress, or done. Add 'status: planned' or 'status: in_progress' to task frontmatter", rawColumn)
152
+ }
153
+ if task.Column == ColumnInProgress && !IsCanonicalStage(task.Stage) && task.Stage != "" {
154
+ return fmt.Errorf("invalid phase %q: use build, test, or audit. Add 'phase: build' to task frontmatter", task.Stage)
143
155
  }
144
- return ValidateTaskLifecycle(task)
156
+ if task.Column != ColumnInProgress && task.Stage != "" {
157
+ return nil
158
+ }
159
+ return nil
145
160
  }
146
161
 
147
162
  func firstStage(values ...ProgressStage) ProgressStage {
@@ -163,7 +178,7 @@ func firstList(values ...[]string) []string {
163
178
  }
164
179
 
165
180
  func extractChecklistItems(content, heading string) []CheckItem {
166
- normalized := strings.ReplaceAll(content, "\r\n", "\n")
181
+ normalized := normalizeLineEndings(content)
167
182
  start := strings.Index(normalized, heading)
168
183
  if start == -1 {
169
184
  return nil
@@ -175,25 +190,33 @@ func extractChecklistItems(content, heading string) []CheckItem {
175
190
  }
176
191
 
177
192
  items := []CheckItem{}
193
+ var current *CheckItem
178
194
  for _, line := range strings.Split(section, "\n") {
179
195
  trimmed := strings.TrimSpace(line)
180
196
  if strings.HasPrefix(trimmed, "- [x] ") {
181
197
  items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[6:]), Done: true})
198
+ current = &items[len(items)-1]
182
199
  continue
183
200
  }
184
201
  if strings.HasPrefix(trimmed, "- [ ] ") {
185
202
  items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[6:]), Done: false})
203
+ current = &items[len(items)-1]
186
204
  continue
187
205
  }
188
206
  if strings.HasPrefix(trimmed, "- ") {
189
207
  items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[2:]), Done: false})
208
+ current = &items[len(items)-1]
209
+ continue
210
+ }
211
+ if trimmed != "" && current != nil {
212
+ current.Text = strings.TrimSpace(current.Text + " " + trimmed)
190
213
  }
191
214
  }
192
215
  return items
193
216
  }
194
217
 
195
218
  func extractChecklistSection(content, heading string) []string {
196
- normalized := strings.ReplaceAll(content, "\r\n", "\n")
219
+ normalized := normalizeLineEndings(content)
197
220
  start := strings.Index(normalized, heading)
198
221
  if start == -1 {
199
222
  return nil