Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

All commands run inside `nix develop` shell (or use `nix develop -c '<command>'`):

```bash
# Build
go build -o spectr # Build binary
nix build # Build via Nix

# Lint & Format
nix develop -c lint # Run golangci-lint + markdownlint on spectr/
golangci-lint run # Go linting only

# Test
nix develop -c tests # Run tests with race detector (gotestsum)
go test ./... # Basic test run
go test -v ./internal/validation/... # Specific package

# Single test
go test -run TestValidateSpec ./internal/validation/

# Format (via treefmt)
nix fmt # Format Go + Nix files
```

## Architecture Overview

Spectr is a CLI tool for spec-driven development. Key concepts:
- specs/ - Current truth: what IS built (requirements + scenarios)
- changes/ - Proposals: what SHOULD change (deltas against specs)
- archive/ - Completed changes with timestamps

### Code Structure

```
cmd/ # CLI commands (thin layer using Kong framework)
├── root.go # CLI setup with kong.Context
├── init.go # spectr init
├── list.go # spectr list
├── validate.go # spectr validate
├── accept.go # spectr accept (converts tasks.md → tasks.jsonc)
├── pr.go # spectr pr archive|new (git worktree + PR creation)
└── view.go # spectr view

internal/ # Business logic (not importable externally)
├── validation/ # Validation rules for specs and changes
├── parsers/ # Requirement and delta parsing from markdown
├── archive/ # Archive workflow and spec merging
├── discovery/ # File discovery utilities
├── initialize/ # Init wizard and AI tool templates
├── tui/ # Interactive terminal UI (Bubble Tea)
├── domain/ # Core domain types (Spec, Change, Requirement)
├── pr/ # Pull request creation via git worktree
└── git/ # Git operations
```

### Key Dependencies
- Kong: CLI framework (`github.com/alecthomas/kong`)
- Bubble Tea/Bubbles/Lipgloss: TUI framework (Charmbracelet)
- Afero: Filesystem abstraction for testing

### Data Flow
1. Commands in `cmd/` parse flags and call `internal/` packages
2. `discovery/` finds spec/change files in `spectr/` directory
3. `parsers/` extracts requirements and scenarios from markdown
4. `validation/` enforces rules (scenarios required, format checks)
5. `archive/` merges delta specs into main specs

## Spectr Workflow

See `spectr/AGENTS.md` for detailed spec-driven development instructions:
- Create proposals in `spectr/changes/<id>/` with `proposal.md`, `tasks.md`, delta specs
- Run `spectr validate <id>` before implementation
- Run `spectr accept <id>` to convert tasks.md to tasks.jsonc
- Track task status in `tasks.jsonc` during implementation
- Archive completed changes with `spectr pr archive <id>`

## Delta Spec Format

```markdown
## ADDED Requirements
### Requirement: Feature Name
The system SHALL...

#### Scenario: Success case
- WHEN condition
- THEN result

## MODIFIED Requirements
### Requirement: Existing Feature
[Complete updated requirement with all scenarios]

## REMOVED Requirements
### Requirement: Old Feature
Reason: Why removing
Migration: How to handle
```

## Testing Patterns

Tests use table-driven style with `t.Run()`:
```go
tests := []struct {
name string
input string
wantErr bool
}{...}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {...})
}
```

Test fixtures in `testdata/`. TUI tests use `charmbracelet/x/exp/teatest`.

## Orchestration Model

This project uses an orchestrator pattern for complex tasks:
- Orchestrator (you): Maintains big picture, creates todo lists, delegates
- coder agent: Implements ONE specific todo item (`.claude/agents/coder.md`)
- tester agent: Verifies implementations with Playwright
- stuck agent: Escalates to human when blocked

Workflow: Create todos → delegate to coder → verify with tester → mark complete

<!-- spectr:start -->
# Spectr Instructions

These instructions are for AI assistants working in this project.

Always open `@/spectr/AGENTS.md` when the request:

- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big
performance/security work
- Sounds ambiguous and you need the authoritative spec before coding

Use `@/spectr/AGENTS.md` to learn:

- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines

When delegating tasks from a change proposal to subagents:

- Provide the proposal path: `spectr/changes/<id>/proposal.md`
- Include task context: `spectr/changes/<id>/tasks.jsonc`
- Reference delta specs: `spectr/changes/<id>/specs/<capability>/spec.md`

<!-- spectr:end -->
46 changes: 44 additions & 2 deletions cmd/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@ import (
"errors"
"fmt"
"os"
"sync"

"github.com/connerohnesorge/spectr/internal/discovery"
)

// discoveryCache caches the results of GetDiscoveredRoots to avoid
// redundant filesystem traversals within a single command execution.
var (
cachedRoots []discovery.SpectrRoot
errCachedRoots error
cachedCwd string
discoveryCacheMu sync.Mutex
)

// GetDiscoveredRoots returns all discovered spectr roots from the current
// working directory. It wraps discovery.FindSpectrRoots with cwd handling.
// Results are cached per working directory to avoid redundant
// filesystem traversals within a single command execution.
func GetDiscoveredRoots() ([]discovery.SpectrRoot, error) {
cwd, err := os.Getwd()
if err != nil {
Expand All @@ -19,15 +31,45 @@ func GetDiscoveredRoots() ([]discovery.SpectrRoot, error) {
)
}

discoveryCacheMu.Lock()
defer discoveryCacheMu.Unlock()

// If cwd changed, invalidate cache
if cachedCwd != cwd {
cachedRoots = nil
errCachedRoots = nil
cachedCwd = cwd
}

// Return cached result if available
if cachedRoots != nil || errCachedRoots != nil {
return cachedRoots, errCachedRoots
}

// Perform discovery
roots, err := discovery.FindSpectrRoots(cwd)
if err != nil {
return nil, fmt.Errorf(
errCachedRoots = fmt.Errorf(
"failed to discover spectr roots: %w",
err,
)

return nil, errCachedRoots
}

return roots, nil
cachedRoots = roots

return cachedRoots, nil
}

// ResetDiscoveryCache clears the discovery cache. This is primarily
// intended for testing where the working directory changes.
func ResetDiscoveryCache() {
discoveryCacheMu.Lock()
defer discoveryCacheMu.Unlock()
cachedRoots = nil
errCachedRoots = nil
cachedCwd = ""
}

// GetSingleRoot returns the first discovered root, or an error if no roots
Expand Down
Loading
Loading