- Add diagnostic warnings when cycles detected after dep add (bd-309) - Add semantic validation for parent-child dependency direction (bd-308) - Document cycle handling behavior in code, README, and DESIGN (bd-310) Changes: - cmd/bd/dep.go: Add DetectCycles() call and warning after dep add - internal/storage/sqlite/dependencies.go: Add parent-child direction validation and comprehensive cycle prevention comments - internal/storage/sqlite/dependencies_test.go: Add TestParentChildValidation - README.md: Add dependency types and cycle prevention section with examples - DESIGN.md: Add detailed cycle prevention design rationale and trade-offs
1097 lines
31 KiB
Markdown
1097 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
|
||
- Typical case (small trees): <10ms overhead
|
||
- Pathological case (deep chains): O(depth × branching) but limited by depth=100
|
||
|
||
**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
|
||
|