chore: remove issue ID references from comments and changelogs

Strip (bd-xxx), (gt-xxx) suffixes from code comments and changelog
entries. The descriptions remain meaningful without the ephemeral
issue IDs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-28 10:05:16 -08:00
parent b4deb96924
commit f46cc2e798
82 changed files with 1175 additions and 1182 deletions

View File

@@ -17,7 +17,7 @@ func validateBatchIssues(issues []*types.Issue) error {
}
// validateBatchIssuesWithCustomStatuses validates all issues in a batch,
// allowing custom statuses in addition to built-in ones (bd-1pj6).
// allowing custom statuses in addition to built-in ones.
func validateBatchIssuesWithCustomStatuses(issues []*types.Issue, customStatuses []string) error {
now := time.Now()
for i, issue := range issues {
@@ -67,7 +67,7 @@ func (s *SQLiteStorage) generateBatchIDs(ctx context.Context, conn *sql.Conn, is
var prefix string
err := conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix)
if err == sql.ErrNoRows || prefix == "" {
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
// CRITICAL: Reject operation if issue_prefix config is missing
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
} else if err != nil {
return fmt.Errorf("failed to get config: %w", err)
@@ -214,7 +214,7 @@ func checkForExistingIDs(ctx context.Context, conn *sql.Conn, issues []*types.Is
// }
//
// // After importing with explicit IDs, sync counters to prevent collisions
// REMOVED (bd-c7af): SyncAllCounters example - no longer needed with hash IDs
// REMOVED: SyncAllCounters example - no longer needed with hash IDs
//
// Performance:
// - 100 issues: ~30ms (vs ~900ms with CreateIssue loop)
@@ -250,7 +250,7 @@ func (s *SQLiteStorage) CreateIssuesWithFullOptions(ctx context.Context, issues
return nil
}
// Fetch custom statuses for validation (bd-1pj6)
// Fetch custom statuses for validation
customStatuses, err := s.GetCustomStatuses(ctx)
if err != nil {
return fmt.Errorf("failed to get custom statuses: %w", err)
@@ -268,7 +268,7 @@ func (s *SQLiteStorage) CreateIssuesWithFullOptions(ctx context.Context, issues
}
defer func() { _ = conn.Close() }()
// Use retry logic with exponential backoff to handle SQLITE_BUSY under concurrent load (bd-ola6)
// Use retry logic with exponential backoff to handle SQLITE_BUSY under concurrent load
if err := beginImmediateWithRetry(ctx, conn, 5, 10*time.Millisecond); err != nil {
return fmt.Errorf("failed to begin immediate transaction: %w", err)
}

View File

@@ -33,7 +33,7 @@ func (s *SQLiteStorage) AddDependency(ctx context.Context, dep *types.Dependency
return fmt.Errorf("issue %s not found", dep.IssueID)
}
// External refs (external:<project>:<capability>) don't need target validation (bd-zmmy)
// External refs (external:<project>:<capability>) don't need target validation
// They are resolved lazily at query time by CheckExternalDep
isExternalRef := strings.HasPrefix(dep.DependsOnID, "external:")
@@ -169,7 +169,7 @@ func (s *SQLiteStorage) AddDependency(ctx context.Context, dep *types.Dependency
return wrapDBError("mark issues dirty after adding dependency", err)
}
// Invalidate blocked issues cache since dependencies changed (bd-5qim)
// Invalidate blocked issues cache since dependencies changed
// Only invalidate for types that affect ready work calculation
if dep.Type.AffectsReadyWork() {
if err := s.invalidateBlockedCache(ctx, tx); err != nil {
@@ -231,7 +231,7 @@ func (s *SQLiteStorage) RemoveDependency(ctx context.Context, issueID, dependsOn
return wrapDBError("mark issues dirty after removing dependency", err)
}
// Invalidate blocked issues cache if this was a blocking dependency (bd-5qim)
// Invalidate blocked issues cache if this was a blocking dependency
if needsCacheInvalidation {
if err := s.invalidateBlockedCache(ctx, tx); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
@@ -627,7 +627,7 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
nodes = append(nodes, &node)
}
// Fetch external dependencies for all issues in the tree (bd-vks2)
// Fetch external dependencies for all issues in the tree
// External deps like "external:project:capability" don't exist in the issues
// table, so the recursive CTE above doesn't find them. We add them as
// synthetic leaf nodes here.
@@ -871,14 +871,14 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
// Messaging fields (bd-kwro)
// Messaging fields
var sender sql.NullString
var wisp sql.NullInt64
// Pinned field (bd-7h5)
// Pinned field
var pinned sql.NullInt64
// Template field (beads-1ra)
// Template field
var isTemplate sql.NullInt64
// Gate fields (bd-udsi)
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
var timeoutNs sql.NullInt64
@@ -929,22 +929,22 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
if originalType.Valid {
issue.OriginalType = originalType.String
}
// Messaging fields (bd-kwro)
// Messaging fields
if sender.Valid {
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
// Pinned field
if pinned.Valid && pinned.Int64 != 0 {
issue.Pinned = true
}
// Template field (beads-1ra)
// Template field
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Gate fields (bd-udsi)
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
}
@@ -993,14 +993,14 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
// Messaging fields (bd-kwro)
// Messaging fields
var sender sql.NullString
var wisp sql.NullInt64
// Pinned field (bd-7h5)
// Pinned field
var pinned sql.NullInt64
// Template field (beads-1ra)
// Template field
var isTemplate sql.NullInt64
// Gate fields (bd-udsi)
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
var timeoutNs sql.NullInt64
@@ -1050,22 +1050,22 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
if originalType.Valid {
issue.OriginalType = originalType.String
}
// Messaging fields (bd-kwro)
// Messaging fields
if sender.Valid {
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
// Pinned field
if pinned.Valid && pinned.Int64 != 0 {
issue.Pinned = true
}
// Template field (beads-1ra)
// Template field
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Gate fields (bd-udsi)
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
}

View File

@@ -85,19 +85,19 @@ func getMigrationDescription(name string) string {
"source_repo_column": "Adds source_repo column for multi-repo support",
"repo_mtimes_table": "Adds repo_mtimes table for multi-repo hydration caching",
"child_counters_table": "Adds child_counters table for hierarchical ID generation with ON DELETE CASCADE",
"blocked_issues_cache": "Adds blocked_issues_cache table for GetReadyWork performance optimization (bd-5qim)",
"orphan_detection": "Detects orphaned child issues and logs them for user action (bd-3852)",
"close_reason_column": "Adds close_reason column to issues table for storing closure explanations (bd-uyu)",
"tombstone_columns": "Adds tombstone columns (deleted_at, deleted_by, delete_reason, original_type) for inline soft-delete (bd-vw8)",
"messaging_fields": "Adds messaging fields (sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by) for inter-agent communication (bd-kwro)",
"blocked_issues_cache": "Adds blocked_issues_cache table for GetReadyWork performance optimization",
"orphan_detection": "Detects orphaned child issues and logs them for user action",
"close_reason_column": "Adds close_reason column to issues table for storing closure explanations",
"tombstone_columns": "Adds tombstone columns (deleted_at, deleted_by, delete_reason, original_type) for inline soft-delete",
"messaging_fields": "Adds messaging fields (sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by) for inter-agent communication",
"edge_consolidation": "Adds metadata and thread_id columns to dependencies table for edge schema consolidation (Decision 004)",
"migrate_edge_fields": "Migrates existing issue fields (replies_to, relates_to, duplicate_of, superseded_by) to dependency edges (Decision 004 Phase 3)",
"drop_edge_columns": "Drops deprecated edge columns (replies_to, relates_to, duplicate_of, superseded_by) from issues table (Decision 004 Phase 4)",
"pinned_column": "Adds pinned column for persistent context markers (bd-7h5)",
"is_template_column": "Adds is_template column for template molecules (beads-1ra)",
"remove_depends_on_fk": "Removes FK constraint on depends_on_id to allow external references (bd-zmmy)",
"additional_indexes": "Adds performance optimization indexes for common query patterns (bd-h0we)",
"gate_columns": "Adds gate columns (await_type, await_id, timeout_ns, waiters) for async coordination (bd-udsi)",
"pinned_column": "Adds pinned column for persistent context markers",
"is_template_column": "Adds is_template column for template molecules",
"remove_depends_on_fk": "Removes FK constraint on depends_on_id to allow external references",
"additional_indexes": "Adds performance optimization indexes for common query patterns",
"gate_columns": "Adds gate columns (await_type, await_id, timeout_ns, waiters) for async coordination",
}
if desc, ok := descriptions[name]; ok {

View File

@@ -7,7 +7,7 @@ import (
// MigrateBlockedIssuesCache creates the blocked_issues_cache table for performance optimization
// This cache materializes the recursive CTE computation from GetReadyWork to avoid
// expensive recursive queries on every call (bd-5qim)
// expensive recursive queries on every call
func MigrateBlockedIssuesCache(db *sql.DB) error {
// Check if table already exists
var tableName string

View File

@@ -6,7 +6,7 @@ import (
)
// MigrateTombstoneColumns adds tombstone support columns to the issues table.
// These columns support inline soft-delete (bd-vw8) replacing deletions.jsonl:
// These columns support inline soft-delete, replacing deletions.jsonl:
// - deleted_at: when the issue was deleted
// - deleted_by: who deleted the issue
// - delete_reason: why the issue was deleted
@@ -43,7 +43,7 @@ func MigrateTombstoneColumns(db *sql.DB) error {
}
}
// Add partial index on deleted_at for efficient TTL queries (bd-saa)
// Add partial index on deleted_at for efficient TTL queries
// Only indexes non-NULL values, making it very efficient for tombstone filtering
_, err := db.Exec(`CREATE INDEX IF NOT EXISTS idx_issues_deleted_at ON issues(deleted_at) WHERE deleted_at IS NOT NULL`)
if err != nil {

View File

@@ -6,7 +6,7 @@ import (
)
// MigrateMessagingFields adds messaging and graph link support columns to the issues table.
// These columns support inter-agent communication (bd-kwro):
// These columns support inter-agent communication:
// - sender: who sent this message
// - ephemeral: can be bulk-deleted when closed
// - replies_to: issue ID for conversation threading

View File

@@ -6,7 +6,7 @@ import (
)
// MigratePinnedColumn adds the pinned column to the issues table.
// Pinned issues are persistent context markers that should not be treated as work items (bd-7h5).
// Pinned issues are persistent context markers that should not be treated as work items.
func MigratePinnedColumn(db *sql.DB) error {
// Check if column already exists
var columnExists bool

View File

@@ -6,14 +6,14 @@ import (
)
// MigrateAdditionalIndexes adds performance optimization indexes identified
// during schema review (bd-h0we).
// during schema review.
//
// Indexes added:
// - idx_issues_updated_at: For GetStaleIssues date filtering (bd-bha9)
// - idx_issues_status_priority: For common list query patterns (bd-a9y3)
// - idx_labels_label_issue: Covering index for label lookups (bd-jke6)
// - idx_dependencies_issue_type: For blocked issues queries (bd-8x3w)
// - idx_events_issue_type: For close reason queries (bd-lk39)
// - idx_issues_updated_at: For GetStaleIssues date filtering
// - idx_issues_status_priority: For common list query patterns
// - idx_labels_label_issue: Covering index for label lookups
// - idx_dependencies_issue_type: For blocked issues queries
// - idx_events_issue_type: For close reason queries
func MigrateAdditionalIndexes(db *sql.DB) error {
indexes := []struct {
name string

View File

@@ -5,7 +5,7 @@ import (
"fmt"
)
// MigrateGateColumns adds gate-related columns to the issues table for async coordination (bd-udsi).
// MigrateGateColumns adds gate-related columns to the issues table for async coordination.
// Gate fields enable agents to wait on external conditions (CI completion, human approval, etc.)
func MigrateGateColumns(db *sql.DB) error {
columns := []struct {

View File

@@ -6,7 +6,7 @@ import (
)
// MigrateAgentFields adds agent-specific fields to the issues table.
// These fields support the agent-as-bead pattern (gt-v2gkv, gt-h5sza):
// These fields support the agent-as-bead pattern:
// - hook_bead: current work attached to agent's hook (0..1 cardinality)
// - role_bead: reference to role definition bead
// - agent_state: agent-reported state (idle|running|stuck|stopped)

View File

@@ -59,23 +59,23 @@ func formatJSONStringArray(arr []string) string {
return string(data)
}
// REMOVED (bd-8e05): getNextIDForPrefix and AllocateNextID - sequential ID generation
// REMOVED: getNextIDForPrefix and AllocateNextID - sequential ID generation
// no longer needed with hash-based IDs
// Migration functions moved to migrations.go (bd-fc2d, bd-b245)
// Migration functions moved to migrations.go
// getNextChildNumber atomically generates the next child number for a parent ID
// Uses the child_counters table for atomic, cross-process child ID generation
// Hash ID generation functions moved to hash_ids.go (bd-90a5)
// Hash ID generation functions moved to hash_ids.go
// REMOVED (bd-c7af): SyncAllCounters - no longer needed with hash IDs
// REMOVED: SyncAllCounters - no longer needed with hash IDs
// REMOVED (bd-166): derivePrefixFromPath was causing duplicate issues with wrong prefix
// REMOVED: derivePrefixFromPath was causing duplicate issues with wrong prefix
// The database should ALWAYS have issue_prefix config set explicitly (by 'bd init' or auto-import)
// Never derive prefix from filename - it leads to silent data corruption
// CreateIssue creates a new issue
func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
// Fetch custom statuses for validation (bd-1pj6)
// Fetch custom statuses for validation
customStatuses, err := s.GetCustomStatuses(ctx)
if err != nil {
return fmt.Errorf("failed to get custom statuses: %w", err)
@@ -116,7 +116,7 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
return fmt.Errorf("validation failed: %w", err)
}
// Compute content hash (bd-95)
// Compute content hash
if issue.ContentHash == "" {
issue.ContentHash = issue.ComputeContentHash()
}
@@ -138,7 +138,7 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
// We use raw Exec instead of BeginTx because database/sql doesn't support transaction
// modes in BeginTx, and modernc.org/sqlite's BeginTx always uses DEFERRED mode.
//
// Use retry logic with exponential backoff to handle SQLITE_BUSY under concurrent load (bd-ola6)
// Use retry logic with exponential backoff to handle SQLITE_BUSY under concurrent load
if err := beginImmediateWithRetry(ctx, conn, 5, 10*time.Millisecond); err != nil {
return fmt.Errorf("failed to begin immediate transaction: %w", err)
}
@@ -156,14 +156,14 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
var configPrefix string
err = conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&configPrefix)
if err == sql.ErrNoRows || configPrefix == "" {
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
// CRITICAL: Reject operation if issue_prefix config is missing
// This prevents duplicate issues with wrong prefix
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
} else if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
// Use IDPrefix override if set, combined with config prefix (bd-hobo)
// Use IDPrefix override if set, combined with config prefix
// e.g., configPrefix="bd" + IDPrefix="wisp" → "bd-wisp"
prefix := configPrefix
if issue.IDPrefix != "" {
@@ -172,14 +172,14 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
// Generate or validate ID
if issue.ID == "" {
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
// Generate hash-based ID with adaptive length based on database size
generatedID, err := GenerateIssueID(ctx, conn, prefix, issue, actor)
if err != nil {
return wrapDBError("generate issue ID", err)
}
issue.ID = generatedID
} else {
// Validate that explicitly provided ID matches the configured prefix (bd-177)
// Validate that explicitly provided ID matches the configured prefix
if err := ValidateIssueIDPrefix(issue.ID, prefix); err != nil {
return wrapDBError("validate issue ID prefix", err)
}
@@ -235,7 +235,7 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
}
// validateBatchIssues validates all issues in a batch and sets timestamps
// Batch operation functions moved to batch_ops.go (bd-c796)
// Batch operation functions moved to batch_ops.go
// GetIssue retrieves an issue by ID
func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue, error) {
@@ -260,19 +260,19 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
// Messaging fields (bd-kwro)
// Messaging fields
var sender sql.NullString
var wisp sql.NullInt64
// Pinned field (bd-7h5)
// Pinned field
var pinned sql.NullInt64
// Template field (beads-1ra)
// Template field
var isTemplate sql.NullInt64
// Gate fields (bd-udsi)
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
var timeoutNs sql.NullInt64
var waiters sql.NullString
// Agent fields (gt-h5sza)
// Agent fields
var hookBead sql.NullString
var roleBead sql.NullString
var agentState sql.NullString
@@ -353,22 +353,22 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
if originalType.Valid {
issue.OriginalType = originalType.String
}
// Messaging fields (bd-kwro)
// Messaging fields
if sender.Valid {
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
// Pinned field
if pinned.Valid && pinned.Int64 != 0 {
issue.Pinned = true
}
// Template field (beads-1ra)
// Template field
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Gate fields (bd-udsi)
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
}
@@ -381,7 +381,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
if waiters.Valid && waiters.String != "" {
issue.Waiters = parseJSONStringArray(waiters.String)
}
// Agent fields (gt-h5sza)
// Agent fields
if hookBead.Valid {
issue.HookBead = hookBead.String
}
@@ -503,14 +503,14 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
// Messaging fields (bd-kwro)
// Messaging fields
var sender sql.NullString
var wisp sql.NullInt64
// Pinned field (bd-7h5)
// Pinned field
var pinned sql.NullInt64
// Template field (beads-1ra)
// Template field
var isTemplate sql.NullInt64
// Gate fields (bd-udsi)
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
var timeoutNs sql.NullInt64
@@ -585,22 +585,22 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
if originalType.Valid {
issue.OriginalType = originalType.String
}
// Messaging fields (bd-kwro)
// Messaging fields
if sender.Valid {
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
// Pinned field
if pinned.Valid && pinned.Int64 != 0 {
issue.Pinned = true
}
// Template field (beads-1ra)
// Template field
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Gate fields (bd-udsi)
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
}
@@ -638,14 +638,14 @@ var allowedUpdateFields = map[string]bool{
"estimated_minutes": true,
"external_ref": true,
"closed_at": true,
// Messaging fields (bd-kwro)
// Messaging fields
"sender": true,
"wisp": true, // Database column is 'ephemeral', mapped in UpdateIssue
// Pinned field (bd-7h5)
// Pinned field
"pinned": true,
// NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
// Use AddDependency() to create graph edges instead
// Agent slot fields (gt-h5sza)
// Agent slot fields
"hook_bead": true,
"role_bead": true,
"agent_state": true,
@@ -655,7 +655,7 @@ var allowedUpdateFields = map[string]bool{
}
// validatePriority validates a priority value
// Validation functions moved to validators.go (bd-d9e0)
// Validation functions moved to validators.go
// determineEventType determines the event type for an update based on old and new status
func determineEventType(oldIssue *types.Issue, updates map[string]interface{}) types.EventType {
@@ -734,7 +734,7 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[
return fmt.Errorf("issue %s not found", id)
}
// Fetch custom statuses for validation (bd-1pj6)
// Fetch custom statuses for validation
customStatuses, err := s.GetCustomStatuses(ctx)
if err != nil {
return wrapDBError("get custom statuses", err)
@@ -767,7 +767,7 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[
// Auto-manage closed_at when status changes (enforce invariant)
setClauses, args = manageClosedAt(oldIssue, updates, setClauses, args)
// Recompute content_hash if any content fields changed (bd-95)
// Recompute content_hash if any content fields changed
contentChanged := false
contentFields := []string{"title", "description", "design", "acceptance_criteria", "notes", "status", "priority", "issue_type", "assignee", "external_ref"}
for _, field := range contentFields {
@@ -886,7 +886,7 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
// Invalidate blocked issues cache if status changed (bd-5qim)
// Invalidate blocked issues cache if status changed
// Status changes affect which issues are blocked (blockers must be open/in_progress/blocked)
if _, statusChanged := updates["status"]; statusChanged {
if err := s.invalidateBlockedCache(ctx, tx); err != nil {
@@ -1023,14 +1023,14 @@ func (s *SQLiteStorage) RenameDependencyPrefix(ctx context.Context, oldPrefix, n
return nil
}
// RenameCounterPrefix is a no-op with hash-based IDs (bd-8e05)
// RenameCounterPrefix is a no-op with hash-based IDs
// Kept for backward compatibility with rename-prefix command
func (s *SQLiteStorage) RenameCounterPrefix(ctx context.Context, oldPrefix, newPrefix string) error {
// Hash-based IDs don't use counters, so nothing to update
return nil
}
// ResetCounter is a no-op with hash-based IDs (bd-8e05)
// ResetCounter is a no-op with hash-based IDs
// Kept for backward compatibility
func (s *SQLiteStorage) ResetCounter(ctx context.Context, prefix string) error {
// Hash-based IDs don't use counters, so nothing to reset
@@ -1086,7 +1086,7 @@ func (s *SQLiteStorage) CloseIssue(ctx context.Context, id string, reason string
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
// Invalidate blocked issues cache since status changed to closed (bd-5qim)
// Invalidate blocked issues cache since status changed to closed
// Closed issues don't block others, so this affects blocking calculations
if err := s.invalidateBlockedCache(ctx, tx); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
@@ -1155,7 +1155,7 @@ func (s *SQLiteStorage) CreateTombstone(ctx context.Context, id string, actor st
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
// Invalidate blocked issues cache since status changed (bd-5qim)
// Invalidate blocked issues cache since status changed
// Tombstone issues don't block others, so this affects blocking calculations
if err := s.invalidateBlockedCache(ctx, tx); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
@@ -1188,7 +1188,7 @@ func (s *SQLiteStorage) DeleteIssue(ctx context.Context, id string) error {
return fmt.Errorf("failed to delete events: %w", err)
}
// Delete comments (no FK cascade on this table) (bd-687g)
// Delete comments (no FK cascade on this table)
_, err = tx.ExecContext(ctx, `DELETE FROM comments WHERE issue_id = ?`, id)
if err != nil {
return fmt.Errorf("failed to delete comments: %w", err)
@@ -1218,7 +1218,7 @@ func (s *SQLiteStorage) DeleteIssue(ctx context.Context, id string) error {
return wrapDBError("commit delete transaction", err)
}
// REMOVED (bd-c7af): Counter sync after deletion - no longer needed with hash IDs
// REMOVED: Counter sync after deletion - no longer needed with hash IDs
return nil
}
@@ -1272,7 +1272,7 @@ func (s *SQLiteStorage) DeleteIssues(ctx context.Context, ids []string, cascade
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
// REMOVED (bd-c7af): Counter sync after deletion - no longer needed with hash IDs
// REMOVED: Counter sync after deletion - no longer needed with hash IDs
return result, nil
}
@@ -1424,7 +1424,7 @@ func (s *SQLiteStorage) populateDeleteStats(ctx context.Context, tx *sql.Tx, inC
}
func (s *SQLiteStorage) executeDelete(ctx context.Context, tx *sql.Tx, inClause string, args []interface{}, result *DeleteIssuesResult) error {
// Note: This method now creates tombstones instead of hard-deleting (bd-3b4)
// Note: This method now creates tombstones instead of hard-deleting
// Only dependencies are deleted - issues are converted to tombstones
// 1. Delete dependencies - tombstones don't block other issues
@@ -1500,7 +1500,7 @@ func (s *SQLiteStorage) executeDelete(ctx context.Context, tx *sql.Tx, inClause
}
}
// 4. Invalidate blocked issues cache since statuses changed (bd-5qim)
// 4. Invalidate blocked issues cache since statuses changed
if err := s.invalidateBlockedCache(ctx, tx); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
}
@@ -1591,7 +1591,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
whereClauses = append(whereClauses, "status = ?")
args = append(args, *filter.Status)
} else if !filter.IncludeTombstones {
// Exclude tombstones by default unless explicitly filtering for them (bd-1bu)
// Exclude tombstones by default unless explicitly filtering for them
whereClauses = append(whereClauses, "status != ?")
args = append(args, types.StatusTombstone)
}
@@ -1686,7 +1686,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", ")))
}
// Wisp filtering (bd-kwro.9)
// Wisp filtering
if filter.Ephemeral != nil {
if *filter.Ephemeral {
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
@@ -1695,7 +1695,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
}
}
// Pinned filtering (bd-7h5)
// Pinned filtering
if filter.Pinned != nil {
if *filter.Pinned {
whereClauses = append(whereClauses, "pinned = 1")
@@ -1704,7 +1704,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
}
}
// Template filtering (beads-1ra)
// Template filtering
if filter.IsTemplate != nil {
if *filter.IsTemplate {
whereClauses = append(whereClauses, "is_template = 1")
@@ -1713,7 +1713,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
}
}
// Parent filtering (bd-yqhh): filter children by parent issue
// Parent filtering: filter children by parent issue
if filter.ParentID != nil {
whereClauses = append(whereClauses, "id IN (SELECT issue_id FROM dependencies WHERE type = 'parent-child' AND depends_on_id = ?)")
args = append(args, *filter.ParentID)

View File

@@ -13,16 +13,16 @@ import (
// GetReadyWork returns issues with no open blockers
// By default, shows both 'open' and 'in_progress' issues so epics/tasks
// ready to close are visible (bd-165)
// Excludes pinned issues which are persistent anchors, not actionable work (bd-92u)
// ready to close are visible.
// Excludes pinned issues which are persistent anchors, not actionable work.
func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilter) ([]*types.Issue, error) {
whereClauses := []string{
"i.pinned = 0", // Exclude pinned issues (bd-92u)
"(i.ephemeral = 0 OR i.ephemeral IS NULL)", // Exclude wisps (hq-t15s)
"i.pinned = 0", // Exclude pinned issues
"(i.ephemeral = 0 OR i.ephemeral IS NULL)", // Exclude wisps
}
args := []interface{}{}
// Default to open OR in_progress if not specified (bd-165)
// Default to open OR in_progress if not specified
if filter.Status == "" {
whereClauses = append(whereClauses, "i.status IN ('open', 'in_progress')")
} else {
@@ -30,12 +30,12 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
args = append(args, filter.Status)
}
// Filter by issue type (gt-ktf3: MQ integration)
// Filter by issue type for MQ integration
if filter.Type != "" {
whereClauses = append(whereClauses, "i.issue_type = ?")
args = append(args, filter.Type)
} else {
// Exclude workflow types from ready work by default (gt-7xtn)
// Exclude workflow types from ready work by default
// These are internal workflow items, not work for polecats to claim:
// - merge-request: processed by Refinery
// - gate: async wait conditions
@@ -123,7 +123,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
}
orderBySQL := buildOrderByClause(sortPolicy)
// Use blocked_issues_cache for performance (bd-5qim)
// Use blocked_issues_cache for performance
// This optimization replaces the recursive CTE that computed blocked issues on every query.
// Performance improvement: 752ms → 29ms on 10K issues (25x speedup).
//
@@ -162,7 +162,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
return nil, err
}
// Filter out issues with unsatisfied external dependencies (bd-zmmy)
// Filter out issues with unsatisfied external dependencies
// Only check if external_projects are configured
if len(config.GetExternalProjects()) > 0 && len(issues) > 0 {
issues, err = s.filterByExternalDeps(ctx, issues)
@@ -324,14 +324,14 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
// Messaging fields (bd-kwro)
// Messaging fields
var sender sql.NullString
var ephemeral sql.NullInt64
// Pinned field (bd-7h5)
// Pinned field
var pinned sql.NullInt64
// Template field (beads-1ra)
// Template field
var isTemplate sql.NullInt64
// Gate fields (bd-udsi)
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
var timeoutNs sql.NullInt64
@@ -395,22 +395,22 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
if originalType.Valid {
issue.OriginalType = originalType.String
}
// Messaging fields (bd-kwro)
// Messaging fields
if sender.Valid {
issue.Sender = sender.String
}
if ephemeral.Valid && ephemeral.Int64 != 0 {
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
// Pinned field
if pinned.Valid && pinned.Int64 != 0 {
issue.Pinned = true
}
// Template field (beads-1ra)
// Template field
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Gate fields (bd-udsi)
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
}
@@ -431,18 +431,18 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
}
// GetBlockedIssues returns issues that are blocked by dependencies or have status=blocked
// Note: Pinned issues are excluded from the output (beads-ei4)
// Note: Includes external: references in blocked_by list (bd-om4a)
// Note: Pinned issues are excluded from the output.
// Note: Includes external: references in blocked_by list.
func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkFilter) ([]*types.BlockedIssue, error) {
// Use UNION to combine:
// 1. Issues with open/in_progress/blocked status that have dependency blockers
// 2. Issues with status=blocked (even if they have no dependency blockers)
// Use GROUP_CONCAT to get all blocker IDs in a single query (no N+1)
// Exclude pinned issues (beads-ei4)
// Exclude pinned issues.
//
// For blocked_by_count and blocker_ids:
// - Count local blockers (open issues) + external refs (external:*)
// - External refs are always considered "open" until resolved (bd-om4a)
// - External refs are always considered "open" until resolved
// Build additional WHERE clauses for filtering
var filterClauses []string
@@ -572,7 +572,7 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
blocked = append(blocked, &issue)
}
// Filter out satisfied external dependencies from BlockedBy lists (bd-396j)
// Filter out satisfied external dependencies from BlockedBy lists
// Only check if external_projects are configured
if len(config.GetExternalProjects()) > 0 && len(blocked) > 0 {
blocked = filterBlockedByExternalDeps(ctx, blocked)

View File

@@ -120,7 +120,7 @@ func NewWithTimeout(ctx context.Context, path string, busyTimeout time.Duration)
// For all in-memory databases (including file::memory:), force single connection.
// SQLite's in-memory databases are isolated per connection by default.
// Without this, different connections in the pool can't see each other's writes (bd-b121, bd-yvlc).
// Without this, different connections in the pool can't see each other's writes.
isInMemory := path == ":memory:" ||
(strings.HasPrefix(path, "file:") && strings.Contains(path, "mode=memory"))
if isInMemory {
@@ -130,7 +130,7 @@ func NewWithTimeout(ctx context.Context, path string, busyTimeout time.Duration)
// For file-based databases in daemon mode, limit connection pool to prevent
// connection exhaustion under concurrent load. SQLite WAL mode supports
// 1 writer + unlimited readers, but we limit to prevent goroutine pile-up
// on write lock contention (bd-qhws).
// on write lock contention.
maxConns := runtime.NumCPU() + 1 // 1 writer + N readers
db.SetMaxOpenConns(maxConns)
db.SetMaxIdleConns(2)
@@ -159,7 +159,7 @@ func NewWithTimeout(ctx context.Context, path string, busyTimeout time.Duration)
return nil, err
}
// Verify schema compatibility after migrations (bd-ckvw)
// Verify schema compatibility after migrations
// First attempt
if err := verifySchemaCompatibility(db); err != nil {
// Schema probe failed - retry migrations once
@@ -191,7 +191,7 @@ func NewWithTimeout(ctx context.Context, path string, busyTimeout time.Duration)
busyTimeout: busyTimeout,
}
// Hydrate from multi-repo config if configured (bd-307)
// Hydrate from multi-repo config if configured
// Skip for in-memory databases (used in tests)
if path != ":memory:" {
_, err := storage.HydrateFromMultiRepo(ctx)

View File

@@ -48,7 +48,7 @@ func (s *SQLiteStorage) RunInTransaction(ctx context.Context, fn func(tx storage
defer func() { _ = conn.Close() }()
// Start IMMEDIATE transaction to acquire write lock early.
// Use retry logic with exponential backoff to handle SQLITE_BUSY (bd-ola6)
// Use retry logic with exponential backoff to handle SQLITE_BUSY
if err := beginImmediateWithRetry(ctx, conn, 5, 10*time.Millisecond); err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
@@ -91,7 +91,7 @@ func (s *SQLiteStorage) RunInTransaction(ctx context.Context, fn func(tx storage
// CreateIssue creates a new issue within the transaction.
func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
// Fetch custom statuses for validation (bd-1pj6)
// Fetch custom statuses for validation
customStatuses, err := t.GetCustomStatuses(ctx)
if err != nil {
return fmt.Errorf("failed to get custom statuses: %w", err)
@@ -132,7 +132,7 @@ func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, a
return fmt.Errorf("validation failed: %w", err)
}
// Compute content hash (bd-95)
// Compute content hash
if issue.ContentHash == "" {
issue.ContentHash = issue.ComputeContentHash()
}
@@ -141,13 +141,13 @@ func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, a
var configPrefix string
err = t.conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&configPrefix)
if err == sql.ErrNoRows || configPrefix == "" {
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
// CRITICAL: Reject operation if issue_prefix config is missing
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
} else if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
// Use IDPrefix override if set, combined with config prefix (bd-hobo)
// Use IDPrefix override if set, combined with config prefix
// e.g., configPrefix="bd" + IDPrefix="wisp" → "bd-wisp"
prefix := configPrefix
if issue.IDPrefix != "" {
@@ -156,14 +156,14 @@ func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, a
// Generate or validate ID
if issue.ID == "" {
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
// Generate hash-based ID with adaptive length based on database size
generatedID, err := GenerateIssueID(ctx, t.conn, prefix, issue, actor)
if err != nil {
return fmt.Errorf("failed to generate issue ID: %w", err)
}
issue.ID = generatedID
} else {
// Validate that explicitly provided ID matches the configured prefix (bd-177)
// Validate that explicitly provided ID matches the configured prefix
if err := ValidateIssueIDPrefix(issue.ID, prefix); err != nil {
return fmt.Errorf("failed to validate issue ID prefix: %w", err)
}
@@ -207,7 +207,7 @@ func (t *sqliteTxStorage) CreateIssues(ctx context.Context, issues []*types.Issu
return nil
}
// Fetch custom statuses for validation (bd-1pj6)
// Fetch custom statuses for validation
customStatuses, err := t.GetCustomStatuses(ctx)
if err != nil {
return fmt.Errorf("failed to get custom statuses: %w", err)
@@ -370,7 +370,7 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma
return fmt.Errorf("issue %s not found", id)
}
// Fetch custom statuses for validation (bd-1pj6)
// Fetch custom statuses for validation
customStatuses, err := t.GetCustomStatuses(ctx)
if err != nil {
return fmt.Errorf("failed to get custom statuses: %w", err)
@@ -398,7 +398,7 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma
// Auto-manage closed_at when status changes
setClauses, args = manageClosedAt(oldIssue, updates, setClauses, args)
// Recompute content_hash if any content fields changed (bd-95)
// Recompute content_hash if any content fields changed
contentChanged := false
contentFields := []string{"title", "description", "design", "acceptance_criteria", "notes", "status", "priority", "issue_type", "assignee", "external_ref"}
for _, field := range contentFields {
@@ -449,7 +449,7 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
// Invalidate blocked issues cache if status changed (bd-1c4h)
// Invalidate blocked issues cache if status changed
// Status changes affect which issues are blocked (blockers must be open/in_progress/blocked)
if _, statusChanged := updates["status"]; statusChanged {
if err := t.parent.invalidateBlockedCache(ctx, t.conn); err != nil {
@@ -555,7 +555,7 @@ func (t *sqliteTxStorage) CloseIssue(ctx context.Context, id string, reason stri
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
// Invalidate blocked issues cache since status changed to closed (bd-1c4h)
// Invalidate blocked issues cache since status changed to closed
// Closed issues don't block others, so this affects blocking calculations
if err := t.parent.invalidateBlockedCache(ctx, t.conn); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
@@ -617,7 +617,7 @@ func (t *sqliteTxStorage) AddDependency(ctx context.Context, dep *types.Dependen
return fmt.Errorf("issue %s not found", dep.IssueID)
}
// External refs (external:<project>:<capability>) don't need target validation (bd-zmmy)
// External refs (external:<project>:<capability>) don't need target validation
// They are resolved lazily at query time by CheckExternalDep
isExternalRef := strings.HasPrefix(dep.DependsOnID, "external:")
@@ -720,7 +720,7 @@ func (t *sqliteTxStorage) AddDependency(ctx context.Context, dep *types.Dependen
}
}
// Invalidate blocked cache for blocking dependencies (bd-1c4h, bd-kzda)
// Invalidate blocked cache for blocking dependencies
if dep.Type.AffectsReadyWork() {
if err := t.parent.invalidateBlockedCache(ctx, t.conn); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
@@ -732,13 +732,13 @@ func (t *sqliteTxStorage) AddDependency(ctx context.Context, dep *types.Dependen
// RemoveDependency removes a dependency within the transaction.
func (t *sqliteTxStorage) RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error {
// First, check what type of dependency is being removed (bd-1c4h)
// First, check what type of dependency is being removed
var depType types.DependencyType
err := t.conn.QueryRowContext(ctx, `
SELECT type FROM dependencies WHERE issue_id = ? AND depends_on_id = ?
`, issueID, dependsOnID).Scan(&depType)
// Store whether cache needs invalidation before deletion (bd-1c4h, bd-kzda)
// Store whether cache needs invalidation before deletion
needsCacheInvalidation := false
if err == nil {
needsCacheInvalidation = depType.AffectsReadyWork()
@@ -777,7 +777,7 @@ func (t *sqliteTxStorage) RemoveDependency(ctx context.Context, issueID, depends
return fmt.Errorf("failed to mark depends-on issue dirty: %w", err)
}
// Invalidate blocked cache if this was a blocking dependency (bd-1c4h)
// Invalidate blocked cache if this was a blocking dependency
if needsCacheInvalidation {
if err := t.parent.invalidateBlockedCache(ctx, t.conn); err != nil {
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
@@ -993,7 +993,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
whereClauses = append(whereClauses, "status = ?")
args = append(args, *filter.Status)
} else if !filter.IncludeTombstones {
// Exclude tombstones by default unless explicitly filtering for them (bd-1bu)
// Exclude tombstones by default unless explicitly filtering for them
whereClauses = append(whereClauses, "status != ?")
args = append(args, types.StatusTombstone)
}
@@ -1088,7 +1088,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", ")))
}
// Wisp filtering (bd-kwro.9)
// Wisp filtering
if filter.Ephemeral != nil {
if *filter.Ephemeral {
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
@@ -1097,7 +1097,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
}
}
// Pinned filtering (bd-7h5)
// Pinned filtering
if filter.Pinned != nil {
if *filter.Pinned {
whereClauses = append(whereClauses, "pinned = 1")
@@ -1106,7 +1106,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
}
}
// Parent filtering (bd-yqhh): filter children by parent issue
// Parent filtering: filter children by parent issue
if filter.ParentID != nil {
whereClauses = append(whereClauses, "id IN (SELECT issue_id FROM dependencies WHERE type = 'parent-child' AND depends_on_id = ?)")
args = append(args, *filter.ParentID)
@@ -1171,14 +1171,14 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
// Messaging fields (bd-kwro)
// Messaging fields
var sender sql.NullString
var wisp sql.NullInt64
// Pinned field (bd-7h5)
// Pinned field
var pinned sql.NullInt64
// Template field (beads-1ra)
// Template field
var isTemplate sql.NullInt64
// Gate fields (bd-udsi)
// Gate fields
var awaitType sql.NullString
var awaitID sql.NullString
var timeoutNs sql.NullInt64
@@ -1239,22 +1239,22 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
if originalType.Valid {
issue.OriginalType = originalType.String
}
// Messaging fields (bd-kwro)
// Messaging fields
if sender.Valid {
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
// Pinned field
if pinned.Valid && pinned.Int64 != 0 {
issue.Pinned = true
}
// Template field (beads-1ra)
// Template field
if isTemplate.Valid && isTemplate.Int64 != 0 {
issue.IsTemplate = true
}
// Gate fields (bd-udsi)
// Gate fields
if awaitType.Valid {
issue.AwaitType = awaitType.String
}