savepoint 1.0.2 → 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 (233) hide show
  1. package/.claude/settings.local.json +12 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +37 -36
  4. package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
  5. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  6. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +14 -1
  7. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
  8. package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
  9. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  10. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +25 -16
  11. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
  12. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
  13. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
  14. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
  15. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
  16. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
  17. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  18. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  19. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  20. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  21. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  22. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  23. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  24. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  25. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  26. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  27. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  28. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  29. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  30. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  31. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  32. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  33. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  34. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  35. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  36. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  37. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  38. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  39. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  40. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  41. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  42. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  43. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  44. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  45. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  46. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  47. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  48. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  49. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  50. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  51. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  52. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  53. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  54. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  55. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  56. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  57. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  58. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  59. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  60. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  61. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  62. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  63. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  64. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  65. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  66. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  67. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  68. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  69. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  70. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  71. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  72. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  73. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  74. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  75. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  76. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  77. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  78. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  79. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  80. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  81. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  83. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  84. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  85. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  86. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  87. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  88. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  89. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  90. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  91. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  92. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  93. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  94. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  95. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  96. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  97. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  98. package/.savepoint/releases/v1.1/v1.1-PRD.md +67 -7
  99. package/.savepoint/router.md +9 -16
  100. package/AGENTS.md +38 -23
  101. package/README.md +0 -1
  102. package/agent-skills/savepoint-audit/SKILL.md +86 -34
  103. package/agent-skills/savepoint-build-task/SKILL.md +7 -2
  104. package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
  105. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  106. package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
  107. package/agent-skills/savepoint-system-design/SKILL.md +7 -2
  108. package/agent_skills_test.go +91 -0
  109. package/cmd/board.go +59 -0
  110. package/cmd/board_test.go +137 -0
  111. package/cmd/doctor.go +53 -0
  112. package/cmd/doctor_test.go +146 -0
  113. package/cmd/init.go +63 -0
  114. package/cmd/init_test.go +104 -0
  115. package/internal/board/board.go +40 -36
  116. package/internal/board/board_test.go +27 -82
  117. package/internal/board/card.go +43 -23
  118. package/internal/board/card_test.go +41 -5
  119. package/internal/board/column.go +44 -13
  120. package/internal/board/column_test.go +5 -2
  121. package/internal/board/detail.go +0 -47
  122. package/internal/board/epic_panel.go +118 -22
  123. package/internal/board/epic_panel_test.go +302 -17
  124. package/internal/board/help.go +1 -0
  125. package/internal/board/help_test.go +1 -0
  126. package/internal/board/integration_test.go +266 -0
  127. package/internal/board/interfaces.go +65 -0
  128. package/internal/board/interfaces_test.go +114 -0
  129. package/internal/board/io.go +93 -0
  130. package/internal/board/model.go +79 -118
  131. package/internal/board/plain.go +88 -0
  132. package/internal/board/plain_test.go +117 -0
  133. package/internal/board/release.go +1 -9
  134. package/internal/board/release_test.go +6 -6
  135. package/internal/board/status.go +4 -4
  136. package/internal/board/theme.go +24 -0
  137. package/internal/board/theme_test.go +31 -0
  138. package/internal/board/transitions.go +113 -88
  139. package/internal/board/transitions_test.go +164 -141
  140. package/internal/board/tui.go +32 -0
  141. package/internal/board/update.go +325 -215
  142. package/internal/board/update_test.go +299 -18
  143. package/internal/board/util.go +76 -0
  144. package/internal/board/view.go +31 -28
  145. package/internal/board/view_test.go +12 -2
  146. package/internal/board/watch.go +35 -5
  147. package/internal/buildtool/main.go +2 -10
  148. package/internal/buildtool/main_test.go +46 -0
  149. package/internal/data/config.go +17 -3
  150. package/internal/data/config_test.go +49 -0
  151. package/internal/data/discover.go +26 -0
  152. package/internal/data/discover_test.go +34 -10
  153. package/internal/data/errors.go +4 -0
  154. package/internal/data/lifecycle.go +13 -6
  155. package/internal/data/lifecycle_test.go +14 -11
  156. package/internal/data/parser.go +21 -6
  157. package/internal/data/parser_test.go +31 -7
  158. package/internal/data/task.go +0 -9
  159. package/internal/data/write.go +85 -11
  160. package/internal/data/write_test.go +167 -0
  161. package/internal/doctor/checks.go +567 -0
  162. package/internal/doctor/checks_test.go +716 -0
  163. package/internal/doctor/gates.go +193 -0
  164. package/internal/doctor/gates_test.go +166 -0
  165. package/internal/doctor/interfaces.go +64 -0
  166. package/internal/doctor/interfaces_test.go +104 -0
  167. package/internal/doctor/repairs.go +80 -0
  168. package/internal/doctor/repairs_test.go +81 -0
  169. package/internal/doctor/report.go +157 -0
  170. package/internal/doctor/report_test.go +89 -0
  171. package/internal/init/clipboard.go +146 -0
  172. package/internal/init/clipboard_test.go +74 -0
  173. package/internal/init/install.go +16 -0
  174. package/internal/init/integration_test.go +197 -0
  175. package/internal/init/prompt.go +14 -0
  176. package/internal/init/prompt_test.go +77 -0
  177. package/internal/init/scaffold.go +59 -0
  178. package/internal/init/scaffold_test.go +179 -0
  179. package/internal/init/template_freshness_test.go +56 -0
  180. package/internal/init/validate.go +85 -0
  181. package/internal/init/validate_test.go +141 -0
  182. package/internal/init/write.go +73 -0
  183. package/internal/init/write_test.go +91 -0
  184. package/internal/styles/styles_test.go +133 -0
  185. package/internal/testutil/fixture.go +113 -0
  186. package/internal/testutil/fs.go +26 -0
  187. package/main.go +101 -4
  188. package/package.json +2 -2
  189. package/project-audit/audit_report_glm_5.1.md +411 -0
  190. package/project-audit/audit_report_opus_4.6 +406 -0
  191. package/project-audit/consolidated-audit-report.md +456 -0
  192. package/savepoint +0 -0
  193. package/templates/project/.savepoint/Design.md +2 -2
  194. package/templates/project/.savepoint/router.md +10 -10
  195. package/templates/project/AGENTS.md +33 -21
  196. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  197. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  198. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  199. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  200. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  201. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  202. package/templates/prompts/audit-reconciliation.prompt.md +33 -28
  203. package/templates/prompts/design.prompt.md +3 -1
  204. package/.savepoint/audit/v1/E01/proposals.md +0 -168
  205. package/.savepoint/audit/v1/E01/snapshot.md +0 -78
  206. package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
  207. package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
  208. package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
  209. package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
  210. package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
  211. package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
  212. package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
  213. package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
  214. package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
  215. package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
  216. package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
  217. package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
  218. package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
  219. package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
  220. package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
  221. package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
  222. package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
  223. package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
  224. package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
  225. package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
  226. package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
  227. package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
  228. package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
  229. package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
  230. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
  231. package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
  232. package/ink-cli-ui-design.zip +0 -0
  233. package/savepoint.exe +0 -0
@@ -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
  )
@@ -24,18 +25,20 @@ func RenderCard(t data.Task, width int, focused bool, routerState *data.RouterSt
24
25
  inner = 2
25
26
  }
26
27
 
27
- var glyph string
28
- if t.Status != "" {
29
- glyph = statusGlyph(t.Status)
30
- } else if t.Column == data.ColumnDone {
31
- glyph = styles.GlyphBuild.Render(glyphBuild)
32
- } else if isRouterPriority(t, routerState) {
33
- glyph = styles.TagDone.Render(glyphBuild)
34
- } else {
35
- glyph = phaseGlyphStyled(t.Stage)
36
- }
37
- // glyph is 1 rune + 1 space prefix; leave room for "▣ "
38
- idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), inner-2))
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
41
+ }
39
42
  titleLine := styles.CardMeta.Render(strings.Join(WrapText(t.Title, inner), "\n"))
40
43
 
41
44
  content := idLine + "\n" + titleLine
@@ -46,6 +49,33 @@ func RenderCard(t data.Task, width int, focused bool, routerState *data.RouterSt
46
49
  return styles.Card.Width(width).Render(content)
47
50
  }
48
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
+
49
79
  func phaseGlyphStyled(stage data.ProgressStage) string {
50
80
  switch stage {
51
81
  case data.StageTest:
@@ -96,14 +126,4 @@ func shortID(id string) string {
96
126
  return id
97
127
  }
98
128
 
99
- // truncate clips s to max runes, appending "…" if clipped.
100
- func truncate(s string, max int) string {
101
- runes := []rune(s)
102
- if len(runes) <= max {
103
- return s
104
- }
105
- if max <= 1 {
106
- return "…"
107
- }
108
- return string(runes[:max-1]) + "…"
109
- }
129
+
@@ -194,13 +194,12 @@ func TestRenderCard_doneTaskUsesOrangeBuildGlyph(t *testing.T) {
194
194
  func TestRenderCard_explicitStatusUsesUnifiedGlyph(t *testing.T) {
195
195
  tests := []struct {
196
196
  name string
197
- status data.TaskStatus
197
+ status string
198
198
  glyph string
199
199
  }{
200
- {"planned", data.StatusPlanned, "○"},
201
- {"in progress", data.StatusInProgress, ""},
202
- {"done", data.StatusDone, ""},
203
- {"audited", data.StatusAudited, "✓"},
200
+ {"planned", string(data.ColumnPlanned), "○"},
201
+ {"done", string(data.ColumnDone), ""},
202
+ {"audited", "audited", ""},
204
203
  }
205
204
 
206
205
  for _, tt := range tests {
@@ -216,3 +215,40 @@ func TestRenderCard_explicitStatusUsesUnifiedGlyph(t *testing.T) {
216
215
  })
217
216
  }
218
217
  }
218
+
219
+ func TestRenderCard_inProgressShowsPhaseText(t *testing.T) {
220
+ tests := []struct {
221
+ name string
222
+ stage data.ProgressStage
223
+ label string
224
+ glyph string
225
+ }{
226
+ {"build", data.StageBuild, "BUILD", glyphBuild},
227
+ {"test", data.StageTest, "TEST", glyphTest},
228
+ {"audit", data.StageAudit, "AUDIT", glyphAudit},
229
+ }
230
+
231
+ for _, tt := range tests {
232
+ t.Run(tt.name, func(t *testing.T) {
233
+ task := data.Task{ID: "T1", Column: data.ColumnInProgress, Status: string(data.ColumnInProgress), Stage: tt.stage}
234
+ got := RenderCard(task, 30, false, nil)
235
+ if !strings.Contains(got, tt.label) {
236
+ t.Errorf("RenderCard missing phase label %q", tt.label)
237
+ }
238
+ if !strings.Contains(got, tt.glyph) {
239
+ t.Errorf("RenderCard missing phase glyph %q", tt.glyph)
240
+ }
241
+ if strings.Contains(got, "▶") {
242
+ t.Error("RenderCard should not use generic in_progress glyph when phase is available")
243
+ }
244
+ })
245
+ }
246
+ }
247
+
248
+ func TestRenderCard_doneShowsDoneText(t *testing.T) {
249
+ task := data.Task{ID: "T1", Column: data.ColumnDone, Status: string(data.ColumnDone)}
250
+ got := RenderCard(task, 30, false, nil)
251
+ if !strings.Contains(got, "DONE") {
252
+ t.Error("RenderCard missing DONE phase label")
253
+ }
254
+ }
@@ -28,17 +28,54 @@ func RenderColumn(tasks []data.Task, col data.ColumnType, width, maxHeight, offs
28
28
  if len(tasks) == 0 {
29
29
  lines = append(lines, styles.TaskItem.Render("(empty)"))
30
30
  } else {
31
- limit := visibleColumnTaskLimit(maxHeight)
32
- end := min(offset+limit, len(tasks))
31
+ contentBudget := maxHeight - 2
32
+ if contentBudget < 1 {
33
+ contentBudget = 1
34
+ }
35
+
36
+ reserveAbove := 0
37
+ if offset > 0 {
38
+ reserveAbove = 1
39
+ }
40
+
41
+ type cardEntry struct {
42
+ card string
43
+ lines int
44
+ }
45
+ cardEntries := make([]cardEntry, 0, len(tasks)-offset)
46
+ for i := offset; i < len(tasks); i++ {
47
+ c := RenderCard(tasks[i], inner, focused && i == focusedTask, routerState)
48
+ cardEntries = append(cardEntries, cardEntry{card: c, lines: strings.Count(c, "\n") + 1})
49
+ }
50
+
51
+ usedLines := reserveAbove
52
+ endIdx := 0
53
+ for endIdx < len(cardEntries) {
54
+ needsMore := endIdx < len(cardEntries)-1
55
+ bottomReserve := 0
56
+ if needsMore {
57
+ bottomReserve = 1
58
+ }
59
+ if usedLines+cardEntries[endIdx].lines+bottomReserve > contentBudget {
60
+ break
61
+ }
62
+ usedLines += cardEntries[endIdx].lines
63
+ endIdx++
64
+ }
65
+
66
+ if endIdx == 0 && len(cardEntries) > 0 {
67
+ endIdx = 1
68
+ }
69
+
33
70
  if offset > 0 {
34
71
  lines = append(lines, renderScrollIndicator("↑", offset, "above"))
35
72
  }
36
- for i, t := range tasks[offset:end] {
37
- taskIndex := offset + i
38
- lines = append(lines, RenderCard(t, inner, focused && taskIndex == focusedTask, routerState))
73
+ for i := 0; i < endIdx; i++ {
74
+ lines = append(lines, cardEntries[i].card)
39
75
  }
40
- if end < len(tasks) {
41
- lines = append(lines, renderScrollIndicator("↓", len(tasks)-end, "more"))
76
+ if endIdx < len(cardEntries) {
77
+ remaining := len(tasks) - (offset + endIdx)
78
+ lines = append(lines, renderScrollIndicator("↓", remaining, "more"))
42
79
  }
43
80
  }
44
81
 
@@ -88,9 +125,3 @@ func columnTitle(col data.ColumnType) string {
88
125
  }
89
126
  }
90
127
 
91
- func taskLabel(t data.Task) string {
92
- if t.Title == "" {
93
- return t.ID
94
- }
95
- return fmt.Sprintf("%s %s", t.ID, t.Title)
96
- }
@@ -118,12 +118,15 @@ func TestRenderColumn_viewportShowsScrollIndicators(t *testing.T) {
118
118
  if !strings.Contains(got, "↑ 1 above") {
119
119
  t.Error("RenderColumn missing above indicator")
120
120
  }
121
- if !strings.Contains(got, "↓ 1 more") {
122
- t.Error("RenderColumn missing more indicator")
121
+ if !strings.Contains(got, "↓ 2 more") {
122
+ t.Errorf("RenderColumn missing more indicator, got:\n%s", got)
123
123
  }
124
124
  if strings.Contains(got, "Task one") {
125
125
  t.Error("RenderColumn should not render tasks above viewport")
126
126
  }
127
+ if strings.Contains(got, "Task three") {
128
+ t.Error("RenderColumn should not render tasks that don't fit budget")
129
+ }
127
130
  if strings.Contains(got, "Task four") {
128
131
  t.Error("RenderColumn should not render tasks below viewport")
129
132
  }
@@ -182,51 +182,4 @@ func phaseLabel(s data.ProgressStage) string {
182
182
  }
183
183
  }
184
184
 
185
- func WrapText(s string, width int) []string {
186
- if width < 4 {
187
- width = 4
188
- }
189
- words := strings.Fields(s)
190
- if len(words) == 0 {
191
- return nil
192
- }
193
- lines := []string{}
194
- current := ""
195
- for _, word := range words {
196
- if len([]rune(word)) > width {
197
- if current != "" {
198
- lines = append(lines, current)
199
- current = ""
200
- }
201
- lines = append(lines, SplitLongWord(word, width)...)
202
- continue
203
- }
204
- if current == "" {
205
- current = word
206
- continue
207
- }
208
- if len([]rune(current))+1+len([]rune(word)) <= width {
209
- current += " " + word
210
- continue
211
- }
212
- lines = append(lines, current)
213
- current = word
214
- }
215
- if current != "" {
216
- lines = append(lines, current)
217
- }
218
- return lines
219
- }
220
185
 
221
- func SplitLongWord(word string, width int) []string {
222
- runes := []rune(word)
223
- lines := []string{}
224
- for len(runes) > width {
225
- lines = append(lines, string(runes[:width]))
226
- runes = runes[width:]
227
- }
228
- if len(runes) > 0 {
229
- lines = append(lines, string(runes))
230
- }
231
- return lines
232
- }
@@ -8,33 +8,40 @@ import (
8
8
  )
9
9
 
10
10
  // RenderEpicDetail renders an overlay showing the content of an E##-Detail.md file.
11
- func RenderEpicDetail(epicSlug, content string, overlayW, maxHeight, offset int) string {
11
+ func RenderEpicDetail(epicSlug, content string, overlayW, maxHeight, offset int, tab int) string {
12
12
  inner := overlayW - detailBorderPad
13
13
  if inner < 4 {
14
14
  inner = 4
15
15
  }
16
16
 
17
+ tabIndicator := renderTabIndicator(tab, inner)
17
18
  lines := []string{
18
19
  styles.EpicTitleFocused.Render("EPIC DETAIL"),
19
- strings.Repeat("─", inner),
20
+ tabIndicator,
20
21
  }
21
22
 
22
23
  body := epicDetailBody(content, inner)
23
- body = append(body, "", styles.CardMeta.Render("esc:close"))
24
- lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead, offset)...)
24
+ body = append(body, "", styles.CardMeta.Render("1:Detail 2:Audit esc:close"))
25
+ lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead-1, offset)...)
25
26
 
26
27
  return styles.EpicDetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
27
28
  }
28
29
 
29
- // epicDetailBody parses markdown content into display lines, stripping frontmatter.
30
- func epicDetailBody(content string, width int) []string {
31
- if strings.TrimSpace(content) == "" || content == "(no detail available)" {
32
- return []string{styles.CardMeta.Render("(no detail available)")}
30
+ func renderTabIndicator(tab int, width int) string {
31
+ var detail, audit string
32
+ if tab == 0 {
33
+ detail = styles.EpicItemFocused.Render("DETAIL [1]")
34
+ audit = styles.CardMeta.Render("AUDIT [2]")
35
+ } else {
36
+ detail = styles.CardMeta.Render("DETAIL [1]")
37
+ audit = styles.EpicItemFocused.Render("AUDIT [2]")
33
38
  }
39
+ return detail + styles.CardMeta.Render(" │ ") + audit
40
+ }
34
41
 
42
+ // stripFrontmatter removes YAML frontmatter (between leading --- markers) from content.
43
+ func stripFrontmatter(content string) []string {
35
44
  lines := strings.Split(content, "\n")
36
-
37
- // Strip YAML frontmatter between leading --- markers.
38
45
  start := 0
39
46
  if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" {
40
47
  for i := 1; i < len(lines); i++ {
@@ -44,7 +51,16 @@ func epicDetailBody(content string, width int) []string {
44
51
  }
45
52
  }
46
53
  }
47
- lines = lines[start:]
54
+ return lines[start:]
55
+ }
56
+
57
+ // epicDetailBody parses markdown content into display lines, stripping frontmatter.
58
+ func epicDetailBody(content string, width int) []string {
59
+ if strings.TrimSpace(content) == "" || content == "(no detail available)" {
60
+ return []string{styles.CardMeta.Render("(no detail available)")}
61
+ }
62
+
63
+ lines := stripFrontmatter(content)
48
64
 
49
65
  var body []string
50
66
  skip := false
@@ -77,11 +93,87 @@ func epicDetailBody(content string, width int) []string {
77
93
  return body
78
94
  }
79
95
 
96
+ // RenderEpicAuditTab renders an overlay showing audit findings from an E##-Audit.md file.
97
+ func RenderEpicAuditTab(epicSlug, content string, overlayW, maxHeight, offset int, tab int) string {
98
+ inner := overlayW - detailBorderPad
99
+ if inner < 4 {
100
+ inner = 4
101
+ }
102
+
103
+ tabIndicator := renderTabIndicator(tab, inner)
104
+ lines := []string{
105
+ styles.GlyphAudit.Render("EPIC AUDIT"),
106
+ tabIndicator,
107
+ }
108
+
109
+ body := epicAuditBody(content, inner)
110
+ body = append(body, "", styles.CardMeta.Render("1:Detail 2:Audit esc:close"))
111
+ lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead-1, offset)...)
112
+
113
+ return styles.EpicDetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
114
+ }
115
+
116
+ var epicAuditHiddenSectionHeadings = map[string]struct{}{
117
+ "12. Distribution & build": {},
118
+ "Acceptance Criteria": {},
119
+ "Architectural notes": {},
120
+ "Boundaries": {},
121
+ "Context Files": {},
122
+ "Implemented As": {},
123
+ "Implemented as": {},
124
+ "Implementation Plan": {},
125
+ "Manual audit override": {},
126
+ "Proposed Changes": {},
127
+ "Quality Review": {},
128
+ "With": {},
129
+ }
130
+
131
+ func epicAuditBody(content string, width int) []string {
132
+ if strings.TrimSpace(content) == "" || content == "(no audit available)" {
133
+ return []string{styles.CardMeta.Render("(no audit available)")}
134
+ }
135
+
136
+ lines := stripFrontmatter(content)
137
+
138
+ var body []string
139
+ inHiddenSection := false
140
+
141
+ for _, line := range lines {
142
+ trimmed := strings.TrimRight(line, " \t\r")
143
+ switch {
144
+ case strings.HasPrefix(trimmed, "## "):
145
+ sectionName := strings.TrimPrefix(trimmed, "## ")
146
+ _, inHiddenSection = epicAuditHiddenSectionHeadings[sectionName]
147
+ if !inHiddenSection {
148
+ body = append(body, "", styles.EpicItemFocused.Render(sectionName))
149
+ }
150
+ case inHiddenSection:
151
+ case strings.HasPrefix(trimmed, "### "):
152
+ body = append(body, styles.EpicItemFocused.Render(strings.TrimPrefix(trimmed, "### ")))
153
+ case strings.HasPrefix(trimmed, "- [x] ") || strings.HasPrefix(trimmed, "- [X] "):
154
+ text := strings.TrimPrefix(strings.TrimPrefix(trimmed, "- [x] "), "- [X] ")
155
+ body = append(body, renderChecklistSentences(text, "[x] ", width, styles.TagDone)...)
156
+ case strings.HasPrefix(trimmed, "- [ ] "):
157
+ text := strings.TrimPrefix(trimmed, "- [ ] ")
158
+ body = append(body, renderChecklistSentences(text, "[ ] ", width, styles.CardMeta)...)
159
+ case strings.HasPrefix(trimmed, "- "):
160
+ body = append(body, styles.CardMeta.Render("• "+strings.TrimPrefix(trimmed, "- ")))
161
+ case trimmed == "":
162
+ body = append(body, "")
163
+ default:
164
+ for _, wrapped := range WrapText(trimmed, width) {
165
+ body = append(body, styles.CardMeta.Render(wrapped))
166
+ }
167
+ }
168
+ }
169
+ return body
170
+ }
171
+
80
172
  const epicActiveMarker = "►"
81
173
 
82
174
  // RenderEpicSidebar renders the fixed left sidebar listing epics with active indicator.
83
175
  // If epics is empty and selected is non-empty, selected is shown as the sole entry.
84
- func RenderEpicSidebar(epics []string, selected string, width int, focus bool, cursor int, status map[string]string) string {
176
+ func RenderEpicSidebar(epics []string, selected string, width int, focus bool, cursor int, status map[string]string, maxHeight int) string {
85
177
  inner := width - epicPanelOverhead
86
178
  if inner < 2 {
87
179
  inner = 2
@@ -114,6 +206,20 @@ func RenderEpicSidebar(epics []string, selected string, width int, focus bool, c
114
206
  if len(list) == 0 {
115
207
  lines = append(lines, styles.TaskItem.Render("(none)"))
116
208
  }
209
+ if maxHeight > 0 && len(lines) > maxHeight {
210
+ items := lines[2:]
211
+ available := maxHeight - 3
212
+ if available < 1 {
213
+ available = 1
214
+ }
215
+ clipped := make([]string, 0, maxHeight)
216
+ clipped = append(clipped, lines[0], lines[1])
217
+ clipped = append(clipped, items[:min(available, len(items))]...)
218
+ if len(items) > available {
219
+ clipped = append(clipped, renderScrollIndicator("↓", len(items)-available, "more"))
220
+ }
221
+ lines = clipped
222
+ }
117
223
  style := styles.EpicPanel.Width(width)
118
224
  if focus && len(epics) > 0 {
119
225
  style = styles.EpicPanelFocused.Width(width)
@@ -154,13 +260,3 @@ func RenderEpicDropdown(epics []string, cursor int, width int) string {
154
260
  lines = append(lines, "", styles.CardMeta.Render("↑↓:nav enter:select esc:cancel"))
155
261
  return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
156
262
  }
157
-
158
- // epicIndex returns the index of selected in epics, or 0 if not found.
159
- func epicIndex(epics []string, selected string) int {
160
- for i, e := range epics {
161
- if e == selected {
162
- return i
163
- }
164
- }
165
- return 0
166
- }