Development Guidelines for taskmd
February 16, 2026 · View on GitHub
This document provides guidelines and conventions for developing the taskmd project. These instructions are designed to help maintain code quality, consistency, and reliability across the codebase.
Prerequisites
Before developing, ensure you have the following tools installed:
- Go (1.22+): https://go.dev/dl/
- pnpm:
npm install -g pnpm(for web frontend) - golangci-lint: Required for
make lintandmake lint-fix- macOS:
brew install golangci-lint - go install:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - Other: See golangci-lint install docs
- macOS:
- Git hooks: Run
git config core.hooksPath .githooksto enable project git hooks (see Git Hooks Setup)
Testing Requirements
CLI Testing Policy
IMPORTANT: All new CLI features MUST include comprehensive tests.
When implementing new CLI commands or features:
-
Create test files in the same package with
_test.gosuffix- Example:
internal/cli/graph.go→internal/cli/graph_test.go
- Example:
-
Required test coverage includes:
- ✅ Happy path tests - Verify the feature works correctly with valid inputs
- ✅ Format tests - Test all output formats (JSON, YAML, ASCII, etc.)
- ✅ Flag tests - Test all command-line flags and their combinations
- ✅ Error handling - Test invalid inputs and edge cases
- ✅ Integration tests - Test with real temporary files when applicable
-
Test naming convention:
func TestCommandName_FeatureDescription(t *testing.T)Example:
TestGraphCommand_ExcludeStatus_BugFix -
Test structure:
func TestMyFeature(t *testing.T) { // Setup tmpDir := createTestFiles(t) // Reset flags to known state flagVar = defaultValue // Execute err := runCommand(cmd, args) // Verify if err != nil { t.Fatalf("unexpected error: %v", err) } // Add specific assertions } -
Use test helpers:
- Create helper functions for common setup (e.g.,
createTestTaskFiles) - Use
t.TempDir()for temporary directories - Use
t.Helper()in helper functions
- Create helper functions for common setup (e.g.,
-
Examples of good test coverage:
- See
internal/cli/graph_test.gofor a comprehensive example - See
internal/graph/graph_test.gofor package-level tests
- See
Running Tests
# Run unit/integration tests
cd apps/cli && go test ./...
# Run specific test
go test ./internal/cli -run TestGraphCommand
# Run with verbose output
go test -v ./internal/cli -run TestGraphCommand
# Run with coverage
go test -cover ./...
Running E2E Tests
E2E tests build the real binary and invoke it as a subprocess, testing the full CLI including config loading, argument parsing, and output formatting.
# Run all e2e tests
cd apps/cli && make e2e
# Run a specific e2e test group
go test -tags e2e -count=1 -run TestConfig ./internal/e2e/...
# Run with verbose output
go test -tags e2e -count=1 -v ./internal/e2e/...
Note: make test does not include e2e tests. Run make e2e separately.
Test Coverage Goals
- CLI commands: Minimum 80% coverage
- Core packages (graph, parser, validator): Minimum 90% coverage
- Critical paths: 100% coverage
Code Quality Standards
Linting
The project uses golangci-lint for code quality checks, following Go conventions:
# Run linter (from apps/cli directory)
make lint
# Or run directly
golangci-lint run
# Auto-fix issues where possible
make lint-fix
# Run go mod tidy
make tidy
# Run all checks (test, lint, vet)
make check
Key quality metrics enforced:
- Function length: Max 60 lines per function (excluding comments)
- Promotes single responsibility principle
- Improves testability and readability
- Cyclomatic complexity: Max 15 complexity per function
- Catches overly complex branching logic
- Cognitive complexity: Max 20 cognitive complexity
- Measures how hard code is to understand
- Code quality: errcheck, staticcheck, govet, unused code detection
- Formatting: gofmt, goimports
Note on file length: Unlike some languages, Go doesn't enforce file-length limits. Instead, the focus is on keeping functions small and focused. If functions stay under 60 lines, files naturally remain manageable.
Common lint rules:
- Use
anyinstead ofinterface{} - No unused imports or variables
- Proper error handling (don't ignore errors)
- Consistent naming conventions
- No magic numbers - use named constants
Go Conventions
-
Error handling:
// Good if err != nil { return fmt.Errorf("operation failed: %w", err) } // Bad - don't ignore errors _ = someOperation() -
Use context for cleanup:
func TestSomething(t *testing.T) { tmpDir := t.TempDir() // Auto-cleanup // ... test code } -
Consistent formatting:
- Use
gofmt(automatically done by most editors) - Use
goimportsfor import organization
- Use
CLI Command Development
Adding a New Command
- Create the command file:
internal/cli/<command>.go - Define flags as package-level variables
- Register command in
init()function withrootCmd.AddCommand() - Create RunE function with signature
func(cmd *cobra.Command, args []string) error - Use GetGlobalFlags() for common flags (verbose, format, etc.)
- Add comprehensive tests in
internal/cli/<command>_test.go
Command Structure Template
package cli
import (
"github.com/spf13/cobra"
)
var (
// Command-specific flags
myFlag string
)
var myCmd = &cobra.Command{
Use: "mycommand [args]",
Short: "Brief description",
Long: `Detailed description with examples`,
Args: cobra.MaximumNArgs(1),
RunE: runMyCommand,
}
func init() {
rootCmd.AddCommand(myCmd)
myCmd.Flags().StringVar(&myFlag, "flag", "default", "description")
}
func runMyCommand(cmd *cobra.Command, args []string) error {
flags := GetGlobalFlags()
// Implementation
return nil
}
Task Management
When working on tasks, use the CLI to manage task status and dependencies.
During development: Use taskmd-dev to test your changes (see Building and Deployment section).
For stable operations: Use taskmd (the Homebrew-installed version).
Task File Conventions
When working on tasks:
-
Check task status before starting:
taskmd list tasks/cli # Or during development: taskmd-dev list tasks/cli -
Update task status as you work:
- Mark as
in-progresswhen starting - Mark as
completedwhen done - Check off subtasks
- [x]as you complete them
taskmd set 042 --status in-progress # Or during development: taskmd-dev set 042 --status in-progress - Mark as
-
Maintain a worklog as you work (unless
worklogs: falsein.taskmd.yaml):- Create/append to
tasks/<group>/.worklogs/<ID>.md - Add timestamped entries when starting, making decisions, hitting blockers, or finishing
- See the agent template (e.g.,
CLAUDE.md) for format details and examples
- Create/append to
-
Reference the task specification document:
- See
docs/taskmd_specification.mdfor task format conventions - Follow the defined frontmatter schema
- See
Task Dependencies
- Always check task dependencies before starting work
- Ensure dependent tasks are completed first
- Use the graph command to visualize dependencies:
taskmd graph --format ascii --exclude-status completed # Or during development: taskmd-dev graph --format ascii --exclude-status completed
Building and Deployment
Development Workflow
When working on the CLI, use the development build to test your changes while keeping the Homebrew-installed stable version available.
Install Development Binary
cd apps/cli
make install-dev
This installs the binary as taskmd-dev in ~/bin/, keeping your stable taskmd (from Homebrew) available.
Note: Ensure ~/bin is in your PATH. It should already be configured in ~/.zshrc.
Testing Your Changes
ALWAYS use taskmd-dev when testing changes you've made to the source code:
# After making code changes
cd apps/cli
make install-dev
# Test your changes
taskmd-dev list
taskmd-dev next
taskmd-dev set 042 --status completed
# Compare with stable version if needed
taskmd list # Uses Homebrew version
Quick Build for Testing
For rapid iteration without installing:
cd apps/cli
make build
# Run directly
./taskmd list
Build Options
| Command | Output | Use Case |
|---|---|---|
make build | apps/cli/taskmd | Quick local testing |
make install-dev | ~/bin/taskmd-dev | Development (recommended) |
make install | $GOPATH/bin/taskmd | Replace system binary |
make build-full | apps/cli/taskmd (with web) | Full build with embedded web assets |
Production Builds
For release builds with version information:
cd apps/cli
go build -ldflags="-X 'main.Version=1.0.0' -X 'main.GitCommit=$(git rev-parse HEAD)' -X 'main.BuildDate=$(date)'" -o bin/taskmd ./cmd/taskmd
Documentation
When to Update Documentation
Update documentation when:
- Adding new CLI commands or flags
- Changing existing behavior
- Adding new task file conventions
- Implementing new output formats
Specification Sync
The taskmd specification lives in docs/taskmd_specification.md (the canonical source). Two copies must stay in sync:
apps/cli/internal/cli/templates/TASKMD_SPEC.md(embedded in the CLI binary)apps/docs/reference/specification.md(docs site)
After editing docs/taskmd_specification.md, always run:
cd apps/cli && make sync-spec
This copies the canonical spec to both locations. A test (TestSpecTemplate_MatchesCanonicalSpec) will fail if they drift.
Documentation Locations
- CLI commands: Help text in the command definition
- Task format:
docs/taskmd_specification.md(canonical spec — see Specification Sync above) - Development: This file (
CLAUDE.md) - Project overview:
PLAN.md
Common Patterns
Scanner Usage
taskScanner := scanner.NewScanner(scanDir, flags.Verbose)
result, err := taskScanner.Scan()
if err != nil {
return fmt.Errorf("scan failed: %w", err)
}
tasks := result.Tasks
Output Formatting
Support multiple output formats consistently:
switch flags.Format {
case "json":
return outputJSON(data, outFile)
case "yaml":
return outputYAML(data, outFile)
case "table":
return outputTable(data, outFile)
default:
return fmt.Errorf("unsupported format: %s", flags.Format)
}
File Writing
var outFile *os.File
if outputPath != "" {
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
outFile = f
} else {
outFile = os.Stdout
}
Git Workflow
Git Hooks Setup
This project uses a .githooks/ directory for version-controlled git hooks. After cloning, configure Git to use them:
git config core.hooksPath .githooks
Active hooks:
- pre-commit: Runs
taskmd validateto check task files before each commit. The commit is blocked if validation fails.
Commit Messages
Follow conventional commit format:
type(scope): brief description
Longer description if needed
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Types:
feat: New featurefix: Bug fixtest: Adding testsdocs: Documentation changesrefactor: Code refactoringchore: Maintenance tasks
Before Committing
- Run tests:
make testorgo test ./... - Run e2e tests:
make e2e - Run linter:
make lintorgolangci-lint run - Build successfully:
make buildorgo build ./... - Test with development binary: Install and test your changes
make install-dev taskmd-dev list # Test basic functionality taskmd-dev next # Test your specific changes - Update relevant documentation
All checks (test, lint, build, manual testing) should pass before committing.
Troubleshooting
Tests Failing
- Check if flags are properly reset between tests
- Verify temporary directories are being used
- Look for race conditions or shared state
Build Failures
- Check for missing dependencies:
go mod tidy - Verify Go version compatibility
- Check for syntax errors:
go vet ./...
Linting Issues
- Run auto-fix:
golangci-lint run --fix - Check for unused imports
- Verify error handling patterns
Performance Considerations
Large Repositories
When working with large task repositories:
- Use streaming where possible
- Avoid loading all tasks into memory at once
- Consider pagination for output
- Use efficient data structures (maps for lookups)
Testing Performance
For performance-critical code:
- Add benchmark tests:
func BenchmarkMyFunction(b *testing.B) - Run benchmarks:
go test -bench=. - Profile if needed:
go test -cpuprofile=cpu.prof
Security
Input Validation
Always validate:
- File paths (prevent directory traversal)
- Task IDs (prevent injection)
- User input in flags
Safe File Operations
// Good - validate paths
if !strings.HasPrefix(filepath.Clean(userPath), basePath) {
return fmt.Errorf("invalid path")
}
// Good - proper permissions
os.WriteFile(path, data, 0644)
Questions or Issues?
- Check existing tasks for similar implementations
- Review test files for usage examples
- Refer to
docs/taskmd_specification.mdfor task format questions - Use the graph command to understand dependencies
Last Updated: 2026-02-08 Maintained By: taskmd contributors