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
package/Makefile CHANGED
@@ -1,7 +1,9 @@
1
- .PHONY: build test run clean
1
+ .PHONY: build test run clean build-linux build-darwin build-all dist smoke-test
2
+
3
+ VERSION ?=
2
4
 
3
5
  build:
4
- go build -o savepoint main.go
6
+ go run ./internal/buildtool -version "$(VERSION)" build
5
7
 
6
8
  test:
7
9
  go test ./...
@@ -10,4 +12,18 @@ run:
10
12
  go run main.go
11
13
 
12
14
  clean:
13
- rm -f savepoint
15
+ go run ./internal/buildtool -version "$(VERSION)" clean
16
+
17
+ build-linux:
18
+ go run ./internal/buildtool -version "$(VERSION)" build-linux
19
+
20
+ build-darwin:
21
+ go run ./internal/buildtool -version "$(VERSION)" build-darwin
22
+
23
+ build-all: build-linux build-darwin
24
+
25
+ dist:
26
+ go run ./internal/buildtool -version "$(VERSION)" dist
27
+
28
+ smoke-test:
29
+ go run ./internal/buildtool -version "$(VERSION)" smoke-test
package/README.md CHANGED
@@ -34,11 +34,11 @@ Savepoint turns your project into a series of hard gates, enforced by **six bund
34
34
 
35
35
  Small scopes. Small context windows. No wandering.
36
36
 
37
- - **The PRD Gate (`draft-prd`):** The agent interviews you to ensure your idea is crisp enough for a V1 before writing any architecture.
38
- - **The Design Gate (`system-design`):** Write down what you actually want. If you can't explain it simply in a markdown file, the AI is going to make a mess of it.
39
- - **The Plan Gate (`create-plan` & `create-task`):** Break the idea down into small, manageable steps. No giant leaps. This becomes the checklist the AI _must_ follow.
40
- - **The Build Gate (`build-task`):** The AI writes code for one small step at a time. The scope stays tight so it doesn't wander off into the weeds. It logs "Drift Notes" instead of cowboy-coding architectural changes.
41
- - **The Audit Gate (`audit`):** Before moving on, we stop and check. Does the code match the plan? We don't advance until the map matches the territory.
37
+ - **The PRD Gate (`savepoint-draft-prd`):** The agent interviews you to ensure your idea is crisp enough for a V1 before writing any architecture.
38
+ - **The Design Gate (`savepoint-system-design`):** Write down what you actually want. If you can't explain it simply in a markdown file, the AI is going to make a mess of it.
39
+ - **The Plan Gate (`savepoint-create-plan` & `savepoint-create-task`):** Break the idea down into small, manageable steps. No giant leaps. This becomes the checklist the AI _must_ follow.
40
+ - **The Build Gate (`savepoint-build-task`):** The AI writes code for one small step at a time. The scope stays tight so it doesn't wander off into the weeds. It logs "Drift Notes" instead of cowboy-coding architectural changes.
41
+ - **The Audit Gate (`savepoint-audit`):** Before moving on, we stop and check. Does the code match the plan? We don't advance until the map matches the territory.
42
42
 
43
43
  > 🔒 **The Audit Loop** — When the last task in an epic moves to `done`, the next epic stays _locked_ until your docs (`Design.md`, `AGENTS.md`, and the epic's own design) are reconciled with the actual code via the audit agent. **No existing markdown-first task tool has this gate.**
44
44
 
@@ -48,7 +48,7 @@ Small scopes. Small context windows. No wandering.
48
48
 
49
49
  Built for a cinematic, technical feel without the bloat.
50
50
 
51
- - **Cinematic TUI:** Built with Ink. It feels like a proper terminal tool, not a web app ported to a CLI.
51
+ - **Cinematic TUI:** Built with Go and Bubble Tea. It feels like a proper terminal tool, lightning-fast and dependency-free.
52
52
  - **File-First:** Your project's state lives in markdown and JSON files right next to your code.
53
53
  - **Agent-Agnostic:** Claude, Cursor, Aider, Gemini — if it reads markdown and edits files, it works. No MCP server. No per-agent adapters.
54
54
  - **Token-Efficient by Design:** Tasks read <2KB of context. Audits stay under ~15KB. No more burning your AI budget on bloated backlogs.
@@ -76,3 +76,4 @@ I’m sharing it to prove a point: The real power of AI isn't just the size of t
76
76
 
77
77
  **License:** MIT
78
78
  **Status:** Recursive Construction (v1 MVP in progress)
79
+ us:** Recursive Construction (v1 MVP in progress)
@@ -10,8 +10,8 @@ The Audit Gate is Savepoint's wedge. It prevents projects from degrading into ch
10
10
  This skill is activated when the `.savepoint/router.md` state is `audit-pending`.
11
11
 
12
12
  ## Input
13
- - `.savepoint/audit/{E##-slug}/snapshot.md` (what changed).
14
- - `.savepoint/releases/v1/epics/{E##-slug}/Design.md` (the epic design).
13
+ - `.savepoint/audit/{release}/{E##-slug}/snapshot.md` (what changed).
14
+ - `.savepoint/releases/{release}/epics/{E##-slug}/E##-Detail.md` (the epic design).
15
15
  - `.savepoint/Design.md` (project architecture).
16
16
  - `AGENTS.md` (agent guide and codebase map).
17
17
  - The source and test files modified during the Epic.
@@ -21,15 +21,15 @@ This skill is activated when the `.savepoint/router.md` state is `audit-pending`
21
21
  1. **Fresh Eyes Check:** If you are the exact same agent session that just built the `build-task` code for this Epic, you MUST STOP. Tell the user: "Epic complete. Start a new agent session for the audit."
22
22
  2. **Verify ACs:** Review the completed tasks for the Epic. Ensure the Acceptance Criteria were actually met by the committed code.
23
23
  3. **Process Drift Notes (Reconciliation):** Read every task file in the Epic and look for `## Drift Notes`.
24
- 4. **Draft Proposals:** Based on the code changes and the Drift Notes, write exactly ONE file: `.savepoint/audit/{E##-slug}/proposals.md`. It must contain:
24
+ 4. **Draft Proposals:** Based on the code changes and the Drift Notes, write exactly ONE file: `.savepoint/audit/{release}/{E##-slug}/proposals.md`. It must contain:
25
25
  * **Design.md section:** Propose updates to merge the epic's architectural changes into the project-level `Design.md`.
26
26
  * **AGENTS.md section:** Propose updates to refresh the Codebase Map table with new or changed modules.
27
- * **Epic-Design.md section:** Add "Implemented as:" notes showing where reality deviated from the original plan.
27
+ * **Epic-E##-Detail.md section:** Add "Implemented as:" notes showing where reality deviated from the original plan.
28
28
  * **Quality Review section:** List any minor code-style infractions that must be fixed before the next Epic.
29
29
  5. **Review Format:** Use `## Target File`, `## Replace`, and `## With` formatting in the proposals document so it is easy for a human (or an agent) to apply later.
30
- 6. **Handoff:** Do not apply the proposals yourself. Do not mark the epic audited. Stop and prompt the user to review `.savepoint/audit/{E##-slug}/proposals.md` (often via the TUI). Once the user approves, the proposals are applied, and the router moves to the next Epic.
30
+ 6. **Handoff:** Do not apply the proposals yourself. Do not mark the epic audited. Stop and prompt the user to review `.savepoint/audit/{release}/{E##-slug}/proposals.md` (often via the TUI). Once the user approves, the proposals are applied, and the router moves to the next Epic.
31
31
 
32
32
  ## Constraints
33
33
  - **Do not write product code.** You are an auditor.
34
34
  - **Do not apply the changes immediately.** Write the proposals document first.
35
- - **One proposals file.** Do not create multiple proposal files.
35
+ - **One proposals file.** Do not create multiple proposal files.
@@ -7,11 +7,11 @@ Act as a disciplined coding agent that strictly follows Savepoint's implementati
7
7
  The `build-task` skill is the execution engine. It reads the detailed task plan, writes the code, and proves that the Acceptance Criteria (ACs) have been met. It is strictly constrained to the scope of the single active task. It does not rewrite architecture, and it does not fix unrelated bugs.
8
8
 
9
9
  ## Trigger
10
- This skill is activated when the `.savepoint/router.md` state is `in-progress` and points to a specific task file.
10
+ This skill is activated when the `.savepoint/router.md` state is `task-building` and points to a specific task file.
11
11
 
12
12
  ## Input
13
13
  - `.savepoint/router.md` (Current state).
14
- - The active epic Design: `.savepoint/releases/v1/epics/{E##-epic}/Design.md`.
14
+ - The active epic E##-Detail.md: `.savepoint/releases/v1/epics/{E##-epic}/E##-Detail.md`.
15
15
  - The active task file: `.savepoint/releases/v1/epics/{E##-epic}/tasks/{T###}-*.md`.
16
16
  - Directly touched source/test files.
17
17
 
@@ -7,18 +7,18 @@ Turn the Product Requirements Document (PRD) and the architectural `Design.md` i
7
7
  Savepoint enforces small scopes to prevent AI agents from "wandering." The `create-plan` skill acts as the Technical Project Manager. It takes the grand vision and the architectural blueprint and slices it into a sequence of achievable, testable milestones (Epics).
8
8
 
9
9
  ## Trigger
10
- This skill is activated when the `.savepoint/router.md` state is `planning`.
10
+ This skill is activated when the `.savepoint/router.md` state is `pre-implementation`.
11
11
 
12
12
  ## Input
13
13
  - `.savepoint/PRD.md` (Vision and constraints)
14
14
  - `.savepoint/Design.md` (Architecture and layout)
15
- - `.savepoint/releases/v1/PRD.md` (Optional: Release-scoped PRD)
15
+ - `.savepoint/releases/v1/v1-PRD.md` (Optional: Release-scoped PRD)
16
16
 
17
17
  ## Workflow
18
18
 
19
19
  1. **Read the Context:** Consume the PRD and Design documents to understand the scope and technical constraints.
20
20
  2. **Define Epics:** Group the work into high-level features or milestones. Name them clearly (e.g., `E01-scaffolding`, `E02-database`, `E03-auth`). Each Epic must represent a deliverable slice of value.
21
- 3. **Draft Epic Designs:** For each Epic, create a shell `Design.md` inside `.savepoint/releases/v1/epics/{E##-epic-name}/Design.md`. This file should describe the *delta* (what this specific epic adds to the overall architecture).
21
+ 3. **Draft Epic Designs:** For each Epic, create a shell `E##-Detail.md` inside `.savepoint/releases/v1/epics/{E##-epic-name}/E##-Detail.md`. This file should describe the *delta* (what this specific epic adds to the overall architecture).
22
22
  4. **Breakdown Tasks (High Level):** Inside each Epic folder, list out the high-level tasks required to complete it. Do not write full implementation plans yet—just identify the discrete chunks of work (e.g., `T001-setup-repo.md`, `T002-init-db.md`).
23
23
  5. **Order and Dependency:** Ensure the Epics and tasks are ordered logically. You cannot build the frontend auth UI (E03) before the database models (E02) exist.
24
24
  6. **Handoff:** Update `.savepoint/router.md` to `state: task-breakdown` and point it at the first Epic. Prompt the user to review the Epic list.
@@ -7,10 +7,10 @@ Take high-level tasks identified during the planning phase and build detailed, a
7
7
  A task plan is a contract between the planner and the builder. If the task plan is vague, the resulting code will be buggy. The `create-task` skill acts as a Senior Engineer writing tickets for a Junior Developer (the `build-task` agent). It must define exactly *what* constitutes success and *how* to achieve it, without actually writing the code.
8
8
 
9
9
  ## Trigger
10
- This skill is activated when the `.savepoint/router.md` state is `task-breakdown` and the router points to a specific task file.
10
+ This skill is activated when the `.savepoint/router.md` state is `epic-task-breakdown` and the router points to a specific epic.
11
11
 
12
12
  ## Input
13
- - `.savepoint/releases/v1/epics/{E##-epic}/Design.md` (The active Epic's design).
13
+ - `.savepoint/releases/v1/epics/{E##-epic}/E##-Detail.md` (The active Epic's design).
14
14
  - The high-level task markdown file (e.g., `.savepoint/releases/v1/epics/{E##-epic}/tasks/T001-slug.md`).
15
15
 
16
16
  ## Workflow
@@ -7,7 +7,7 @@ Help the user write a structured, sufficiently detailed Product Requirements Doc
7
7
  In the Savepoint workflow, the PRD is the absolute source of truth. If the PRD is a vague brain-dump, the resulting architecture and code will be a mess. Your job as the `draft-prd` agent is to act as a strict Product Manager. You do not write code. You interrogate the user's idea until it is crisp enough to build a V1.
8
8
 
9
9
  ## Trigger
10
- This skill is activated when the `.savepoint/router.md` state is `draft-prd` or when the user explicitly asks you to help them write or refine their PRD.
10
+ This skill is activated when the `.savepoint/router.md` state is `pre-implementation` or when the user explicitly asks you to help them write or refine their PRD.
11
11
 
12
12
  ## Input
13
13
  - The user's initial idea, brain-dump, or `.txt` file outline.
@@ -7,7 +7,7 @@ Translate the Product Requirements Document (PRD) into the initial architectural
7
7
  Before any tasks can be planned, the project needs a high-level technical direction. The `system-design` skill acts as the Staff Engineer. It reads the vision and constraints from the PRD and makes authoritative technical decisions regarding architecture, directory layout, dependency strategies, and workflow rules.
8
8
 
9
9
  ## Trigger
10
- This skill is activated when the `.savepoint/router.md` state is `design` or when the user explicitly asks to design the system based on the PRD.
10
+ This skill is activated when the `.savepoint/router.md` state is `epic-design` or when the user explicitly asks to design the system based on the PRD.
11
11
 
12
12
  ## Input
13
13
  - `.savepoint/PRD.md` (The source of truth for "what" and "why").
package/go.mod CHANGED
@@ -2,7 +2,10 @@ module github.com/opencode/savepoint
2
2
 
3
3
  go 1.26.2
4
4
 
5
- require github.com/charmbracelet/bubbletea v1.3.10
5
+ require (
6
+ github.com/charmbracelet/bubbletea v1.3.10
7
+ github.com/fsnotify/fsnotify v1.10.0
8
+ )
6
9
 
7
10
  require (
8
11
  github.com/atotto/clipboard v0.1.4 // indirect
package/go.sum CHANGED
@@ -32,6 +32,8 @@ github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w
32
32
  github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
33
33
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
34
34
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
35
+ github.com/fsnotify/fsnotify v1.10.0 h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M=
36
+ github.com/fsnotify/fsnotify v1.10.0/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
35
37
  github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
36
38
  github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
37
39
  github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
@@ -6,10 +6,14 @@ import (
6
6
  "path/filepath"
7
7
 
8
8
  tea "github.com/charmbracelet/bubbletea"
9
+ "github.com/charmbracelet/lipgloss"
10
+ "github.com/muesli/termenv"
9
11
  "github.com/opencode/savepoint/internal/data"
10
12
  )
11
13
 
12
14
  func Run() error {
15
+ lipgloss.SetColorProfile(termenv.ANSI256)
16
+
13
17
  model, err := newProjectModel(".")
14
18
  if err != nil {
15
19
  return err
@@ -39,42 +43,77 @@ func newProjectModel(start string) (Model, error) {
39
43
  return Model{}, err
40
44
  }
41
45
 
42
- releases, err := d.ListReleases(root)
46
+ tasks, releaseIDs, releaseEpics, epicStatuses, err := loadBoardData(root)
47
+ if err != nil {
48
+ return Model{}, err
49
+ }
50
+
51
+ release := firstKnown(routerState.Release, releaseIDs)
52
+ epic := firstKnown(routerState.Epic, releaseEpics[release])
53
+
54
+ model := NewModel(tasks, release, epic)
55
+ model.Root = root
56
+ model.RouterTask = routerState.Task
57
+ model.RouterState = routerState
58
+ model.Releases = releaseIDs
59
+ model.ReleaseEpics = releaseEpics
60
+ model.EpicStatus = epicStatuses
61
+ model.refreshEpicsForRelease()
62
+ model.refreshTasks()
63
+
64
+ watcher, err := newWatcher(root)
43
65
  if err != nil {
44
66
  return Model{}, err
45
67
  }
68
+ model.Watcher = watcher
69
+
70
+ return model, nil
71
+ }
72
+
73
+ func loadBoardData(root string) ([]data.Task, []string, map[string][]string, map[string]string, error) {
74
+ d := data.NewDiscover()
75
+ releases, err := d.ListReleases(root)
76
+ if err != nil {
77
+ return nil, nil, nil, nil, err
78
+ }
46
79
 
47
80
  releaseIDs := make([]string, 0, len(releases))
48
81
  releaseEpics := make(map[string][]string, len(releases))
49
- tasks := []data.Task{}
82
+ var tasks []data.Task
83
+ epicStatuses := make(map[string]string)
50
84
 
51
85
  for _, release := range releases {
52
86
  releaseIDs = append(releaseIDs, release.ID)
53
87
  epics, err := d.ListEpics(root, release.ID)
54
88
  if err != nil {
55
- return Model{}, err
89
+ return nil, nil, nil, nil, err
56
90
  }
57
91
  for _, epic := range epics {
58
92
  releaseEpics[release.ID] = append(releaseEpics[release.ID], epic.ID)
59
93
  epicTasks, err := loadEpicTasks(d, root, release.ID, epic.ID)
60
94
  if err != nil {
61
- return Model{}, err
95
+ return nil, nil, nil, nil, err
62
96
  }
63
97
  tasks = append(tasks, epicTasks...)
98
+
99
+ detailPath := filepath.Join(epic.Path, shortID(epic.ID)+"-Detail.md")
100
+ if raw, err := os.ReadFile(detailPath); err == nil {
101
+ parser := data.NewParser()
102
+ if fm, err := parser.ParseFrontmatter(string(raw)); err == nil {
103
+ if status, ok := fm["status"].(string); ok {
104
+ epicStatuses[epic.ID] = status
105
+ }
106
+ }
107
+ }
64
108
  }
65
109
  }
66
110
 
67
- release := firstKnown(routerState.Release, releaseIDs)
68
- epic := firstKnown(routerState.Epic, releaseEpics[release])
69
-
70
- model := NewModel(tasks, release, epic)
71
- model.Root = root
72
- model.Releases = releaseIDs
73
- model.ReleaseEpics = releaseEpics
74
- model.refreshEpicsForRelease()
75
- model.refreshTasks()
111
+ return tasks, releaseIDs, releaseEpics, epicStatuses, nil
112
+ }
76
113
 
77
- return model, nil
114
+ func loadAllTasks(root string) ([]data.Task, error) {
115
+ tasks, _, _, _, err := loadBoardData(root)
116
+ return tasks, err
78
117
  }
79
118
 
80
119
  func readRouterState(root string) (*data.RouterState, error) {
@@ -103,6 +142,14 @@ func loadEpicTasks(d *data.Discover, root, release, epic string) ([]data.Task, e
103
142
  if err != nil {
104
143
  return nil, err
105
144
  }
145
+ fi, err := os.Stat(taskInfo.Path)
146
+ if err != nil {
147
+ return nil, err
148
+ }
149
+ task.Path = taskInfo.Path
150
+ task.Mtime = fi.ModTime()
151
+ task.Release = release
152
+ task.Epic = epic
106
153
  tasks = append(tasks, *task)
107
154
  }
108
155
  return tasks, nil
@@ -114,6 +161,11 @@ func firstKnown(preferred string, values []string) string {
114
161
  return preferred
115
162
  }
116
163
  }
164
+ for _, value := range values {
165
+ if shortID(value) == shortID(preferred) {
166
+ return value
167
+ }
168
+ }
117
169
  if len(values) == 0 {
118
170
  return ""
119
171
  }
@@ -66,6 +66,111 @@ next_action: "test"
66
66
  if len(tasks) != 1 || tasks[0].ID != "E03-live/T001-live" {
67
67
  t.Errorf("visible in-progress tasks = %v, want E03-live/T001-live", tasks)
68
68
  }
69
+ if model.Watcher == nil {
70
+ t.Fatal("Watcher is nil, want auto-refresh watcher")
71
+ }
72
+ t.Cleanup(func() { model.Watcher.Close() })
73
+ }
74
+
75
+ func TestNewProjectModelUsesPathReleaseForTaskWithoutReleaseFrontmatter(t *testing.T) {
76
+ projectRoot := t.TempDir()
77
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
78
+ writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
79
+
80
+ ## Current state
81
+
82
+ `+"```"+`yaml
83
+ state: task-building
84
+ release: v1.1
85
+ epic: E01-tui-optimisation
86
+ task: E01-tui-optimisation/T001-border-resize-fix
87
+ next_action: "test"
88
+ `+"```"+`
89
+ `)
90
+ writeTaskWithoutRelease(t, savepointRoot, "v1.1", "E01-tui-optimisation", "T001-border-resize-fix", data.ColumnInProgress)
91
+
92
+ model, err := newProjectModel(projectRoot)
93
+ if err != nil {
94
+ t.Fatalf("newProjectModel() error = %v", err)
95
+ }
96
+
97
+ if model.SelectedRelease != "v1.1" {
98
+ t.Errorf("SelectedRelease = %q, want v1.1", model.SelectedRelease)
99
+ }
100
+ tasks := model.Tasks[data.ColumnInProgress]
101
+ if len(tasks) != 1 {
102
+ t.Fatalf("visible in-progress tasks = %v, want one v1.1 task", tasks)
103
+ }
104
+ if tasks[0].Release != "v1.1" {
105
+ t.Errorf("Task.Release = %q, want v1.1", tasks[0].Release)
106
+ }
107
+ if model.Watcher != nil {
108
+ t.Cleanup(func() { model.Watcher.Close() })
109
+ }
110
+ }
111
+
112
+ func TestNewProjectModelResolvesShortRouterEpicToFullEpicID(t *testing.T) {
113
+ projectRoot := t.TempDir()
114
+ savepointRoot := filepath.Join(projectRoot, ".savepoint")
115
+ writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
116
+
117
+ ## Current state
118
+
119
+ `+"```"+`yaml
120
+ state: task-building
121
+ release: v1.1
122
+ epic: E03
123
+ task: T001
124
+ next_action: "Build v1.1 E03/T001"
125
+ `+"```"+`
126
+ `)
127
+ writeTask(t, savepointRoot, "v1.1", "E01-tui-optimisation", "T007-column-focus-border-stability", data.ColumnInProgress)
128
+ writeTask(t, savepointRoot, "v1.1", "E03-ui-visual-refinement", "T001-border-resize-fix", data.ColumnInProgress)
129
+
130
+ model, err := newProjectModel(projectRoot)
131
+ if err != nil {
132
+ t.Fatalf("newProjectModel() error = %v", err)
133
+ }
134
+
135
+ if model.SelectedEpic != "E03-ui-visual-refinement" {
136
+ t.Errorf("SelectedEpic = %q, want E03-ui-visual-refinement", model.SelectedEpic)
137
+ }
138
+ tasks := model.Tasks[data.ColumnInProgress]
139
+ if len(tasks) != 1 || tasks[0].ID != "E03-ui-visual-refinement/T001-border-resize-fix" {
140
+ t.Errorf("visible in-progress tasks = %v, want E03-ui-visual-refinement/T001-border-resize-fix", tasks)
141
+ }
142
+ if model.Watcher != nil {
143
+ t.Cleanup(func() { model.Watcher.Close() })
144
+ }
145
+ }
146
+
147
+ func TestUpdateReloadMsgRefreshesReleaseEpicIndex(t *testing.T) {
148
+ m := NewModel(nil, "v1", "E01-old")
149
+ m.Releases = []string{"v1"}
150
+ m.ReleaseEpics = map[string][]string{"v1": []string{"E01-old"}}
151
+
152
+ task := data.Task{
153
+ ID: "E02-new/T001-new",
154
+ Release: "v1",
155
+ Epic: "E02-new",
156
+ Column: data.ColumnPlanned,
157
+ }
158
+ got, _ := m.Update(reloadMsg{
159
+ tasks: []data.Task{task},
160
+ releases: []string{"v1"},
161
+ releaseEpics: map[string][]string{"v1": []string{"E02-new"}},
162
+ })
163
+ updated := requireModel(t, got)
164
+
165
+ if updated.SelectedEpic != "E02-new" {
166
+ t.Errorf("SelectedEpic = %q, want E02-new", updated.SelectedEpic)
167
+ }
168
+ if len(updated.Epics) != 1 || updated.Epics[0] != "E02-new" {
169
+ t.Errorf("Epics = %v, want [E02-new]", updated.Epics)
170
+ }
171
+ if len(updated.Tasks[data.ColumnPlanned]) != 1 {
172
+ t.Errorf("planned tasks = %v, want reloaded task visible", updated.Tasks[data.ColumnPlanned])
173
+ }
69
174
  }
70
175
 
71
176
  func writeTask(t *testing.T, root, release, epic, task string, column data.ColumnType) {
@@ -88,6 +193,25 @@ depends_on: []
88
193
  writeFile(t, path, content)
89
194
  }
90
195
 
196
+ func writeTaskWithoutRelease(t *testing.T, root, release, epic, task string, column data.ColumnType) {
197
+ t.Helper()
198
+ path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
199
+ phase := ""
200
+ if column == data.ColumnInProgress {
201
+ phase = "phase: build\n"
202
+ }
203
+ content := `---
204
+ id: ` + epic + `/` + task + `
205
+ status: ` + string(column) + `
206
+ ` + phase + `objective: "Test task"
207
+ depends_on: []
208
+ ---
209
+
210
+ # Test task
211
+ `
212
+ writeFile(t, path, content)
213
+ }
214
+
91
215
  func writeFile(t *testing.T, path, content string) {
92
216
  t.Helper()
93
217
  if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
@@ -17,16 +17,26 @@ const (
17
17
  )
18
18
 
19
19
  // RenderCard renders a task card with phase glyph, truncated ID+title, and focus styling.
20
- func RenderCard(t data.Task, width int, focused bool) string {
20
+ // When router state matches t's release/epic/task, a green priority glyph replaces the phase glyph.
21
+ func RenderCard(t data.Task, width int, focused bool, routerState *data.RouterState) string {
21
22
  inner := width - cardOverhead
22
23
  if inner < 2 {
23
24
  inner = 2
24
25
  }
25
26
 
26
- glyph := phaseGlyphStyled(t.Stage)
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
+ }
27
37
  // glyph is 1 rune + 1 space prefix; leave room for "▣ "
28
38
  idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), inner-2))
29
- titleLine := styles.CardMeta.Render(truncate(t.Title, inner))
39
+ titleLine := styles.CardMeta.Render(strings.Join(WrapText(t.Title, inner), "\n"))
30
40
 
31
41
  content := idLine + "\n" + titleLine
32
42
 
@@ -47,6 +57,33 @@ func phaseGlyphStyled(stage data.ProgressStage) string {
47
57
  }
48
58
  }
49
59
 
60
+ func isRouterPriority(t data.Task, state *data.RouterState) bool {
61
+ if state == nil || state.Task == "" {
62
+ return false
63
+ }
64
+ if shortID(t.ID) != shortID(state.Task) {
65
+ return false
66
+ }
67
+ if state.Release != "" && t.Release != "" && t.Release != state.Release {
68
+ return false
69
+ }
70
+ routerEpic := state.Epic
71
+ if routerEpic == "" {
72
+ routerEpic = taskEpic(state.Task)
73
+ }
74
+ if routerEpic != "" && t.Epic != "" && shortID(t.Epic) != shortID(routerEpic) {
75
+ return false
76
+ }
77
+ return true
78
+ }
79
+
80
+ func taskEpic(taskID string) string {
81
+ if idx := strings.LastIndex(taskID, "/"); idx >= 0 {
82
+ return taskID[:idx]
83
+ }
84
+ return ""
85
+ }
86
+
50
87
  // shortID strips the epic prefix and slug from a task ID.
51
88
  // "E06-atari-noir-layout/T004-component-refinement" → "T004"
52
89
  func shortID(id string) string {