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.
- package/.claude/settings.local.json +8 -1
- package/.savepoint/Design.md +26 -17
- package/.savepoint/audit/v1/E01/proposals.md +168 -0
- package/.savepoint/audit/v1/E01/snapshot.md +78 -0
- package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/proposals.md +7 -7
- package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/snapshot.md +2 -2
- package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/AGENTS.md +5 -5
- package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/proposals.md +20 -20
- package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/snapshot.md +1 -1
- package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/proposals.md +11 -11
- package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/snapshot.md +1 -1
- package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/proposals.md +14 -14
- package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/snapshot.md +1 -1
- package/.savepoint/audit/{E05-init-command → v1/E05-init-command}/snapshot.md +1 -1
- package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/proposals.md +4 -4
- package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/snapshot.md +1 -1
- package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +130 -0
- package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +84 -0
- package/.savepoint/audit/{E07-audit-pipeline → v1/E07-audit-pipeline}/snapshot.md +6 -6
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/proposals.md +114 -0
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +41 -0
- package/.savepoint/audit/v1.1/E04-epic-navigation/proposals.md +156 -0
- package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +48 -0
- package/.savepoint/config.yml +3 -3
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/E06-Detail.md +62 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +7 -7
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +10 -8
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +16 -9
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +27 -22
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/E01-Detail.md +40 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-next-activity-header.md +56 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +38 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +28 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +51 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +45 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +68 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Detail.md +49 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +37 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +38 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +36 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +59 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +32 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +34 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +30 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +33 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +88 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +30 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +46 -0
- package/.savepoint/releases/v1.1/v1.1-PRD.md +79 -0
- package/.savepoint/router.md +33 -105
- package/.savepoint/visual-identity.md +4 -3
- package/AGENTS.md +56 -113
- package/Makefile +19 -3
- package/README.md +7 -6
- package/agent-skills/savepoint-audit/SKILL.md +6 -6
- package/agent-skills/savepoint-build-task/SKILL.md +2 -2
- package/agent-skills/savepoint-create-plan/SKILL.md +3 -3
- package/agent-skills/savepoint-create-task/SKILL.md +2 -2
- package/agent-skills/savepoint-draft-prd/SKILL.md +1 -1
- package/agent-skills/savepoint-system-design/SKILL.md +1 -1
- package/go.mod +4 -1
- package/go.sum +2 -0
- package/internal/board/board.go +66 -14
- package/internal/board/board_test.go +124 -0
- package/internal/board/card.go +40 -3
- package/internal/board/card_test.go +121 -14
- package/internal/board/column.go +40 -5
- package/internal/board/column_test.go +65 -10
- package/internal/board/detail.go +115 -23
- package/internal/board/detail_test.go +132 -25
- package/internal/board/epic_panel.go +105 -8
- package/internal/board/epic_panel_test.go +343 -5
- package/internal/board/layout.go +12 -2
- package/internal/board/layout_test.go +17 -0
- package/internal/board/model.go +146 -23
- package/internal/board/render_policy_test.go +77 -0
- package/internal/board/status.go +23 -0
- package/internal/board/update.go +300 -9
- package/internal/board/update_test.go +166 -0
- package/internal/board/view.go +141 -17
- package/internal/board/view_test.go +161 -3
- package/internal/board/watch.go +100 -0
- package/internal/buildtool/main.go +219 -0
- package/internal/data/parser.go +39 -1
- package/internal/data/parser_test.go +43 -2
- package/internal/data/task.go +22 -2
- package/internal/styles/palette.go +9 -7
- package/internal/styles/styles.go +42 -25
- package/main.go +9 -0
- package/package.json +5 -4
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
- package/templates/project/.savepoint/router.md +6 -5
- package/templates/project/AGENTS.md +47 -101
- package/templates/prompts/audit-reconciliation.prompt.md +6 -6
- package/templates/prompts/epic-design.prompt.md +3 -3
- package/templates/prompts/task-breakdown.prompt.md +1 -1
- package/templates/prompts/task-building.prompt.md +1 -1
- package/templates/prompts/task-planning.prompt.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +0 -42
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +0 -26
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -35
- package/main.exe +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/Design.md +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/epic-Design.md +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/quality-review.md +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/snapshot.md +0 -0
- /package/.savepoint/audit/{E02-data-model → v1/E02-data-model}/snapshot.md +0 -0
- /package/.savepoint/audit/{E03-cli-foundation → v1/E03-cli-foundation}/snapshot.md +0 -0
- /package/.savepoint/audit/{E04-templates-and-prompts → v1/E04-templates-and-prompts}/snapshot.md +0 -0
- /package/.savepoint/audit/{E06-tui-board → v1/E06-tui-board}/snapshot.md +0 -0
- /package/.savepoint/audit/{E08-board-workflow-cleanup → v1/E08-board-workflow-cleanup}/snapshot.md +0 -0
- /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
- /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
|
+
}
|
package/internal/data/parser.go
CHANGED
|
@@ -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:
|
|
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
|
|
214
|
-
t.
|
|
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
|
}
|
package/internal/data/task.go
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
package data
|
|
2
2
|
|
|
3
|
-
import
|
|
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 []
|
|
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 = "#
|
|
6
|
-
Surface = "#
|
|
7
|
-
Surface2 = "#
|
|
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 = "
|
|
19
|
-
Surface256 = "
|
|
20
|
-
Surface2256 = "
|
|
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) //
|
|
14
|
-
clrSurfaceDark = color(Surface, Surface256, Surface16) //
|
|
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(
|
|
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(
|
|
73
|
-
BorderForeground(
|
|
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(
|
|
89
|
+
BorderStyle(boxBorder).
|
|
85
90
|
BorderForeground(clrOrange).
|
|
86
|
-
Background(clrSurface).
|
|
87
91
|
Padding(0, 1)
|
|
88
92
|
|
|
89
|
-
CardMeta
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
21
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|