Files
beads/DESIGN.md
Steve Yegge 21bd7809b5 Add cycle detection performance benchmarks (bd-311)
- Created comprehensive benchmark suite for cycle detection
- Tested linear chains, tree structures, and dense graphs
- Results: 3-4ms overhead per AddDependency is acceptable
- Documented findings in test file and DESIGN.md
- Closed bd-311 and epic bd-307
2025-10-16 13:32:44 -07:00

1102 lines
31 KiB
Markdown

# Beads - Dependency-Aware Issue Tracker
**Tagline**: "Issues chained together like beads"
## Vision
A lightweight, standalone issue tracker that makes dependency graphs first-class citizens. The killer feature: automatic detection of "ready work" - issues with no open blockers.
**Philosophy**:
- SQLite by default (zero setup, single binary + database file)
- PostgreSQL for teams/scale (multi-user, better performance)
- CLI-first, with clean output for both humans and scripts
- Dependencies are the core primitive, not an afterthought
- Full audit trail of all changes
---
## Architecture Diagrams
### System Architecture
```mermaid
graph TB
subgraph "CLI Layer"
CLI[bd CLI Commands]
Cobra[Cobra Framework]
end
subgraph "Storage Interface"
Storage[Storage Interface<br/>Ready for multiple backends]
end
subgraph "Current Implementation"
SQLite[SQLite Storage<br/>✅ Implemented]
end
subgraph "Planned Future"
Postgres[PostgreSQL Storage<br/>📋 Planned Phase 3]
end
subgraph "Data Layer"
DB1[(SQLite DB<br/>~/.beads/beads.db)]
DB2[(PostgreSQL<br/>Not yet implemented)]
end
CLI --> Cobra
Cobra --> Storage
Storage --> SQLite
Storage -.future.-> Postgres
SQLite --> DB1
Postgres -.-> DB2
style CLI fill:#e1f5ff
style Storage fill:#fff4e1
style SQLite fill:#4caf50,color:#fff
style Postgres fill:#bdbdbd,color:#fff
style DB1 fill:#e8f5e9
style DB2 fill:#f5f5f5
```
**Current Status:** Only SQLite is implemented. PostgreSQL support is tracked as bd-181 (storage driver interface).
### Entity Relationship Diagram
```mermaid
erDiagram
ISSUES ||--o{ DEPENDENCIES : "depends on"
ISSUES ||--o{ LABELS : "tagged with"
ISSUES ||--o{ EVENTS : "has history"
ISSUES {
string id PK "bd-1, bd-2, ..."
string title "max 500 chars"
string description
string design
string acceptance_criteria
string notes
string status "open|in_progress|blocked|closed"
int priority "0-4"
string issue_type "bug|feature|task|epic|chore"
string assignee
int estimated_minutes
datetime created_at
datetime updated_at
datetime closed_at
}
DEPENDENCIES {
string issue_id FK
string depends_on_id FK
string type "blocks|related|parent-child|discovered-from"
datetime created_at
string created_by
}
LABELS {
string issue_id FK
string label
}
EVENTS {
int64 id PK
string issue_id FK
string event_type
string actor
string old_value
string new_value
string comment
datetime created_at
}
```
### Dependency Flow & Ready Work Calculation
```mermaid
graph LR
subgraph "Issue States"
Open[Open Issues]
InProgress[In Progress]
Blocked[Blocked]
Closed[Closed]
end
subgraph "Dependency Analysis"
Check{Has open<br/>blockers?}
Ready[Ready Work<br/>bd ready]
NotReady[Not Ready<br/>Dependencies exist]
end
Open --> Check
Check -->|No blockers| Ready
Check -->|Has blockers| NotReady
NotReady -.depends on.-> InProgress
NotReady -.depends on.-> Open
NotReady -.depends on.-> Blocked
Closed -->|Unblocks| Open
style Ready fill:#4caf50,color:#fff
style NotReady fill:#ff9800,color:#fff
style Blocked fill:#f44336,color:#fff
style Closed fill:#9e9e9e,color:#fff
```
### CLI Command Structure
```mermaid
graph TB
BD[bd]
BD --> Init[init]
BD --> Create[create]
BD --> Update[update]
BD --> Show[show]
BD --> List[list]
BD --> Search[search]
BD --> Close[close]
BD --> Reopen[reopen]
BD --> Comment[comment]
BD --> Dep[dep]
Dep --> DepAdd[dep add]
Dep --> DepRemove[dep remove]
Dep --> DepTree[dep tree]
Dep --> DepCycles[dep cycles]
BD --> Label[label]
Label --> LabelAdd[label add]
Label --> LabelRemove[label remove]
Label --> LabelList[label list]
Label --> LabelIssues[label issues]
BD --> Ready[ready]
BD --> Blocked[blocked]
BD --> Stats[stats]
BD --> Config[config]
BD --> Export[export]
BD --> Import[import]
style BD fill:#2196f3,color:#fff
style Dep fill:#ff9800,color:#fff
style Label fill:#9c27b0,color:#fff
style Ready fill:#4caf50,color:#fff
style Export fill:#00bcd4,color:#fff
style Import fill:#00bcd4,color:#fff
```
### Data Flow: Creating an Issue with Dependencies
```mermaid
sequenceDiagram
participant User
participant CLI
participant Storage
participant SQLite
participant Events
User->>CLI: bd create "Fix bug"
CLI->>Storage: CreateIssue(issue, actor)
Storage->>SQLite: BEGIN TRANSACTION
Storage->>SQLite: INSERT INTO issues
Storage->>Events: INSERT event (created)
Storage->>SQLite: COMMIT
SQLite-->>Storage: issue ID (bd-42)
Storage-->>CLI: Success
CLI-->>User: Created bd-42
User->>CLI: bd dep add bd-42 bd-10
CLI->>Storage: AddDependency(bd-42, bd-10)
Storage->>SQLite: BEGIN TRANSACTION
Storage->>SQLite: INSERT INTO dependencies
Storage->>SQLite: Check for cycles
Storage->>Events: INSERT event (dep_added)
Storage->>SQLite: COMMIT
Storage-->>CLI: Success
CLI-->>User: Added dependency
User->>CLI: bd ready
CLI->>Storage: GetReadyWork()
Storage->>SQLite: Query ready_issues view
SQLite-->>Storage: Issues with no blockers
Storage-->>CLI: [bd-5, bd-12, ...]
CLI-->>User: Display ready work
```
### Collaborative Workflow: Humans + AI Agents + Git
```mermaid
graph TB
subgraph "Human Developer"
Human[👤 Human<br/>Developer]
end
subgraph "AI Worker Agents"
Agent1[🤖 Agent 1<br/>Feature Dev]
Agent2[🤖 Agent 2<br/>Bug Fixing]
Agent3[🤖 Agent 3<br/>Testing]
end
subgraph "Shared Project Repository"
Git[(🗂️ Git Repository)]
BeadsDB[(.beads/issues.jsonl)]
Code[(Source Code)]
end
subgraph "Local Workspaces"
HumanDB[(SQLite DB)]
Agent1DB[(SQLite DB)]
Agent2DB[(SQLite DB)]
end
Human -->|bd ready| HumanDB
Human -->|bd create/update| HumanDB
Human -->|bd export| BeadsDB
Human -->|git commit/push| Git
Agent1 -->|bd ready --json| Agent1DB
Agent1 -->|bd update status| Agent1DB
Agent1 -->|discovers bugs| Agent1DB
Agent1 -->|bd export| BeadsDB
Agent2 -->|bd ready --json| Agent2DB
Agent2 -->|bd close issue| Agent2DB
Agent2 -->|bd export| BeadsDB
Agent3 -->|bd create test task| Agent3DB
Agent3 -->|bd dep add| Agent3DB
Agent3 -->|bd export| BeadsDB
Git -->|git pull| Human
Git -->|git pull| Agent1
Git -->|git pull| Agent2
BeadsDB -->|bd import| HumanDB
BeadsDB -->|bd import| Agent1DB
BeadsDB -->|bd import| Agent2DB
Agent1 -->|git push| Git
Agent2 -->|git push| Git
Agent3 -->|git push| Git
Code -.lives alongside.-> BeadsDB
style Human fill:#64b5f6,color:#fff
style Agent1 fill:#81c784,color:#fff
style Agent2 fill:#81c784,color:#fff
style Agent3 fill:#81c784,color:#fff
style Git fill:#f06292,color:#fff
style BeadsDB fill:#ffd54f,color:#000
style HumanDB fill:#e0e0e0
style Agent1DB fill:#e0e0e0
style Agent2DB fill:#e0e0e0
```
**Workflow Steps:**
1. **Pull** - Everyone pulls latest code + `.beads/issues.jsonl` from git
2. **Import** - Run `bd import -i .beads/issues.jsonl` to sync local SQLite cache
3. **Query** - Run `bd ready` to find unblocked work
4. **Work** - Update issues as work progresses (`bd update`, `bd create`, `bd close`)
5. **Discover** - Agents create new issues when they find bugs/TODOs
6. **Export** - Run `bd export -o .beads/issues.jsonl` before committing
7. **Push** - Commit both code changes and `.beads/issues.jsonl` together
8. **Merge** - On conflicts, use `bd import --resolve-collisions` to auto-resolve
**Benefits:**
- Single source of truth (`.beads/issues.jsonl` in git)
- Fast local queries (SQLite cache)
- Offline work supported
- Automatic collision resolution on merge
- Full audit trail preserved
---
## Core Data Model
### Issues
```go
type Issue struct {
ID string // "bd-1", "bd-2" (beads- prefix)
Title string // max 500 chars
Description string // problem statement (what/why)
Design string // solution design (how)
AcceptanceCriteria string // definition of done
Notes string // working notes
Status Status // open, in_progress, blocked, closed
Priority int // 0 (highest) to 4 (lowest), default 2
IssueType IssueType // bug, feature, task, epic, chore
Assignee string // optional
EstimatedMinutes *int // optional
CreatedAt time.Time
UpdatedAt time.Time
ClosedAt *time.Time
}
type Status string
const (
StatusOpen Status = "open"
StatusInProgress Status = "in_progress"
StatusBlocked Status = "blocked"
StatusClosed Status = "closed"
)
type IssueType string
const (
TypeBug IssueType = "bug"
TypeFeature IssueType = "feature"
TypeTask IssueType = "task"
TypeEpic IssueType = "epic"
TypeChore IssueType = "chore"
)
```
### Dependencies
```go
type Dependency struct {
IssueID string // the issue that depends
DependsOnID string // the issue it depends on
Type DependencyType // relationship type
CreatedAt time.Time
CreatedBy string
}
type DependencyType string
const (
DepBlocks DependencyType = "blocks" // hard blocker
DepRelated DependencyType = "related" // soft relationship
DepParentChild DependencyType = "parent-child" // epic/subtask
DepDiscoveredFrom DependencyType = "discovered-from" // discovered during work
)
```
#### Cycle Prevention Design
**Goal**: Maintain a directed acyclic graph (DAG) across all dependency types.
**Rationale**: Cycles create three major problems:
1. **Ready Work Calculation**: Issues in a cycle appear blocked by each other, hiding them from `bd ready` even though there's no clear blocking reason. Example: A depends on B, B depends on A → neither appears as ready work.
2. **Semantic Confusion**: Circular dependencies are conceptually problematic. If A depends on B and B depends on A (directly or through other issues), which should be done first? The answer is ambiguous.
3. **Traversal Complexity**: Operations like `bd dep tree`, blocking propagation, and hierarchy display rely on DAG structure. Cycles require special handling (cycle detection, path marking) or risk infinite loops.
**Implementation Strategy**:
- **Prevention over Detection**: We prevent cycles at insertion time in `AddDependency`, so cycles can never exist in the database.
- **Cross-Type Checking**: We check ALL dependency types, not just `blocks`. Cross-type cycles (e.g., A blocks B, B parent-child A) are just as problematic as single-type cycles.
- **Recursive CTE**: SQLite recursive common table expression traverses from `DependsOnID` to check if `IssueID` is reachable. If yes, adding the edge would complete a cycle.
- **Depth Limit**: Traversal limited to 100 levels to prevent excessive query cost and handle edge cases.
- **Transaction Safety**: Cycle check happens in transaction before INSERT, so no partial state on rejection.
**Performance Considerations**:
- Cycle check runs on every `AddDependency` call (not skippable)
- Indexed on `dependencies.issue_id` for efficient traversal
- Cost grows with dependency graph depth, not total issue count
**Benchmark Results** (Apple M4 Max, October 2025):
- Linear chains: ~3.4-3.7ms per AddDependency (100-1000 issues)
- Tree structures: ~3.3-3.5ms per AddDependency (100-1000 issues)
- Dense graphs (5+ deps per issue): Can be slow for large graphs, but rare in practice
**Conclusion**: 3-4ms overhead is acceptable for typical workflows. No optimization needed.
**User Experience**:
- Clear error messages: "cannot add dependency: would create a cycle (bd-3 → bd-1 → bd-2 → bd-3)"
- After successful addition, we run `DetectCycles()` and warn if any cycles exist elsewhere
- `bd dep cycles` command for manual cycle detection and diagnosis
**Trade-offs**:
- ✅ Prevents semantic confusion and broken ready work calculation
- ✅ Keeps code simple (no cycle handling in traversals)
- ⚠️ Small performance overhead on every dependency addition
- ⚠️ Cannot represent certain real-world patterns (mutual blockers must be modeled differently)
**Alternative Considered**: Allow cycles and handle during traversal with cycle detection and path tracking. Rejected because it adds complexity everywhere dependencies are used and doesn't solve the semantic ambiguity problem.
### Labels
```go
type Label struct {
IssueID string
Label string // freeform tag
}
```
### Events (Audit Trail)
```go
type Event struct {
ID int64
IssueID string
EventType EventType
Actor string // who made the change
OldValue *string // before state (JSON)
NewValue *string // after state (JSON)
Comment *string // for comment events
CreatedAt time.Time
}
type EventType string
const (
EventCreated EventType = "created"
EventUpdated EventType = "updated"
EventStatusChanged EventType = "status_changed"
EventCommented EventType = "commented"
EventClosed EventType = "closed"
EventReopened EventType = "reopened"
EventDependencyAdded EventType = "dependency_added"
EventDependencyRemoved EventType = "dependency_removed"
EventLabelAdded EventType = "label_added"
EventLabelRemoved EventType = "label_removed"
)
```
---
## Backend Abstraction
### Storage Interface
```go
// Storage defines the interface for issue storage backends
type Storage interface {
// Issues
CreateIssue(ctx context.Context, issue *Issue, actor string) error
GetIssue(ctx context.Context, id string) (*Issue, error)
UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error
CloseIssue(ctx context.Context, id string, reason string, actor string) error
SearchIssues(ctx context.Context, query string, filter IssueFilter) ([]*Issue, error)
// Dependencies
AddDependency(ctx context.Context, dep *Dependency, actor string) error
RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error
GetDependencies(ctx context.Context, issueID string) ([]*Issue, error)
GetDependents(ctx context.Context, issueID string) ([]*Issue, error)
GetDependencyTree(ctx context.Context, issueID string, maxDepth int) ([]*TreeNode, error)
DetectCycles(ctx context.Context) ([][]*Issue, error)
// Labels
AddLabel(ctx context.Context, issueID, label, actor string) error
RemoveLabel(ctx context.Context, issueID, label, actor string) error
GetLabels(ctx context.Context, issueID string) ([]string, error)
GetIssuesByLabel(ctx context.Context, label string) ([]*Issue, error)
// Ready Work & Blocking
GetReadyWork(ctx context.Context, filter WorkFilter) ([]*Issue, error)
GetBlockedIssues(ctx context.Context) ([]*BlockedIssue, error)
// Events
AddComment(ctx context.Context, issueID, actor, comment string) error
GetEvents(ctx context.Context, issueID string, limit int) ([]*Event, error)
// Statistics
GetStatistics(ctx context.Context) (*Statistics, error)
// Lifecycle
Close() error
}
type IssueFilter struct {
Status *Status
Priority *int
IssueType *IssueType
Assignee *string
Labels []string
Limit int
}
type WorkFilter struct {
Status Status // default: open
Priority *int // filter by priority
Assignee *string
Limit int // default: 10
}
type BlockedIssue struct {
Issue
BlockedByCount int
BlockedBy []string // issue IDs
}
type TreeNode struct {
Issue
Depth int
Truncated bool // if hit max depth
}
type Statistics struct {
TotalIssues int
OpenIssues int
InProgressIssues int
ClosedIssues int
BlockedIssues int
ReadyIssues int
AverageLeadTime float64 // hours from open to closed
}
```
### Backend Implementations
```
storage/
storage.go // Interface definition
sqlite/
sqlite.go // SQLite implementation
migrations.go // Schema migrations
postgres/
postgres.go // PostgreSQL implementation
migrations.go // Schema migrations
factory.go // Backend factory
```
### Factory Pattern
```go
type Config struct {
Backend string // "sqlite" or "postgres"
// SQLite config
Path string // default: ~/.beads/beads.db
// PostgreSQL config
Host string
Port int
Database string
User string
Password string
SSLMode string
}
func NewStorage(config Config) (Storage, error) {
switch config.Backend {
case "sqlite":
return sqlite.New(config.Path)
case "postgres":
return postgres.New(config.Host, config.Port, config.Database,
config.User, config.Password, config.SSLMode)
default:
return nil, fmt.Errorf("unknown backend: %s", config.Backend)
}
}
```
---
## Schema Design
### SQLite Schema
```sql
-- Issues table
CREATE TABLE issues (
id TEXT PRIMARY KEY,
title TEXT NOT NULL CHECK(length(title) <= 500),
description TEXT NOT NULL DEFAULT '',
design TEXT NOT NULL DEFAULT '',
acceptance_criteria TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'open',
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority >= 0 AND priority <= 4),
issue_type TEXT NOT NULL DEFAULT 'task',
assignee TEXT,
estimated_minutes INTEGER,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
closed_at DATETIME
);
CREATE INDEX idx_issues_status ON issues(status);
CREATE INDEX idx_issues_priority ON issues(priority);
CREATE INDEX idx_issues_assignee ON issues(assignee);
CREATE INDEX idx_issues_created_at ON issues(created_at);
-- Dependencies table
CREATE TABLE dependencies (
issue_id TEXT NOT NULL,
depends_on_id TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'blocks',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT NOT NULL,
PRIMARY KEY (issue_id, depends_on_id),
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
FOREIGN KEY (depends_on_id) REFERENCES issues(id) ON DELETE CASCADE
);
CREATE INDEX idx_dependencies_issue ON dependencies(issue_id);
CREATE INDEX idx_dependencies_depends_on ON dependencies(depends_on_id);
-- Labels table (many-to-many)
CREATE TABLE labels (
issue_id TEXT NOT NULL,
label TEXT NOT NULL,
PRIMARY KEY (issue_id, label),
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
);
CREATE INDEX idx_labels_label ON labels(label);
-- Events table (audit trail)
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id TEXT NOT NULL,
event_type TEXT NOT NULL,
actor TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
comment TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
);
CREATE INDEX idx_events_issue ON events(issue_id);
CREATE INDEX idx_events_created_at ON events(created_at);
-- Ready work view (materialized via trigger)
-- Issues with no open dependencies
CREATE VIEW ready_issues AS
SELECT i.*
FROM issues i
WHERE i.status = 'open'
AND NOT EXISTS (
SELECT 1 FROM dependencies d
JOIN issues blocked ON d.depends_on_id = blocked.id
WHERE d.issue_id = i.id
AND d.type = 'blocks'
AND blocked.status IN ('open', 'in_progress', 'blocked')
);
-- Blocked issues view
CREATE VIEW blocked_issues AS
SELECT
i.*,
COUNT(d.depends_on_id) as blocked_by_count
FROM issues i
JOIN dependencies d ON i.id = d.issue_id
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE i.status IN ('open', 'in_progress', 'blocked')
AND d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked')
GROUP BY i.id;
```
### PostgreSQL Schema Extensions
PostgreSQL can leverage more advanced features:
```sql
-- Use JSONB for flexible metadata
ALTER TABLE issues ADD COLUMN metadata JSONB;
CREATE INDEX idx_issues_metadata ON issues USING GIN(metadata);
-- Use array type for labels (alternative to junction table)
-- (Keep junction table for compatibility, but could optimize)
-- Use recursive CTEs for dependency trees (more efficient)
-- Implement as stored function:
CREATE OR REPLACE FUNCTION get_dependency_tree(root_issue_id TEXT, max_depth INT DEFAULT 50)
RETURNS TABLE (
issue_id TEXT,
title TEXT,
status TEXT,
priority INT,
depth INT,
path TEXT[]
) AS $$
WITH RECURSIVE tree AS (
SELECT
i.id as issue_id,
i.title,
i.status,
i.priority,
0 as depth,
ARRAY[i.id] as path
FROM issues i
WHERE i.id = root_issue_id
UNION ALL
SELECT
i.id,
i.title,
i.status,
i.priority,
t.depth + 1,
t.path || i.id
FROM issues i
JOIN dependencies d ON i.id = d.depends_on_id
JOIN tree t ON d.issue_id = t.issue_id
WHERE t.depth < max_depth
AND NOT (i.id = ANY(t.path)) -- cycle detection
)
SELECT * FROM tree ORDER BY depth, priority;
$$ LANGUAGE SQL;
-- Cycle detection using CTEs
CREATE OR REPLACE FUNCTION detect_dependency_cycles()
RETURNS TABLE (cycle_path TEXT[]) AS $$
WITH RECURSIVE paths AS (
SELECT
issue_id,
depends_on_id,
ARRAY[issue_id, depends_on_id] as path,
false as is_cycle
FROM dependencies
UNION ALL
SELECT
p.issue_id,
d.depends_on_id,
p.path || d.depends_on_id,
d.depends_on_id = ANY(p.path)
FROM paths p
JOIN dependencies d ON p.depends_on_id = d.issue_id
WHERE NOT p.is_cycle
AND array_length(p.path, 1) < 100
)
SELECT DISTINCT path
FROM paths
WHERE is_cycle
ORDER BY path;
$$ LANGUAGE SQL;
```
---
## CLI Design
### Command Structure
```
beads [global options] <command> [command options]
Global Options:
--db <path> Database path (default: ~/.beads/beads.db)
--backend <type> Backend type: sqlite, postgres (default: sqlite)
--config <path> Config file path (default: ~/.beads/config.yaml)
--format <format> Output format: text, json, yaml (default: text)
--no-color Disable colored output
Commands:
init Initialize a new beads database
create Create a new issue
update Update an issue
show Show issue details
list List issues
search Search issues by text
close Close one or more issues
reopen Reopen a closed issue
comment Add a comment to an issue
dep add Add a dependency
dep remove Remove a dependency
dep tree Show dependency tree
dep cycles Detect dependency cycles
label add Add a label
label remove Remove a label
label list List all labels
label issues List issues with label
ready Show ready work (no blockers)
blocked Show blocked issues
stats Show statistics
config Manage configuration
export Export database to JSON/YAML
import Import from JSON/YAML
migrate Migrate from other issue trackers
help Show help
version Show version
```
### Example Commands
```bash
# Initialize
beads init # Creates ~/.beads/beads.db
beads init --db ./project.db # Project-local database
beads init --backend postgres # Interactive setup for PostgreSQL
# Create
beads create "Fix login bug" \
--description "Users can't log in with Google OAuth" \
--priority 1 \
--type bug \
--label "auth,critical"
# Update
beads update bd-1 --status in_progress --assignee "alice"
# Show
beads show bd-1 # Full details with dependencies
beads show bd-1 --format json # JSON output
# List
beads list # All open issues
beads list --status closed # Closed issues
beads list --priority 1 # P1 issues
beads list --label "auth" # Issues with label
# Dependencies
beads dep add bd-2 bd-1 # bd-2 depends on bd-1
beads dep tree bd-2 # Show full tree
beads dep cycles # Check for cycles
# Ready work
beads ready # Top 10 ready issues
beads ready --limit 20 --assignee alice
# Comments
beads comment bd-1 "Started investigation"
beads comment bd-1 --file notes.md # From file
# Close
beads close bd-1 "Fixed in commit abc123"
beads close bd-1 bd-2 bd-3 --reason "Duplicate"
# Search
beads search "oauth" # Full-text search
beads search "oauth" --status open # With filters
# Stats
beads stats # Overall statistics
beads stats --format json # Machine-readable
# Export/Import
beads export --output backup.json
beads import --input backup.json
beads migrate --from github --repo owner/repo
```
---
## Configuration
### Config File (~/.beads/config.yaml)
```yaml
# Default backend
backend: sqlite
# SQLite config
sqlite:
path: ~/.beads/beads.db
# PostgreSQL config
postgres:
host: localhost
port: 5432
database: beads
user: beads
password: ""
sslmode: prefer
# Display preferences
display:
color: true
format: text # text, json, yaml
date_format: "2006-01-02 15:04"
# Issue defaults
defaults:
priority: 2
type: task
status: open
# ID prefix (default: "bd-")
id_prefix: "bd-"
# Actor name (for audit trail)
actor: $USER
```
---
## Current Status & Roadmap
We dogfood beads for all project tracking! For current work and roadmap, see:
- `bd list` - All tracked issues
- `bd ready` - Available work
- `.beads/issues.jsonl` - Full issue database
The project is mature and feature-complete for 1.0. Most core features are implemented.
---
## Key Design Decisions
### Why SQLite Default?
1. **Zero setup**: Single binary + database file
2. **Portability**: Database is a file, easy to backup/share
3. **Performance**: More than enough for <100k issues
4. **Simplicity**: No server to run
5. **Git-friendly**: Can commit database file for small teams
### Why Support PostgreSQL?
1. **Scale**: Better for large teams (>10 people)
2. **Concurrency**: Better multi-user support
3. **Features**: Recursive CTEs, JSONB, full-text search
4. **Existing infrastructure**: Teams already running PostgreSQL
### ID Prefix: "bd-" vs "beads-"
- **bd-**: Shorter, easier to type
- **beads-**: More explicit
- **Configurable**: Let users choose in config
I lean toward **bd-** for brevity.
### Dependency Types
- **blocks**: Hard blocker (affects ready work calculation)
- **related**: Soft relationship (just for context)
- **parent-child**: Epic/subtask hierarchy
Only "blocks" affects ready work detection.
### Status vs. Blocked Field
Should we have a separate `blocked` status, or compute it dynamically?
**Decision**: Compute dynamically
- `blocked` status is redundant with dependency graph
- Auto-blocking based on dependencies is error-prone
- Let users manually set `blocked` if they want (e.g., blocked on external dependency)
- `ready` command shows what's actually unblocked
### Event Storage
Full audit trail in `events` table. This enables:
- Change history for issues
- Comment threads
- "Who changed what when" debugging
- Potential undo/revert functionality
---
## What to Port from VibeCoder
### ✅ Keep
- Core data model (issues, dependencies, labels, events)
- Ready work detection algorithm
- Dependency tree traversal
- Cycle detection
- CLI structure (create, update, show, list, etc.)
- Priority system (1-5)
- Issue types (bug, feature, task, epic, chore)
### ❌ Leave Behind
- MCP server (can add later as separate project)
- VibeCoder-specific concepts (missions, campaigns, amps)
- Temporal workflows
- Web portal integration
- Mission tracking
- Campaign aggregation views
### 🤔 Maybe Later
- Web UI (keep CLI-first)
- API server mode
- TUI with bubble tea
- GitHub/GitLab sync
- Email notifications
- Webhooks
---
## Go Dependencies
Minimal dependencies:
```go
// Core
database/sql
modernc.org/sqlite // SQLite driver (pure Go, no CGO)
// CLI
github.com/spf13/cobra // CLI framework
github.com/spf13/viper // Config management
github.com/fatih/color // Terminal colors
// Serialization
gopkg.in/yaml.v3 // YAML support
// Testing
github.com/stretchr/testify // Test assertions
```
No frameworks, no ORMs. Keep it simple.
**Note on SQLite Driver**: We use `modernc.org/sqlite`, a pure Go implementation that enables:
- Cross-compilation without C toolchain
- Faster builds (no CGO overhead)
- Static binary distribution
- Deployment in CGO-restricted environments
**Concurrency Limitation**: The pure Go driver may experience "database is locked" errors under extreme concurrent load (100+ simultaneous operations). This is acceptable because:
- Normal usage with WAL mode handles typical concurrent operations well
- The limitation only appears in stress tests, not real-world usage
- For very high concurrency needs (many simultaneous writers), consider the CGO-enabled `github.com/mattn/go-sqlite3` driver or PostgreSQL
---
## Open Questions
1. **Multi-database support**: Should one beads installation manage multiple databases?
- Probably yes: `beads --db project1.db` vs `beads --db project2.db`
2. **Git integration**: Should beads auto-commit the database?
- Probably no: Let users manage their own git workflow
- But provide hooks/examples
3. **Web UI**: Build one, or keep it CLI-only?
- Start CLI-only
- Web UI as separate project later (beads-web?)
4. **API server**: Should beads run as a server?
- Start as CLI tool
- Add `beads serve` command later for HTTP API
5. **Migrations**: How to handle schema changes?
- Embed migrations in binary
- Track schema version in database
- Auto-migrate on startup (with backup)
6. **Concurrency**: SQLite WAL mode for better concurrency?
- Yes, enable by default
- Document limitations (single writer at a time)
7. **Full-text search**: SQLite FTS5 or simple LIKE queries?
- Start with LIKE queries (simpler)
- Add FTS5 in phase 2
8. **Transactions**: Where do we need them?
- Issue creation (issue + labels + event)
- Dependency changes (dep + event + cycle check)
- Bulk operations (close multiple issues)
---
## Success Metrics
Beads is successful if:
1. **Installation**: `go install github.com/user/beads@latest` just works
2. **First use**: `beads init && beads create "test"` works in <10 seconds
3. **Performance**: Can handle 10k issues with instant CLI responses
4. **Portability**: Database file can be moved between machines, checked into git
5. **Adoption**: Used by at least 3 other developers/teams within 6 months