savepoint 1.0.0
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 +20 -0
- package/.prettierignore +4 -0
- package/.savepoint/Design.md +196 -0
- package/.savepoint/PRD.md +58 -0
- package/.savepoint/audit/E01-go-setup/proposals.md +166 -0
- package/.savepoint/audit/E01-go-setup/snapshot.md +71 -0
- package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +66 -0
- package/.savepoint/audit/E01-scaffolding/proposals/Design.md +210 -0
- package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +117 -0
- package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +101 -0
- package/.savepoint/audit/E01-scaffolding/snapshot.md +54 -0
- package/.savepoint/audit/E02-data-model/snapshot.md +128 -0
- package/.savepoint/audit/E02-data-readers/proposals.md +123 -0
- package/.savepoint/audit/E02-data-readers/snapshot.md +54 -0
- package/.savepoint/audit/E03-board-tui-core/proposals.md +146 -0
- package/.savepoint/audit/E03-board-tui-core/snapshot.md +57 -0
- package/.savepoint/audit/E03-cli-foundation/snapshot.md +106 -0
- package/.savepoint/audit/E04-board-components/proposals.md +118 -0
- package/.savepoint/audit/E04-board-components/snapshot.md +77 -0
- package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +115 -0
- package/.savepoint/audit/E05-init-command/snapshot.md +125 -0
- package/.savepoint/audit/E05-phase-transitions/proposals.md +83 -0
- package/.savepoint/audit/E05-phase-transitions/snapshot.md +36 -0
- package/.savepoint/audit/E06-tui-board/snapshot.md +64 -0
- package/.savepoint/audit/E07-audit-pipeline/snapshot.md +165 -0
- package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +65 -0
- package/.savepoint/config.yml +27 -0
- package/.savepoint/releases/v1/PRD.md +66 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/Design.md +39 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +42 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T002-entrypoint.md +23 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T003-directory-structure.md +24 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T004-makefile.md +23 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/Design.md +61 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T001-task-struct.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T002-frontmatter-parser.md +30 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T003-router-reader.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T004-config-reader.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T005-discovery.md +30 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/Design.md +38 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T001-model.md +29 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T002-update-loop.md +30 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T003-view.md +34 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T004-styles.md +29 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +42 -0
- package/.savepoint/releases/v1/epics/E04-board-components/Design.md +44 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T001-column.md +34 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +33 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T003-epic-panel.md +49 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T004-detail-overlay.md +40 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T005-release-dropdown.md +33 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +34 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/Design.md +38 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T001-phase-stepping.md +29 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T002-gates.md +31 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T003-write-task.md +31 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T004-write-router.md +31 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +42 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T001-color-system.md +39 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +52 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +52 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +53 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T005-restore-nav-hints.md +39 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +36 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +38 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +41 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +61 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/Design.md +39 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T001-archive-epics.md +20 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T002-rewrite-prd.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T003-create-epic-stubs.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T004-update-router.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/Design.md +118 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/handoff.md +9 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T001-package-baseline.md +45 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T002-typescript-build.md +48 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T003-vitest-smoke.md +43 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T004-lint-format-gates.md +45 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T005-scaffold-verification.md +40 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/Design.md +142 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T001-domain-ids-status.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T002-markdown-frontmatter-boundary.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T003-task-documents.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T004-release-epic-router-config-readers.md +30 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T005-dependency-validation.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T006-epic-task-set-reader.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T007-quality-gates.md +31 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/Design.md +40 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T001-phase-types.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T002-phase-frontmatter.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T003-simplify-config.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T004-simplify-router-domain.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/Design.md +122 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T001-argument-parser-contract.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T002-help-text-generation.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T003-terminal-environment-detection.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T004-command-stub-modules.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T005-cli-runner-dispatch.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T006-entrypoint-quality-gates.md +32 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/Design.md +43 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T001-strip-args.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T002-strip-help.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T003-strip-run.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T004-delete-commands.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T005-update-cli-tests.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/Design.md +48 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T001-board-data-phases.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T002-phase-rendering.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T003-detail-pane-phases.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T004-phase-transitions.md +42 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T005-phase-gates.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T006-phase-write-back.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T007-remove-audit-flow.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T008-board-tests.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/Design.md +85 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T001-project-template-assets.md +17 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T002-release-and-prompt-assets.md +20 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T003-template-registry-renderer.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T004-template-integrity-tests.md +17 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T005-template-closeout-quality-gates.md +16 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/Design.md +88 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T001-init-cli-contract.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T002-target-validation.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T003-scaffold-writer.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T004-magic-prompt-and-clipboard.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T005-dev-deps-install-option.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T006-init-command-integration.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/Design.md +53 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T001-delete-dead-src.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T002-delete-dead-tests.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T003-delete-assets.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T004-clean-savepoint.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T005-rewrite-agents-md.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T006-clean-package-json.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T007-verify.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/Design.md +104 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T001-board-command-data.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T002-board-view-state.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T003-transition-gates-and-writes.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T004-terminal-theme.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T005-ink-board-ui.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T006-board-integration-audit-entry.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/Design.md +88 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T001-audit-cli-contract.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T002-quality-gate-runner.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T003-snapshot-and-prompt.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T004-audit-orchestration-router.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T005-proposal-validation-apply.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T006-audit-review-state.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T007-audit-review-ui.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T008-audit-pipeline-integration.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/Design.md +103 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T001-acceptance-criteria-model.md +30 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T002-release-task-set-reader.md +33 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T003-board-data-and-plain-output.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T004-board-selection-state.md +33 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T005-ink-board-layout-cleanup.md +37 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T006-task-detail-popup.md +36 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T007-templates-acceptance-criteria.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T008-board-workflow-integration.md +41 -0
- package/.savepoint/releases/v1/epics/_archived/E09-doctor-command/Design.md +70 -0
- package/.savepoint/releases/v1/epics/_archived/E10-docs-and-packaging/Design.md +68 -0
- package/.savepoint/releases/v1/epics/_archived/E11-release-validation/Design.md +68 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +26 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +35 -0
- package/.savepoint/router.md +136 -0
- package/.savepoint/visual-identity.md +124 -0
- package/AGENTS.md +141 -0
- package/CLAUDE.md +1 -0
- package/GEMINI.md +1 -0
- package/LICENSE +21 -0
- package/Makefile +13 -0
- package/README.md +78 -0
- package/agent-skills/ink-tui-design/SKILL.md +309 -0
- package/agent-skills/ink-tui-design/references/component-patterns.md +371 -0
- package/agent-skills/ink-tui-design/references/hooks-guide.md +436 -0
- package/agent-skills/ink-tui-design/references/ink-gotchas.md +330 -0
- package/agent-skills/ink-tui-design/references/testing-patterns.md +384 -0
- package/agent-skills/savepoint-audit/SKILL.md +35 -0
- package/agent-skills/savepoint-build-task/SKILL.md +39 -0
- package/agent-skills/savepoint-create-plan/SKILL.md +28 -0
- package/agent-skills/savepoint-create-task/SKILL.md +31 -0
- package/agent-skills/savepoint-draft-prd/SKILL.md +32 -0
- package/agent-skills/savepoint-system-design/SKILL.md +33 -0
- package/agent-skills/superpowers/brainstorming/SKILL.md +165 -0
- package/agent-skills/superpowers/brainstorming/visual-companion.md +304 -0
- package/agent-skills/superpowers/dispatching-parallel-agents/SKILL.md +193 -0
- package/agent-skills/superpowers/executing-plans/SKILL.md +77 -0
- package/agent-skills/superpowers/finishing-a-development-branch/SKILL.md +213 -0
- package/agent-skills/superpowers/receiving-code-review/SKILL.md +226 -0
- package/agent-skills/superpowers/requesting-code-review/SKILL.md +115 -0
- package/agent-skills/superpowers/requesting-code-review/code-reviewer.md +160 -0
- package/agent-skills/superpowers/subagent-driven-development/SKILL.md +292 -0
- package/agent-skills/superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +27 -0
- package/agent-skills/superpowers/subagent-driven-development/implementer-prompt.md +113 -0
- package/agent-skills/superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/agent-skills/superpowers/systematic-debugging/SKILL.md +305 -0
- package/agent-skills/superpowers/systematic-debugging/condition-based-waiting.md +122 -0
- package/agent-skills/superpowers/systematic-debugging/defense-in-depth.md +130 -0
- package/agent-skills/superpowers/systematic-debugging/root-cause-tracing.md +183 -0
- package/agent-skills/superpowers/test-driven-development/SKILL.md +389 -0
- package/agent-skills/superpowers/test-driven-development/testing-anti-patterns.md +317 -0
- package/agent-skills/superpowers/verification-before-completion/SKILL.md +147 -0
- package/agent-skills/superpowers/writing-plans/SKILL.md +159 -0
- package/agent-skills/superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/assets/banner.png +0 -0
- package/assets/logo.png +0 -0
- package/assets/strawman.png +0 -0
- package/go.mod +33 -0
- package/go.sum +73 -0
- package/ink-cli-ui-design.zip +0 -0
- package/internal/board/board.go +121 -0
- package/internal/board/board_test.go +99 -0
- package/internal/board/card.go +72 -0
- package/internal/board/card_test.go +111 -0
- package/internal/board/column.go +61 -0
- package/internal/board/column_test.go +81 -0
- package/internal/board/detail.go +140 -0
- package/internal/board/detail_test.go +233 -0
- package/internal/board/epic_panel.go +69 -0
- package/internal/board/epic_panel_test.go +246 -0
- package/internal/board/help.go +40 -0
- package/internal/board/help_test.go +85 -0
- package/internal/board/layout.go +58 -0
- package/internal/board/layout_test.go +89 -0
- package/internal/board/model.go +151 -0
- package/internal/board/model_test.go +67 -0
- package/internal/board/release.go +42 -0
- package/internal/board/release_test.go +177 -0
- package/internal/board/transitions.go +88 -0
- package/internal/board/transitions_test.go +141 -0
- package/internal/board/update.go +155 -0
- package/internal/board/update_test.go +128 -0
- package/internal/board/view.go +190 -0
- package/internal/board/view_test.go +147 -0
- package/internal/data/config.go +87 -0
- package/internal/data/config_test.go +73 -0
- package/internal/data/discover.go +152 -0
- package/internal/data/discover_test.go +106 -0
- package/internal/data/errors.go +9 -0
- package/internal/data/lifecycle.go +37 -0
- package/internal/data/lifecycle_test.go +38 -0
- package/internal/data/parser.go +189 -0
- package/internal/data/parser_test.go +216 -0
- package/internal/data/router.go +52 -0
- package/internal/data/router_test.go +35 -0
- package/internal/data/task.go +46 -0
- package/internal/data/task_test.go +51 -0
- package/internal/data/write.go +144 -0
- package/internal/data/write_test.go +456 -0
- package/internal/styles/palette.go +47 -0
- package/internal/styles/styles.go +122 -0
- package/main.exe +0 -0
- package/main.go +11 -0
- package/package.json +25 -0
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
- package/scripts/vitest-preload.cjs +95 -0
- package/templates/project/.savepoint/Design.md +47 -0
- package/templates/project/.savepoint/PRD.md +34 -0
- package/templates/project/.savepoint/config.yml +27 -0
- package/templates/project/.savepoint/router.md +152 -0
- package/templates/project/.savepoint/visual-identity.md +122 -0
- package/templates/project/AGENTS.md +130 -0
- package/templates/prompts/audit-reconciliation.prompt.md +67 -0
- package/templates/prompts/design.prompt.md +43 -0
- package/templates/prompts/epic-design.prompt.md +43 -0
- package/templates/prompts/magic-prompt.prompt.md +7 -0
- package/templates/prompts/prd.prompt.md +42 -0
- package/templates/prompts/task-breakdown.prompt.md +54 -0
- package/templates/prompts/task-building.prompt.md +38 -0
- package/templates/prompts/task-planning.prompt.md +53 -0
- package/templates/release/v1/PRD.md +37 -0
package/go.mod
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module github.com/opencode/savepoint
|
|
2
|
+
|
|
3
|
+
go 1.26.2
|
|
4
|
+
|
|
5
|
+
require github.com/charmbracelet/bubbletea v1.3.10
|
|
6
|
+
|
|
7
|
+
require (
|
|
8
|
+
github.com/atotto/clipboard v0.1.4 // indirect
|
|
9
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
10
|
+
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
|
11
|
+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
|
12
|
+
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
13
|
+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
|
14
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
|
15
|
+
github.com/charmbracelet/x/term v0.2.2 // indirect
|
|
16
|
+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
|
17
|
+
github.com/clipperhouse/stringish v0.1.1 // indirect
|
|
18
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
|
19
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
20
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
|
21
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
22
|
+
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
23
|
+
github.com/mattn/go-runewidth v0.0.19 // indirect
|
|
24
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
25
|
+
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
26
|
+
github.com/muesli/termenv v0.16.0 // indirect
|
|
27
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
28
|
+
github.com/sahilm/fuzzy v0.1.1 // indirect
|
|
29
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
30
|
+
golang.org/x/sys v0.38.0 // indirect
|
|
31
|
+
golang.org/x/text v0.3.8 // indirect
|
|
32
|
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
33
|
+
)
|
package/go.sum
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
|
2
|
+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
|
3
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
4
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
5
|
+
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
|
6
|
+
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
|
7
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
8
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
9
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
10
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
11
|
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
12
|
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
13
|
+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
14
|
+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
15
|
+
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
|
16
|
+
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
|
17
|
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
|
18
|
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
|
19
|
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
20
|
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
21
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
|
22
|
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
|
23
|
+
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
24
|
+
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
25
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
26
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
27
|
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
|
28
|
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
|
29
|
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
30
|
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
31
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
32
|
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
33
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
34
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
35
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
36
|
+
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
37
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
|
38
|
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
39
|
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
40
|
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
41
|
+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
42
|
+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
43
|
+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
44
|
+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
45
|
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
|
46
|
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
47
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
48
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
49
|
+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
50
|
+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
51
|
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
52
|
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
53
|
+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
54
|
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
55
|
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
56
|
+
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
|
57
|
+
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
|
58
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
59
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
60
|
+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
|
61
|
+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
|
62
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
63
|
+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
64
|
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
65
|
+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
66
|
+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
67
|
+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
68
|
+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
69
|
+
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
|
70
|
+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
71
|
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
72
|
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
73
|
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
Binary file
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
|
|
8
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func Run() error {
|
|
13
|
+
model, err := newProjectModel(".")
|
|
14
|
+
if err != nil {
|
|
15
|
+
return err
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
19
|
+
if _, err := p.Run(); err != nil {
|
|
20
|
+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
21
|
+
return err
|
|
22
|
+
}
|
|
23
|
+
return nil
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func newProgramModel() Model {
|
|
27
|
+
return NewModel(nil, "v1", "E03-board-tui-core")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func newProjectModel(start string) (Model, error) {
|
|
31
|
+
d := data.NewDiscover()
|
|
32
|
+
root, err := d.FindSavepointRoot(start)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return Model{}, err
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
routerState, err := readRouterState(root)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return Model{}, err
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
releases, err := d.ListReleases(root)
|
|
43
|
+
if err != nil {
|
|
44
|
+
return Model{}, err
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
releaseIDs := make([]string, 0, len(releases))
|
|
48
|
+
releaseEpics := make(map[string][]string, len(releases))
|
|
49
|
+
tasks := []data.Task{}
|
|
50
|
+
|
|
51
|
+
for _, release := range releases {
|
|
52
|
+
releaseIDs = append(releaseIDs, release.ID)
|
|
53
|
+
epics, err := d.ListEpics(root, release.ID)
|
|
54
|
+
if err != nil {
|
|
55
|
+
return Model{}, err
|
|
56
|
+
}
|
|
57
|
+
for _, epic := range epics {
|
|
58
|
+
releaseEpics[release.ID] = append(releaseEpics[release.ID], epic.ID)
|
|
59
|
+
epicTasks, err := loadEpicTasks(d, root, release.ID, epic.ID)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return Model{}, err
|
|
62
|
+
}
|
|
63
|
+
tasks = append(tasks, epicTasks...)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
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()
|
|
76
|
+
|
|
77
|
+
return model, nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func readRouterState(root string) (*data.RouterState, error) {
|
|
81
|
+
content, err := os.ReadFile(filepath.Join(root, "router.md"))
|
|
82
|
+
if err != nil {
|
|
83
|
+
return nil, err
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return data.NewRouterReader().ReadState(string(content))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func loadEpicTasks(d *data.Discover, root, release, epic string) ([]data.Task, error) {
|
|
90
|
+
taskInfos, err := d.ListTasks(root, release, epic)
|
|
91
|
+
if err != nil {
|
|
92
|
+
return nil, err
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
parser := data.NewParser()
|
|
96
|
+
tasks := make([]data.Task, 0, len(taskInfos))
|
|
97
|
+
for _, taskInfo := range taskInfos {
|
|
98
|
+
content, err := os.ReadFile(taskInfo.Path)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return nil, err
|
|
101
|
+
}
|
|
102
|
+
task, err := parser.ParseTaskFile(taskInfo.Path, string(content))
|
|
103
|
+
if err != nil {
|
|
104
|
+
return nil, err
|
|
105
|
+
}
|
|
106
|
+
tasks = append(tasks, *task)
|
|
107
|
+
}
|
|
108
|
+
return tasks, nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func firstKnown(preferred string, values []string) string {
|
|
112
|
+
for _, value := range values {
|
|
113
|
+
if value == preferred {
|
|
114
|
+
return preferred
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if len(values) == 0 {
|
|
118
|
+
return ""
|
|
119
|
+
}
|
|
120
|
+
return values[0]
|
|
121
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestNewProgramModelUsesBoardCore(t *testing.T) {
|
|
13
|
+
m := newProgramModel()
|
|
14
|
+
m.Width = 100
|
|
15
|
+
|
|
16
|
+
got := m.View()
|
|
17
|
+
if strings.Contains(got, "Welcome to Savepoint") {
|
|
18
|
+
t.Fatal("program model still renders placeholder welcome screen")
|
|
19
|
+
}
|
|
20
|
+
for _, title := range []string{"PLANNED", "IN PROGRESS", "DONE"} {
|
|
21
|
+
if !strings.Contains(got, title) {
|
|
22
|
+
t.Fatalf("program model view missing board column %q", title)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestNewProjectModelLoadsReleasesEpicsAndTasks(t *testing.T) {
|
|
28
|
+
projectRoot := t.TempDir()
|
|
29
|
+
savepointRoot := filepath.Join(projectRoot, ".savepoint")
|
|
30
|
+
writeFile(t, filepath.Join(savepointRoot, "router.md"), `# Agent State Machine
|
|
31
|
+
|
|
32
|
+
## Current state
|
|
33
|
+
|
|
34
|
+
`+"```"+`yaml
|
|
35
|
+
state: task-building
|
|
36
|
+
release: v2
|
|
37
|
+
epic: E03-live
|
|
38
|
+
task: ""
|
|
39
|
+
next_action: "test"
|
|
40
|
+
`+"```"+`
|
|
41
|
+
`)
|
|
42
|
+
writeTask(t, savepointRoot, "v1", "E01-old", "T001-old", data.ColumnPlanned)
|
|
43
|
+
writeTask(t, savepointRoot, "v2", "E03-live", "T001-live", data.ColumnInProgress)
|
|
44
|
+
|
|
45
|
+
model, err := newProjectModel(projectRoot)
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatalf("newProjectModel() error = %v", err)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if model.Root != savepointRoot {
|
|
51
|
+
t.Errorf("Root = %q, want %q", model.Root, savepointRoot)
|
|
52
|
+
}
|
|
53
|
+
if model.SelectedRelease != "v2" {
|
|
54
|
+
t.Errorf("SelectedRelease = %q, want v2", model.SelectedRelease)
|
|
55
|
+
}
|
|
56
|
+
if model.SelectedEpic != "E03-live" {
|
|
57
|
+
t.Errorf("SelectedEpic = %q, want E03-live", model.SelectedEpic)
|
|
58
|
+
}
|
|
59
|
+
if len(model.Releases) != 2 {
|
|
60
|
+
t.Errorf("Releases = %v, want two releases", model.Releases)
|
|
61
|
+
}
|
|
62
|
+
if len(model.Epics) != 1 || model.Epics[0] != "E03-live" {
|
|
63
|
+
t.Errorf("Epics = %v, want [E03-live]", model.Epics)
|
|
64
|
+
}
|
|
65
|
+
tasks := model.Tasks[data.ColumnInProgress]
|
|
66
|
+
if len(tasks) != 1 || tasks[0].ID != "E03-live/T001-live" {
|
|
67
|
+
t.Errorf("visible in-progress tasks = %v, want E03-live/T001-live", tasks)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func writeTask(t *testing.T, root, release, epic, task string, column data.ColumnType) {
|
|
72
|
+
t.Helper()
|
|
73
|
+
path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task+".md")
|
|
74
|
+
phase := ""
|
|
75
|
+
if column == data.ColumnInProgress {
|
|
76
|
+
phase = "phase: build\n"
|
|
77
|
+
}
|
|
78
|
+
content := `---
|
|
79
|
+
id: ` + epic + `/` + task + `
|
|
80
|
+
release: ` + release + `
|
|
81
|
+
status: ` + string(column) + `
|
|
82
|
+
` + phase + `objective: "Test task"
|
|
83
|
+
depends_on: []
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
# Test task
|
|
87
|
+
`
|
|
88
|
+
writeFile(t, path, content)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func writeFile(t *testing.T, path, content string) {
|
|
92
|
+
t.Helper()
|
|
93
|
+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
94
|
+
t.Fatal(err)
|
|
95
|
+
}
|
|
96
|
+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
97
|
+
t.Fatal(err)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/data"
|
|
8
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const (
|
|
12
|
+
glyphBuild = "▣"
|
|
13
|
+
glyphTest = "◇"
|
|
14
|
+
glyphAudit = "◆"
|
|
15
|
+
|
|
16
|
+
cardOverhead = 4 // border (2) + padding (2×1)
|
|
17
|
+
)
|
|
18
|
+
|
|
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 {
|
|
21
|
+
inner := width - cardOverhead
|
|
22
|
+
if inner < 2 {
|
|
23
|
+
inner = 2
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
glyph := phaseGlyphStyled(t.Stage)
|
|
27
|
+
// glyph is 1 rune + 1 space prefix; leave room for "▣ "
|
|
28
|
+
idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), inner-2))
|
|
29
|
+
titleLine := styles.CardMeta.Render(truncate(t.Title, inner))
|
|
30
|
+
|
|
31
|
+
content := idLine + "\n" + titleLine
|
|
32
|
+
|
|
33
|
+
if focused {
|
|
34
|
+
return styles.CardFocused.Width(width).Render(content)
|
|
35
|
+
}
|
|
36
|
+
return styles.Card.Width(width).Render(content)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func phaseGlyphStyled(stage data.ProgressStage) string {
|
|
40
|
+
switch stage {
|
|
41
|
+
case data.StageTest:
|
|
42
|
+
return styles.GlyphTest.Render(glyphTest)
|
|
43
|
+
case data.StageAudit:
|
|
44
|
+
return styles.GlyphAudit.Render(glyphAudit)
|
|
45
|
+
default:
|
|
46
|
+
return styles.GlyphBuild.Render(glyphBuild)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// shortID strips the epic prefix and slug from a task ID.
|
|
51
|
+
// "E06-atari-noir-layout/T004-component-refinement" → "T004"
|
|
52
|
+
func shortID(id string) string {
|
|
53
|
+
if idx := strings.LastIndex(id, "/"); idx >= 0 {
|
|
54
|
+
id = id[idx+1:]
|
|
55
|
+
}
|
|
56
|
+
if idx := strings.Index(id, "-"); idx >= 0 {
|
|
57
|
+
id = id[:idx]
|
|
58
|
+
}
|
|
59
|
+
return id
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// truncate clips s to max runes, appending "…" if clipped.
|
|
63
|
+
func truncate(s string, max int) string {
|
|
64
|
+
runes := []rune(s)
|
|
65
|
+
if len(runes) <= max {
|
|
66
|
+
return s
|
|
67
|
+
}
|
|
68
|
+
if max <= 1 {
|
|
69
|
+
return "…"
|
|
70
|
+
}
|
|
71
|
+
return string(runes[:max-1]) + "…"
|
|
72
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/data"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestRenderCard_containsID(t *testing.T) {
|
|
11
|
+
task := data.Task{ID: "E04/T002", Title: "Build card", Stage: data.StageBuild}
|
|
12
|
+
got := RenderCard(task, 30, false)
|
|
13
|
+
if !strings.Contains(got, "T002") {
|
|
14
|
+
t.Error("RenderCard missing short task ID")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func TestRenderCard_containsTitle(t *testing.T) {
|
|
19
|
+
task := data.Task{ID: "T1", Title: "My title", Stage: data.StageBuild}
|
|
20
|
+
got := RenderCard(task, 30, false)
|
|
21
|
+
if !strings.Contains(got, "My title") {
|
|
22
|
+
t.Error("RenderCard missing task title")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func TestRenderCard_containsBuildGlyph(t *testing.T) {
|
|
27
|
+
task := data.Task{ID: "T1", Stage: data.StageBuild}
|
|
28
|
+
got := RenderCard(task, 30, false)
|
|
29
|
+
if !strings.Contains(got, glyphBuild) {
|
|
30
|
+
t.Errorf("RenderCard missing build glyph %q", glyphBuild)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func TestRenderCard_containsTestGlyph(t *testing.T) {
|
|
35
|
+
task := data.Task{ID: "T1", Stage: data.StageTest}
|
|
36
|
+
got := RenderCard(task, 30, false)
|
|
37
|
+
if !strings.Contains(got, glyphTest) {
|
|
38
|
+
t.Errorf("RenderCard missing test glyph %q", glyphTest)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestRenderCard_containsAuditGlyph(t *testing.T) {
|
|
43
|
+
task := data.Task{ID: "T1", Stage: data.StageAudit}
|
|
44
|
+
got := RenderCard(task, 30, false)
|
|
45
|
+
if !strings.Contains(got, glyphAudit) {
|
|
46
|
+
t.Errorf("RenderCard missing audit glyph %q", glyphAudit)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestRenderCard_focusedDoesNotPanic(t *testing.T) {
|
|
51
|
+
task := data.Task{ID: "T1", Title: "hello", Stage: data.StageBuild}
|
|
52
|
+
got := RenderCard(task, 30, true)
|
|
53
|
+
if got == "" {
|
|
54
|
+
t.Error("RenderCard focused returned empty string")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func TestRenderCard_titleTruncated(t *testing.T) {
|
|
59
|
+
long := "This is a very long title that should be truncated for sure"
|
|
60
|
+
task := data.Task{ID: "T1", Title: long, Stage: data.StageBuild}
|
|
61
|
+
got := RenderCard(task, 20, false)
|
|
62
|
+
if strings.Contains(got, long) {
|
|
63
|
+
t.Error("RenderCard should truncate long title")
|
|
64
|
+
}
|
|
65
|
+
if !strings.Contains(got, "…") {
|
|
66
|
+
t.Error("RenderCard should include ellipsis when title truncated")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func TestRenderCard_idTruncated(t *testing.T) {
|
|
71
|
+
long := "E04-board-components/T999-very-long-id"
|
|
72
|
+
task := data.Task{ID: long, Stage: data.StageBuild}
|
|
73
|
+
got := RenderCard(task, 20, false)
|
|
74
|
+
if strings.Contains(got, long) {
|
|
75
|
+
t.Error("RenderCard should truncate long ID")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func TestTruncate_shortString(t *testing.T) {
|
|
80
|
+
if truncate("hi", 10) != "hi" {
|
|
81
|
+
t.Error("truncate should not clip short string")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func TestTruncate_exactLength(t *testing.T) {
|
|
86
|
+
if truncate("hello", 5) != "hello" {
|
|
87
|
+
t.Error("truncate should not clip string at exact max")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func TestTruncate_clipsWithEllipsis(t *testing.T) {
|
|
92
|
+
got := truncate("hello", 4)
|
|
93
|
+
if got != "hel…" {
|
|
94
|
+
t.Errorf("truncate got %q, want %q", got, "hel…")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func TestTruncate_maxOne(t *testing.T) {
|
|
99
|
+
got := truncate("hello", 1)
|
|
100
|
+
if got != "…" {
|
|
101
|
+
t.Errorf("truncate(max=1) got %q, want %q", got, "…")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func TestRenderCard_defaultStageUsesBuildGlyph(t *testing.T) {
|
|
106
|
+
task := data.Task{ID: "T1", Stage: ""}
|
|
107
|
+
got := RenderCard(task, 30, false)
|
|
108
|
+
if !strings.Contains(got, glyphBuild) {
|
|
109
|
+
t.Error("RenderCard with empty stage should use build glyph")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/data"
|
|
8
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// RenderColumn renders a board column: header with label+count, task list, bordered container.
|
|
12
|
+
func RenderColumn(tasks []data.Task, col data.ColumnType, width, focusedTask int, focused bool) string {
|
|
13
|
+
inner := width - colOverhead
|
|
14
|
+
if inner < minColWidth {
|
|
15
|
+
inner = minColWidth
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
title := columnTitle(col)
|
|
19
|
+
header := fmt.Sprintf("%s (%d)", title, len(tasks))
|
|
20
|
+
if focused {
|
|
21
|
+
header = styles.ColumnTitleFocused.Render(header)
|
|
22
|
+
} else {
|
|
23
|
+
header = styles.ColumnTitle.Render(header)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
lines := []string{header, strings.Repeat("─", inner)}
|
|
27
|
+
if len(tasks) == 0 {
|
|
28
|
+
lines = append(lines, styles.TaskItem.Render("(empty)"))
|
|
29
|
+
} else {
|
|
30
|
+
for i, t := range tasks {
|
|
31
|
+
lines = append(lines, RenderCard(t, inner, focused && i == focusedTask))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
content := strings.Join(lines, "\n")
|
|
36
|
+
st := styles.Column.Width(width)
|
|
37
|
+
if focused {
|
|
38
|
+
st = styles.ColumnFocused.Width(width)
|
|
39
|
+
}
|
|
40
|
+
return st.Render(content)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func columnTitle(col data.ColumnType) string {
|
|
44
|
+
switch col {
|
|
45
|
+
case data.ColumnPlanned:
|
|
46
|
+
return "PLANNED"
|
|
47
|
+
case data.ColumnInProgress:
|
|
48
|
+
return "IN PROGRESS"
|
|
49
|
+
case data.ColumnDone:
|
|
50
|
+
return "DONE"
|
|
51
|
+
default:
|
|
52
|
+
return strings.ToUpper(string(col))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func taskLabel(t data.Task) string {
|
|
57
|
+
if t.Title == "" {
|
|
58
|
+
return t.ID
|
|
59
|
+
}
|
|
60
|
+
return fmt.Sprintf("%s %s", t.ID, t.Title)
|
|
61
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/data"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestRenderColumn_headerContainsLabel(t *testing.T) {
|
|
11
|
+
got := RenderColumn(nil, data.ColumnPlanned, 30, 0, false)
|
|
12
|
+
if !strings.Contains(got, "PLANNED") {
|
|
13
|
+
t.Error("RenderColumn missing PLANNED label")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func TestRenderColumn_headerContainsCount(t *testing.T) {
|
|
18
|
+
tasks := []data.Task{{ID: "T1", Title: "Task one", Column: data.ColumnPlanned}}
|
|
19
|
+
got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false)
|
|
20
|
+
if !strings.Contains(got, "(1)") {
|
|
21
|
+
t.Error("RenderColumn missing task count")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestRenderColumn_emptyShowsPlaceholder(t *testing.T) {
|
|
26
|
+
got := RenderColumn(nil, data.ColumnDone, 30, 0, false)
|
|
27
|
+
if !strings.Contains(got, "(empty)") {
|
|
28
|
+
t.Error("RenderColumn missing (empty) for empty column")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func TestRenderColumn_focusedDoesNotPanic(t *testing.T) {
|
|
33
|
+
tasks := []data.Task{{ID: "T1", Column: data.ColumnInProgress}}
|
|
34
|
+
got := RenderColumn(tasks, data.ColumnInProgress, 30, 0, true)
|
|
35
|
+
if got == "" {
|
|
36
|
+
t.Error("RenderColumn returned empty string for focused column")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func TestRenderColumn_allColumnTitles(t *testing.T) {
|
|
41
|
+
cases := []struct {
|
|
42
|
+
col data.ColumnType
|
|
43
|
+
label string
|
|
44
|
+
}{
|
|
45
|
+
{data.ColumnPlanned, "PLANNED"},
|
|
46
|
+
{data.ColumnInProgress, "IN PROGRESS"},
|
|
47
|
+
{data.ColumnDone, "DONE"},
|
|
48
|
+
}
|
|
49
|
+
for _, tc := range cases {
|
|
50
|
+
got := RenderColumn(nil, tc.col, 30, 0, false)
|
|
51
|
+
if !strings.Contains(got, tc.label) {
|
|
52
|
+
t.Errorf("RenderColumn missing label %q for col %q", tc.label, tc.col)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func TestRenderColumn_taskTitleRendered(t *testing.T) {
|
|
58
|
+
tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned}}
|
|
59
|
+
got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, false)
|
|
60
|
+
if !strings.Contains(got, "Build it") {
|
|
61
|
+
t.Error("RenderColumn missing task title")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func TestRenderColumn_rendersTaskCards(t *testing.T) {
|
|
66
|
+
tasks := []data.Task{{ID: "T2", Title: "Build it", Column: data.ColumnPlanned, Stage: data.StageAudit}}
|
|
67
|
+
got := RenderColumn(tasks, data.ColumnPlanned, 30, 0, true)
|
|
68
|
+
if !strings.Contains(got, glyphAudit) {
|
|
69
|
+
t.Error("RenderColumn should render task phase glyph from card")
|
|
70
|
+
}
|
|
71
|
+
if !strings.Contains(got, "╭") {
|
|
72
|
+
t.Error("RenderColumn should render bordered card")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestRenderColumn_emptyCountIsZero(t *testing.T) {
|
|
77
|
+
got := RenderColumn(nil, data.ColumnPlanned, 30, 0, false)
|
|
78
|
+
if !strings.Contains(got, "(0)") {
|
|
79
|
+
t.Error("RenderColumn missing (0) count for empty column")
|
|
80
|
+
}
|
|
81
|
+
}
|