From 1d4eb6d94cb173b92b939f0a2a3f2b4605dabde9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 16:15:17 -0800 Subject: [PATCH 1/6] refactor: Consolidate duplicated step collection functions in cook.go (bd-9btu) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced three near-identical functions with a single unified collectSteps() that uses a callback strategy for label handling: - Removed collectStepsRecursive() (DB version) - Removed collectStepsToSubgraph() (in-memory version) - Removed collectStepIDMappings() (now handled by unified function) The new collectSteps() function: - Takes optional labelHandler callback for DB path (extracts labels separately) - Takes optional issueMap for in-memory path - Always builds idMapping for dependency resolution - Eliminates ~60 lines of duplicated code šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/cook.go | 283 +++++++++++++++---------------------------------- 1 file changed, 88 insertions(+), 195 deletions(-) diff --git a/cmd/bd/cook.go b/cmd/bd/cook.go index ec015b09..e7dab0ae 100644 --- a/cmd/bd/cook.go +++ b/cmd/bd/cook.go @@ -410,15 +410,13 @@ func cookFormulaToSubgraph(f *formula.Formula, protoID string) (*TemplateSubgrap issueMap[protoID] = rootIssue // Collect issues for each step (use protoID as parent for step IDs) - collectStepsToSubgraph(f.Steps, protoID, issueMap, &issues, &deps) + // The unified collectSteps builds both issueMap and idMapping + idMapping := make(map[string]string) + collectSteps(f.Steps, protoID, idMapping, issueMap, &issues, &deps, nil) // nil = keep labels on issues - // Collect dependencies from depends_on - stepIDMapping := make(map[string]string) + // Collect dependencies from depends_on using the idMapping built above for _, step := range f.Steps { - collectStepIDMappings(step, protoID, stepIDMapping) - } - for _, step := range f.Steps { - collectDependenciesToSubgraph(step, stepIDMapping, &deps) + collectDependencies(step, idMapping, &deps) } return &TemplateSubgraph{ @@ -429,145 +427,99 @@ func cookFormulaToSubgraph(f *formula.Formula, protoID string) (*TemplateSubgrap }, nil } -// collectStepsToSubgraph collects issues and dependencies for steps and their children. -// This is the in-memory version that doesn't create labels (since those require DB). -func collectStepsToSubgraph(steps []*formula.Step, parentID string, issueMap map[string]*types.Issue, - issues *[]*types.Issue, deps *[]*types.Dependency) { +// processStepToIssue converts a formula.Step to a types.Issue. +// The issue includes all fields including Labels populated from step.Labels and waits_for. +// This is the shared core logic used by both DB-persisted and in-memory cooking. +func processStepToIssue(step *formula.Step, parentID string) *types.Issue { + // Generate issue ID (formula-name.step-id) + issueID := fmt.Sprintf("%s.%s", parentID, step.ID) + + // Determine issue type (children override to epic) + issueType := stepTypeToIssueType(step.Type) + if len(step.Children) > 0 { + issueType = types.TypeEpic + } + + // Determine priority + priority := 2 + if step.Priority != nil { + priority = *step.Priority + } + + issue := &types.Issue{ + ID: issueID, + Title: step.Title, // Keep {{variables}} for substitution at pour time + Description: step.Description, + Status: types.StatusOpen, + Priority: priority, + IssueType: issueType, + Assignee: step.Assignee, + IsTemplate: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + SourceFormula: step.SourceFormula, // Source tracing + SourceLocation: step.SourceLocation, // Source tracing + } + + // Populate labels from step + issue.Labels = append(issue.Labels, step.Labels...) + + // Add gate label for waits_for field + if step.WaitsFor != "" { + gateLabel := fmt.Sprintf("gate:%s", step.WaitsFor) + issue.Labels = append(issue.Labels, gateLabel) + } + + return issue +} + +// collectSteps collects issues and dependencies for steps and their children. +// This is the unified implementation used by both DB-persisted and in-memory cooking. +// +// Parameters: +// - idMapping: step.ID → issue.ID (always populated, used for dependency resolution) +// - issueMap: issue.ID → issue (optional, nil for DB path, populated for in-memory path) +// - labelHandler: callback for each label (if nil, labels stay on issue; if set, labels are +// extracted and issue.Labels is cleared - use for DB path) +func collectSteps(steps []*formula.Step, parentID string, + idMapping map[string]string, + issueMap map[string]*types.Issue, + issues *[]*types.Issue, + deps *[]*types.Dependency, + labelHandler func(issueID, label string)) { for _, step := range steps { - // Generate issue ID (formula-name.step-id) - issueID := fmt.Sprintf("%s.%s", parentID, step.ID) - - // Determine issue type (children override to epic) - issueType := stepTypeToIssueType(step.Type) - if len(step.Children) > 0 { - issueType = types.TypeEpic - } - - // Determine priority - priority := 2 - if step.Priority != nil { - priority = *step.Priority - } - - issue := &types.Issue{ - ID: issueID, - Title: step.Title, // Keep {{variables}} for substitution at pour time - Description: step.Description, - Status: types.StatusOpen, - Priority: priority, - IssueType: issueType, - Assignee: step.Assignee, - IsTemplate: true, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - SourceFormula: step.SourceFormula, // Source tracing - SourceLocation: step.SourceLocation, // Source tracing - } - - // Store labels in the issue's Labels field for in-memory use - issue.Labels = append(issue.Labels, step.Labels...) - - // Add gate label for waits_for field - if step.WaitsFor != "" { - gateLabel := fmt.Sprintf("gate:%s", step.WaitsFor) - issue.Labels = append(issue.Labels, gateLabel) - } - + issue := processStepToIssue(step, parentID) *issues = append(*issues, issue) - issueMap[issueID] = issue + + // Build mappings + idMapping[step.ID] = issue.ID + if issueMap != nil { + issueMap[issue.ID] = issue + } + + // Handle labels: extract via callback (DB path) or keep on issue (in-memory path) + if labelHandler != nil { + for _, label := range issue.Labels { + labelHandler(issue.ID, label) + } + issue.Labels = nil // DB stores labels separately + } // Add parent-child dependency *deps = append(*deps, &types.Dependency{ - IssueID: issueID, + IssueID: issue.ID, DependsOnID: parentID, Type: types.DepParentChild, }) // Recursively collect children if len(step.Children) > 0 { - collectStepsToSubgraph(step.Children, issueID, issueMap, issues, deps) + collectSteps(step.Children, issue.ID, idMapping, issueMap, issues, deps, labelHandler) } } } -// collectStepIDMappings builds a map from step ID to full issue ID -func collectStepIDMappings(step *formula.Step, parentID string, mapping map[string]string) { - issueID := fmt.Sprintf("%s.%s", parentID, step.ID) - mapping[step.ID] = issueID - - for _, child := range step.Children { - collectStepIDMappings(child, issueID, mapping) - } -} - -// collectDependenciesToSubgraph collects blocking dependencies from depends_on and needs fields. -func collectDependenciesToSubgraph(step *formula.Step, idMapping map[string]string, deps *[]*types.Dependency) { - issueID := idMapping[step.ID] - - // Process depends_on field - for _, depID := range step.DependsOn { - depIssueID, ok := idMapping[depID] - if !ok { - continue // Will be caught during validation - } - - *deps = append(*deps, &types.Dependency{ - IssueID: issueID, - DependsOnID: depIssueID, - Type: types.DepBlocks, - }) - } - - // Process needs field - simpler alias for sibling dependencies - for _, needID := range step.Needs { - needIssueID, ok := idMapping[needID] - if !ok { - continue // Will be caught during validation - } - - *deps = append(*deps, &types.Dependency{ - IssueID: issueID, - DependsOnID: needIssueID, - Type: types.DepBlocks, - }) - } - - // Process waits_for field - fanout gate dependency - if step.WaitsFor != "" { - waitsForSpec := formula.ParseWaitsFor(step.WaitsFor) - if waitsForSpec != nil { - // Determine spawner ID - spawnerStepID := waitsForSpec.SpawnerID - if spawnerStepID == "" && len(step.Needs) > 0 { - // Infer spawner from first need - spawnerStepID = step.Needs[0] - } - - if spawnerStepID != "" { - if spawnerIssueID, ok := idMapping[spawnerStepID]; ok { - // Create WaitsFor dependency with metadata - meta := types.WaitsForMeta{ - Gate: waitsForSpec.Gate, - } - metaJSON, _ := json.Marshal(meta) - - *deps = append(*deps, &types.Dependency{ - IssueID: issueID, - DependsOnID: spawnerIssueID, - Type: types.DepWaitsFor, - Metadata: string(metaJSON), - }) - } - } - } - } - - // Recursively handle children - for _, child := range step.Children { - collectDependenciesToSubgraph(child, idMapping, deps) - } -} // resolveAndCookFormula loads a formula by name, resolves it, applies all transformations, // and returns an in-memory TemplateSubgraph ready for instantiation. @@ -694,7 +646,10 @@ func cookFormula(ctx context.Context, s storage.Storage, f *formula.Formula, pro labels = append(labels, struct{ issueID, label string }{protoID, MoleculeLabel}) // Collect issues for each step (use protoID as parent for step IDs) - collectStepsRecursive(f.Steps, protoID, idMapping, &issues, &deps, &labels) + // Use labelHandler to extract labels for separate DB storage + collectSteps(f.Steps, protoID, idMapping, nil, &issues, &deps, func(issueID, label string) { + labels = append(labels, struct{ issueID, label string }{issueID, label}) + }) // Collect dependencies from depends_on for _, step := range f.Steps { @@ -753,70 +708,8 @@ func cookFormula(ctx context.Context, s storage.Storage, f *formula.Formula, pro }, nil } -// collectStepsRecursive collects issues, dependencies, and labels for steps and their children. -func collectStepsRecursive(steps []*formula.Step, parentID string, idMapping map[string]string, - issues *[]*types.Issue, deps *[]*types.Dependency, labels *[]struct{ issueID, label string }) { - - for _, step := range steps { - // Generate issue ID (formula-name.step-id) - issueID := fmt.Sprintf("%s.%s", parentID, step.ID) - - // Determine issue type (children override to epic) - issueType := stepTypeToIssueType(step.Type) - if len(step.Children) > 0 { - issueType = types.TypeEpic - } - - // Determine priority - priority := 2 - if step.Priority != nil { - priority = *step.Priority - } - - issue := &types.Issue{ - ID: issueID, - Title: step.Title, // Keep {{variables}} for substitution at pour time - Description: step.Description, - Status: types.StatusOpen, - Priority: priority, - IssueType: issueType, - Assignee: step.Assignee, - IsTemplate: true, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - SourceFormula: step.SourceFormula, // Source tracing - SourceLocation: step.SourceLocation, // Source tracing - } - *issues = append(*issues, issue) - - // Collect labels - for _, label := range step.Labels { - *labels = append(*labels, struct{ issueID, label string }{issueID, label}) - } - - // Add gate label for waits_for field - if step.WaitsFor != "" { - gateLabel := fmt.Sprintf("gate:%s", step.WaitsFor) - *labels = append(*labels, struct{ issueID, label string }{issueID, gateLabel}) - } - - idMapping[step.ID] = issueID - - // Add parent-child dependency - *deps = append(*deps, &types.Dependency{ - IssueID: issueID, - DependsOnID: parentID, - Type: types.DepParentChild, - }) - - // Recursively collect children - if len(step.Children) > 0 { - collectStepsRecursive(step.Children, issueID, idMapping, issues, deps, labels) - } - } -} - -// collectDependencies collects blocking dependencies from depends_on and needs fields. +// collectDependencies collects blocking dependencies from depends_on, needs, and waits_for fields. +// This is the shared implementation used by both DB-persisted and in-memory subgraph cooking. func collectDependencies(step *formula.Step, idMapping map[string]string, deps *[]*types.Dependency) { issueID := idMapping[step.ID] From 40b65b8dbe424199ecd52d8fa1bef02f96fb5831 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 16:15:33 -0800 Subject: [PATCH 2/6] bd sync: closed bd-9btu after refactoring cook.go --- .beads/issues.jsonl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2388cc23..db1395b9 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -27,7 +27,7 @@ {"id":"bd-1pr6","title":"Time-dependent worktree detection tests may be flaky","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-26T01:21:22.065353-08:00","updated_at":"2025-12-26T01:21:22.065353-08:00"} {"id":"bd-1ri4","title":"Test prefix bd-","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-27T14:24:41.240483-08:00","created_by":"stevey","updated_at":"2025-12-27T14:24:50.419881-08:00","deleted_at":"2025-12-27T14:24:50.419881-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} {"id":"bd-1slh","title":"Investigate charmbracelet-based TUI for beads","description":"Now that we've merged the create-form command (PR #603) which uses charmbracelet/huh, investigate whether beads should have a more comprehensive TUI.\n\nConsiderations:\n- Should this be in core or a separate binary (bd-tui)?\n- What functionality would benefit from a TUI? (list view, issue details, search, bulk operations)\n- Plugin/extension architecture vs build tags vs separate binary\n- Dependency cost vs user experience tradeoff\n- Target audience: humans who want interactive workflows vs CLI/scripting users\n\nRelated: PR #603 added charmbracelet/huh dependency for create-form command.","notes":"Foundation is in place (lipgloss, huh), but not a priority right now","status":"deferred","priority":3,"issue_type":"feature","created_at":"2025-12-17T14:20:51.503563-08:00","updated_at":"2025-12-20T23:31:34.354023-08:00"} -{"id":"bd-1tkd","title":"Code smell: ComputeContentHash() is 100+ lines of repetitive code","description":"The ComputeContentHash() method in internal/types/types.go (lines 87-190) is over 100 lines of repetitive code:\n\n```go\nh.Write([]byte(i.Title))\nh.Write([]byte{0}) // separator\nh.Write([]byte(i.Description))\nh.Write([]byte{0})\nh.Write([]byte(i.Design))\n// ... repeated 40+ times\n```\n\nConsider:\n1. Using reflection to iterate over struct fields tagged for hashing\n2. Creating a helper function that takes field name and value\n3. Using a slice of fields to hash and iterating over it\n4. At minimum, extracting the repetitive pattern into a helper\n\nLocation: internal/types/types.go:87-190","acceptance_criteria":"1. ComputeContentHash() refactored to reduce repetition\n2. Helper function or data-driven approach replaces repetitive h.Write() calls\n3. Hash output unchanged for same input (backwards compatible)\n4. All existing tests pass: go test ./internal/types/...\n5. Benchmark shows no significant performance regression","status":"open","priority":3,"issue_type":"chore","created_at":"2025-12-28T15:31:55.514941-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:37:17.0941-08:00","dependencies":[{"issue_id":"bd-1tkd","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.204723-08:00","created_by":"daemon"}]} +{"id":"bd-1tkd","title":"Code smell: ComputeContentHash() is 100+ lines of repetitive code","description":"attached_args: Refactor ComputeContentHash repetitive code\n\nThe ComputeContentHash() method in internal/types/types.go (lines 87-190) is over 100 lines of repetitive code:\n\n```go\nh.Write([]byte(i.Title))\nh.Write([]byte{0}) // separator\nh.Write([]byte(i.Description))\nh.Write([]byte{0})\nh.Write([]byte(i.Design))\n// ... repeated 40+ times\n```\n\nConsider:\n1. Using reflection to iterate over struct fields tagged for hashing\n2. Creating a helper function that takes field name and value\n3. Using a slice of fields to hash and iterating over it\n4. At minimum, extracting the repetitive pattern into a helper\n\nLocation: internal/types/types.go:87-190","acceptance_criteria":"1. ComputeContentHash() refactored to reduce repetition\n2. Helper function or data-driven approach replaces repetitive h.Write() calls\n3. Hash output unchanged for same input (backwards compatible)\n4. All existing tests pass: go test ./internal/types/...\n5. Benchmark shows no significant performance regression","status":"pinned","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:31:55.514941-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:02:15.871379-08:00","dependencies":[{"issue_id":"bd-1tkd","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.204723-08:00","created_by":"daemon"}]} {"id":"bd-1tw","title":"Fix G104 errors unhandled in internal/storage/sqlite/queries.go:1186","description":"Linting issue: G104: Errors unhandled (gosec) at internal/storage/sqlite/queries.go:1186:2. Error: rows.Close()","status":"tombstone","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:13.051671889-07:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-20j","title":"sync branch not match config","description":"./bd sync\n→ Exporting pending changes to JSONL...\n→ No changes to commit\n→ Pulling from sync branch 'gh-386'...\nError pulling from sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/worktree-db-fail/.git: not a directory\nmatt@blufin-framation ~/d/b/worktree-db-fail (worktree-db-fail) [1]\u003e bd config list\n\nConfiguration:\n auto_compact_enabled = false\n compact_batch_size = 50\n compact_model = claude-3-5-haiku-20241022\n compact_parallel_workers = 5\n compact_tier1_days = 30\n compact_tier1_dep_levels = 2\n compact_tier2_commits = 100\n compact_tier2_days = 90\n compact_tier2_dep_levels = 5\n compaction_enabled = false\n issue_prefix = worktree-db-fail\n sync.branch = worktree-db-fail","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-08T06:49:04.449094018-07:00","updated_at":"2025-12-08T06:49:04.449094018-07:00"} {"id":"bd-28db","title":"Add 'bd status' command for issue database overview","description":"Implement a bd status command that provides a quick snapshot of the issue database state, similar to how git status shows working tree state.\n\nExpected output: Show summary including counts by state (open, in-progress, blocked, closed), recent activity (last 7 days), and quick overview without needing multiple queries.\n\nExample output showing issue counts, recent activity stats, and pointer to bd list for details.\n\nProposed options: --all (show all issues), --assigned (show issues assigned to current user), --json (JSON format output)\n\nUse cases: Quick project health check, onboarding for new contributors, integration with shell prompts or CI/CD, daily standup reference","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-02T17:25:59.203549-08:00","updated_at":"2025-12-21T17:54:00.205191-08:00","closed_at":"2025-12-21T17:54:00.205191-08:00","close_reason":"Already implemented - bd status shows summary and activity"} @@ -100,7 +100,7 @@ {"id":"bd-5e9q","title":"Add backwards-compatible aliases and deprecation warnings","description":"## Task\nEnsure backwards compatibility by adding hidden aliases for all moved commands.\n\n## Implementation\n\n### Create aliases.go\nNew file to centralize alias management:\n```go\npackage main\n\nimport (\n \"fmt\"\n \"os\"\n \n \"github.com/spf13/cobra\"\n)\n\n// Deprecated command aliases for backwards compatibility\n// These will be removed in v0.XX.0\n\nfunc init() {\n // migrate-* → migrate *\n addDeprecatedAlias(\"migrate-hash-ids\", \"migrate hash-ids\")\n addDeprecatedAlias(\"migrate-issues\", \"migrate issues\")\n addDeprecatedAlias(\"migrate-sync\", \"migrate sync\")\n addDeprecatedAlias(\"migrate-tombstones\", \"migrate tombstones\")\n \n // top-level → admin *\n addDeprecatedAlias(\"cleanup\", \"admin cleanup\")\n addDeprecatedAlias(\"compact\", \"admin compact\")\n addDeprecatedAlias(\"reset\", \"admin reset\")\n \n // top-level → formula *\n addDeprecatedAlias(\"cook\", \"formula cook\")\n}\n\nfunc addDeprecatedAlias(old, new string) {\n cmd := \u0026cobra.Command{\n Use: old,\n Hidden: true,\n Run: func(cmd *cobra.Command, args []string) {\n fmt.Fprintf(os.Stderr, \"āš ļø '%s' is deprecated, use '%s' instead\\n\", old, new)\n // Forward execution to new command\n },\n }\n rootCmd.AddCommand(cmd)\n}\n```\n\n### Deprecation behavior\n1. First release: Show warning, execute command\n2. One release later: Show warning, still execute\n3. Third release: Remove aliases entirely\n\n## Files to create\n- cmd/bd/aliases.go\n\n## Files to modify \n- cmd/bd/main.go (ensure aliases loaded)\n","status":"closed","priority":2,"issue_type":"task","assignee":"opal","created_at":"2025-12-27T15:11:33.54574-08:00","created_by":"mayor","updated_at":"2025-12-27T16:06:08.157887-08:00","closed_at":"2025-12-27T16:06:08.157887-08:00","close_reason":"Added aliases.go with infrastructure for backwards-compatible command aliases. Aliases only activate after commands are moved to their new locations.","pinned":true} {"id":"bd-5exm","title":"Merge: bd-49kw","description":"branch: polecat/nux\ntarget: main\nsource_issue: bd-49kw\nrig: beads","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-23T20:43:23.156375-08:00","updated_at":"2025-12-23T21:21:57.693169-08:00","closed_at":"2025-12-23T21:21:57.693169-08:00","close_reason":"stale - no code pushed"} {"id":"bd-5hrq","title":"bd doctor: detect issues referenced in commits but still open","description":"Add a doctor check that finds 'orphaned' issues - ones referenced in git commit messages (e.g., 'fix bug (bd-xxx)') but still marked as open in beads.\n\n**Detection logic:**\n1. Get all open issue IDs from beads\n2. Parse git log for issue ID references matching pattern \\(prefix-[a-z0-9.]+\\)\n3. Report issues that appear in commits but are still open\n\n**Output:**\n⚠ Warning: N issues referenced in commits but still open\n bd-xxx: 'Issue title' (commit abc123)\n bd-yyy: 'Issue title' (commit def456)\n \n These may be implemented but not closed. Run 'bd show \u003cid\u003e' to check.\n\n**Implementation:**\n- Add check to doctor/checks.go\n- Use git log parsing (already have git utilities)\n- Match against configured issue_prefix","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-21T21:48:08.473165-08:00","updated_at":"2025-12-21T21:55:37.795109-08:00","closed_at":"2025-12-21T21:55:37.795109-08:00","close_reason":"Implemented CheckOrphanedIssues in git.go with 8 test cases. Detects issues referenced in commits (e.g., 'fix bug (bd-xxx)') that are still open. Shows warning with issue IDs and commit hashes."} -{"id":"bd-5l59","title":"Code smell: Issue struct is a God Object (50+ fields)","description":"The Issue struct in internal/types/types.go has 50+ fields covering many different concerns:\n\n- Basic issue tracking (ID, Title, Description, Status, Priority)\n- Messaging/communication (Sender, Ephemeral, etc.)\n- Agent identity (HookBead, RoleBead, AgentState, RoleType, Rig)\n- Gate/async coordination (AwaitType, AwaitID, Timeout, Waiters)\n- Source tracing (SourceFormula, SourceLocation)\n- HOP validation/entities (Creator, Validations)\n- Compaction (CompactionLevel, CompactedAt, OriginalSize)\n- Tombstones (DeletedAt, DeletedBy, DeleteReason)\n\nConsider:\n1. Extracting field groups into embedded structs (e.g., AgentFields, GateFields)\n2. Using composition pattern to make the struct more modular\n3. At minimum, grouping related fields together with section comments\n\nLocation: internal/types/types.go:12-82","acceptance_criteria":"1. Related fields grouped with clear section comments (e.g., // Agent identity fields)\n2. Consider extracting into embedded structs if grouping reveals clean boundaries\n3. All existing tests pass: go test ./internal/types/...\n4. ComputeContentHash() still works correctly after any struct changes\n5. JSON serialization unchanged (no breaking changes to JSONL format)","status":"open","priority":3,"issue_type":"chore","created_at":"2025-12-28T15:31:34.021236-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:37:14.582707-08:00","dependencies":[{"issue_id":"bd-5l59","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.187103-08:00","created_by":"daemon"}]} +{"id":"bd-5l59","title":"Code smell: Issue struct is a God Object (50+ fields)","description":"attached_args: Refactor Issue struct God Object\n\nThe Issue struct in internal/types/types.go has 50+ fields covering many different concerns:\n\n- Basic issue tracking (ID, Title, Description, Status, Priority)\n- Messaging/communication (Sender, Ephemeral, etc.)\n- Agent identity (HookBead, RoleBead, AgentState, RoleType, Rig)\n- Gate/async coordination (AwaitType, AwaitID, Timeout, Waiters)\n- Source tracing (SourceFormula, SourceLocation)\n- HOP validation/entities (Creator, Validations)\n- Compaction (CompactionLevel, CompactedAt, OriginalSize)\n- Tombstones (DeletedAt, DeletedBy, DeleteReason)\n\nConsider:\n1. Extracting field groups into embedded structs (e.g., AgentFields, GateFields)\n2. Using composition pattern to make the struct more modular\n3. At minimum, grouping related fields together with section comments\n\nLocation: internal/types/types.go:12-82","acceptance_criteria":"1. Related fields grouped with clear section comments (e.g., // Agent identity fields)\n2. Consider extracting into embedded structs if grouping reveals clean boundaries\n3. All existing tests pass: go test ./internal/types/...\n4. ComputeContentHash() still works correctly after any struct changes\n5. JSON serialization unchanged (no breaking changes to JSONL format)","status":"pinned","priority":3,"issue_type":"chore","assignee":"gt-beads-furiosa","created_at":"2025-12-28T15:31:34.021236-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:57:07.884245-08:00","dependencies":[{"issue_id":"bd-5l59","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.187103-08:00","created_by":"daemon"}]} {"id":"bd-5qim","title":"Optimize GetReadyWork performance - 752ms on 10K database (target: \u003c50ms)","notes":"# Performance Analysis (10K Issue Database)\n\nAnalyzed using CPU profiles from benchmark suite on Apple M2 Pro.\n\n## Operation Performance\n\n| Operation | Time | Allocations | Memory |\n|----------------------------------|---------|-------------|--------|\n| bd ready (GetReadyWork) | ~752ms | 167,466 | 16MB |\n| bd list (SearchIssues no filter) | ~11.6ms | 89,214 | 5.8MB |\n| bd list (SearchIssues filtered) | ~9.2ms | 62,365 | 3.5MB |\n| bd create (CreateIssue) | ~2.6ms | 146 | 8.6KB |\n| bd update (UpdateIssue) | ~0.32ms | 364 | 15KB |\n| bd close (UpdateIssue) | ~0.32ms | 364 | 15KB |\n\n**Target: \u003c50ms for all operations on 10K database**\n\n**Current issue: GetReadyWork is 15x over target (752ms vs 50ms)**\n\n## Root Cause\n\nGetReadyWork (internal/storage/sqlite/ready.go:90-128) uses recursive CTE to propagate blocking:\n- 65x slower than SearchIssues\n- Recalculates entire blocked issue tree on every call\n- Algorithm:\n 1. Find directly blocked issues via 'blocks' dependencies\n 2. Recursively propagate blockage to descendants (max depth: 50)\n 3. Exclude all blocked issues from results\n\n## CPU Profile Analysis\n\n- Database syscalls (pthread_cond_signal, syscall6): ~75%\n- SQLite engine overhead: inherent to recursive CTE\n- Application code (query construction): \u003c1%\n\n**Bottleneck is the recursive CTE query execution, not application code.**\n\n## Optimization Recommendations\n\n### High Impact (Likely to achieve \u003c50ms target)\n\n1. **Cache blocked issue calculation**\n - Add `blocked_issues` table updated on dependency changes\n - Trade write complexity for read speed (ready called \u003e\u003e dependency changes)\n - Eliminates recursive CTE on every read\n\n2. **Add/verify database indexes**\n ```sql\n CREATE INDEX IF NOT EXISTS idx_dependencies_blocked \n ON dependencies(issue_id, type, depends_on_id);\n CREATE INDEX IF NOT EXISTS idx_issues_status \n ON issues(status);\n ```\n\n### Medium Impact\n\n3. **Reduce allocations** (167K allocations for GetReadyWork)\n - Profile `scanIssues()` for object pooling opportunities\n - Reuse slice capacity for repeated calls\n\n### Low Impact (Not recommended)\n- Query optimization for CRUD operations (already \u003c3ms)\n- Connection pooling tuning (not showing in profiles)\n\n## Verification\n\nRun benchmarks to validate optimization:\n```bash\nmake bench-quick\ngo tool pprof -http=:8080 internal/storage/sqlite/bench-cpu-*.prof\n```\n\nProfile files automatically generated in `internal/storage/sqlite/`.","status":"tombstone","priority":0,"issue_type":"bug","created_at":"2025-11-14T09:02:46.507526-08:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-5rj1","title":"Merge: bd-gqxd","description":"branch: polecat/furiosa\ntarget: main\nsource_issue: bd-gqxd\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T16:40:21.707706-08:00","updated_at":"2025-12-23T19:12:08.349245-08:00","closed_at":"2025-12-23T19:12:08.349245-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-5s91","title":"CLI API Audit for OSS Launch","description":"Comprehensive CLI API audit before OSS launch.\n\n## Tasks\n1. Review gt command groupings - propose consolidation\n2. Review bd command groupings \n3. Document gt vs bd ownership boundaries\n4. Identify naming inconsistencies\n5. Document flag vs subcommand criteria\n\n## Context\n- Pre-OSS launch cleanup\n- Goal: clean, consistent, discoverable CLI surface","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-25T00:15:41.355013-08:00","updated_at":"2025-12-25T13:26:25.587476-08:00","closed_at":"2025-12-25T13:26:25.587476-08:00","close_reason":"Completed"} @@ -176,7 +176,7 @@ {"id":"bd-95k8","title":"Pinned field available in beads v0.37.0","description":"Hey max,\n\nHeads up on your mail overhaul work:\n\n1. **Pinned field is available** - beads v0.37.0 (released by dave earlier) includes the pinned field on issues. You'll want to add this to BeadsMessage in types.go.\n\n2. **Database migration** - Check if existing .beads databases need migration to support the pinned field. Run `bd doctor` to see if it flags anything.\n\n3. **Sorting task** - Once you have the pinned field, gt-ngu1 (pinned beads first in mail inbox) needs implementing. Since messages now come from `bd list --type=message`, you'll need to either:\n - Sort in listBeads() after fetching, or\n - Ensure bd list returns pinned items first (may already do this?)\n\nCheck what version of bd you're building against.\n\n-- Mayor","status":"closed","priority":2,"issue_type":"message","assignee":"gastown/crew/max","created_at":"2025-12-20T17:51:57.315956-08:00","updated_at":"2025-12-21T17:52:18.542169-08:00","closed_at":"2025-12-21T17:52:18.542169-08:00","close_reason":"Stale message - pinned field already available","labels":["from:beads-crew-dave","thread:thread-71ac20c7e432"]} {"id":"bd-987a","title":"bd mol run: panic slice bounds out of range in mol_run.go:130","description":"## Problem\nbd mol run panics after successfully creating the molecule:\n\n```\nāœ“ Molecule running: created 9 issues\n Root issue: gt-i4lo (pinned, in_progress)\n Assignee: stevey\n\nNext steps:\n bd ready # Find unblocked work in this molecule\npanic: runtime error: slice bounds out of range [:8] with length 7\n\ngoroutine 1 [running]:\nmain.runMolRun(0x1014fc0c0, {0x140001e0f80, 0x1, 0x10089daad?})\n /Users/stevey/gt/beads/crew/dave/cmd/bd/mol_run.go:130 +0xc38\n```\n\n## Reproduction\n```bash\nbd --no-daemon mol run gt-lwuu --var issue=gt-test123\n```\nWhere gt-lwuu is a mol-polecat-work proto with 8 child steps.\n\n## Impact\nThe molecule IS created successfully - the panic happens after creation when formatting the \"Next steps\" output.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-21T21:48:55.396018-08:00","updated_at":"2025-12-21T22:57:46.827469-08:00","closed_at":"2025-12-21T22:57:46.827469-08:00","close_reason":"Fixed: removed unsafe rootID[:8] slice - now uses full ID"} {"id":"bd-9avq","title":"Fix wisp leak in nodb mode writeIssuesToJSONL","description":"writeIssuesToJSONL in nodb.go calls writeJSONLAtomic without filtering wisps. This means any wisps created during a nodb session would be written to issues.jsonl, leaking ephemeral data into the git-tracked file.\n\nFix: Add the same wisp filter pattern used in sync_export.go, autoflush.go, and export.go:\n\n```go\n// Filter out wisps before writing\nfiltered := make([]*types.Issue, 0, len(issues))\nfor _, issue := range issues {\n if !issue.Wisp {\n filtered = append(filtered, issue)\n }\n}\nissues = filtered\n```\n\nLocation: cmd/bd/nodb.go:writeIssuesToJSONL()","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-24T21:15:47.980048-08:00","updated_at":"2025-12-24T21:18:42.607711-08:00","closed_at":"2025-12-24T21:18:42.607711-08:00","close_reason":"Added wisp filter in writeIssuesToJSONL"} -{"id":"bd-9btu","title":"Code smell: Duplicated step collection functions in cook.go","description":"Two pairs of nearly identical functions exist in cook.go:\n\n1. collectStepsRecursive() (line 754) vs collectStepsToSubgraph() (line 415)\n - Both iterate over steps and create issues\n - Main difference: one writes to DB, one builds in-memory subgraph\n \n2. collectDependencies() (line 833) vs collectDependenciesToSubgraph() (line 502)\n - Both collect dependencies from steps\n - Nearly identical logic\n\nConsider:\n1. Extract common step/dependency processing logic\n2. Use a strategy pattern or callback for the DB vs in-memory difference\n3. Create a single function that returns data, then have callers decide how to persist\n\nLocation: cmd/bd/cook.go:415-567 and cmd/bd/cook.go:754-898","acceptance_criteria":"1. Common step collection logic extracted to shared function(s)\n2. collectStepsRecursive and collectStepsToSubgraph unified or share core logic\n3. collectDependencies and collectDependenciesToSubgraph unified or share core logic\n4. All existing tests pass: go test ./cmd/bd/... -run Cook\n5. No functional change to cook command behavior","status":"open","priority":2,"issue_type":"chore","created_at":"2025-12-28T15:31:57.401447-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:36:53.443253-08:00","dependencies":[{"issue_id":"bd-9btu","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.168908-08:00","created_by":"daemon"}]} +{"id":"bd-9btu","title":"Code smell: Duplicated step collection functions in cook.go","description":"attached_args: Fix duplicated step collection functions in cook.go\n\nTwo pairs of nearly identical functions exist in cook.go:\n\n1. collectStepsRecursive() (line 754) vs collectStepsToSubgraph() (line 415)\n - Both iterate over steps and create issues\n - Main difference: one writes to DB, one builds in-memory subgraph\n \n2. collectDependencies() (line 833) vs collectDependenciesToSubgraph() (line 502)\n - Both collect dependencies from steps\n - Nearly identical logic\n\nConsider:\n1. Extract common step/dependency processing logic\n2. Use a strategy pattern or callback for the DB vs in-memory difference\n3. Create a single function that returns data, then have callers decide how to persist\n\nLocation: cmd/bd/cook.go:415-567 and cmd/bd/cook.go:754-898","acceptance_criteria":"1. Common step collection logic extracted to shared function(s)\n2. collectStepsRecursive and collectStepsToSubgraph unified or share core logic\n3. collectDependencies and collectDependenciesToSubgraph unified or share core logic\n4. All existing tests pass: go test ./cmd/bd/... -run Cook\n5. No functional change to cook command behavior","status":"pinned","priority":2,"issue_type":"chore","assignee":"gt-beads-nux","created_at":"2025-12-28T15:31:57.401447-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:57:04.650238-08:00","dependencies":[{"issue_id":"bd-9btu","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.168908-08:00","created_by":"daemon"}]} {"id":"bd-9cdc","title":"Update docs for import bug fix","description":"Update AGENTS.md, README.md, TROUBLESHOOTING.md with import.orphan_handling config documentation. Document resurrection behavior, tombstones, config modes. Add troubleshooting section for import failures with deleted parents.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-04T12:32:30.770415-08:00","updated_at":"2025-12-21T21:14:08.328627-08:00","closed_at":"2025-12-21T21:14:08.328627-08:00","close_reason":"Already completed - documentation in CONFIG.md, CLI_REFERENCE.md, and TROUBLESHOOTING.md"} {"id":"bd-9g1z","title":"Fix or remove TestFindJSONLPathDefault (issue #356)","description":"Code health review found .test-skip permanently skips TestFindJSONLPathDefault.\n\nThe test references issue #356 about wrong JSONL filename expectations (issues.jsonl vs beads.jsonl).\n\nTest file: internal/beads/beads_test.go\n\nThe underlying migration from beads.jsonl to issues.jsonl may be complete, so either:\n1. Fix the test expectations\n2. Remove the test if no longer needed\n3. Document why it remains skipped","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-16T18:17:31.33975-08:00","updated_at":"2025-12-22T21:24:50.357688-08:00","closed_at":"2025-12-22T21:24:50.357688-08:00","close_reason":"Test now passes - removed from .test-skip. Code was fixed in utils.FindJSONLInDir to return issues.jsonl as default."} {"id":"bd-9gvf","title":"Add prefix-based routing with routes.jsonl","description":"## Summary\n\nWhen running `bd show gt-xyz` from a location where that bead doesn't exist locally,\nbd should check a `routes.jsonl` file to find where beads with that prefix live.\n\n## Problem\n\nFrom ~/gt (town root):\n- `bd show hq-xxx` works (hq-* beads are local)\n- `bd show gt-xxx` fails (gt-* beads live in gastown/mayor/rig)\n\nThis breaks `gt hook`, `gt handoff`, and `gt prime` which need to verify beads exist.\n\n## Solution\n\nAdd `.beads/routes.jsonl` for prefix-based routing:\n\n\\`\\`\\`json\n{\"prefix\": \"gt-\", \"path\": \"gastown/mayor/rig\"}\n{\"prefix\": \"hq-\", \"path\": \".\"}\n{\"prefix\": \"bd-\", \"path\": \"beads/crew/dave\"}\n\\`\\`\\`\n\n### Lookup Algorithm\n\n1. Try local .beads/ first (current behavior)\n2. If not found, check for routes.jsonl in current .beads/\n3. Match ID prefix against routes\n4. If match found, resolve path relative to routes file location and retry\n5. If still not found, error as usual\n\n### Affected Commands\n\nRead operations that take a bead ID:\n- bd show \u003cid\u003e\n- bd update \u003cid\u003e\n- bd close \u003cid\u003e\n- bd dep add \u003cid\u003e \u003cdep\u003e (both IDs)\n\nWrite operations (bd create) still go to local .beads/ - no change needed.\n\n## Implementation Notes\n\n1. New file: routes.jsonl in .beads/ directory\n2. New function: resolveBeadsPath(id string) (string, error)\n3. Update: beads.Show(), beads.Update(), beads.Close(), etc. to use resolver\n4. Caching: Can cache routes in memory since file rarely changes\n\n## Context\n\nThis unblocks the gt handoff flow for Mayor. Currently gt handoff gt-xyz fails\nbecause it can't verify the bead exists when running from town root.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-26T14:18:19.399691-08:00","updated_at":"2025-12-26T14:36:43.551435-08:00","closed_at":"2025-12-26T14:36:43.551435-08:00","close_reason":"Implemented prefix-based routing with routes.jsonl. Tested with gt- and bd- prefixes from town root."} @@ -375,7 +375,7 @@ {"id":"bd-jvu","title":"Add bd update --parent flag to change issue parent","description":"Allow changing an issue's parent with bd update --parent \u003cnew-parent-id\u003e. Useful for reorganizing tasks under different epics or moving issues between hierarchies. Should update the parent-child dependency relationship.","status":"tombstone","priority":3,"issue_type":"feature","created_at":"2025-12-17T22:24:07.274485-08:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"feature"} {"id":"bd-kblo","title":"bd prime should mention when beads is redirected","description":"## Problem\n\nAgents running in a redirected clone don't know they're sharing beads with other clones. This can cause confusion when molecules or issues seem to 'appear' or 'disappear'.\n\n## Proposed Solution\n\nWhen `bd prime` runs and detects a redirect, include it in the output:\n\n```\nBeads: /Users/stevey/gt/beads/mayor/rig/.beads\n (redirected from crew/dave - you share issues with other clones)\n```\n\n## Why\n\nVisibility over magic. If agents can see the redirect, they can reason about it.\n\n## Related\n\n- bd where command (shows this on demand)\n- gt redirect following (ensures gt matches bd behavior)","status":"closed","priority":3,"issue_type":"feature","assignee":"beads/crew/dave","created_at":"2025-12-27T21:15:55.026907-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-27T21:33:33.765635-08:00","closed_at":"2025-12-27T21:33:33.765635-08:00","close_reason":"Closed"} {"id":"bd-kff0","title":"Integrate detect-pollution into bd doctor","description":"## Task\nMove `bd detect-pollution` → `bd doctor --check=pollution`\n\n## Current state\n- detect-pollution already shows deprecation hint pointing to doctor\n- Doctor command exists with multiple checks\n\n## Implementation\n\n### 1. Update doctor.go\n- Add pollution check to the doctor checks\n- Add `--check=pollution` flag option\n- Integrate detect-pollution logic\n\n### 2. Update detect_pollution.go\n- Mark as hidden (deprecated)\n- Forward to doctor --check=pollution\n- Keep for one release cycle\n\n### 3. Update docs\n- Remove detect-pollution from any command lists\n- Update doctor docs to include pollution check\n\n## Files to modify\n- cmd/bd/doctor.go\n- cmd/bd/detect_pollution.go\n","status":"closed","priority":2,"issue_type":"task","assignee":"onyx","created_at":"2025-12-27T15:11:10.46326-08:00","created_by":"mayor","updated_at":"2025-12-27T16:04:58.471341-08:00","closed_at":"2025-12-27T16:04:58.471341-08:00","close_reason":"Integrated detect-pollution into bd doctor --check=pollution","pinned":true} -{"id":"bd-kkka","title":"Dead code: fetchAndRebaseInWorktree() marked DEPRECATED but still exists","description":"The function fetchAndRebaseInWorktree() in internal/syncbranch/worktree.go (lines 811-830) is marked as DEPRECATED with a comment:\n\n```go\n// fetchAndRebaseInWorktree is DEPRECATED - kept for reference only.\n// Use contentMergeRecovery instead to avoid tombstone resurrection.\n```\n\nSince contentMergeRecovery() is the replacement and is being used, this dead code should be removed to reduce maintenance burden.\n\nLocation: internal/syncbranch/worktree.go:811-830","acceptance_criteria":"1. fetchAndRebaseInWorktree() function removed from worktree.go\n2. No remaining references to the function in codebase\n3. All existing tests pass: go test ./internal/syncbranch/...\n4. Sync functionality unchanged (contentMergeRecovery still used)","status":"open","priority":3,"issue_type":"chore","created_at":"2025-12-28T15:32:21.97865-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:37:18.882088-08:00","dependencies":[{"issue_id":"bd-kkka","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.241483-08:00","created_by":"daemon"}]} +{"id":"bd-kkka","title":"Dead code: fetchAndRebaseInWorktree() marked DEPRECATED but still exists","description":"attached_args: Remove dead code fetchAndRebaseInWorktree\n\nThe function fetchAndRebaseInWorktree() in internal/syncbranch/worktree.go (lines 811-830) is marked as DEPRECATED with a comment:\n\n```go\n// fetchAndRebaseInWorktree is DEPRECATED - kept for reference only.\n// Use contentMergeRecovery instead to avoid tombstone resurrection.\n```\n\nSince contentMergeRecovery() is the replacement and is being used, this dead code should be removed to reduce maintenance burden.\n\nLocation: internal/syncbranch/worktree.go:811-830","acceptance_criteria":"1. fetchAndRebaseInWorktree() function removed from worktree.go\n2. No remaining references to the function in codebase\n3. All existing tests pass: go test ./internal/syncbranch/...\n4. Sync functionality unchanged (contentMergeRecovery still used)","status":"closed","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:32:21.97865-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:01:14.87213-08:00","closed_at":"2025-12-28T16:01:14.87213-08:00","close_reason":"Removed deprecated fetchAndRebaseInWorktree function and its test. No remaining references. Tests pass.","dependencies":[{"issue_id":"bd-kkka","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.241483-08:00","created_by":"daemon"}]} {"id":"bd-knta","title":"Deacon Patrol","description":"Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup.","status":"tombstone","priority":2,"issue_type":"molecule","created_at":"2025-12-26T13:08:21.233771-08:00","updated_at":"2025-12-27T00:10:54.179341-08:00","deleted_at":"2025-12-27T00:10:54.179341-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"molecule"} {"id":"bd-kptp","title":"Merge: bd-qioh","description":"branch: polecat/Errata\ntarget: main\nsource_issue: bd-qioh\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T13:46:08.832073-08:00","updated_at":"2025-12-23T19:12:08.350136-08:00","closed_at":"2025-12-23T19:12:08.350136-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-kpy","title":"Sync race: rebase-based divergence recovery resurrects tombstones","description":"## Problem\nWhen two repos sync simultaneously, tombstones can be resurrected:\n\n1. Repo A deletes issue (creates tombstone), pushes to sync branch\n2. Repo B (with 'closed' status) exports and tries to push\n3. Push fails (non-fast-forward)\n4. fetchAndRebaseInWorktree does git rebase\n5. Git rebase applies B's 'closed' patch on top of A's 'tombstone'\n6. TEXT-level rebase doesn't invoke beads merge driver\n7. 'closed' overwrites 'tombstone' = resurrection\n\n## Root Cause\nCommitToSyncBranch uses git rebase for divergence recovery, but rebase is text-level, not content-level. The proper content-level merge in PullFromSyncBranch handles tombstones correctly, but it runs AFTER the problematic push.\n\n## Proposed Fix\nOption 1: Don't push in CommitToSyncBranch - let PullFromSyncBranch handle merge+push\nOption 2: Replace git rebase with content-level merge in fetchAndRebaseInWorktree\nOption 3: Reorder sync steps: Export → Pull/Merge → Commit → Push\n\n## Workaround Applied\nExcluded tombstones from orphan detection warnings (commit 1e97d9cc).\n\nSee also: bd-3852 (Add orphan detection migration)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-17T23:29:33.049272-08:00","updated_at":"2025-12-24T22:41:09.184574-08:00","closed_at":"2025-12-24T22:41:09.184574-08:00","close_reason":"Fixed by replacing git rebase with content-level merge in divergence recovery"} From efb56e9dd854ea3607f4cd947bdc64589ed3704a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 16:23:45 -0800 Subject: [PATCH 3/6] refactor: Break up 280-line flushToJSONLWithState into focused helpers (bd-9hc9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted 8 helper functions from the monolithic flushToJSONLWithState: - recordFlushFailure/recordFlushSuccess: Failure tracking and counter management - readExistingJSONL: Parse existing JSONL file for incremental merging - fetchAndMergeIssues: Fetch dirty issues from DB and merge into map - filterWisps: Remove ephemeral (wisp) issues from export - filterByMultiRepoPrefix: Multi-repo prefix filtering for non-primary repos - updateFlushExportMetadata: Store hashes and timestamps after export - getIssuesToExport: Determine full vs incremental export issue list Main function now reads as a clear pipeline: 1. Validate integrity -> 2. Get issues -> 3. Read existing JSONL 4. Merge from DB -> 5. Filter wisps -> 6. Filter by prefix 7. Write atomically -> 8. Update metadata Benefits: - Each helper is single-responsibility and testable - Main function reduced from ~280 to ~94 lines - Logic is clearly separated and documented - Easier to understand and maintain šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/autoflush.go | 477 +++++++++++++++++++++++--------------------- 1 file changed, 250 insertions(+), 227 deletions(-) diff --git a/cmd/bd/autoflush.go b/cmd/bd/autoflush.go index f2b4dfda..30186ea6 100644 --- a/cmd/bd/autoflush.go +++ b/cmd/bd/autoflush.go @@ -18,6 +18,7 @@ import ( "github.com/steveyegge/beads/internal/beads" "github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/debug" + "github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/ui" "github.com/steveyegge/beads/internal/utils" @@ -462,6 +463,194 @@ func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error) return exportedIDs, nil } +// recordFlushFailure records a flush failure, incrementing the failure counter +// and displaying warnings after consecutive failures. +func recordFlushFailure(err error) { + flushMutex.Lock() + flushFailureCount++ + lastFlushError = err + failCount := flushFailureCount + flushMutex.Unlock() + + // Always show the immediate warning + fmt.Fprintf(os.Stderr, "Warning: auto-flush failed: %v\n", err) + + // Show prominent warning after 3+ consecutive failures + if failCount >= 3 { + fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("āš ļø CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!")) + fmt.Fprintf(os.Stderr, "%s\n", ui.RenderFail("āš ļø Your JSONL file may be out of sync with the database.")) + fmt.Fprintf(os.Stderr, "%s\n\n", ui.RenderFail("āš ļø Run 'bd export -o .beads/issues.jsonl' manually to fix.")) + } +} + +// recordFlushSuccess records a successful flush, resetting the failure counter. +func recordFlushSuccess() { + flushMutex.Lock() + flushFailureCount = 0 + lastFlushError = nil + flushMutex.Unlock() +} + +// readExistingJSONL reads an existing JSONL file into a map for incremental merging. +// Returns empty map if file doesn't exist or can't be read. +func readExistingJSONL(jsonlPath string) (map[string]*types.Issue, error) { + issueMap := make(map[string]*types.Issue) + + existingFile, err := os.Open(jsonlPath) + if err != nil { + if os.IsNotExist(err) { + return issueMap, nil // File doesn't exist, return empty map + } + return nil, fmt.Errorf("failed to open existing JSONL: %w", err) + } + defer existingFile.Close() + + scanner := bufio.NewScanner(existingFile) + // Increase buffer to handle large JSON lines + // Default scanner limit is 64KB which can cause silent truncation + scanner.Buffer(make([]byte, 0, 1024), 2*1024*1024) // 2MB max line size + + lineNum := 0 + for scanner.Scan() { + lineNum++ + line := scanner.Text() + if line == "" { + continue + } + var issue types.Issue + if err := json.Unmarshal([]byte(line), &issue); err == nil { + issue.SetDefaults() // Apply defaults for omitted fields (beads-399) + issueMap[issue.ID] = &issue + } else { + // Warn about malformed JSONL lines + fmt.Fprintf(os.Stderr, "Warning: skipping malformed JSONL line %d: %v\n", lineNum, err) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read existing JSONL: %w", err) + } + + return issueMap, nil +} + +// fetchAndMergeIssues fetches dirty issues from the database and merges them into issueMap. +// Issues that no longer exist are removed from the map. +func fetchAndMergeIssues(ctx context.Context, s storage.Storage, dirtyIDs []string, issueMap map[string]*types.Issue) error { + for _, issueID := range dirtyIDs { + issue, err := s.GetIssue(ctx, issueID) + if err != nil { + return fmt.Errorf("failed to get issue %s: %w", issueID, err) + } + if issue == nil { + // Issue was deleted, remove from map + delete(issueMap, issueID) + continue + } + + // Get dependencies for this issue + deps, err := s.GetDependencyRecords(ctx, issueID) + if err != nil { + return fmt.Errorf("failed to get dependencies for %s: %w", issueID, err) + } + issue.Dependencies = deps + + // Update map + issueMap[issueID] = issue + } + return nil +} + +// filterWisps removes ephemeral (wisp) issues from the map and returns a slice. +// Wisps should never be exported to JSONL. +func filterWisps(issueMap map[string]*types.Issue) []*types.Issue { + issues := make([]*types.Issue, 0, len(issueMap)) + wispsSkipped := 0 + for _, issue := range issueMap { + if issue.Ephemeral { + wispsSkipped++ + continue + } + issues = append(issues, issue) + } + if wispsSkipped > 0 { + debug.Logf("auto-flush: filtered %d wisps from export", wispsSkipped) + } + return issues +} + +// filterByMultiRepoPrefix filters issues by prefix in multi-repo mode. +// Non-primary repos should only export issues matching their own prefix. +func filterByMultiRepoPrefix(ctx context.Context, s storage.Storage, issues []*types.Issue) []*types.Issue { + multiRepo := config.GetMultiRepoConfig() + if multiRepo == nil { + return issues + } + + // Get our configured prefix + prefix, prefixErr := s.GetConfig(ctx, "issue_prefix") + if prefixErr != nil || prefix == "" { + return issues + } + + // Determine if we're the primary repo + cwd, _ := os.Getwd() + primaryPath := multiRepo.Primary + if primaryPath == "" || primaryPath == "." { + primaryPath = cwd + } + + // Normalize paths for comparison + absCwd, _ := filepath.Abs(cwd) + absPrimary, _ := filepath.Abs(primaryPath) + + if absCwd == absPrimary { + return issues // Primary repo exports all issues + } + + // Filter to only issues matching our prefix + filtered := make([]*types.Issue, 0, len(issues)) + prefixWithDash := prefix + if !strings.HasSuffix(prefixWithDash, "-") { + prefixWithDash = prefix + "-" + } + for _, issue := range issues { + if strings.HasPrefix(issue.ID, prefixWithDash) { + filtered = append(filtered, issue) + } + } + debug.Logf("multi-repo filter: %d issues -> %d (prefix %s)", len(issues), len(filtered), prefix) + return filtered +} + +// updateFlushExportMetadata stores hashes and timestamps after a successful flush export. +func updateFlushExportMetadata(ctx context.Context, s storage.Storage, jsonlPath string) { + jsonlData, err := os.ReadFile(jsonlPath) + if err != nil { + return // Non-fatal, just skip metadata update + } + + hasher := sha256.New() + hasher.Write(jsonlData) + exportedHash := hex.EncodeToString(hasher.Sum(nil)) + + if err := s.SetMetadata(ctx, "jsonl_content_hash", exportedHash); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to update jsonl_content_hash after export: %v\n", err) + } + + // Store JSONL file hash for integrity validation + if err := s.SetJSONLFileHash(ctx, exportedHash); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to update jsonl_file_hash after export: %v\n", err) + } + + // Update last_import_time so staleness check doesn't see JSONL as "newer" (fixes #399) + // Use RFC3339Nano to preserve nanosecond precision. + exportTime := time.Now().Format(time.RFC3339Nano) + if err := s.SetMetadata(ctx, "last_import_time", exportTime); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_time after export: %v\n", err) + } +} + // flushState captures the state needed for a flush operation type flushState struct { forceDirty bool // Force flush even if isDirty is false @@ -507,30 +696,13 @@ func flushToJSONLWithState(state flushState) { storeMutex.Unlock() ctx := rootCtx - + // Validate JSONL integrity BEFORE checking isDirty // This detects if JSONL and export_hashes are out of sync (e.g., after git operations) - // If export_hashes was cleared, we need to do a full export even if nothing is dirty integrityNeedsFullExport, err := validateJSONLIntegrity(ctx, jsonlPath) if err != nil { - // Special case: missing JSONL is not fatal, just forces full export if !os.IsNotExist(err) { - // Record failure without clearing isDirty (we didn't do any work yet) - flushMutex.Lock() - flushFailureCount++ - lastFlushError = err - failCount := flushFailureCount - flushMutex.Unlock() - - // Always show the immediate warning - fmt.Fprintf(os.Stderr, "Warning: auto-flush failed: %v\n", err) - - // Show prominent warning after 3+ consecutive failures - if failCount >= 3 { - fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("āš ļø CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!")) - fmt.Fprintf(os.Stderr, "%s\n", ui.RenderFail("āš ļø Your JSONL file may be out of sync with the database.")) - fmt.Fprintf(os.Stderr, "%s\n\n", ui.RenderFail("āš ļø Run 'bd export -o .beads/issues.jsonl' manually to fix.")) - } + recordFlushFailure(err) return } // Missing JSONL: treat as "force full export" case @@ -538,235 +710,86 @@ func flushToJSONLWithState(state flushState) { } // Check if we should proceed with export - // Use only the state parameter - don't read global flags - // Caller is responsible for passing correct forceDirty/forceFullExport values if !state.forceDirty && !integrityNeedsFullExport { - // Nothing to do: not forced and no integrity issue return } // Determine export mode fullExport := state.forceFullExport || integrityNeedsFullExport - // Helper to record failure - recordFailure := func(err error) { - flushMutex.Lock() - flushFailureCount++ - lastFlushError = err - failCount := flushFailureCount - flushMutex.Unlock() - - // Always show the immediate warning - fmt.Fprintf(os.Stderr, "Warning: auto-flush failed: %v\n", err) - - // Show prominent warning after 3+ consecutive failures - if failCount >= 3 { - fmt.Fprintf(os.Stderr, "\n%s\n", ui.RenderFail("āš ļø CRITICAL: Auto-flush has failed "+fmt.Sprint(failCount)+" times consecutively!")) - fmt.Fprintf(os.Stderr, "%s\n", ui.RenderFail("āš ļø Your JSONL file may be out of sync with the database.")) - fmt.Fprintf(os.Stderr, "%s\n\n", ui.RenderFail("āš ļø Run 'bd export -o .beads/issues.jsonl' manually to fix.")) - } - } - - // Helper to record success - recordSuccess := func() { - flushMutex.Lock() - flushFailureCount = 0 - lastFlushError = nil - flushMutex.Unlock() - } - // Determine which issues to export - var dirtyIDs []string - - if fullExport { - // Full export: get ALL issues (needed after ID-changing operations like renumber) - allIssues, err2 := store.SearchIssues(ctx, "", types.IssueFilter{}) - if err2 != nil { - recordFailure(fmt.Errorf("failed to get all issues: %w", err2)) - return - } - dirtyIDs = make([]string, len(allIssues)) - for i, issue := range allIssues { - dirtyIDs[i] = issue.ID - } - } else { - // Incremental export: get only dirty issue IDs - var err2 error - dirtyIDs, err2 = store.GetDirtyIssues(ctx) - if err2 != nil { - recordFailure(fmt.Errorf("failed to get dirty issues: %w", err2)) - return - } - - // No dirty issues? Nothing to do! - if len(dirtyIDs) == 0 { - recordSuccess() - return - } - } - - // Read existing JSONL into a map (skip for full export - we'll rebuild from scratch) - issueMap := make(map[string]*types.Issue) - if !fullExport { - if existingFile, err := os.Open(jsonlPath); err == nil { - scanner := bufio.NewScanner(existingFile) - // Increase buffer to handle large JSON lines - // Default scanner limit is 64KB which can cause silent truncation - scanner.Buffer(make([]byte, 0, 1024), 2*1024*1024) // 2MB max line size - lineNum := 0 - for scanner.Scan() { - lineNum++ - line := scanner.Text() - if line == "" { - continue - } - var issue types.Issue - if err := json.Unmarshal([]byte(line), &issue); err == nil { - issue.SetDefaults() // Apply defaults for omitted fields (beads-399) - issueMap[issue.ID] = &issue - } else { - // Warn about malformed JSONL lines - fmt.Fprintf(os.Stderr, "Warning: skipping malformed JSONL line %d: %v\n", lineNum, err) - } - } - // Check for scanner errors - if err := scanner.Err(); err != nil { - _ = existingFile.Close() - recordFailure(fmt.Errorf("failed to read existing JSONL: %w", err)) - return - } - _ = existingFile.Close() - } - } - - // Fetch only dirty issues from DB - for _, issueID := range dirtyIDs { - issue, err := store.GetIssue(ctx, issueID) - if err != nil { - recordFailure(fmt.Errorf("failed to get issue %s: %w", issueID, err)) - return - } - if issue == nil { - // Issue was deleted, remove from map - delete(issueMap, issueID) - continue - } - - // Get dependencies for this issue - deps, err := store.GetDependencyRecords(ctx, issueID) - if err != nil { - recordFailure(fmt.Errorf("failed to get dependencies for %s: %w", issueID, err)) - return - } - issue.Dependencies = deps - - // Update map - issueMap[issueID] = issue - } - - // Convert map to slice (will be sorted by writeJSONLAtomic) - // Filter out wisps - they should never be exported to JSONL - // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. - // This prevents "zombie" issues that resurrect after mol squash deletes them. - issues := make([]*types.Issue, 0, len(issueMap)) - wispsSkipped := 0 - for _, issue := range issueMap { - if issue.Ephemeral { - wispsSkipped++ - continue - } - issues = append(issues, issue) - } - if wispsSkipped > 0 { - debug.Logf("auto-flush: filtered %d wisps from export", wispsSkipped) - } - - // Filter issues by prefix in multi-repo mode for non-primary repos (fixes GH #437) - // In multi-repo mode, non-primary repos should only export issues that match - // their own prefix. Issues from other repos (hydrated for unified view) should - // NOT be written to the local JSONL. - multiRepo := config.GetMultiRepoConfig() - if multiRepo != nil { - // Get our configured prefix - prefix, prefixErr := store.GetConfig(ctx, "issue_prefix") - if prefixErr == nil && prefix != "" { - // Determine if we're the primary repo - cwd, _ := os.Getwd() - primaryPath := multiRepo.Primary - if primaryPath == "" || primaryPath == "." { - primaryPath = cwd - } - - // Normalize paths for comparison - absCwd, _ := filepath.Abs(cwd) - absPrimary, _ := filepath.Abs(primaryPath) - - isPrimary := absCwd == absPrimary - - if !isPrimary { - // Filter to only issues matching our prefix - filtered := make([]*types.Issue, 0, len(issues)) - prefixWithDash := prefix - if !strings.HasSuffix(prefixWithDash, "-") { - prefixWithDash = prefix + "-" - } - for _, issue := range issues { - if strings.HasPrefix(issue.ID, prefixWithDash) { - filtered = append(filtered, issue) - } - } - debug.Logf("multi-repo filter: %d issues -> %d (prefix %s)", len(issues), len(filtered), prefix) - issues = filtered - } - } - } - - // Write atomically using common helper - exportedIDs, err := writeJSONLAtomic(jsonlPath, issues) + dirtyIDs, err := getIssuesToExport(ctx, fullExport) if err != nil { - recordFailure(err) + recordFlushFailure(err) + return + } + if len(dirtyIDs) == 0 && !fullExport { + recordFlushSuccess() return } - // Clear only the dirty issues that were actually exported (fixes race condition) - // Don't clear issues that were skipped due to timestamp-only changes + // Read existing JSONL into a map (skip for full export - we'll rebuild from scratch) + var issueMap map[string]*types.Issue + if fullExport { + issueMap = make(map[string]*types.Issue) + } else { + issueMap, err = readExistingJSONL(jsonlPath) + if err != nil { + recordFlushFailure(err) + return + } + } + + // Fetch dirty issues from DB and merge into map + if err := fetchAndMergeIssues(ctx, store, dirtyIDs, issueMap); err != nil { + recordFlushFailure(err) + return + } + + // Convert map to slice, filtering out wisps + issues := filterWisps(issueMap) + + // Filter by prefix in multi-repo mode + issues = filterByMultiRepoPrefix(ctx, store, issues) + + // Write atomically + exportedIDs, err := writeJSONLAtomic(jsonlPath, issues) + if err != nil { + recordFlushFailure(err) + return + } + + // Clear dirty issues that were exported if len(exportedIDs) > 0 { if err := store.ClearDirtyIssuesByID(ctx, exportedIDs); err != nil { - // Don't fail the whole flush for this, but warn fmt.Fprintf(os.Stderr, "Warning: failed to clear dirty issues: %v\n", err) } } - // Store hash of exported JSONL (enables hash-based auto-import) - // Renamed from last_import_hash to jsonl_content_hash - jsonlData, err := os.ReadFile(jsonlPath) - if err == nil { - hasher := sha256.New() - hasher.Write(jsonlData) - exportedHash := hex.EncodeToString(hasher.Sum(nil)) - if err := store.SetMetadata(ctx, "jsonl_content_hash", exportedHash); err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to update jsonl_content_hash after export: %v\n", err) - } + // Update metadata (hashes, timestamps) + updateFlushExportMetadata(ctx, store, jsonlPath) - // Store JSONL file hash for integrity validation - if err := store.SetJSONLFileHash(ctx, exportedHash); err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to update jsonl_file_hash after export: %v\n", err) - } + recordFlushSuccess() +} - // Update last_import_time so staleness check doesn't see JSONL as "newer" (fixes #399) - // CheckStaleness() compares last_import_time against JSONL mtime. After export, - // the JSONL mtime is updated, so we must also update last_import_time to prevent - // false "stale" detection on subsequent reads. - // - // Use RFC3339Nano to preserve nanosecond precision. The file mtime has nanosecond - // precision, so using RFC3339 (second precision) would cause the stored time to be - // slightly earlier than the file mtime, triggering false staleness. - exportTime := time.Now().Format(time.RFC3339Nano) - if err := store.SetMetadata(ctx, "last_import_time", exportTime); err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_time after export: %v\n", err) +// getIssuesToExport determines which issue IDs need to be exported. +// For full export, returns all issue IDs. For incremental, returns only dirty IDs. +func getIssuesToExport(ctx context.Context, fullExport bool) ([]string, error) { + if fullExport { + allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + return nil, fmt.Errorf("failed to get all issues: %w", err) } + ids := make([]string, len(allIssues)) + for i, issue := range allIssues { + ids[i] = issue.ID + } + return ids, nil } - // Success! FlushManager manages its local state in run() goroutine. - recordSuccess() + dirtyIDs, err := store.GetDirtyIssues(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get dirty issues: %w", err) + } + return dirtyIDs, nil } From dedf5cfe92762d55fcf434c2d61e56ff9e106fe5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 16:23:54 -0800 Subject: [PATCH 4/6] bd sync: closed bd-9hc9 after refactoring autoflush.go --- .beads/issues.jsonl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index db1395b9..64a08341 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -27,7 +27,7 @@ {"id":"bd-1pr6","title":"Time-dependent worktree detection tests may be flaky","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-26T01:21:22.065353-08:00","updated_at":"2025-12-26T01:21:22.065353-08:00"} {"id":"bd-1ri4","title":"Test prefix bd-","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-27T14:24:41.240483-08:00","created_by":"stevey","updated_at":"2025-12-27T14:24:50.419881-08:00","deleted_at":"2025-12-27T14:24:50.419881-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} {"id":"bd-1slh","title":"Investigate charmbracelet-based TUI for beads","description":"Now that we've merged the create-form command (PR #603) which uses charmbracelet/huh, investigate whether beads should have a more comprehensive TUI.\n\nConsiderations:\n- Should this be in core or a separate binary (bd-tui)?\n- What functionality would benefit from a TUI? (list view, issue details, search, bulk operations)\n- Plugin/extension architecture vs build tags vs separate binary\n- Dependency cost vs user experience tradeoff\n- Target audience: humans who want interactive workflows vs CLI/scripting users\n\nRelated: PR #603 added charmbracelet/huh dependency for create-form command.","notes":"Foundation is in place (lipgloss, huh), but not a priority right now","status":"deferred","priority":3,"issue_type":"feature","created_at":"2025-12-17T14:20:51.503563-08:00","updated_at":"2025-12-20T23:31:34.354023-08:00"} -{"id":"bd-1tkd","title":"Code smell: ComputeContentHash() is 100+ lines of repetitive code","description":"attached_args: Refactor ComputeContentHash repetitive code\n\nThe ComputeContentHash() method in internal/types/types.go (lines 87-190) is over 100 lines of repetitive code:\n\n```go\nh.Write([]byte(i.Title))\nh.Write([]byte{0}) // separator\nh.Write([]byte(i.Description))\nh.Write([]byte{0})\nh.Write([]byte(i.Design))\n// ... repeated 40+ times\n```\n\nConsider:\n1. Using reflection to iterate over struct fields tagged for hashing\n2. Creating a helper function that takes field name and value\n3. Using a slice of fields to hash and iterating over it\n4. At minimum, extracting the repetitive pattern into a helper\n\nLocation: internal/types/types.go:87-190","acceptance_criteria":"1. ComputeContentHash() refactored to reduce repetition\n2. Helper function or data-driven approach replaces repetitive h.Write() calls\n3. Hash output unchanged for same input (backwards compatible)\n4. All existing tests pass: go test ./internal/types/...\n5. Benchmark shows no significant performance regression","status":"pinned","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:31:55.514941-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:02:15.871379-08:00","dependencies":[{"issue_id":"bd-1tkd","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.204723-08:00","created_by":"daemon"}]} +{"id":"bd-1tkd","title":"Code smell: ComputeContentHash() is 100+ lines of repetitive code","description":"attached_args: Refactor ComputeContentHash repetitive code\n\nThe ComputeContentHash() method in internal/types/types.go (lines 87-190) is over 100 lines of repetitive code:\n\n```go\nh.Write([]byte(i.Title))\nh.Write([]byte{0}) // separator\nh.Write([]byte(i.Description))\nh.Write([]byte{0})\nh.Write([]byte(i.Design))\n// ... repeated 40+ times\n```\n\nConsider:\n1. Using reflection to iterate over struct fields tagged for hashing\n2. Creating a helper function that takes field name and value\n3. Using a slice of fields to hash and iterating over it\n4. At minimum, extracting the repetitive pattern into a helper\n\nLocation: internal/types/types.go:87-190","acceptance_criteria":"1. ComputeContentHash() refactored to reduce repetition\n2. Helper function or data-driven approach replaces repetitive h.Write() calls\n3. Hash output unchanged for same input (backwards compatible)\n4. All existing tests pass: go test ./internal/types/...\n5. Benchmark shows no significant performance regression","status":"closed","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:31:55.514941-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:10:21.307593-08:00","closed_at":"2025-12-28T16:10:21.307593-08:00","close_reason":"Refactoring complete. hashFieldWriter helper replaces 100+ lines of repetitive h.Write() with clean typed methods. All tests pass.","dependencies":[{"issue_id":"bd-1tkd","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.204723-08:00","created_by":"daemon"}]} {"id":"bd-1tw","title":"Fix G104 errors unhandled in internal/storage/sqlite/queries.go:1186","description":"Linting issue: G104: Errors unhandled (gosec) at internal/storage/sqlite/queries.go:1186:2. Error: rows.Close()","status":"tombstone","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:13.051671889-07:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-20j","title":"sync branch not match config","description":"./bd sync\n→ Exporting pending changes to JSONL...\n→ No changes to commit\n→ Pulling from sync branch 'gh-386'...\nError pulling from sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/worktree-db-fail/.git: not a directory\nmatt@blufin-framation ~/d/b/worktree-db-fail (worktree-db-fail) [1]\u003e bd config list\n\nConfiguration:\n auto_compact_enabled = false\n compact_batch_size = 50\n compact_model = claude-3-5-haiku-20241022\n compact_parallel_workers = 5\n compact_tier1_days = 30\n compact_tier1_dep_levels = 2\n compact_tier2_commits = 100\n compact_tier2_days = 90\n compact_tier2_dep_levels = 5\n compaction_enabled = false\n issue_prefix = worktree-db-fail\n sync.branch = worktree-db-fail","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-08T06:49:04.449094018-07:00","updated_at":"2025-12-08T06:49:04.449094018-07:00"} {"id":"bd-28db","title":"Add 'bd status' command for issue database overview","description":"Implement a bd status command that provides a quick snapshot of the issue database state, similar to how git status shows working tree state.\n\nExpected output: Show summary including counts by state (open, in-progress, blocked, closed), recent activity (last 7 days), and quick overview without needing multiple queries.\n\nExample output showing issue counts, recent activity stats, and pointer to bd list for details.\n\nProposed options: --all (show all issues), --assigned (show issues assigned to current user), --json (JSON format output)\n\nUse cases: Quick project health check, onboarding for new contributors, integration with shell prompts or CI/CD, daily standup reference","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-02T17:25:59.203549-08:00","updated_at":"2025-12-21T17:54:00.205191-08:00","closed_at":"2025-12-21T17:54:00.205191-08:00","close_reason":"Already implemented - bd status shows summary and activity"} @@ -100,7 +100,7 @@ {"id":"bd-5e9q","title":"Add backwards-compatible aliases and deprecation warnings","description":"## Task\nEnsure backwards compatibility by adding hidden aliases for all moved commands.\n\n## Implementation\n\n### Create aliases.go\nNew file to centralize alias management:\n```go\npackage main\n\nimport (\n \"fmt\"\n \"os\"\n \n \"github.com/spf13/cobra\"\n)\n\n// Deprecated command aliases for backwards compatibility\n// These will be removed in v0.XX.0\n\nfunc init() {\n // migrate-* → migrate *\n addDeprecatedAlias(\"migrate-hash-ids\", \"migrate hash-ids\")\n addDeprecatedAlias(\"migrate-issues\", \"migrate issues\")\n addDeprecatedAlias(\"migrate-sync\", \"migrate sync\")\n addDeprecatedAlias(\"migrate-tombstones\", \"migrate tombstones\")\n \n // top-level → admin *\n addDeprecatedAlias(\"cleanup\", \"admin cleanup\")\n addDeprecatedAlias(\"compact\", \"admin compact\")\n addDeprecatedAlias(\"reset\", \"admin reset\")\n \n // top-level → formula *\n addDeprecatedAlias(\"cook\", \"formula cook\")\n}\n\nfunc addDeprecatedAlias(old, new string) {\n cmd := \u0026cobra.Command{\n Use: old,\n Hidden: true,\n Run: func(cmd *cobra.Command, args []string) {\n fmt.Fprintf(os.Stderr, \"āš ļø '%s' is deprecated, use '%s' instead\\n\", old, new)\n // Forward execution to new command\n },\n }\n rootCmd.AddCommand(cmd)\n}\n```\n\n### Deprecation behavior\n1. First release: Show warning, execute command\n2. One release later: Show warning, still execute\n3. Third release: Remove aliases entirely\n\n## Files to create\n- cmd/bd/aliases.go\n\n## Files to modify \n- cmd/bd/main.go (ensure aliases loaded)\n","status":"closed","priority":2,"issue_type":"task","assignee":"opal","created_at":"2025-12-27T15:11:33.54574-08:00","created_by":"mayor","updated_at":"2025-12-27T16:06:08.157887-08:00","closed_at":"2025-12-27T16:06:08.157887-08:00","close_reason":"Added aliases.go with infrastructure for backwards-compatible command aliases. Aliases only activate after commands are moved to their new locations.","pinned":true} {"id":"bd-5exm","title":"Merge: bd-49kw","description":"branch: polecat/nux\ntarget: main\nsource_issue: bd-49kw\nrig: beads","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-23T20:43:23.156375-08:00","updated_at":"2025-12-23T21:21:57.693169-08:00","closed_at":"2025-12-23T21:21:57.693169-08:00","close_reason":"stale - no code pushed"} {"id":"bd-5hrq","title":"bd doctor: detect issues referenced in commits but still open","description":"Add a doctor check that finds 'orphaned' issues - ones referenced in git commit messages (e.g., 'fix bug (bd-xxx)') but still marked as open in beads.\n\n**Detection logic:**\n1. Get all open issue IDs from beads\n2. Parse git log for issue ID references matching pattern \\(prefix-[a-z0-9.]+\\)\n3. Report issues that appear in commits but are still open\n\n**Output:**\n⚠ Warning: N issues referenced in commits but still open\n bd-xxx: 'Issue title' (commit abc123)\n bd-yyy: 'Issue title' (commit def456)\n \n These may be implemented but not closed. Run 'bd show \u003cid\u003e' to check.\n\n**Implementation:**\n- Add check to doctor/checks.go\n- Use git log parsing (already have git utilities)\n- Match against configured issue_prefix","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-21T21:48:08.473165-08:00","updated_at":"2025-12-21T21:55:37.795109-08:00","closed_at":"2025-12-21T21:55:37.795109-08:00","close_reason":"Implemented CheckOrphanedIssues in git.go with 8 test cases. Detects issues referenced in commits (e.g., 'fix bug (bd-xxx)') that are still open. Shows warning with issue IDs and commit hashes."} -{"id":"bd-5l59","title":"Code smell: Issue struct is a God Object (50+ fields)","description":"attached_args: Refactor Issue struct God Object\n\nThe Issue struct in internal/types/types.go has 50+ fields covering many different concerns:\n\n- Basic issue tracking (ID, Title, Description, Status, Priority)\n- Messaging/communication (Sender, Ephemeral, etc.)\n- Agent identity (HookBead, RoleBead, AgentState, RoleType, Rig)\n- Gate/async coordination (AwaitType, AwaitID, Timeout, Waiters)\n- Source tracing (SourceFormula, SourceLocation)\n- HOP validation/entities (Creator, Validations)\n- Compaction (CompactionLevel, CompactedAt, OriginalSize)\n- Tombstones (DeletedAt, DeletedBy, DeleteReason)\n\nConsider:\n1. Extracting field groups into embedded structs (e.g., AgentFields, GateFields)\n2. Using composition pattern to make the struct more modular\n3. At minimum, grouping related fields together with section comments\n\nLocation: internal/types/types.go:12-82","acceptance_criteria":"1. Related fields grouped with clear section comments (e.g., // Agent identity fields)\n2. Consider extracting into embedded structs if grouping reveals clean boundaries\n3. All existing tests pass: go test ./internal/types/...\n4. ComputeContentHash() still works correctly after any struct changes\n5. JSON serialization unchanged (no breaking changes to JSONL format)","status":"pinned","priority":3,"issue_type":"chore","assignee":"gt-beads-furiosa","created_at":"2025-12-28T15:31:34.021236-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:57:07.884245-08:00","dependencies":[{"issue_id":"bd-5l59","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.187103-08:00","created_by":"daemon"}]} +{"id":"bd-5l59","title":"Code smell: Issue struct is a God Object (50+ fields)","description":"attached_args: Refactor Issue struct God Object\n\nThe Issue struct in internal/types/types.go has 50+ fields covering many different concerns:\n\n- Basic issue tracking (ID, Title, Description, Status, Priority)\n- Messaging/communication (Sender, Ephemeral, etc.)\n- Agent identity (HookBead, RoleBead, AgentState, RoleType, Rig)\n- Gate/async coordination (AwaitType, AwaitID, Timeout, Waiters)\n- Source tracing (SourceFormula, SourceLocation)\n- HOP validation/entities (Creator, Validations)\n- Compaction (CompactionLevel, CompactedAt, OriginalSize)\n- Tombstones (DeletedAt, DeletedBy, DeleteReason)\n\nConsider:\n1. Extracting field groups into embedded structs (e.g., AgentFields, GateFields)\n2. Using composition pattern to make the struct more modular\n3. At minimum, grouping related fields together with section comments\n\nLocation: internal/types/types.go:12-82","acceptance_criteria":"1. Related fields grouped with clear section comments (e.g., // Agent identity fields)\n2. Consider extracting into embedded structs if grouping reveals clean boundaries\n3. All existing tests pass: go test ./internal/types/...\n4. ComputeContentHash() still works correctly after any struct changes\n5. JSON serialization unchanged (no breaking changes to JSONL format)","status":"pinned","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:31:34.021236-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:16:46.442736-08:00","dependencies":[{"issue_id":"bd-5l59","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.187103-08:00","created_by":"daemon"}]} {"id":"bd-5qim","title":"Optimize GetReadyWork performance - 752ms on 10K database (target: \u003c50ms)","notes":"# Performance Analysis (10K Issue Database)\n\nAnalyzed using CPU profiles from benchmark suite on Apple M2 Pro.\n\n## Operation Performance\n\n| Operation | Time | Allocations | Memory |\n|----------------------------------|---------|-------------|--------|\n| bd ready (GetReadyWork) | ~752ms | 167,466 | 16MB |\n| bd list (SearchIssues no filter) | ~11.6ms | 89,214 | 5.8MB |\n| bd list (SearchIssues filtered) | ~9.2ms | 62,365 | 3.5MB |\n| bd create (CreateIssue) | ~2.6ms | 146 | 8.6KB |\n| bd update (UpdateIssue) | ~0.32ms | 364 | 15KB |\n| bd close (UpdateIssue) | ~0.32ms | 364 | 15KB |\n\n**Target: \u003c50ms for all operations on 10K database**\n\n**Current issue: GetReadyWork is 15x over target (752ms vs 50ms)**\n\n## Root Cause\n\nGetReadyWork (internal/storage/sqlite/ready.go:90-128) uses recursive CTE to propagate blocking:\n- 65x slower than SearchIssues\n- Recalculates entire blocked issue tree on every call\n- Algorithm:\n 1. Find directly blocked issues via 'blocks' dependencies\n 2. Recursively propagate blockage to descendants (max depth: 50)\n 3. Exclude all blocked issues from results\n\n## CPU Profile Analysis\n\n- Database syscalls (pthread_cond_signal, syscall6): ~75%\n- SQLite engine overhead: inherent to recursive CTE\n- Application code (query construction): \u003c1%\n\n**Bottleneck is the recursive CTE query execution, not application code.**\n\n## Optimization Recommendations\n\n### High Impact (Likely to achieve \u003c50ms target)\n\n1. **Cache blocked issue calculation**\n - Add `blocked_issues` table updated on dependency changes\n - Trade write complexity for read speed (ready called \u003e\u003e dependency changes)\n - Eliminates recursive CTE on every read\n\n2. **Add/verify database indexes**\n ```sql\n CREATE INDEX IF NOT EXISTS idx_dependencies_blocked \n ON dependencies(issue_id, type, depends_on_id);\n CREATE INDEX IF NOT EXISTS idx_issues_status \n ON issues(status);\n ```\n\n### Medium Impact\n\n3. **Reduce allocations** (167K allocations for GetReadyWork)\n - Profile `scanIssues()` for object pooling opportunities\n - Reuse slice capacity for repeated calls\n\n### Low Impact (Not recommended)\n- Query optimization for CRUD operations (already \u003c3ms)\n- Connection pooling tuning (not showing in profiles)\n\n## Verification\n\nRun benchmarks to validate optimization:\n```bash\nmake bench-quick\ngo tool pprof -http=:8080 internal/storage/sqlite/bench-cpu-*.prof\n```\n\nProfile files automatically generated in `internal/storage/sqlite/`.","status":"tombstone","priority":0,"issue_type":"bug","created_at":"2025-11-14T09:02:46.507526-08:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-5rj1","title":"Merge: bd-gqxd","description":"branch: polecat/furiosa\ntarget: main\nsource_issue: bd-gqxd\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T16:40:21.707706-08:00","updated_at":"2025-12-23T19:12:08.349245-08:00","closed_at":"2025-12-23T19:12:08.349245-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-5s91","title":"CLI API Audit for OSS Launch","description":"Comprehensive CLI API audit before OSS launch.\n\n## Tasks\n1. Review gt command groupings - propose consolidation\n2. Review bd command groupings \n3. Document gt vs bd ownership boundaries\n4. Identify naming inconsistencies\n5. Document flag vs subcommand criteria\n\n## Context\n- Pre-OSS launch cleanup\n- Goal: clean, consistent, discoverable CLI surface","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-25T00:15:41.355013-08:00","updated_at":"2025-12-25T13:26:25.587476-08:00","closed_at":"2025-12-25T13:26:25.587476-08:00","close_reason":"Completed"} @@ -170,17 +170,17 @@ {"id":"bd-8x3w","title":"Add composite index (issue_id, type) on dependencies table","description":"GetBlockedIssues uses EXISTS clauses that filter by issue_id AND type together.\n\n**Query pattern (ready.go:427-429):**\n```sql\nEXISTS (\n SELECT 1 FROM dependencies d2\n WHERE d2.issue_id = i.id AND d2.type = 'blocks'\n)\n```\n\n**Problem:** Only idx_dependencies_issue exists. SQLite must filter type after index lookup.\n\n**Solution:** Add migration:\n```sql\nCREATE INDEX IF NOT EXISTS idx_dependencies_issue_type ON dependencies(issue_id, type);\n```\n\n**Note:** This complements the existing idx_dependencies_depends_on_type for the reverse direction.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-22T22:58:52.876846-08:00","updated_at":"2025-12-22T23:15:13.840789-08:00","closed_at":"2025-12-22T23:15:13.840789-08:00","close_reason":"Implemented in migration 026_additional_indexes.go","dependencies":[{"issue_id":"bd-8x3w","depends_on_id":"bd-h0we","type":"discovered-from","created_at":"2025-12-22T22:58:52.877536-08:00","created_by":"daemon"}]} {"id":"bd-8x43","title":"Add 'bd where' command to show active beads location","description":"## Problem\n\nWhen using redirects, it's unclear which beads database is actually being used. This caused debugging confusion during the v0.39.0 release.\n\n## Proposed Solution\n\nAdd `bd where` command:\n\n```bash\nbd where\n# → /Users/stevey/gt/beads/mayor/rig/.beads (via redirect from crew/dave/.beads)\n\nbd where --json\n# → {\"path\": \"...\", \"redirected_from\": \"...\", \"prefix\": \"bd\"}\n```\n\n## Alternatives Considered\n\n- `bd info --beads-path` - but `bd where` is more discoverable\n- Just fix the UX so it's never confusing - defense in depth is better\n\n## Desire Paths\n\nThis supports the 'desire paths' design principle: make the system's behavior visible so agents can debug and understand it.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-27T21:15:52.666948-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-27T21:27:50.301295-08:00","closed_at":"2025-12-27T21:27:50.301295-08:00","close_reason":"Closed"} {"id":"bd-8y9t","title":"Remove bd mol spawn - use pour/wisp only","description":"Remove the spawn command from bd mol. Replace with:\n\n- bd pour \u003cproto\u003e - Instantiate as mol (liquid, persistent)\n- bd wisp \u003cproto\u003e - Instantiate as wisp (vapor, ephemeral)\n\nRationale:\n- 'spawn' doesn't fit the chemistry metaphor\n- Two phase transitions (pour/wisp) are clearer than one command with flags\n- Avoids confusion about defaults\n\nImplementation:\n1. Add bd pour command (currently bd mol spawn --pour)\n2. Add bd wisp command (currently bd mol spawn without --pour)\n3. Remove bd mol spawn\n4. Update all docs/tests\n\nGas Town tracking: gt-ingm.6","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-24T12:40:51.698189-08:00","updated_at":"2025-12-24T12:52:55.307-08:00","closed_at":"2025-12-24T12:52:55.307-08:00","close_reason":"Removed bd mol spawn, use bd pour or bd wisp create instead"} -{"id":"bd-8zbo","title":"Code smell: runCook function is ~275 lines","description":"The runCook() function in cmd/bd/cook.go (lines 83-357) is ~275 lines handling:\n\n1. Flag parsing and validation\n2. Mode determination (compile vs runtime)\n3. Formula parsing and resolution\n4. Control flow, advice, and expansion application\n5. Dry-run output\n6. Ephemeral mode JSON output\n7. Persist mode database operations\n8. Result display\n\nConsider extracting:\n- parseAndValidateFlags() - flag handling\n- loadAndResolveFormula() - parse, resolve, apply transformations\n- outputDryRun() - dry run display logic\n- outputEphemeral() - ephemeral JSON output\n- persistFormula() - database persistence logic\n\nThe function has multiple exit points and nested conditionals that make it hard to follow.\n\nLocation: cmd/bd/cook.go:83-357","acceptance_criteria":"1. runCook() broken into smaller functions (\u003c100 lines each)\n2. Each phase (parse, resolve, dry-run, ephemeral, persist) in separate function\n3. All existing tests pass: go test ./cmd/bd/... -run Cook\n4. Cook command behavior unchanged\n5. Error messages and exit codes preserved","status":"open","priority":3,"issue_type":"chore","created_at":"2025-12-28T15:33:08.808191-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:37:19.860494-08:00","dependencies":[{"issue_id":"bd-8zbo","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.25998-08:00","created_by":"daemon"}]} +{"id":"bd-8zbo","title":"Code smell: runCook function is ~275 lines","description":"The runCook() function in cmd/bd/cook.go (lines 83-357) is ~275 lines handling:\n\n1. Flag parsing and validation\n2. Mode determination (compile vs runtime)\n3. Formula parsing and resolution\n4. Control flow, advice, and expansion application\n5. Dry-run output\n6. Ephemeral mode JSON output\n7. Persist mode database operations\n8. Result display\n\nConsider extracting:\n- parseAndValidateFlags() - flag handling\n- loadAndResolveFormula() - parse, resolve, apply transformations\n- outputDryRun() - dry run display logic\n- outputEphemeral() - ephemeral JSON output\n- persistFormula() - database persistence logic\n\nThe function has multiple exit points and nested conditionals that make it hard to follow.\n\nLocation: cmd/bd/cook.go:83-357","acceptance_criteria":"1. runCook() broken into smaller functions (\u003c100 lines each)\n2. Each phase (parse, resolve, dry-run, ephemeral, persist) in separate function\n3. All existing tests pass: go test ./cmd/bd/... -run Cook\n4. Cook command behavior unchanged\n5. Error messages and exit codes preserved","status":"closed","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:33:08.808191-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:16:03.597667-08:00","closed_at":"2025-12-28T16:16:03.597667-08:00","close_reason":"Refactored runCook from 275 lines to 44 lines. Extracted 6 helper functions, all under 100 lines each. Build passes.","dependencies":[{"issue_id":"bd-8zbo","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.25998-08:00","created_by":"daemon"}]} {"id":"bd-90v","title":"bd prime: AI context loading and Claude Code integration","description":"Implement `bd prime` command and Claude Code hooks for context recovery. Hooks work with BOTH MCP server and CLI approaches - they solve the context memory problem (keeping bd workflow fresh after compaction) not the tool access problem (MCP vs CLI).","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-11-11T23:31:12.119012-08:00","updated_at":"2025-12-25T22:26:54.442614-08:00","closed_at":"2025-12-25T22:26:54.442614-08:00","close_reason":"bd prime is implemented and working; bd setup cursor deferred - Cursor integration not a priority"} {"id":"bd-9115","title":"CLI cleanup: Consolidate bd top-level commands","description":"## Problem\nbd has 76 top-level commands, making it hard to discover and remember.\n\n## Proposed Changes\n\n### Remove from top-level (nest instead):\n- `bd cook` → `bd formula cook` (cooking is a formula operation)\n- `bd pin` → already covered by `bd mol` commands\n- `bd thanks` → `bd info --thanks` or just README\n- `bd quickstart` → `bd help quickstart` or docs\n- `bd detect-pollution` → `bd doctor --check=pollution`\n\n### Consolidate migrations under `bd migrate`:\n- `bd migrate-hash-ids` → `bd migrate hash-ids`\n- `bd migrate-issues` → `bd migrate issues`\n- `bd migrate-sync` → `bd migrate sync`\n- `bd migrate-tombstones` → `bd migrate tombstones`\n\n### Consolidate admin under `bd admin`:\n- `bd cleanup` → `bd admin cleanup`\n- `bd compact` → `bd admin compact`\n- `bd reset` → `bd admin reset`\n\n## Backwards Compatibility\nKeep old commands as hidden aliases for one release cycle.","status":"closed","priority":2,"issue_type":"task","assignee":"mayor","created_at":"2025-12-27T14:28:03.299469-08:00","created_by":"mayor","updated_at":"2025-12-27T16:19:44.580326-08:00","closed_at":"2025-12-27T16:19:44.580326-08:00","close_reason":"CLI consolidation complete: cook→formula, migrate-*, admin command, detect-pollution→doctor, aliases, pin removal, thanks/quickstart relocation, docs updated","dependencies":[{"issue_id":"bd-9115","depends_on_id":"bd-w3z7","type":"blocks","created_at":"2025-12-27T15:11:44.88081-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-do8e","type":"blocks","created_at":"2025-12-27T15:11:44.905058-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-3u8m","type":"blocks","created_at":"2025-12-27T15:11:44.929207-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-x0zl","type":"blocks","created_at":"2025-12-27T15:11:44.953799-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-wb9g","type":"blocks","created_at":"2025-12-27T15:11:44.977559-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-twbi","type":"blocks","created_at":"2025-12-27T15:11:45.002403-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-kff0","type":"blocks","created_at":"2025-12-27T15:11:45.027177-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-bxqv","type":"blocks","created_at":"2025-12-27T15:11:45.051629-08:00","created_by":"daemon"},{"issue_id":"bd-9115","depends_on_id":"bd-5e9q","type":"blocks","created_at":"2025-12-27T15:11:45.07537-08:00","created_by":"daemon"}]} {"id":"bd-95k8","title":"Pinned field available in beads v0.37.0","description":"Hey max,\n\nHeads up on your mail overhaul work:\n\n1. **Pinned field is available** - beads v0.37.0 (released by dave earlier) includes the pinned field on issues. You'll want to add this to BeadsMessage in types.go.\n\n2. **Database migration** - Check if existing .beads databases need migration to support the pinned field. Run `bd doctor` to see if it flags anything.\n\n3. **Sorting task** - Once you have the pinned field, gt-ngu1 (pinned beads first in mail inbox) needs implementing. Since messages now come from `bd list --type=message`, you'll need to either:\n - Sort in listBeads() after fetching, or\n - Ensure bd list returns pinned items first (may already do this?)\n\nCheck what version of bd you're building against.\n\n-- Mayor","status":"closed","priority":2,"issue_type":"message","assignee":"gastown/crew/max","created_at":"2025-12-20T17:51:57.315956-08:00","updated_at":"2025-12-21T17:52:18.542169-08:00","closed_at":"2025-12-21T17:52:18.542169-08:00","close_reason":"Stale message - pinned field already available","labels":["from:beads-crew-dave","thread:thread-71ac20c7e432"]} {"id":"bd-987a","title":"bd mol run: panic slice bounds out of range in mol_run.go:130","description":"## Problem\nbd mol run panics after successfully creating the molecule:\n\n```\nāœ“ Molecule running: created 9 issues\n Root issue: gt-i4lo (pinned, in_progress)\n Assignee: stevey\n\nNext steps:\n bd ready # Find unblocked work in this molecule\npanic: runtime error: slice bounds out of range [:8] with length 7\n\ngoroutine 1 [running]:\nmain.runMolRun(0x1014fc0c0, {0x140001e0f80, 0x1, 0x10089daad?})\n /Users/stevey/gt/beads/crew/dave/cmd/bd/mol_run.go:130 +0xc38\n```\n\n## Reproduction\n```bash\nbd --no-daemon mol run gt-lwuu --var issue=gt-test123\n```\nWhere gt-lwuu is a mol-polecat-work proto with 8 child steps.\n\n## Impact\nThe molecule IS created successfully - the panic happens after creation when formatting the \"Next steps\" output.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-21T21:48:55.396018-08:00","updated_at":"2025-12-21T22:57:46.827469-08:00","closed_at":"2025-12-21T22:57:46.827469-08:00","close_reason":"Fixed: removed unsafe rootID[:8] slice - now uses full ID"} {"id":"bd-9avq","title":"Fix wisp leak in nodb mode writeIssuesToJSONL","description":"writeIssuesToJSONL in nodb.go calls writeJSONLAtomic without filtering wisps. This means any wisps created during a nodb session would be written to issues.jsonl, leaking ephemeral data into the git-tracked file.\n\nFix: Add the same wisp filter pattern used in sync_export.go, autoflush.go, and export.go:\n\n```go\n// Filter out wisps before writing\nfiltered := make([]*types.Issue, 0, len(issues))\nfor _, issue := range issues {\n if !issue.Wisp {\n filtered = append(filtered, issue)\n }\n}\nissues = filtered\n```\n\nLocation: cmd/bd/nodb.go:writeIssuesToJSONL()","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-24T21:15:47.980048-08:00","updated_at":"2025-12-24T21:18:42.607711-08:00","closed_at":"2025-12-24T21:18:42.607711-08:00","close_reason":"Added wisp filter in writeIssuesToJSONL"} -{"id":"bd-9btu","title":"Code smell: Duplicated step collection functions in cook.go","description":"attached_args: Fix duplicated step collection functions in cook.go\n\nTwo pairs of nearly identical functions exist in cook.go:\n\n1. collectStepsRecursive() (line 754) vs collectStepsToSubgraph() (line 415)\n - Both iterate over steps and create issues\n - Main difference: one writes to DB, one builds in-memory subgraph\n \n2. collectDependencies() (line 833) vs collectDependenciesToSubgraph() (line 502)\n - Both collect dependencies from steps\n - Nearly identical logic\n\nConsider:\n1. Extract common step/dependency processing logic\n2. Use a strategy pattern or callback for the DB vs in-memory difference\n3. Create a single function that returns data, then have callers decide how to persist\n\nLocation: cmd/bd/cook.go:415-567 and cmd/bd/cook.go:754-898","acceptance_criteria":"1. Common step collection logic extracted to shared function(s)\n2. collectStepsRecursive and collectStepsToSubgraph unified or share core logic\n3. collectDependencies and collectDependenciesToSubgraph unified or share core logic\n4. All existing tests pass: go test ./cmd/bd/... -run Cook\n5. No functional change to cook command behavior","status":"pinned","priority":2,"issue_type":"chore","assignee":"gt-beads-nux","created_at":"2025-12-28T15:31:57.401447-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:57:04.650238-08:00","dependencies":[{"issue_id":"bd-9btu","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.168908-08:00","created_by":"daemon"}]} +{"id":"bd-9btu","title":"Code smell: Duplicated step collection functions in cook.go","description":"attached_args: Fix duplicated step collection functions in cook.go\n\nTwo pairs of nearly identical functions exist in cook.go:\n\n1. collectStepsRecursive() (line 754) vs collectStepsToSubgraph() (line 415)\n - Both iterate over steps and create issues\n - Main difference: one writes to DB, one builds in-memory subgraph\n \n2. collectDependencies() (line 833) vs collectDependenciesToSubgraph() (line 502)\n - Both collect dependencies from steps\n - Nearly identical logic\n\nConsider:\n1. Extract common step/dependency processing logic\n2. Use a strategy pattern or callback for the DB vs in-memory difference\n3. Create a single function that returns data, then have callers decide how to persist\n\nLocation: cmd/bd/cook.go:415-567 and cmd/bd/cook.go:754-898","acceptance_criteria":"1. Common step collection logic extracted to shared function(s)\n2. collectStepsRecursive and collectStepsToSubgraph unified or share core logic\n3. collectDependencies and collectDependenciesToSubgraph unified or share core logic\n4. All existing tests pass: go test ./cmd/bd/... -run Cook\n5. No functional change to cook command behavior","status":"closed","priority":2,"issue_type":"chore","assignee":"gt-beads-nux","created_at":"2025-12-28T15:31:57.401447-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:15:06.211995-08:00","closed_at":"2025-12-28T16:15:06.211995-08:00","close_reason":"Consolidated duplicated step collection functions into unified collectSteps() with callback strategy","dependencies":[{"issue_id":"bd-9btu","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.168908-08:00","created_by":"daemon"}]} {"id":"bd-9cdc","title":"Update docs for import bug fix","description":"Update AGENTS.md, README.md, TROUBLESHOOTING.md with import.orphan_handling config documentation. Document resurrection behavior, tombstones, config modes. Add troubleshooting section for import failures with deleted parents.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-04T12:32:30.770415-08:00","updated_at":"2025-12-21T21:14:08.328627-08:00","closed_at":"2025-12-21T21:14:08.328627-08:00","close_reason":"Already completed - documentation in CONFIG.md, CLI_REFERENCE.md, and TROUBLESHOOTING.md"} {"id":"bd-9g1z","title":"Fix or remove TestFindJSONLPathDefault (issue #356)","description":"Code health review found .test-skip permanently skips TestFindJSONLPathDefault.\n\nThe test references issue #356 about wrong JSONL filename expectations (issues.jsonl vs beads.jsonl).\n\nTest file: internal/beads/beads_test.go\n\nThe underlying migration from beads.jsonl to issues.jsonl may be complete, so either:\n1. Fix the test expectations\n2. Remove the test if no longer needed\n3. Document why it remains skipped","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-16T18:17:31.33975-08:00","updated_at":"2025-12-22T21:24:50.357688-08:00","closed_at":"2025-12-22T21:24:50.357688-08:00","close_reason":"Test now passes - removed from .test-skip. Code was fixed in utils.FindJSONLInDir to return issues.jsonl as default."} {"id":"bd-9gvf","title":"Add prefix-based routing with routes.jsonl","description":"## Summary\n\nWhen running `bd show gt-xyz` from a location where that bead doesn't exist locally,\nbd should check a `routes.jsonl` file to find where beads with that prefix live.\n\n## Problem\n\nFrom ~/gt (town root):\n- `bd show hq-xxx` works (hq-* beads are local)\n- `bd show gt-xxx` fails (gt-* beads live in gastown/mayor/rig)\n\nThis breaks `gt hook`, `gt handoff`, and `gt prime` which need to verify beads exist.\n\n## Solution\n\nAdd `.beads/routes.jsonl` for prefix-based routing:\n\n\\`\\`\\`json\n{\"prefix\": \"gt-\", \"path\": \"gastown/mayor/rig\"}\n{\"prefix\": \"hq-\", \"path\": \".\"}\n{\"prefix\": \"bd-\", \"path\": \"beads/crew/dave\"}\n\\`\\`\\`\n\n### Lookup Algorithm\n\n1. Try local .beads/ first (current behavior)\n2. If not found, check for routes.jsonl in current .beads/\n3. Match ID prefix against routes\n4. If match found, resolve path relative to routes file location and retry\n5. If still not found, error as usual\n\n### Affected Commands\n\nRead operations that take a bead ID:\n- bd show \u003cid\u003e\n- bd update \u003cid\u003e\n- bd close \u003cid\u003e\n- bd dep add \u003cid\u003e \u003cdep\u003e (both IDs)\n\nWrite operations (bd create) still go to local .beads/ - no change needed.\n\n## Implementation Notes\n\n1. New file: routes.jsonl in .beads/ directory\n2. New function: resolveBeadsPath(id string) (string, error)\n3. Update: beads.Show(), beads.Update(), beads.Close(), etc. to use resolver\n4. Caching: Can cache routes in memory since file rarely changes\n\n## Context\n\nThis unblocks the gt handoff flow for Mayor. Currently gt handoff gt-xyz fails\nbecause it can't verify the bead exists when running from town root.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-26T14:18:19.399691-08:00","updated_at":"2025-12-26T14:36:43.551435-08:00","closed_at":"2025-12-26T14:36:43.551435-08:00","close_reason":"Implemented prefix-based routing with routes.jsonl. Tested with gt- and bd- prefixes from town root."} -{"id":"bd-9hc9","title":"Code smell: Long function flushToJSONLWithState (~280 lines)","description":"The flushToJSONLWithState() function in cmd/bd/autoflush.go (lines 490-772) is ~280 lines with:\n\n1. Multiple nested conditionals\n2. Inline helper function definitions (recordFailure, recordSuccess)\n3. Complex multi-repo filtering logic\n4. Mixed concerns (validation, export, error handling)\n\nConsider breaking into:\n- validateJSONLIntegrity() - already exists but could absorb more\n- getIssuesToExport(fullExport bool) - determines what to export\n- mergeWithExistingJSONL(issueMap, dirtyIDs) - handles incremental merge\n- filterByPrefix(issues) - multi-repo prefix filtering\n- performExport(issues, jsonlPath) - the actual write\n- updateMetadataAfterExport() - hash/timestamp updates\n\nLocation: cmd/bd/autoflush.go:490-772","acceptance_criteria":"1. flushToJSONLWithState() broken into smaller functions (\u003c100 lines each)\n2. Each extracted function has single responsibility\n3. All existing tests pass: go test ./cmd/bd/... -run Flush\n4. Auto-flush behavior unchanged\n5. Error handling and failure counting still works correctly","status":"open","priority":3,"issue_type":"chore","created_at":"2025-12-28T15:32:20.909843-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T15:37:17.962414-08:00","dependencies":[{"issue_id":"bd-9hc9","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.223682-08:00","created_by":"daemon"}]} +{"id":"bd-9hc9","title":"Code smell: Long function flushToJSONLWithState (~280 lines)","description":"The flushToJSONLWithState() function in cmd/bd/autoflush.go (lines 490-772) is ~280 lines with:\n\n1. Multiple nested conditionals\n2. Inline helper function definitions (recordFailure, recordSuccess)\n3. Complex multi-repo filtering logic\n4. Mixed concerns (validation, export, error handling)\n\nConsider breaking into:\n- validateJSONLIntegrity() - already exists but could absorb more\n- getIssuesToExport(fullExport bool) - determines what to export\n- mergeWithExistingJSONL(issueMap, dirtyIDs) - handles incremental merge\n- filterByPrefix(issues) - multi-repo prefix filtering\n- performExport(issues, jsonlPath) - the actual write\n- updateMetadataAfterExport() - hash/timestamp updates\n\nLocation: cmd/bd/autoflush.go:490-772","acceptance_criteria":"1. flushToJSONLWithState() broken into smaller functions (\u003c100 lines each)\n2. Each extracted function has single responsibility\n3. All existing tests pass: go test ./cmd/bd/... -run Flush\n4. Auto-flush behavior unchanged\n5. Error handling and failure counting still works correctly","status":"pinned","priority":3,"issue_type":"chore","assignee":"gt-beads-nux","created_at":"2025-12-28T15:32:20.909843-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:15:34.82732-08:00","dependencies":[{"issue_id":"bd-9hc9","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.223682-08:00","created_by":"daemon"}]} {"id":"bd-9l0h","title":"Run tests and linting","description":"go test -short ./... \u0026\u0026 golangci-lint run ./...","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:53:19.527602-08:00","updated_at":"2025-12-20T21:55:29.660914-08:00","closed_at":"2025-12-20T21:55:29.660914-08:00","close_reason":"Tests passed, go vet clean","dependencies":[{"issue_id":"bd-9l0h","depends_on_id":"bd-an4s","type":"parent-child","created_at":"2025-12-20T21:53:19.529203-08:00","created_by":"daemon"},{"issue_id":"bd-9l0h","depends_on_id":"bd-gocx","type":"blocks","created_at":"2025-12-20T21:53:29.753682-08:00","created_by":"daemon"}]} {"id":"bd-9qj5","title":"Merge: bd-c7y5","description":"branch: polecat/toast\ntarget: main\nsource_issue: bd-c7y5\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T20:45:02.626929-08:00","updated_at":"2025-12-23T21:21:57.699742-08:00","closed_at":"2025-12-23T21:21:57.699742-08:00","close_reason":"stale - no code pushed"} {"id":"bd-9szg","title":"Test prefix bd","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-27T14:24:41.470665-08:00","created_by":"stevey","updated_at":"2025-12-27T14:24:50.420887-08:00","deleted_at":"2025-12-27T14:24:50.420887-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} @@ -296,6 +296,7 @@ {"id":"bd-etyv","title":"Smart --var detection for mol distill","description":"Implemented bidirectional syntax support for mol distill --var flag.\n\n**Problem:**\n- spawn uses: --var variable=value (assignment style)\n- distill used: --var value=variable (substitution style)\n- Agents would naturally guess spawn-style for both\n\n**Solution:**\nSmart detection that accepts BOTH syntaxes by checking which side appears in the epic text:\n- --var branch=feature-auth → finds 'feature-auth' in text → works\n- --var feature-auth=branch → finds 'feature-auth' in text → also works\n\n**Changes:**\n- Added parseDistillVar() with smart detection\n- Added collectSubgraphText() helper\n- Restructured runMolDistill to load subgraph before parsing vars\n- Updated help text to document both syntaxes\n- Added comprehensive tests in mol_test.go\n\n**Edge cases handled:**\n- Both sides found: prefers spawn-style (more common guess)\n- Neither found: helpful error message\n- Empty sides: validation error\n- Values containing '=' (e.g., KEY=VALUE): works via SplitN\n\nEmbodies the Beads philosophy: watch what agents do, make their guess correct.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T11:08:50.83923-08:00","updated_at":"2025-12-21T11:08:56.432536-08:00","closed_at":"2025-12-21T11:08:56.432536-08:00","close_reason":"Implemented"} {"id":"bd-exy3","title":"Ephemeral patrol molecules leaking into beads","description":"## Problem\n\nEphemeral patrol orchestration molecules (e.g., mol-deacon-patrol, wisp step issues) keep appearing in `bd ready` output in gastown beads.\n\nThese are ephemeral/wisp issues that should:\n1. Never be synced to the beads-sync branch\n2. Live only in .beads-wisp/ (ephemeral storage)\n3. Be squashed to digests, not persisted as regular beads\n\n## Examples found\n\n- gt-wisp-6ue: mol-deacon-patrol\n- gt-pacdm: mol-deacon-patrol \n- gt-wisp-mpm: Check own context limit\n- gt-wisp-lkc: Clean dead sessions\n\n## Investigation needed\n\n1. Where are these being created? (gt mol bond? manual bd create?)\n2. Why are they using the gt- prefix instead of staying ephemeral?\n3. Is the wisp storage (.beads-wisp/) being used correctly?\n4. Is bd sync accidentally picking up ephemeral issues?\n\n## Expected behavior\n\nPatrol molecules and their steps should be ephemeral and never appear in `bd ready` or `bd list`.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-27T17:04:16.51075-08:00","created_by":"gastown/crew/joe","updated_at":"2025-12-27T19:18:16.809273-08:00","closed_at":"2025-12-27T19:18:16.809273-08:00","close_reason":"Moved to gastown: gt-glisw"} {"id":"bd-eyto","title":"Time-dependent tests may be flaky near TTL boundary","description":"Several tombstone merge tests use time.Now() to create test data: time.Now().Add(-24 * time.Hour), time.Now().Add(-60 * 24 * time.Hour), etc. While these work reliably in practice (24h vs 30d TTL has large margin), they could theoretically be flaky if: 1) Tests run slowly, 2) System clock changes during test, 3) TTL constants change. Recommendation: Consider using a fixed reference time or time injection for deterministic tests. Lower priority since current margin is large. Files: internal/merge/merge_test.go:1337-1338, 1352-1353, 1548-1549, 1590-1591","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T16:37:02.348143-08:00","updated_at":"2025-12-05T16:37:02.348143-08:00"} +{"id":"bd-f0wm","title":"Swarm status doesn't update after task completions","description":"During swarm bd-784c, the swarm status remained at '9% (1/11 tasks merged)' even after 5 tasks were completed and closed.\n\n**Observed:**\n```\ngt swarm status bd-784c\nProgress: 9% (1/11 tasks merged)\n```\n\nBut bd list showed 4+ issues closed:\n- bd-j3dj, bd-kkka, bd-1tkd, bd-9btu all closed\n\n**Expected:**\nSwarm status should reflect actual completion count, either by:\n1. Polling issue status from beads\n2. Updating when polecats close issues\n3. Updating when commits are pushed to integration branch\n\n**Impact:**\nCoordinator (me) had to manually check bd list to know actual progress.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-28T16:17:49.27816-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:17:49.27816-08:00"} {"id":"bd-f3ll","title":"Merge: bd-ot0w","description":"branch: polecat/dementus\ntarget: main\nsource_issue: bd-ot0w\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T23:20:33.495772-08:00","updated_at":"2025-12-20T23:17:27.000252-08:00","closed_at":"2025-12-20T23:17:27.000252-08:00","close_reason":"Branches nuked, MRs obsolete"} {"id":"bd-f526","title":"Deacon Patrol","description":"Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup.","status":"open","priority":2,"issue_type":"molecule","created_at":"2025-12-27T18:14:20.809207-08:00","created_by":"deacon","updated_at":"2025-12-27T18:14:20.809207-08:00"} {"id":"bd-f5cc","title":"Thread Test","description":"Testing the thread feature","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:01.244501-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","dependencies":[{"issue_id":"bd-f5cc","depends_on_id":"bd-x36g","type":"supersedes","created_at":"2025-12-18T13:45:31.137191-08:00","created_by":"migration"}],"deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"message"} @@ -305,6 +306,7 @@ {"id":"bd-fbl9","title":"Test parent task","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-27T23:26:53.012747-08:00","created_by":"beads/crew/emma","updated_at":"2025-12-27T23:27:40.44858-08:00","closed_at":"2025-12-27T23:27:40.44858-08:00","close_reason":"Test issues, cleanup"} {"id":"bd-fcl1","title":"Merge: bd-au0.5","description":"branch: polecat/Searcher\ntarget: main\nsource_issue: bd-au0.5\nrig: beads","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-23T13:39:11.946667-08:00","updated_at":"2025-12-23T19:12:08.346454-08:00","closed_at":"2025-12-23T19:12:08.346454-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-fd0w","title":"Witness Patrol","description":"Per-rig worker monitor patrol loop with progressive nudging.","status":"open","priority":2,"issue_type":"molecule","created_at":"2025-12-27T18:14:26.008274-08:00","created_by":"deacon","updated_at":"2025-12-27T18:14:26.008274-08:00"} +{"id":"bd-fedb","title":"Polecats should spawn with auto-accept mode enabled","description":"During swarm bd-784c, polecats (Toast, Nux) were spawned without --dangerously-skip-permissions or equivalent auto-accept mode.\n\n**Problem:**\nEvery edit, bash command, and tool use required manual confirmation via tmux send-keys. This defeats the purpose of autonomous polecat operation.\n\n**Expected:**\nPolecats in a swarm should run with permissions bypassed so they can work autonomously.\n\n**Workaround used:**\n- Manually sent Enter keys via tmux to accept prompts\n- Eventually polecats entered 'bypass permissions on' mode after restart\n\n**Suggestion:**\n- gt sling should pass --dangerously-skip-permissions by default for polecats\n- Or polecats should have a .claude/settings.local.json pre-configured for auto-accept","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-28T16:17:47.061327-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:17:47.061327-08:00"} {"id":"bd-fej5","title":"bd hook: detect agent from cwd instead of defaulting to $USER","description":"**Problem:**\n`bd hook` without `--agent` defaults to `$USER` (e.g., \"stevey\") instead of detecting the agent identity from the current working directory.\n\n**Expected behavior:**\nWhen running from `/Users/stevey/gt/beads/crew/dave`, the agent should be detected as `beads/crew/dave`.\n\n**Current behavior:**\n```bash\n$ pwd\n/Users/stevey/gt/beads/crew/dave\n$ bd hook\nHook: stevey\n (empty)\n\n$ bd hook --agent beads/crew/dave\nHook: beads/crew/dave\n šŸ“Œ bd-hobo (mol) - open\n```\n\n**Fix:**\nbd hook should use the same cwd-based agent detection that other commands use (similar to how `gt mail` determines identity from cwd).","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-24T20:01:27.613892-08:00","updated_at":"2025-12-25T12:41:10.65257-08:00","closed_at":"2025-12-25T12:41:10.65257-08:00","close_reason":"Obsolete: fix belongs in gastown - gt should set BD_ACTOR env var when spawning agents. bd already checks BD_ACTOR."} {"id":"bd-ffjt","title":"Unify template.go and mol.go under bd mol","description":"Consolidate the two DAG-template systems into one under the mol command. mol.go (on rictus branch) has the right UX (catalog/show/bond), template.go has the mechanics. Merge them, deprecate bd template commands.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T23:52:13.208972-08:00","updated_at":"2025-12-21T00:01:59.283765-08:00","closed_at":"2025-12-21T00:01:59.283765-08:00","close_reason":"Implemented mol commands with deprecation for template commands"} {"id":"bd-fgw3","title":"Update local installation","description":"Run install script or brew upgrade to get new version locally: curl -fsSL .../install.sh | bash","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:05.052016-08:00","updated_at":"2025-12-20T00:49:51.928221-08:00","closed_at":"2025-12-20T00:25:52.805029-08:00","dependencies":[{"issue_id":"bd-fgw3","depends_on_id":"bd-6s61","type":"parent-child","created_at":"2025-12-19T22:56:15.248427-08:00","created_by":"daemon"},{"issue_id":"bd-fgw3","depends_on_id":"bd-si4g","type":"blocks","created_at":"2025-12-19T22:56:23.497325-08:00","created_by":"daemon"}]} @@ -377,6 +379,7 @@ {"id":"bd-kff0","title":"Integrate detect-pollution into bd doctor","description":"## Task\nMove `bd detect-pollution` → `bd doctor --check=pollution`\n\n## Current state\n- detect-pollution already shows deprecation hint pointing to doctor\n- Doctor command exists with multiple checks\n\n## Implementation\n\n### 1. Update doctor.go\n- Add pollution check to the doctor checks\n- Add `--check=pollution` flag option\n- Integrate detect-pollution logic\n\n### 2. Update detect_pollution.go\n- Mark as hidden (deprecated)\n- Forward to doctor --check=pollution\n- Keep for one release cycle\n\n### 3. Update docs\n- Remove detect-pollution from any command lists\n- Update doctor docs to include pollution check\n\n## Files to modify\n- cmd/bd/doctor.go\n- cmd/bd/detect_pollution.go\n","status":"closed","priority":2,"issue_type":"task","assignee":"onyx","created_at":"2025-12-27T15:11:10.46326-08:00","created_by":"mayor","updated_at":"2025-12-27T16:04:58.471341-08:00","closed_at":"2025-12-27T16:04:58.471341-08:00","close_reason":"Integrated detect-pollution into bd doctor --check=pollution","pinned":true} {"id":"bd-kkka","title":"Dead code: fetchAndRebaseInWorktree() marked DEPRECATED but still exists","description":"attached_args: Remove dead code fetchAndRebaseInWorktree\n\nThe function fetchAndRebaseInWorktree() in internal/syncbranch/worktree.go (lines 811-830) is marked as DEPRECATED with a comment:\n\n```go\n// fetchAndRebaseInWorktree is DEPRECATED - kept for reference only.\n// Use contentMergeRecovery instead to avoid tombstone resurrection.\n```\n\nSince contentMergeRecovery() is the replacement and is being used, this dead code should be removed to reduce maintenance burden.\n\nLocation: internal/syncbranch/worktree.go:811-830","acceptance_criteria":"1. fetchAndRebaseInWorktree() function removed from worktree.go\n2. No remaining references to the function in codebase\n3. All existing tests pass: go test ./internal/syncbranch/...\n4. Sync functionality unchanged (contentMergeRecovery still used)","status":"closed","priority":3,"issue_type":"chore","assignee":"gt-beads-toast","created_at":"2025-12-28T15:32:21.97865-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:01:14.87213-08:00","closed_at":"2025-12-28T16:01:14.87213-08:00","close_reason":"Removed deprecated fetchAndRebaseInWorktree function and its test. No remaining references. Tests pass.","dependencies":[{"issue_id":"bd-kkka","depends_on_id":"bd-784c","type":"parent-child","created_at":"2025-12-28T15:38:04.241483-08:00","created_by":"daemon"}]} {"id":"bd-knta","title":"Deacon Patrol","description":"Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup.","status":"tombstone","priority":2,"issue_type":"molecule","created_at":"2025-12-26T13:08:21.233771-08:00","updated_at":"2025-12-27T00:10:54.179341-08:00","deleted_at":"2025-12-27T00:10:54.179341-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"molecule"} +{"id":"bd-kp9y","title":"gt swarm dispatch command not working","description":"The 'gt swarm dispatch' command shown in help doesn't appear to work as expected.\n\n**Observed:**\n```\n$ gt swarm dispatch bd-784c\n[prints help text instead of dispatching]\n```\n\n**Expected:**\nShould dispatch the next ready task from the epic to an available worker.\n\n**Workaround:**\nHad to manually use 'gt sling \u003cissue\u003e \u003cpolecat\u003e' for each task dispatch.\n\n**Impact:**\n- Manual task dispatch defeats swarm automation\n- Coordinator has to track which tasks are ready and which polecats are free\n\n**Suggestion:**\nImplement or fix 'gt swarm dispatch' to:\n1. Find next unassigned task in epic\n2. Find idle polecat in swarm\n3. Sling task to polecat automatically","status":"open","priority":3,"issue_type":"bug","created_at":"2025-12-28T16:18:10.320094-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:18:10.320094-08:00"} {"id":"bd-kptp","title":"Merge: bd-qioh","description":"branch: polecat/Errata\ntarget: main\nsource_issue: bd-qioh\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T13:46:08.832073-08:00","updated_at":"2025-12-23T19:12:08.350136-08:00","closed_at":"2025-12-23T19:12:08.350136-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-kpy","title":"Sync race: rebase-based divergence recovery resurrects tombstones","description":"## Problem\nWhen two repos sync simultaneously, tombstones can be resurrected:\n\n1. Repo A deletes issue (creates tombstone), pushes to sync branch\n2. Repo B (with 'closed' status) exports and tries to push\n3. Push fails (non-fast-forward)\n4. fetchAndRebaseInWorktree does git rebase\n5. Git rebase applies B's 'closed' patch on top of A's 'tombstone'\n6. TEXT-level rebase doesn't invoke beads merge driver\n7. 'closed' overwrites 'tombstone' = resurrection\n\n## Root Cause\nCommitToSyncBranch uses git rebase for divergence recovery, but rebase is text-level, not content-level. The proper content-level merge in PullFromSyncBranch handles tombstones correctly, but it runs AFTER the problematic push.\n\n## Proposed Fix\nOption 1: Don't push in CommitToSyncBranch - let PullFromSyncBranch handle merge+push\nOption 2: Replace git rebase with content-level merge in fetchAndRebaseInWorktree\nOption 3: Reorder sync steps: Export → Pull/Merge → Commit → Push\n\n## Workaround Applied\nExcluded tombstones from orphan detection warnings (commit 1e97d9cc).\n\nSee also: bd-3852 (Add orphan detection migration)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-17T23:29:33.049272-08:00","updated_at":"2025-12-24T22:41:09.184574-08:00","closed_at":"2025-12-24T22:41:09.184574-08:00","close_reason":"Fixed by replacing git rebase with content-level merge in divergence recovery"} {"id":"bd-kqo1","title":"Show pin indicator in bd list output","description":"Add a visual indicator (e.g., pin emoji or [P] marker) for pinned issues in bd list output so users can easily identify them.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-18T23:33:47.402549-08:00","updated_at":"2025-12-21T11:30:27.272768-08:00","closed_at":"2025-12-21T11:30:27.272768-08:00","close_reason":"Already implemented - šŸ“Œ emoji shown for pinned issues in bd list output","dependencies":[{"issue_id":"bd-kqo1","depends_on_id":"bd-0vg","type":"blocks","created_at":"2025-12-18T23:33:56.771791-08:00","created_by":"daemon"},{"issue_id":"bd-kqo1","depends_on_id":"bd-7h5","type":"blocks","created_at":"2025-12-18T23:34:07.985271-08:00","created_by":"daemon"}]} @@ -580,6 +583,7 @@ {"id":"bd-rnnr","title":"BondRef data model for compound lineage","description":"Add data model support for tracking compound molecule lineage.\n\nNEW FIELDS on Issue:\n bonded_from: []BondRef // For compounds: constituent protos\n\nNEW TYPE:\n type BondRef struct {\n ProtoID string // Source proto ID\n BondType string // sequential, parallel, conditional\n BondPoint string // Attachment site (issue ID or empty for root)\n }\n\nJSONL SERIALIZATION:\n {\n \"id\": \"proto-feature-tested\",\n \"title\": \"Feature with tests\",\n \"bonded_from\": [\n {\"proto_id\": \"proto-feature\", \"bond_type\": \"root\"},\n {\"proto_id\": \"proto-testing\", \"bond_type\": \"sequential\"}\n ],\n ...\n }\n\nQUERIES:\n- GetCompoundConstituents(id) → []BondRef\n- IsCompound(id) → bool\n- GetCompoundsUsing(protoID) → []Issue // Reverse lookup","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-21T00:59:38.582509-08:00","updated_at":"2025-12-21T01:19:43.922416-08:00","closed_at":"2025-12-21T01:19:43.922416-08:00","close_reason":"Added BondRef type to types.go with ProtoID, BondType, BondPoint fields; added BondedFrom field to Issue; added IsCompound() and GetConstituents() helpers; added BondType constants","dependencies":[{"issue_id":"bd-rnnr","depends_on_id":"bd-o5xe","type":"parent-child","created_at":"2025-12-21T00:59:51.234246-08:00","created_by":"daemon"}]} {"id":"bd-rp4o","title":"Deleted issues resurrect during bd sync (tombstones not propagating)","description":"## Problem\n\nWhen issues are deleted with bd delete --force, they get deleted from the local DB but resurrect during the next bd sync.\n\n## Reproduction\n\n1. Observe orphan warnings (bd-cb64c226.*, bd-cbed9619.*)\n2. Delete them: bd delete bd-cb64c226.1 ... --force\n3. Run bd sync\n4. Orphan warnings reappear - issues were resurrected!\n\n## Root Cause Hypothesis\n\nThe sync branch workflow (beads-sync) has the old state before deletions. When bd sync pulls from beads-sync and copies JSONL to main, the deleted issues are re-imported.\n\nTombstones may not be properly:\n1. Written to beads-sync during export\n2. Propagated during pull/merge\n3. Honored during import\n\n## Related\n\n- bd-7b7h: chicken-and-egg sync.branch bug (same workflow)\n- bd-ncwo: ID-based fallback matching to prevent ghost resurrection\n\n## Files to Investigate\n\n- cmd/bd/sync.go (export/import flow)\n- internal/syncbranch/worktree.go (PullFromSyncBranch, copyJSONLToMainRepo)\n- internal/importer/ (tombstone handling)","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T23:09:43.072696-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-rs5a","title":"Test agent bead","description":"Testing new agent type","status":"closed","priority":2,"issue_type":"agent","created_at":"2025-12-27T23:36:57.594945-08:00","created_by":"mayor","updated_at":"2025-12-27T23:37:15.583616-08:00","closed_at":"2025-12-27T23:37:15.583616-08:00","close_reason":"Test beads for schema change"} +{"id":"bd-ruio","title":"Polecat sessions terminate unexpectedly during work","description":"During swarm bd-784c, polecat sessions (Toast, Nux) terminated mid-task without completing their work.\n\n**Observed:**\n- Polecats were actively working (edits in progress, tests running)\n- Sessions suddenly showed 'not running' in gt polecat status\n- tmux sessions existed but were empty/reset\n- Work was partially complete (some commits made, issue still open)\n\n**Timeline example:**\n1. Toast working on bd-1tkd, making edits\n2. Check status 30s later - session empty, state shows 'idle'\n3. Had to re-sling to restart\n\n**Impact:**\n- Lost work progress\n- Required manual intervention to restart\n- Unclear if work was saved/committed\n\n**Possible causes:**\n- Session timeout?\n- Claude Code crash?\n- Resource limit?\n\n**Suggestion:**\n- Add session health monitoring\n- Auto-restart on unexpected termination\n- Log session exit reasons","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-28T16:18:09.014372-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:18:09.014372-08:00"} {"id":"bd-rupw","title":"Run bump-version.sh 0.30.7","description":"Run ./scripts/bump-version.sh 0.30.7 to update version in all files","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:48.649647-08:00","updated_at":"2025-12-19T22:57:31.512956-08:00","closed_at":"2025-12-19T22:57:31.512956-08:00","dependencies":[{"issue_id":"bd-rupw","depends_on_id":"bd-8pyn","type":"parent-child","created_at":"2025-12-19T22:56:48.653475-08:00","created_by":"stevey"}]} {"id":"bd-rx6o","title":"Test Child Task","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-27T22:15:23.27506-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-27T22:16:35.925357-08:00","deleted_at":"2025-12-27T22:16:35.925357-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"} {"id":"bd-rze6","title":"Digest: Release v0.34.0 @ 2025-12-22 12:16","description":"Released v0.34.0: wisp commands, chemistry UX, cross-project deps","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T12:16:53.033119-08:00","updated_at":"2025-12-22T12:16:53.033119-08:00","closed_at":"2025-12-22T12:16:53.033025-08:00","close_reason":"Squashed from wisp bd-25c (20 issues)"} @@ -591,6 +595,7 @@ {"id":"bd-si4g","title":"Verify release artifacts","description":"Check GitHub releases page - binaries for darwin/linux/windows should be available","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:04.183029-08:00","updated_at":"2025-12-20T00:49:51.92894-08:00","closed_at":"2025-12-20T00:25:52.720816-08:00","dependencies":[{"issue_id":"bd-si4g","depends_on_id":"bd-6s61","type":"parent-child","created_at":"2025-12-19T22:56:15.173619-08:00","created_by":"daemon"},{"issue_id":"bd-si4g","depends_on_id":"bd-otli","type":"blocks","created_at":"2025-12-19T22:56:23.428507-08:00","created_by":"daemon"}]} {"id":"bd-siz1","title":"GH#532: bd sync circular error (suggests running bd sync)","description":"bd sync error message recommends running bd sync to fix the bd sync error. Fix error handling to provide useful guidance. See GitHub issue #532.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:04:00.543573-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-sj5y","title":"Daemon should be singleton and aggressively kill stale instances","description":"Found 2 bd daemons running (PIDs 76868, 77515) during shutdown. The daemon should:\n\n1. Be a singleton - only one instance per rig allowed\n2. On startup, check for existing daemon and kill it before starting\n3. Use a PID file or lock file to enforce this\n\nCurrently stale daemons can accumulate, causing confusion and resource waste.","notes":"**Investigation 2025-12-21:**\n\nThe singleton mechanism is already implemented and working correctly:\n\n1. **daemon.lock** uses flock (exclusive non-blocking) to prevent duplicate daemons\n2. **bd.sock.startlock** coordinates concurrent auto-starts via O_CREATE|O_EXCL\n3. **Registry** tracks all daemons globally in ~/.beads/registry.json\n\nTesting shows:\n- Trying to start a second daemon gives: 'Error: daemon already running (PID X)'\n- Multiple daemons for *different* rigs is expected/correct behavior\n\nThe original report ('Found 2 bd daemons running PIDs 76868, 77515') was likely:\n1. Two daemons for different rigs (expected), OR\n2. An edge case that's since been fixed\n\nConsider closing as RESOLVED or clarifying the original scenario.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-21T01:29:14.778949-08:00","updated_at":"2025-12-21T11:27:34.302585-08:00","closed_at":"2025-12-21T11:27:34.302585-08:00","close_reason":"Singleton mechanism already implemented and verified working. Uses flock-based daemon.lock + O_CREATE|O_EXCL startlock. Testing confirms 'daemon already running' error on duplicate start attempts."} +{"id":"bd-soig","title":"Sling reports 'already pinned' but mol status shows empty hook","description":"During swarm operations, gt sling reported beads were 'already pinned' but gt mol status showed nothing on the hook.\n\n**Observed:**\n```\n$ gt sling bd-9btu beads/Nux\nError: bead bd-9btu is already pinned to gt-beads-nux\n\n$ gt mol status beads/Nux\nNothing on hook - no work slung\n```\n\n**Expected:**\nIf a bead is pinned, mol status should show it. If mol status shows empty, sling should work.\n\n**Workaround:**\nUsed --force flag to re-sling, which worked.\n\n**Root cause hypothesis:**\nAgent bead state may be stored in town beads but polecat's local view doesn't see it, or the pin record exists but session state is stale.","status":"open","priority":3,"issue_type":"bug","created_at":"2025-12-28T16:17:50.561021-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-28T16:17:50.561021-08:00"} {"id":"bd-srsk","title":"Gate eval: Add error visibility for gh CLI failures","description":"Currently evalGHRunGate and evalGHPRGate silently swallow errors when the gh CLI fails (lines 738-741, 780-783 in gate.go).\n\nThis makes debugging difficult - if gh isn't installed, auth fails, or network issues occur, gates silently stall forever with no indication of why.\n\nOptions:\n1. Add debug logging when gh fails\n2. Return error info in the skipped list for aggregate reporting\n3. Add a --verbose flag to gate eval that shows failures\n\nLow priority since the fail-safe behavior (don't close gate on error) is correct.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-25T23:13:11.718635-08:00","updated_at":"2025-12-25T23:13:11.718635-08:00"} {"id":"bd-su45","title":"Protect pinned issues from bd cleanup/compact","description":"Update bd cleanup and bd compact to never delete pinned issues, even if they are closed. Pinned issues should persist indefinitely as reference material.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-18T23:33:46.204783-08:00","updated_at":"2025-12-19T17:43:35.712617-08:00","closed_at":"2025-12-19T00:43:04.06406-08:00","dependencies":[{"issue_id":"bd-su45","depends_on_id":"bd-0vg","type":"blocks","created_at":"2025-12-18T23:33:56.64582-08:00","created_by":"daemon"},{"issue_id":"bd-su45","depends_on_id":"bd-7h5","type":"blocks","created_at":"2025-12-18T23:34:07.857586-08:00","created_by":"daemon"}]} {"id":"bd-sumr","title":"Merge: bd-t4sb","description":"branch: polecat/capable\ntarget: main\nsource_issue: bd-t4sb\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T23:22:21.343724-08:00","updated_at":"2025-12-20T23:17:26.997992-08:00","closed_at":"2025-12-20T23:17:26.997992-08:00","close_reason":"Branches nuked, MRs obsolete"} From 27de61fbd08e00a13b9717c1a041b5c055fe9c54 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 16:34:24 -0800 Subject: [PATCH 5/6] chore: Convert scattered TODOs to tracked issues (bd-lrj8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created 7 new beads to track TODOs that were previously inline comments: - bd-7l27: Integrate migration detection into bd doctor (5 files) - bd-6x6g: Add multi-repo target repo switching in bd create - bd-ag35: Add daemon RPC endpoints for config and stale check - bd-2dwo: Remove deprecated daemon logger function - bd-0qx5: Implement Jira issue timestamp comparison for sync - bd-7zka: Implement formula features (repeat, for_each, branch, gate, split) - bd-h048: Refactor sync_test to use direct import logic Updated 16 TODO comments to include bead ID references for tracking. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/create.go | 4 ++-- cmd/bd/daemon_logger.go | 2 +- cmd/bd/jira.go | 2 +- cmd/bd/migrate.go | 2 +- cmd/bd/migrate_hash_ids.go | 2 +- cmd/bd/migrate_issues.go | 2 +- cmd/bd/migrate_sync.go | 2 +- cmd/bd/migrate_tombstones.go | 2 +- cmd/bd/mol_stale.go | 2 +- cmd/bd/sync_test.go | 2 +- internal/formula/types.go | 10 +++++----- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cmd/bd/create.go b/cmd/bd/create.go index 0a6de264..2e0b3894 100644 --- a/cmd/bd/create.go +++ b/cmd/bd/create.go @@ -161,7 +161,7 @@ var createCmd = &cobra.Command{ repoPath = routing.DetermineTargetRepo(routingConfig, userRole, ".") } - // TODO: Switch to target repo for multi-repo support + // TODO(bd-6x6g): Switch to target repo for multi-repo support // For now, we just log the target repo in debug mode if repoPath != "." { debug.Logf("DEBUG: Target repo: %s\n", repoPath) @@ -205,7 +205,7 @@ var createCmd = &cobra.Command{ // Get database prefix from config var dbPrefix string if daemonClient != nil { - // TODO: Add RPC method to get config in daemon mode + // TODO(bd-ag35): Add RPC method to get config in daemon mode // For now, skip validation in daemon mode (needs RPC enhancement) } else { // Direct mode - check config diff --git a/cmd/bd/daemon_logger.go b/cmd/bd/daemon_logger.go index bf085871..17da2207 100644 --- a/cmd/bd/daemon_logger.go +++ b/cmd/bd/daemon_logger.go @@ -128,7 +128,7 @@ func setupDaemonLogger(logPath string, jsonFormat bool, level slog.Level) (*lumb } // setupDaemonLoggerLegacy is the old signature for backward compatibility during migration. -// TODO: Remove this once all callers are updated to use the new signature. +// TODO(bd-2dwo): Remove this once all callers are updated to use the new signature. func setupDaemonLoggerLegacy(logPath string) (*lumberjack.Logger, daemonLogger) { return setupDaemonLogger(logPath, false, slog.LevelInfo) } diff --git a/cmd/bd/jira.go b/cmd/bd/jira.go index 88141549..2571d4a0 100644 --- a/cmd/bd/jira.go +++ b/cmd/bd/jira.go @@ -630,7 +630,7 @@ func detectJiraConflicts(ctx context.Context) ([]JiraConflict, error) { // Check if updated since last sync if issue.UpdatedAt.After(lastSync) { // This is a potential conflict - for now, mark as conflict - // TODO: In a full implementation, we'd fetch the Jira issue and compare timestamps + // TODO(bd-0qx5): In a full implementation, we'd fetch the Jira issue and compare timestamps conflicts = append(conflicts, JiraConflict{ IssueID: issue.ID, LocalUpdated: issue.UpdatedAt, diff --git a/cmd/bd/migrate.go b/cmd/bd/migrate.go index 14d15bbd..5e8ef980 100644 --- a/cmd/bd/migrate.go +++ b/cmd/bd/migrate.go @@ -19,7 +19,7 @@ import ( _ "github.com/ncruces/go-sqlite3/embed" ) -// TODO: Consider integrating into 'bd doctor' migration detection +// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection var migrateCmd = &cobra.Command{ Use: "migrate", GroupID: "maint", diff --git a/cmd/bd/migrate_hash_ids.go b/cmd/bd/migrate_hash_ids.go index e9139769..f1e743fd 100644 --- a/cmd/bd/migrate_hash_ids.go +++ b/cmd/bd/migrate_hash_ids.go @@ -21,7 +21,7 @@ import ( "github.com/steveyegge/beads/internal/ui" ) -// TODO: Consider integrating into 'bd doctor' migration detection +// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection var migrateHashIDsCmd = &cobra.Command{ Use: "hash-ids", Short: "Migrate sequential IDs to hash-based IDs (legacy)", diff --git a/cmd/bd/migrate_issues.go b/cmd/bd/migrate_issues.go index eaad3f88..b6c05d17 100644 --- a/cmd/bd/migrate_issues.go +++ b/cmd/bd/migrate_issues.go @@ -12,7 +12,7 @@ import ( "github.com/steveyegge/beads/internal/storage/sqlite" ) -// TODO: Consider integrating into 'bd doctor' migration detection +// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection var migrateIssuesCmd = &cobra.Command{ Use: "issues", Short: "Move issues between repositories", diff --git a/cmd/bd/migrate_sync.go b/cmd/bd/migrate_sync.go index ba37dfc9..350315cd 100644 --- a/cmd/bd/migrate_sync.go +++ b/cmd/bd/migrate_sync.go @@ -13,7 +13,7 @@ import ( "github.com/steveyegge/beads/internal/syncbranch" ) -// TODO: Consider integrating into 'bd doctor' migration detection +// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection var migrateSyncCmd = &cobra.Command{ Use: "sync ", Short: "Migrate to sync.branch workflow for multi-clone setups", diff --git a/cmd/bd/migrate_tombstones.go b/cmd/bd/migrate_tombstones.go index cf094bd7..2d1357c4 100644 --- a/cmd/bd/migrate_tombstones.go +++ b/cmd/bd/migrate_tombstones.go @@ -69,7 +69,7 @@ func loadLegacyDeletionsCmd(path string) (map[string]legacyDeletionRecordCmd, [] return records, warnings, nil } -// TODO: Consider integrating into 'bd doctor' migration detection +// TODO(bd-7l27): Consider integrating into 'bd doctor' migration detection var migrateTombstonesCmd = &cobra.Command{ Use: "tombstones", Short: "Convert deletions.jsonl entries to inline tombstones", diff --git a/cmd/bd/mol_stale.go b/cmd/bd/mol_stale.go index 0460f0ee..d510a6fc 100644 --- a/cmd/bd/mol_stale.go +++ b/cmd/bd/mol_stale.go @@ -64,7 +64,7 @@ func runMolStale(cmd *cobra.Command, args []string) { if daemonClient != nil { // For now, stale check requires direct store access - // TODO: Add RPC endpoint for stale check + // TODO(bd-ag35): Add RPC endpoint for stale check fmt.Fprintf(os.Stderr, "Error: mol stale requires direct database access\n") fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol stale\n") os.Exit(1) diff --git a/cmd/bd/sync_test.go b/cmd/bd/sync_test.go index df6ec510..b3f2302e 100644 --- a/cmd/bd/sync_test.go +++ b/cmd/bd/sync_test.go @@ -441,7 +441,7 @@ func TestHasJSONLConflict_MultipleConflicts(t *testing.T) { func TestZFCSkipsExportAfterImport(t *testing.T) { // Skip this test - it calls importFromJSONL which spawns bd import as subprocess, // but os.Executable() returns the test binary during tests, not the bd binary. - // TODO: Refactor to use direct import logic instead of subprocess. + // TODO(bd-h048): Refactor to use direct import logic instead of subprocess. t.Skip("Test requires subprocess spawning which doesn't work in test environment") if testing.Short() { t.Skip("Skipping test that spawns subprocess in short mode") diff --git a/internal/formula/types.go b/internal/formula/types.go index 4b253e69..8c79fcb8 100644 --- a/internal/formula/types.go +++ b/internal/formula/types.go @@ -167,23 +167,23 @@ type Step struct { // Expand references an expansion formula to inline here. // When set, this step is replaced by the expansion's steps. - // TODO(future): Not yet implemented in bd cook. Filed as future work. + // TODO(bd-7zka): Not yet implemented in bd cook. Filed as future work. Expand string `json:"expand,omitempty"` // ExpandVars are variable overrides for the expansion. - // TODO(future): Not yet implemented in bd cook. Filed as future work. + // TODO(bd-7zka): Not yet implemented in bd cook. Filed as future work. ExpandVars map[string]string `json:"expand_vars,omitempty"` // Condition makes this step optional based on a variable. // Format: "{{var}}" (truthy) or "{{var}} == value". - // TODO(future): Not yet implemented in bd cook. Filed as future work. + // TODO(bd-7zka): Not yet implemented in bd cook. Filed as future work. Condition string `json:"condition,omitempty"` // Children are nested steps (for creating epic hierarchies). Children []*Step `json:"children,omitempty"` // Gate defines an async wait condition for this step. - // TODO(future): Not yet implemented in bd cook. Will integrate with bd-udsi gates. + // TODO(bd-7zka): Not yet implemented in bd cook. Will integrate with bd-udsi gates. Gate *Gate `json:"gate,omitempty"` // Loop defines iteration for this step. @@ -207,7 +207,7 @@ type Step struct { } // Gate defines an async wait condition (integrates with bd-udsi). -// TODO(future): Not yet implemented in bd cook. Schema defined for future use. +// TODO(bd-7zka): Not yet implemented in bd cook. Schema defined for future use. type Gate struct { // Type is the condition type: gh:run, gh:pr, timer, human, mail. Type string `json:"type"` From 1bb61c5ffea7d8e1be2d59f7757cef9462021e65 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 16:37:30 -0800 Subject: [PATCH 6/6] refactor: Break up 275-line runCook into focused helpers (bd-8zbo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted 5 helper functions from the monolithic runCook function: - parseCookFlags: Parse and validate command-line flags (~40 lines) - loadAndResolveFormula: Parse, resolve, apply transformations (~60 lines) - outputCookDryRun: Display dry-run preview (~60 lines) - outputCookEphemeral: Output resolved formula as JSON (~30 lines) - persistCookFormula: Create proto bead in database (~45 lines) Main runCook function reduced from ~275 to ~65 lines with clear flow: 1. Parse flags → 2. Validate store → 3. Load formula 4. Extract metadata → 5. Handle mode (dry-run/ephemeral/persist) Benefits: - Each helper is single-responsibility and testable - Added cookFlags struct for type-safe flag passing - Error handling uses proper error returns instead of os.Exit - Clear separation of concerns šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/cook.go | 351 +++++++++++++++++++++++++++---------------------- 1 file changed, 197 insertions(+), 154 deletions(-) diff --git a/cmd/bd/cook.go b/cmd/bd/cook.go index e7dab0ae..8c0325bb 100644 --- a/cmd/bd/cook.go +++ b/cmd/bd/cook.go @@ -99,7 +99,20 @@ type cookResult struct { BondPoints []string `json:"bond_points,omitempty"` } -func runCook(cmd *cobra.Command, args []string) { +// cookFlags holds parsed command-line flags for the cook command +type cookFlags struct { + dryRun bool + persist bool + force bool + searchPaths []string + prefix string + inputVars map[string]string + runtimeMode bool + formulaPath string +} + +// parseCookFlags parses and validates cook command flags +func parseCookFlags(cmd *cobra.Command, args []string) (*cookFlags, error) { dryRun, _ := cmd.Flags().GetBool("dry-run") persist, _ := cmd.Flags().GetBool("persist") force, _ := cmd.Flags().GetBool("force") @@ -113,61 +126,51 @@ func runCook(cmd *cobra.Command, args []string) { for _, v := range varFlags { parts := strings.SplitN(v, "=", 2) if len(parts) != 2 { - fmt.Fprintf(os.Stderr, "Error: invalid variable format '%s', expected 'key=value'\n", v) - os.Exit(1) + return nil, fmt.Errorf("invalid variable format '%s', expected 'key=value'", v) } inputVars[parts[0]] = parts[1] } - // Determine cooking mode + // Validate mode + if mode != "" && mode != "compile" && mode != "runtime" { + return nil, fmt.Errorf("invalid mode '%s', must be 'compile' or 'runtime'", mode) + } + // Runtime mode is triggered by: explicit --mode=runtime OR providing --var flags runtimeMode := mode == "runtime" || len(inputVars) > 0 - if mode != "" && mode != "compile" && mode != "runtime" { - fmt.Fprintf(os.Stderr, "Error: invalid mode '%s', must be 'compile' or 'runtime'\n", mode) - os.Exit(1) - } - // Only need store access if persisting - if persist { - CheckReadonly("cook --persist") + return &cookFlags{ + dryRun: dryRun, + persist: persist, + force: force, + searchPaths: searchPaths, + prefix: prefix, + inputVars: inputVars, + runtimeMode: runtimeMode, + formulaPath: args[0], + }, nil +} - if store == nil { - if daemonClient != nil { - fmt.Fprintf(os.Stderr, "Error: cook --persist requires direct database access\n") - fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon cook %s --persist ...\n", args[0]) - } else { - fmt.Fprintf(os.Stderr, "Error: no database connection\n") - } - os.Exit(1) - } - } - - ctx := rootCtx - - // Create parser with search paths +// loadAndResolveFormula parses a formula file and applies all transformations +func loadAndResolveFormula(formulaPath string, searchPaths []string) (*formula.Formula, error) { parser := formula.NewParser(searchPaths...) // Parse the formula file - formulaPath := args[0] f, err := parser.ParseFile(formulaPath) if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing formula: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("parsing formula: %w", err) } // Resolve inheritance resolved, err := parser.Resolve(f) if err != nil { - fmt.Fprintf(os.Stderr, "Error resolving formula: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("resolving formula: %w", err) } // Apply control flow operators - loops, branches, gates - // This must happen before advice and expansions so they can act on expanded loop steps controlFlowSteps, err := formula.ApplyControlFlow(resolved.Steps, resolved.Compose) if err != nil { - fmt.Fprintf(os.Stderr, "Error applying control flow: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("applying control flow: %w", err) } resolved.Steps = controlFlowSteps @@ -177,11 +180,9 @@ func runCook(cmd *cobra.Command, args []string) { } // Apply inline step expansions - // This processes Step.Expand fields before compose.expand/map rules inlineExpandedSteps, err := formula.ApplyInlineExpansions(resolved.Steps, parser) if err != nil { - fmt.Fprintf(os.Stderr, "Error applying inline expansions: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("applying inline expansions: %w", err) } resolved.Steps = inlineExpandedSteps @@ -189,8 +190,7 @@ func runCook(cmd *cobra.Command, args []string) { if resolved.Compose != nil && (len(resolved.Compose.Expand) > 0 || len(resolved.Compose.Map) > 0) { expandedSteps, err := formula.ApplyExpansions(resolved.Steps, resolved.Compose, parser) if err != nil { - fmt.Fprintf(os.Stderr, "Error applying expansions: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("applying expansions: %w", err) } resolved.Steps = expandedSteps } @@ -200,12 +200,10 @@ func runCook(cmd *cobra.Command, args []string) { for _, aspectName := range resolved.Compose.Aspects { aspectFormula, err := parser.LoadByName(aspectName) if err != nil { - fmt.Fprintf(os.Stderr, "Error loading aspect %q: %v\n", aspectName, err) - os.Exit(1) + return nil, fmt.Errorf("loading aspect %q: %w", aspectName, err) } if aspectFormula.Type != formula.TypeAspect { - fmt.Fprintf(os.Stderr, "Error: %q is not an aspect formula (type=%s)\n", aspectName, aspectFormula.Type) - os.Exit(1) + return nil, fmt.Errorf("%q is not an aspect formula (type=%s)", aspectName, aspectFormula.Type) } if len(aspectFormula.Advice) > 0 { resolved.Steps = formula.ApplyAdvice(resolved.Steps, aspectFormula.Advice) @@ -213,141 +211,119 @@ func runCook(cmd *cobra.Command, args []string) { } } - // Apply prefix to proto ID if specified - protoID := resolved.Formula - if prefix != "" { - protoID = prefix + resolved.Formula - } + return resolved, nil +} - // Extract variables used in the formula - vars := formula.ExtractVariables(resolved) - - // Collect bond points - var bondPoints []string - if resolved.Compose != nil { - for _, bp := range resolved.Compose.BondPoints { - bondPoints = append(bondPoints, bp.ID) +// outputCookDryRun displays a dry-run preview of what would be cooked +func outputCookDryRun(resolved *formula.Formula, protoID string, runtimeMode bool, inputVars map[string]string, vars, bondPoints []string) { + modeLabel := "compile-time" + if runtimeMode { + modeLabel = "runtime" + // Apply defaults for runtime mode display + for name, def := range resolved.Vars { + if _, provided := inputVars[name]; !provided && def.Default != "" { + inputVars[name] = def.Default + } } } - if dryRun { - // Determine mode label for display - modeLabel := "compile-time" - if runtimeMode { - modeLabel = "runtime" - // Apply defaults for runtime mode display - for name, def := range resolved.Vars { - if _, provided := inputVars[name]; !provided && def.Default != "" { - inputVars[name] = def.Default - } - } - } + fmt.Printf("\nDry run: would cook formula %s as proto %s (%s mode)\n\n", resolved.Formula, protoID, modeLabel) - fmt.Printf("\nDry run: would cook formula %s as proto %s (%s mode)\n\n", resolved.Formula, protoID, modeLabel) + // In runtime mode, show substituted steps + if runtimeMode { + substituteFormulaVars(resolved, inputVars) + fmt.Printf("Steps (%d) [variables substituted]:\n", len(resolved.Steps)) + } else { + fmt.Printf("Steps (%d) [{{variables}} shown as placeholders]:\n", len(resolved.Steps)) + } + printFormulaSteps(resolved.Steps, " ") - // In runtime mode, show substituted steps - if runtimeMode { - // Create a copy with substituted values for display - substituteFormulaVars(resolved, inputVars) - fmt.Printf("Steps (%d) [variables substituted]:\n", len(resolved.Steps)) - } else { - fmt.Printf("Steps (%d) [{{variables}} shown as placeholders]:\n", len(resolved.Steps)) - } - printFormulaSteps(resolved.Steps, " ") - - if len(vars) > 0 { - fmt.Printf("\nVariables used: %s\n", strings.Join(vars, ", ")) - } - - // Show variable values in runtime mode - if runtimeMode && len(inputVars) > 0 { - fmt.Printf("\nVariable values:\n") - for name, value := range inputVars { - fmt.Printf(" {{%s}} = %s\n", name, value) - } - } - - if len(bondPoints) > 0 { - fmt.Printf("Bond points: %s\n", strings.Join(bondPoints, ", ")) - } - - // Show variable definitions (more useful in compile-time mode) - if !runtimeMode && len(resolved.Vars) > 0 { - fmt.Printf("\nVariable definitions:\n") - for name, def := range resolved.Vars { - attrs := []string{} - if def.Required { - attrs = append(attrs, "required") - } - if def.Default != "" { - attrs = append(attrs, fmt.Sprintf("default=%s", def.Default)) - } - if len(def.Enum) > 0 { - attrs = append(attrs, fmt.Sprintf("enum=[%s]", strings.Join(def.Enum, ","))) - } - attrStr := "" - if len(attrs) > 0 { - attrStr = fmt.Sprintf(" (%s)", strings.Join(attrs, ", ")) - } - fmt.Printf(" {{%s}}: %s%s\n", name, def.Description, attrStr) - } - } - return + if len(vars) > 0 { + fmt.Printf("\nVariables used: %s\n", strings.Join(vars, ", ")) } - // Ephemeral mode (default): output resolved formula as JSON to stdout - if !persist { - // Runtime mode: substitute variables before output - if runtimeMode { - // Apply defaults from formula variable definitions - for name, def := range resolved.Vars { - if _, provided := inputVars[name]; !provided && def.Default != "" { - inputVars[name] = def.Default - } - } - - // Check for missing required variables - var missingVars []string - for _, v := range vars { - if _, ok := inputVars[v]; !ok { - missingVars = append(missingVars, v) - } - } - if len(missingVars) > 0 { - fmt.Fprintf(os.Stderr, "Error: runtime mode requires all variables to have values\n") - fmt.Fprintf(os.Stderr, "Missing: %s\n", strings.Join(missingVars, ", ")) - fmt.Fprintf(os.Stderr, "Provide with: --var %s=\n", missingVars[0]) - os.Exit(1) - } - - // Substitute variables in the formula - substituteFormulaVars(resolved, inputVars) + // Show variable values in runtime mode + if runtimeMode && len(inputVars) > 0 { + fmt.Printf("\nVariable values:\n") + for name, value := range inputVars { + fmt.Printf(" {{%s}} = %s\n", name, value) } - outputJSON(resolved) - return } - // Persist mode: create proto bead in database (legacy behavior) + if len(bondPoints) > 0 { + fmt.Printf("Bond points: %s\n", strings.Join(bondPoints, ", ")) + } + + // Show variable definitions (more useful in compile-time mode) + if !runtimeMode && len(resolved.Vars) > 0 { + fmt.Printf("\nVariable definitions:\n") + for name, def := range resolved.Vars { + attrs := []string{} + if def.Required { + attrs = append(attrs, "required") + } + if def.Default != "" { + attrs = append(attrs, fmt.Sprintf("default=%s", def.Default)) + } + if len(def.Enum) > 0 { + attrs = append(attrs, fmt.Sprintf("enum=[%s]", strings.Join(def.Enum, ","))) + } + attrStr := "" + if len(attrs) > 0 { + attrStr = fmt.Sprintf(" (%s)", strings.Join(attrs, ", ")) + } + fmt.Printf(" {{%s}}: %s%s\n", name, def.Description, attrStr) + } + } +} + +// outputCookEphemeral outputs the resolved formula as JSON (ephemeral mode) +func outputCookEphemeral(resolved *formula.Formula, runtimeMode bool, inputVars map[string]string, vars []string) error { + if runtimeMode { + // Apply defaults from formula variable definitions + for name, def := range resolved.Vars { + if _, provided := inputVars[name]; !provided && def.Default != "" { + inputVars[name] = def.Default + } + } + + // Check for missing required variables + var missingVars []string + for _, v := range vars { + if _, ok := inputVars[v]; !ok { + missingVars = append(missingVars, v) + } + } + if len(missingVars) > 0 { + return fmt.Errorf("runtime mode requires all variables to have values\nMissing: %s\nProvide with: --var %s=", + strings.Join(missingVars, ", "), missingVars[0]) + } + + // Substitute variables in the formula + substituteFormulaVars(resolved, inputVars) + } + outputJSON(resolved) + return nil +} + +// persistCookFormula creates a proto bead in the database (persist mode) +func persistCookFormula(ctx context.Context, resolved *formula.Formula, protoID string, force bool, vars, bondPoints []string) error { // Check if proto already exists existingProto, err := store.GetIssue(ctx, protoID) if err == nil && existingProto != nil { if !force { - fmt.Fprintf(os.Stderr, "Error: proto %s already exists\n", protoID) - fmt.Fprintf(os.Stderr, "Hint: use --force to replace it\n") - os.Exit(1) + return fmt.Errorf("proto %s already exists (use --force to replace)", protoID) } // Delete existing proto and its children if err := deleteProtoSubgraph(ctx, store, protoID); err != nil { - fmt.Fprintf(os.Stderr, "Error deleting existing proto: %v\n", err) - os.Exit(1) + return fmt.Errorf("deleting existing proto: %w", err) } } // Create the proto bead from the formula result, err := cookFormula(ctx, store, resolved, protoID) if err != nil { - fmt.Fprintf(os.Stderr, "Error cooking formula: %v\n", err) - os.Exit(1) + return fmt.Errorf("cooking formula: %w", err) } // Schedule auto-flush @@ -361,7 +337,7 @@ func runCook(cmd *cobra.Command, args []string) { Variables: vars, BondPoints: bondPoints, }) - return + return nil } fmt.Printf("%s Cooked proto: %s\n", ui.RenderPass("āœ“"), result.ProtoID) @@ -373,6 +349,73 @@ func runCook(cmd *cobra.Command, args []string) { fmt.Printf(" Bond points: %s\n", strings.Join(bondPoints, ", ")) } fmt.Printf("\nTo use: bd mol pour %s --var =\n", result.ProtoID) + return nil +} + +func runCook(cmd *cobra.Command, args []string) { + // Parse and validate flags + flags, err := parseCookFlags(cmd, args) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + // Validate store access for persist mode + if flags.persist { + CheckReadonly("cook --persist") + if store == nil { + if daemonClient != nil { + fmt.Fprintf(os.Stderr, "Error: cook --persist requires direct database access\n") + fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon cook %s --persist ...\n", flags.formulaPath) + } else { + fmt.Fprintf(os.Stderr, "Error: no database connection\n") + } + os.Exit(1) + } + } + + // Load and resolve the formula + resolved, err := loadAndResolveFormula(flags.formulaPath, flags.searchPaths) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + // Apply prefix to proto ID if specified + protoID := resolved.Formula + if flags.prefix != "" { + protoID = flags.prefix + resolved.Formula + } + + // Extract variables and bond points + vars := formula.ExtractVariables(resolved) + var bondPoints []string + if resolved.Compose != nil { + for _, bp := range resolved.Compose.BondPoints { + bondPoints = append(bondPoints, bp.ID) + } + } + + // Handle dry-run mode + if flags.dryRun { + outputCookDryRun(resolved, protoID, flags.runtimeMode, flags.inputVars, vars, bondPoints) + return + } + + // Handle ephemeral mode (default) + if !flags.persist { + if err := outputCookEphemeral(resolved, flags.runtimeMode, flags.inputVars, vars); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return + } + + // Handle persist mode + if err := persistCookFormula(rootCtx, resolved, protoID, flags.force, vars, bondPoints); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } } // cookFormulaResult holds the result of cooking