savepoint 1.0.0 → 1.0.2

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 (136) hide show
  1. package/.claude/settings.local.json +8 -1
  2. package/.savepoint/Design.md +26 -17
  3. package/.savepoint/audit/v1/E01/proposals.md +168 -0
  4. package/.savepoint/audit/v1/E01/snapshot.md +78 -0
  5. package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/proposals.md +7 -7
  6. package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/snapshot.md +2 -2
  7. package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/AGENTS.md +5 -5
  8. package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/proposals.md +20 -20
  9. package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/snapshot.md +1 -1
  10. package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/proposals.md +11 -11
  11. package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/snapshot.md +1 -1
  12. package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/proposals.md +14 -14
  13. package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/snapshot.md +1 -1
  14. package/.savepoint/audit/{E05-init-command → v1/E05-init-command}/snapshot.md +1 -1
  15. package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/proposals.md +4 -4
  16. package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/snapshot.md +1 -1
  17. package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +130 -0
  18. package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +84 -0
  19. package/.savepoint/audit/{E07-audit-pipeline → v1/E07-audit-pipeline}/snapshot.md +6 -6
  20. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/proposals.md +114 -0
  21. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +41 -0
  22. package/.savepoint/audit/v1.1/E04-epic-navigation/proposals.md +156 -0
  23. package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +48 -0
  24. package/.savepoint/config.yml +3 -3
  25. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
  26. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
  27. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
  28. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
  29. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/E06-Detail.md +62 -0
  30. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
  31. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
  32. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
  33. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +7 -7
  34. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +10 -8
  35. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +16 -9
  36. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +27 -22
  37. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/E01-Detail.md +40 -0
  38. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-next-activity-header.md +56 -0
  39. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +38 -0
  40. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +28 -0
  41. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +51 -0
  42. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +45 -0
  43. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +68 -0
  44. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
  45. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Detail.md +49 -0
  46. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +37 -0
  47. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +38 -0
  48. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +36 -0
  49. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +59 -0
  50. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +32 -0
  51. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
  52. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
  53. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
  54. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
  55. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
  56. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
  57. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
  58. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
  59. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
  60. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +45 -0
  61. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +34 -0
  62. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +30 -0
  63. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +33 -0
  64. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +88 -0
  65. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +30 -0
  66. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +46 -0
  67. package/.savepoint/releases/v1.1/v1.1-PRD.md +79 -0
  68. package/.savepoint/router.md +33 -105
  69. package/.savepoint/visual-identity.md +4 -3
  70. package/AGENTS.md +56 -113
  71. package/Makefile +19 -3
  72. package/README.md +7 -6
  73. package/agent-skills/savepoint-audit/SKILL.md +6 -6
  74. package/agent-skills/savepoint-build-task/SKILL.md +2 -2
  75. package/agent-skills/savepoint-create-plan/SKILL.md +3 -3
  76. package/agent-skills/savepoint-create-task/SKILL.md +2 -2
  77. package/agent-skills/savepoint-draft-prd/SKILL.md +1 -1
  78. package/agent-skills/savepoint-system-design/SKILL.md +1 -1
  79. package/go.mod +4 -1
  80. package/go.sum +2 -0
  81. package/internal/board/board.go +66 -14
  82. package/internal/board/board_test.go +124 -0
  83. package/internal/board/card.go +40 -3
  84. package/internal/board/card_test.go +121 -14
  85. package/internal/board/column.go +40 -5
  86. package/internal/board/column_test.go +65 -10
  87. package/internal/board/detail.go +115 -23
  88. package/internal/board/detail_test.go +132 -25
  89. package/internal/board/epic_panel.go +105 -8
  90. package/internal/board/epic_panel_test.go +343 -5
  91. package/internal/board/layout.go +12 -2
  92. package/internal/board/layout_test.go +17 -0
  93. package/internal/board/model.go +146 -23
  94. package/internal/board/render_policy_test.go +77 -0
  95. package/internal/board/status.go +23 -0
  96. package/internal/board/update.go +300 -9
  97. package/internal/board/update_test.go +166 -0
  98. package/internal/board/view.go +141 -17
  99. package/internal/board/view_test.go +161 -3
  100. package/internal/board/watch.go +100 -0
  101. package/internal/buildtool/main.go +219 -0
  102. package/internal/data/parser.go +39 -1
  103. package/internal/data/parser_test.go +43 -2
  104. package/internal/data/task.go +22 -2
  105. package/internal/styles/palette.go +9 -7
  106. package/internal/styles/styles.go +42 -25
  107. package/main.go +9 -0
  108. package/package.json +5 -4
  109. package/savepoint +0 -0
  110. package/savepoint.exe +0 -0
  111. package/templates/project/.savepoint/router.md +6 -5
  112. package/templates/project/AGENTS.md +47 -101
  113. package/templates/prompts/audit-reconciliation.prompt.md +6 -6
  114. package/templates/prompts/epic-design.prompt.md +3 -3
  115. package/templates/prompts/task-breakdown.prompt.md +1 -1
  116. package/templates/prompts/task-building.prompt.md +1 -1
  117. package/templates/prompts/task-planning.prompt.md +1 -1
  118. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +0 -42
  119. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +0 -26
  120. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -35
  121. package/main.exe +0 -0
  122. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/Design.md +0 -0
  123. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/epic-Design.md +0 -0
  124. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/quality-review.md +0 -0
  125. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/snapshot.md +0 -0
  126. /package/.savepoint/audit/{E02-data-model → v1/E02-data-model}/snapshot.md +0 -0
  127. /package/.savepoint/audit/{E03-cli-foundation → v1/E03-cli-foundation}/snapshot.md +0 -0
  128. /package/.savepoint/audit/{E04-templates-and-prompts → v1/E04-templates-and-prompts}/snapshot.md +0 -0
  129. /package/.savepoint/audit/{E06-tui-board → v1/E06-tui-board}/snapshot.md +0 -0
  130. /package/.savepoint/audit/{E08-board-workflow-cleanup → v1/E08-board-workflow-cleanup}/snapshot.md +0 -0
  131. /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
  132. /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
  133. /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
  134. /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
  135. /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
  136. /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
@@ -0,0 +1,219 @@
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
+ "path/filepath"
13
+ "runtime"
14
+ )
15
+
16
+ type target struct {
17
+ os string
18
+ arch string
19
+ }
20
+
21
+ var targets = []target{
22
+ {os: "linux", arch: "amd64"},
23
+ {os: "linux", arch: "arm64"},
24
+ {os: "darwin", arch: "amd64"},
25
+ {os: "darwin", arch: "arm64"},
26
+ }
27
+
28
+ var versionOverride string
29
+
30
+ func main() {
31
+ if err := run(os.Args[1:]); err != nil {
32
+ fmt.Fprintln(os.Stderr, err)
33
+ os.Exit(1)
34
+ }
35
+ }
36
+
37
+ func run(args []string) error {
38
+ flags := flag.NewFlagSet("buildtool", flag.ContinueOnError)
39
+ flags.StringVar(&versionOverride, "version", "", "version to inject into the binary")
40
+ if err := flags.Parse(args); err != nil {
41
+ return err
42
+ }
43
+ if flags.NArg() != 1 {
44
+ return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-all|dist|smoke-test>")
45
+ }
46
+
47
+ switch flags.Arg(0) {
48
+ case "build":
49
+ return buildLocal()
50
+ case "clean":
51
+ return clean()
52
+ case "build-linux":
53
+ return buildMatching("linux")
54
+ case "build-darwin":
55
+ return buildMatching("darwin")
56
+ case "build-all":
57
+ return buildAll()
58
+ case "dist":
59
+ return dist()
60
+ case "smoke-test":
61
+ return smokeTest()
62
+ default:
63
+ return fmt.Errorf("unknown build target %q", flags.Arg(0))
64
+ }
65
+ }
66
+
67
+ func buildLocal() error {
68
+ return runGoBuild(localExecutable(), runtime.GOOS, runtime.GOARCH)
69
+ }
70
+
71
+ func clean() error {
72
+ for _, path := range []string{"savepoint", "savepoint.exe", "dist"} {
73
+ if err := os.RemoveAll(path); err != nil {
74
+ return fmt.Errorf("clean %s: %w", path, err)
75
+ }
76
+ }
77
+ return nil
78
+ }
79
+
80
+ func buildMatching(goos string) error {
81
+ for _, target := range targets {
82
+ if target.os != goos {
83
+ continue
84
+ }
85
+ if err := buildTarget(target); err != nil {
86
+ return err
87
+ }
88
+ }
89
+ return nil
90
+ }
91
+
92
+ func buildAll() error {
93
+ for _, target := range targets {
94
+ if err := buildTarget(target); err != nil {
95
+ return err
96
+ }
97
+ }
98
+ return nil
99
+ }
100
+
101
+ func buildTarget(target target) error {
102
+ output := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
103
+ return runGoBuild(output, target.os, target.arch)
104
+ }
105
+
106
+ func runGoBuild(output, goos, goarch string) error {
107
+ if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil && filepath.Dir(output) != "." {
108
+ return fmt.Errorf("create output dir: %w", err)
109
+ }
110
+
111
+ cmd := exec.Command("go", "build", "-ldflags", "-X main.version="+version(), "-o", output, "main.go")
112
+ cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
113
+ cmd.Stdout = os.Stdout
114
+ cmd.Stderr = os.Stderr
115
+ if err := cmd.Run(); err != nil {
116
+ return fmt.Errorf("build %s/%s: %w", goos, goarch, err)
117
+ }
118
+ return nil
119
+ }
120
+
121
+ func dist() error {
122
+ if err := buildAll(); err != nil {
123
+ return err
124
+ }
125
+ for _, target := range targets {
126
+ name := "savepoint-" + version() + "-" + target.os + "-" + target.arch + ".tar.gz"
127
+ source := filepath.Join("dist", target.os+"-"+target.arch, "savepoint")
128
+ archive := filepath.Join("dist", name)
129
+ if err := writeTarGz(archive, source, "savepoint"); err != nil {
130
+ return err
131
+ }
132
+ }
133
+ return nil
134
+ }
135
+
136
+ func writeTarGz(archivePath, sourcePath, archiveName string) error {
137
+ source, err := os.Open(sourcePath)
138
+ if err != nil {
139
+ return fmt.Errorf("open artifact source: %w", err)
140
+ }
141
+ defer source.Close()
142
+
143
+ info, err := source.Stat()
144
+ if err != nil {
145
+ return fmt.Errorf("stat artifact source: %w", err)
146
+ }
147
+
148
+ archive, err := os.Create(archivePath)
149
+ if err != nil {
150
+ return fmt.Errorf("create archive: %w", err)
151
+ }
152
+ defer archive.Close()
153
+
154
+ gzipWriter := gzip.NewWriter(archive)
155
+ defer gzipWriter.Close()
156
+
157
+ tarWriter := tar.NewWriter(gzipWriter)
158
+ defer tarWriter.Close()
159
+
160
+ header, err := tar.FileInfoHeader(info, "")
161
+ if err != nil {
162
+ return fmt.Errorf("create archive header: %w", err)
163
+ }
164
+ header.Name = archiveName
165
+ if err := tarWriter.WriteHeader(header); err != nil {
166
+ return fmt.Errorf("write archive header: %w", err)
167
+ }
168
+ if _, err := io.Copy(tarWriter, source); err != nil {
169
+ return fmt.Errorf("write archive content: %w", err)
170
+ }
171
+ return nil
172
+ }
173
+
174
+ func smokeTest() error {
175
+ if err := buildLocal(); err != nil {
176
+ return err
177
+ }
178
+ cmd := exec.Command("."+string(os.PathSeparator)+localExecutable(), "--version")
179
+ cmd.Stdout = os.Stdout
180
+ cmd.Stderr = os.Stderr
181
+ if err := cmd.Run(); err != nil {
182
+ return fmt.Errorf("smoke test: %w", err)
183
+ }
184
+ fmt.Println("smoke test passed")
185
+ return nil
186
+ }
187
+
188
+ func version() string {
189
+ if versionOverride != "" {
190
+ return versionOverride
191
+ }
192
+ if value := os.Getenv("VERSION"); value != "" {
193
+ return value
194
+ }
195
+
196
+ cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
197
+ output, err := cmd.Output()
198
+ if err == nil && len(output) > 0 {
199
+ return string(trimSpace(output))
200
+ }
201
+ return "v0.0.0"
202
+ }
203
+
204
+ func localExecutable() string {
205
+ if runtime.GOOS == "windows" {
206
+ return "savepoint.exe"
207
+ }
208
+ return "savepoint"
209
+ }
210
+
211
+ func trimSpace(value []byte) []byte {
212
+ for len(value) > 0 && (value[len(value)-1] == '\n' || value[len(value)-1] == '\r' || value[len(value)-1] == '\t' || value[len(value)-1] == ' ') {
213
+ value = value[:len(value)-1]
214
+ }
215
+ for len(value) > 0 && (value[0] == '\n' || value[0] == '\r' || value[0] == '\t' || value[0] == ' ') {
216
+ value = value[1:]
217
+ }
218
+ return value
219
+ }
@@ -51,7 +51,7 @@ func (p *Parser) ParseTaskFile(path string, content string) (*Task, error) {
51
51
  Points: fields.Points,
52
52
  Tags: fields.Tags,
53
53
  Acceptance: firstList(fields.Acceptance, extractChecklistSection(content, "## Acceptance Criteria")),
54
- Checklist: extractChecklistSection(content, "## Implementation Plan"),
54
+ Checklist: extractChecklistItems(content, "## Implementation Plan"),
55
55
  Notes: fields.Notes,
56
56
  DependsOn: fields.DependsOn,
57
57
  Progress: fields.Progress,
@@ -162,6 +162,44 @@ func firstList(values ...[]string) []string {
162
162
  return nil
163
163
  }
164
164
 
165
+ func extractChecklistItems(content, heading string) []CheckItem {
166
+ normalized := strings.ReplaceAll(content, "\r\n", "\n")
167
+ start := strings.Index(normalized, heading)
168
+ if start == -1 {
169
+ return nil
170
+ }
171
+
172
+ section := normalized[start+len(heading):]
173
+ if next := strings.Index(section, "\n## "); next != -1 {
174
+ section = section[:next]
175
+ }
176
+
177
+ items := []CheckItem{}
178
+ var current *CheckItem
179
+ for _, line := range strings.Split(section, "\n") {
180
+ trimmed := strings.TrimSpace(line)
181
+ if strings.HasPrefix(trimmed, "- [x] ") {
182
+ items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[6:]), Done: true})
183
+ current = &items[len(items)-1]
184
+ continue
185
+ }
186
+ if strings.HasPrefix(trimmed, "- [ ] ") {
187
+ items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[6:]), Done: false})
188
+ current = &items[len(items)-1]
189
+ continue
190
+ }
191
+ if strings.HasPrefix(trimmed, "- ") {
192
+ items = append(items, CheckItem{Text: strings.TrimSpace(trimmed[2:]), Done: false})
193
+ current = &items[len(items)-1]
194
+ continue
195
+ }
196
+ if trimmed != "" && current != nil {
197
+ current.Text = strings.TrimSpace(current.Text + " " + trimmed)
198
+ }
199
+ }
200
+ return items
201
+ }
202
+
165
203
  func extractChecklistSection(content, heading string) []string {
166
204
  normalized := strings.ReplaceAll(content, "\r\n", "\n")
167
205
  start := strings.Index(normalized, heading)
@@ -210,7 +210,48 @@ Notes here.`
210
210
  if len(task.Acceptance) != 2 || task.Acceptance[0] != "First criterion." || task.Acceptance[1] != "Second criterion." {
211
211
  t.Errorf("Task.Acceptance = %v, want markdown criteria", task.Acceptance)
212
212
  }
213
- if len(task.Checklist) != 2 || task.Checklist[0] != "First checklist item." || task.Checklist[1] != "Second checklist item." {
214
- t.Errorf("Task.Checklist = %v, want markdown checklist", task.Checklist)
213
+ if len(task.Checklist) != 2 {
214
+ t.Fatalf("Task.Checklist len = %d, want 2", len(task.Checklist))
215
+ }
216
+ if task.Checklist[0].Text != "First checklist item." || task.Checklist[0].Done {
217
+ t.Errorf("Task.Checklist[0] = %+v, want {Text:\"First checklist item.\", Done:false}", task.Checklist[0])
218
+ }
219
+ if task.Checklist[1].Text != "Second checklist item." || !task.Checklist[1].Done {
220
+ t.Errorf("Task.Checklist[1] = %+v, want {Text:\"Second checklist item.\", Done:true}", task.Checklist[1])
221
+ }
222
+ }
223
+
224
+ func TestParseTaskFile_joinsHardWrappedChecklistItems(t *testing.T) {
225
+ p := NewParser()
226
+ content := `---
227
+ id: E06/T001
228
+ status: planned
229
+ objective: "Style the board"
230
+ ---
231
+
232
+ # Task
233
+
234
+ ## Implementation Plan
235
+
236
+ - [ ] First sentence spans across a hard markdown line break
237
+ before it ends. Second sentence stays in the same checklist item.
238
+ - [x] Already checked sentence wraps
239
+ without becoming another checklist item.
240
+ `
241
+
242
+ task, err := p.ParseTaskFile("test.md", content)
243
+ if err != nil {
244
+ t.Fatalf("ParseTaskFile() error = %v", err)
245
+ }
246
+ if len(task.Checklist) != 2 {
247
+ t.Fatalf("Task.Checklist len = %d, want 2", len(task.Checklist))
248
+ }
249
+ wantFirst := "First sentence spans across a hard markdown line break before it ends. Second sentence stays in the same checklist item."
250
+ if task.Checklist[0].Text != wantFirst || task.Checklist[0].Done {
251
+ t.Errorf("Task.Checklist[0] = %+v, want text %q and Done=false", task.Checklist[0], wantFirst)
252
+ }
253
+ wantSecond := "Already checked sentence wraps without becoming another checklist item."
254
+ if task.Checklist[1].Text != wantSecond || !task.Checklist[1].Done {
255
+ t.Errorf("Task.Checklist[1] = %+v, want text %q and Done=true", task.Checklist[1], wantSecond)
215
256
  }
216
257
  }
@@ -1,6 +1,14 @@
1
1
  package data
2
2
 
3
- import "fmt"
3
+ import (
4
+ "fmt"
5
+ "time"
6
+ )
7
+
8
+ type CheckItem struct {
9
+ Text string
10
+ Done bool
11
+ }
4
12
 
5
13
  type ColumnType string
6
14
 
@@ -18,6 +26,15 @@ const (
18
26
  StageAudit ProgressStage = "audit"
19
27
  )
20
28
 
29
+ type TaskStatus string
30
+
31
+ const (
32
+ StatusPlanned TaskStatus = "planned"
33
+ StatusInProgress TaskStatus = "in_progress"
34
+ StatusDone TaskStatus = "done"
35
+ StatusAudited TaskStatus = "audited"
36
+ )
37
+
21
38
  type Progress struct {
22
39
  Stage ProgressStage `yaml:"stage"`
23
40
  Started bool `yaml:"started"`
@@ -27,6 +44,7 @@ type Task struct {
27
44
  ID string `yaml:"id"`
28
45
  Title string `yaml:"title"`
29
46
  Description string `yaml:"description,omitempty"`
47
+ Status string `yaml:"status,omitempty"`
30
48
  Epic string `yaml:"epic"`
31
49
  Release string `yaml:"release"`
32
50
  Column ColumnType `yaml:"column"`
@@ -35,10 +53,12 @@ type Task struct {
35
53
  Points int `yaml:"points,omitempty"`
36
54
  Tags []string `yaml:"tags,omitempty"`
37
55
  Acceptance []string `yaml:"acceptance,omitempty"`
38
- Checklist []string `yaml:"checklist,omitempty"`
56
+ Checklist []CheckItem `yaml:"checklist,omitempty"`
39
57
  Notes string `yaml:"notes,omitempty"`
40
58
  DependsOn []string `yaml:"depends_on,omitempty"`
41
59
  Progress Progress `yaml:"progress,omitempty"`
60
+ Path string `yaml:"-"`
61
+ Mtime time.Time `yaml:"-"`
42
62
  }
43
63
 
44
64
  func (t Task) String() string {
@@ -1,10 +1,12 @@
1
1
  package styles
2
2
 
3
- // Truecolor hex constants (Atari-Noir palette)
3
+ // Truecolor hex constants (Atari-Noir palette).
4
+ // Background, Surface, and Surface2 intentionally share one black value so the
5
+ // terminal stays visually flat; hierarchy comes from spacing, dividers, and accents.
4
6
  const (
5
- Background = "#121212"
6
- Surface = "#0D0D0D"
7
- Surface2 = "#0F0F0F"
7
+ Background = "#000000"
8
+ Surface = "#000000"
9
+ Surface2 = "#000000"
8
10
  Border = "#1A1A1A"
9
11
  BorderSubtle = "#222222"
10
12
  PrimaryText = "#F0E6DA"
@@ -15,9 +17,9 @@ const (
15
17
 
16
18
  // 256-color (ANSI256) fallbacks — nearest terminal approximations
17
19
  const (
18
- Background256 = "233"
19
- Surface256 = "232"
20
- Surface2256 = "232"
20
+ Background256 = "16"
21
+ Surface256 = "16"
22
+ Surface2256 = "16"
21
23
  Border256 = "234"
22
24
  BorderSubtle256 = "235"
23
25
  PrimaryText256 = "230"
@@ -10,13 +10,15 @@ var (
10
10
  clrOrange = color(AtariOrange, AtariOrange256, AtariOrange16)
11
11
  clrText = color(PrimaryText, PrimaryText256, PrimaryText16)
12
12
  clrBorder = color(BorderSubtle, BorderSubtle256, BorderSubtle16)
13
- clrSurface = color(Surface2, Surface2256, Surface216) // #0F0F0F
14
- clrSurfaceDark = color(Surface, Surface256, Surface16) // #0D0D0D
13
+ clrSurface = color(Surface2, Surface2256, Surface216) // intentionally black
14
+ clrSurfaceDark = color(Surface, Surface256, Surface16) // intentionally black
15
15
  clrGreen = color(NPPGreen, NPPGreen256, NPPGreen16)
16
16
  clrPurple = color(VibePurple, VibePurple256, VibePurple16)
17
17
  clrDim = color(Dim, Dim256, Dim16)
18
18
  )
19
19
 
20
+ var boxBorder = lipgloss.NormalBorder()
21
+
20
22
  var (
21
23
  HeaderIcon = lipgloss.NewStyle().
22
24
  Foreground(clrOrange).
@@ -29,25 +31,21 @@ var (
29
31
  Foreground(clrBorder)
30
32
 
31
33
  HeaderFrame = lipgloss.NewStyle().
32
- BorderStyle(lipgloss.RoundedBorder()).
33
- BorderForeground(clrBorder).
34
34
  Padding(1, 1)
35
35
 
36
- BoardFrame = lipgloss.NewStyle().
37
- BorderStyle(lipgloss.RoundedBorder()).
38
- BorderForeground(clrBorder).
39
- Padding(0, 1)
36
+ BoardFrame = lipgloss.NewStyle()
40
37
 
41
38
  Column = lipgloss.NewStyle().
42
- BorderStyle(lipgloss.RoundedBorder()).
43
- BorderForeground(clrBorder).
44
- Background(clrSurfaceDark).
45
39
  Padding(0, 1)
46
40
 
41
+ ColumnUnfocused = lipgloss.NewStyle().
42
+ BorderStyle(boxBorder).
43
+ BorderForeground(clrBorder).
44
+ Padding(0, 1)
45
+
47
46
  ColumnFocused = lipgloss.NewStyle().
48
- BorderStyle(lipgloss.RoundedBorder()).
47
+ BorderStyle(boxBorder).
49
48
  BorderForeground(clrOrange).
50
- Background(clrSurfaceDark).
51
49
  Padding(0, 1)
52
50
 
53
51
  ColumnTitle = lipgloss.NewStyle().
@@ -65,38 +63,52 @@ var (
65
63
  Foreground(clrOrange)
66
64
 
67
65
  StatusBar = lipgloss.NewStyle().
68
- Foreground(clrText).
69
- Background(clrSurface)
66
+ Foreground(clrText)
70
67
 
71
68
  EpicPanel = lipgloss.NewStyle().
72
- BorderStyle(lipgloss.RoundedBorder()).
73
- BorderForeground(clrPurple).
74
- Background(clrSurface).
69
+ BorderStyle(boxBorder).
70
+ BorderForeground(clrBorder).
75
71
  Padding(0, 1)
76
72
 
73
+ EpicItemFocused = lipgloss.NewStyle().
74
+ Foreground(clrPurple)
75
+
76
+ EpicTitleFocused = lipgloss.NewStyle().
77
+ Foreground(clrPurple).
78
+ Bold(true)
79
+
80
+ EpicPanelFocused = lipgloss.NewStyle().
81
+ BorderStyle(boxBorder).
82
+ BorderForeground(clrPurple).
83
+ Padding(0, 1)
84
+
77
85
  Card = lipgloss.NewStyle().
78
- BorderStyle(lipgloss.RoundedBorder()).
79
- BorderForeground(clrBorder).
80
- Background(clrSurface).
81
86
  Padding(0, 1)
82
87
 
83
88
  CardFocused = lipgloss.NewStyle().
84
- BorderStyle(lipgloss.RoundedBorder()).
89
+ BorderStyle(boxBorder).
85
90
  BorderForeground(clrOrange).
86
- Background(clrSurface).
87
91
  Padding(0, 1)
88
92
 
89
- CardMeta = lipgloss.NewStyle().Foreground(clrDim)
93
+ CardMeta = lipgloss.NewStyle().Foreground(clrDim)
94
+ ScrollIndicator = lipgloss.NewStyle().
95
+ Foreground(clrDim).
96
+ Faint(true)
90
97
 
91
98
  GlyphBuild = lipgloss.NewStyle().Foreground(clrOrange)
92
99
  GlyphTest = lipgloss.NewStyle().Foreground(clrGreen)
93
100
  GlyphAudit = lipgloss.NewStyle().Foreground(clrPurple)
94
101
 
95
102
  DetailOverlay = lipgloss.NewStyle().
96
- BorderStyle(lipgloss.RoundedBorder()).
103
+ BorderStyle(boxBorder).
97
104
  BorderForeground(clrOrange).
98
105
  Padding(0, 1)
99
106
 
107
+ EpicDetailOverlay = lipgloss.NewStyle().
108
+ BorderStyle(boxBorder).
109
+ BorderForeground(clrPurple).
110
+ Padding(0, 1)
111
+
100
112
  // Footer phase styles
101
113
  FooterPhasePlan = lipgloss.NewStyle().
102
114
  Foreground(clrPurple).
@@ -116,6 +128,11 @@ var (
116
128
  FooterHints = lipgloss.NewStyle().
117
129
  Foreground(clrDim)
118
130
 
131
+ HeaderRight = lipgloss.NewStyle().
132
+ Foreground(clrDim)
133
+
134
+ RootLine = lipgloss.NewStyle()
135
+
119
136
  // Tag styles for semantic encoding
120
137
  TagDone = lipgloss.NewStyle().Foreground(clrGreen)
121
138
  TagAI = lipgloss.NewStyle().Foreground(clrPurple)
package/main.go CHANGED
@@ -1,10 +1,19 @@
1
1
  package main
2
2
 
3
3
  import (
4
+ "fmt"
5
+ "os"
6
+
4
7
  "github.com/opencode/savepoint/internal/board"
5
8
  )
6
9
 
10
+ var version = "dev"
11
+
7
12
  func main() {
13
+ if len(os.Args) > 1 && os.Args[1] == "--version" {
14
+ fmt.Println(version)
15
+ os.Exit(0)
16
+ }
8
17
  if err := board.Run(); err != nil {
9
18
  panic(err)
10
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "savepoint",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "It’s a simple, file-based state machine and cinematic Terminal UI (TUI) designed to force you—and your agent (Claude, Cursor, Aider, Gemini)—to slow down, write down what you're actually building, and check your work before moving on.",
5
5
  "keywords": [
6
6
  "board",
@@ -17,9 +17,10 @@
17
17
  },
18
18
  "license": "MIT",
19
19
  "author": "anipatke",
20
- "type": "Go",
21
- "main": "main.go",
20
+ "bin": {
21
+ "savepoint": "./savepoint"
22
+ },
22
23
  "scripts": {
23
24
  "test": "savepoint init"
24
25
  }
25
- }
26
+ }
package/savepoint CHANGED
Binary file
package/savepoint.exe CHANGED
Binary file
@@ -9,7 +9,7 @@ This file routes the agent based on the project's current state. Read this whene
9
9
  3. The active epic Design
10
10
  4. The active task file, when a task is selected
11
11
 
12
- Read `.savepoint/PRD.md` only for project vision changes. Read `.savepoint/Design.md` only for architecture changes or audit closeout. Read `.savepoint/releases/v{{RELEASE_NUMBER}}/PRD.md` only for release planning or epic-order questions.
12
+ Read `.savepoint/PRD.md` only for project vision changes. Read `.savepoint/Design.md` only for architecture changes or audit closeout. Read `.savepoint/releases/{release}/{release}-PRD.md` only for release planning or epic-order questions.
13
13
 
14
14
  **Conditional read (token discipline):** if your active task touches **Ink/TUI implementation**, also read `agent-skills/ink-tui-design/SKILL.md` after Design.md as the execution guide. If it touches **TUI rendering, theme, or visual design**, also read `.savepoint/visual-identity.md` as the visual guardrails. Otherwise skip the extra files — they are tokens you do not need.
15
15
 
@@ -30,7 +30,7 @@ If the user explicitly asks you to audit an epic, perform the audit for that epi
30
30
  Persist the audit artifacts before replying:
31
31
 
32
32
  - Ensure `.savepoint/audit/{E##-epic}/snapshot.md` exists. Create a manual snapshot once if needed.
33
- - Write the proposal bundle to `.savepoint/audit/{E##-epic}/proposals.md`.
33
+ - Write the proposal bundle to `.savepoint/audit/{release}/{E##-epic}/proposals.md`.
34
34
  - Do not stop at chat-only findings. The filesystem artifact is part of the task output.
35
35
 
36
36
  ## State → next action
@@ -43,9 +43,9 @@ The project has its PRD and Design locked but no epics defined yet.
43
43
 
44
44
  **Next action:**
45
45
 
46
- 1. Read `.savepoint/releases/v{{RELEASE_NUMBER}}/PRD.md` — the v{{RELEASE_NUMBER}} release scope (epic list lives there).
46
+ 1. Read `.savepoint/releases/{release}/{release}-PRD.md` — the release scope (epic list lives there).
47
47
  2. Help the user define the epics list and confirm priority.
48
- 3. For each epic in order, create the directory `.savepoint/releases/v{{RELEASE_NUMBER}}/epics/E##-{epic-name}/` with a `Design.md` stub.
48
+ 3. For each epic in order, create the directory `.savepoint/releases/{release}/epics/E##-{epic-name}/` with a `Design.md` stub.
49
49
  4. When epic E01 (scaffolding) is created, transition to `state: epic-design` for that epic.
50
50
 
51
51
  **Do not** start writing code. We are still in planning.
@@ -70,7 +70,7 @@ Epic Design exists but tasks are missing or not fully planned.
70
70
 
71
71
  1. Re-read the epic Design.
72
72
  2. Create or update the full epic task list — each task **independently buildable**, **objective-led**, with declared `depends_on`.
73
- 3. Each task file lives at `.savepoint/releases/v{{RELEASE_NUMBER}}/epics/{E##-epic}/tasks/TNNN-slug.md` with frontmatter:
73
+ 3. Each task file lives at `.savepoint/releases/{release}/epics/{E##-epic}/tasks/TNNN-slug.md` with frontmatter:
74
74
  ```yaml
75
75
  ---
76
76
  id: {E##-epic}/TNNN-slug
@@ -150,3 +150,4 @@ If you are not Claude Opus / Gemini 2.5 Pro / GPT-5.5 / equivalent, surface a wa
150
150
  > _"Heads up — I'm running on a lighter model. Savepoint's planning steps work best with top-tier models because the embedded prompts are detailed. I'll do my best, but consider switching the model for PRD/Design/Task-breakdown steps."_
151
151
 
152
152
  Then proceed.
153
+ proceed.