diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 8633ee6f..664bbe5a 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -19,7 +19,7 @@ {"id":"bd-736d","content_hash":"4743b1f41ff07fee3daa63240f0d5f7ac3f876e928b22c4ce0bee2cdf544e53a","title":"Refactor path canonicalization into helper function","description":"The path canonicalization logic (filepath.Abs + EvalSymlinks) is duplicated in 3 places:\n- beads.go:131-137 (BEADS_DIR handling)\n- cmd/bd/main.go:446-451 (--no-db cleanup)\n- cmd/bd/nodb.go:26-31 (--no-db initialization)\n\nRefactoring suggestion:\nExtract to a helper function like:\n func canonicalizePath(path string) string\n\nThis would:\n- Reduce code duplication\n- Make the logic easier to maintain\n- Ensure consistent behavior across all path handling\n\nRelated to bd-e16b implementation.","status":"open","priority":3,"issue_type":"chore","created_at":"2025-11-02T18:33:47.727443-08:00","updated_at":"2025-11-02T18:33:47.727443-08:00","source_repo":"."} {"id":"bd-77gm","content_hash":"3072888f4e983c3038332d0d01ffd486c9510bb36c4419129821c1366f24e55a","title":"Import reports misleading '0 created, 0 updated' when actually importing all issues","description":"When running 'bd import' on a fresh database (no existing issues), the command reports 'Import complete: 0 created, 0 updated' even though it successfully imported all issues from the JSONL file.\n\n**Steps to reproduce:**\n1. Delete .beads/beads.db\n2. Run: bd import .beads/issues.jsonl\n3. Observe output: 'Import complete: 0 created, 0 updated'\n4. Run: bd list\n5. Confirm: All issues are actually present in the database\n\n**Expected behavior:**\nReport the actual number of issues imported, e.g., 'Import complete: 523 created, 0 updated'\n\n**Actual behavior:**\n'Import complete: 0 created, 0 updated' (misleading - makes user think import failed)\n\n**Impact:**\n- Users think import failed when it succeeded\n- Confusing during database sync operations (e.g., after git pull)\n- Makes debugging harder (can't tell if import actually worked)\n\n**Context:**\nDiscovered during VC session when syncing database after git pull. The misleading message caused confusion about whether the database was properly synced with the canonical JSONL file.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-09T16:20:13.191156-08:00","updated_at":"2025-11-24T10:56:21.6274-08:00","closed_at":"2025-11-24T01:36:56.938858-08:00","source_repo":"."} {"id":"bd-7kg","content_hash":"4e40027d49dd5a3d17e033df8962021409cf46777153ed24fdba2810f4c191dd","title":"Document error handling audit findings","description":"Comprehensive audit document showing all error handling inconsistencies found across cmd/bd/*.go files, with specific examples and recommended refactorings","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-24T00:26:59.45249-08:00","updated_at":"2025-11-24T00:28:51.788141-08:00","closed_at":"2025-11-24T00:28:51.788141-08:00","source_repo":"."} -{"id":"bd-81a","content_hash":"0f43da9e36bc3c5db20f302b82021377685a9425f519a36bab5a2cf1b85f13d8","title":"Add programmatic tip injection API","description":"Allow tips to be programmatically injected at runtime based on detected conditions. This enables dynamic tips (not just pre-defined ones) to be shown with custom priority and frequency.","design":"## API Design\n\nAdd to `cmd/bd/tips.go`:\n\n```go\n// InjectTip adds a dynamic tip to the registry at runtime\nfunc InjectTip(id, message string, priority int, frequency time.Duration, probability float64, condition func() bool) {\n tipsMutex.Lock()\n defer tipsMutex.Unlock()\n \n tips = append(tips, Tip{\n ID: id,\n Condition: condition,\n Message: message,\n Frequency: frequency,\n Priority: priority,\n Probability: probability,\n })\n}\n\n// RemoveTip removes a tip from the registry\nfunc RemoveTip(id string) {\n tipsMutex.Lock()\n defer tipsMutex.Unlock()\n \n for i, tip := range tips {\n if tip.ID == id {\n tips = append(tips[:i], tips[i+1:]...)\n return\n }\n }\n}\n```\n\n## Use Cases\n\n### Example 1: Critical Security Update\n```go\n// In bd version check code\nif criticalSecurityUpdate {\n InjectTip(\n \"security_update\",\n fmt.Sprintf(\"CRITICAL: Security update available (bd %s). Update immediately!\", remoteVersion),\n 100, // Highest priority\n 0, // No frequency limit\n 1.0, // Always show (100% probability)\n func() bool { return true },\n )\n}\n```\n\n### Example 2: New Version Available\n```go\n// In bd version check code\nif remoteVersion \u003e currentVersion {\n InjectTip(\n \"upgrade_available\",\n fmt.Sprintf(\"New bd version %s available (you have %s). Run: go install github.com/steveyegge/beads/cmd/bd@latest\", remoteVersion, currentVersion),\n 90, // High priority\n 7 * 24 * time.Hour, // Weekly\n 0.8, // 80% probability (frequent but not always)\n func() bool { return true },\n )\n}\n```\n\n### Example 3: Large Issue Count Suggestion\n```go\n// In bd list code\nif issueCount \u003e 100 {\n InjectTip(\n \"use_filters\",\n \"You have many issues. Use filters: 'bd list --status=open --priority=1'\",\n 50, // Medium priority\n 14 * 24 * time.Hour, // Bi-weekly\n 0.4, // 40% probability (occasional suggestion)\n func() bool { return true },\n )\n}\n```\n\n### Example 4: No Dependencies Used\n```go\n// After analyzing project\nif hasIssues \u0026\u0026 noDependenciesCreated {\n InjectTip(\n \"try_dependencies\",\n \"Try using dependencies: 'bd dep \u003cissue\u003e \u003cblocks-issue\u003e' to track blockers\",\n 30, // Low priority\n 30 * 24 * time.Hour, // Monthly\n 0.3, // 30% probability (low-key suggestion)\n func() bool { return true },\n )\n}\n```\n\n## Probability Guidelines\n\n- **1.0 (100%)**: Critical security, breaking changes, data loss prevention\n- **0.7-0.9 (70-90%)**: Important updates, major new features\n- **0.4-0.6 (40-60%)**: General tips, workflow improvements, feature discovery\n- **0.1-0.3 (10-30%)**: Nice-to-know features, advanced tips, optional optimizations\n\n## Thread Safety\n- Use mutex to protect tip registry\n- Safe for concurrent command execution\n- Deterministic testing via BEADS_TIP_SEED env var","acceptance_criteria":"- InjectTip() API exists and is documented\n- RemoveTip() API exists\n- Thread-safe with mutex protection\n- Can inject tips from any command\n- Injected tips participate in priority/frequency rotation\n- Unit tests for injection API\n- Example usage in code comments","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-11T23:29:46.645583-08:00","updated_at":"2025-11-11T23:50:12.209135-08:00","source_repo":".","dependencies":[{"issue_id":"bd-81a","depends_on_id":"bd-d4i","type":"blocks","created_at":"2025-11-11T23:29:46.646327-08:00","created_by":"daemon"}]} +{"id":"bd-81a","content_hash":"0f43da9e36bc3c5db20f302b82021377685a9425f519a36bab5a2cf1b85f13d8","title":"Add programmatic tip injection API","description":"Allow tips to be programmatically injected at runtime based on detected conditions. This enables dynamic tips (not just pre-defined ones) to be shown with custom priority and frequency.","design":"## API Design\n\nAdd to `cmd/bd/tips.go`:\n\n```go\n// InjectTip adds a dynamic tip to the registry at runtime\nfunc InjectTip(id, message string, priority int, frequency time.Duration, probability float64, condition func() bool) {\n tipsMutex.Lock()\n defer tipsMutex.Unlock()\n \n tips = append(tips, Tip{\n ID: id,\n Condition: condition,\n Message: message,\n Frequency: frequency,\n Priority: priority,\n Probability: probability,\n })\n}\n\n// RemoveTip removes a tip from the registry\nfunc RemoveTip(id string) {\n tipsMutex.Lock()\n defer tipsMutex.Unlock()\n \n for i, tip := range tips {\n if tip.ID == id {\n tips = append(tips[:i], tips[i+1:]...)\n return\n }\n }\n}\n```\n\n## Use Cases\n\n### Example 1: Critical Security Update\n```go\n// In bd version check code\nif criticalSecurityUpdate {\n InjectTip(\n \"security_update\",\n fmt.Sprintf(\"CRITICAL: Security update available (bd %s). Update immediately!\", remoteVersion),\n 100, // Highest priority\n 0, // No frequency limit\n 1.0, // Always show (100% probability)\n func() bool { return true },\n )\n}\n```\n\n### Example 2: New Version Available\n```go\n// In bd version check code\nif remoteVersion \u003e currentVersion {\n InjectTip(\n \"upgrade_available\",\n fmt.Sprintf(\"New bd version %s available (you have %s). Run: go install github.com/steveyegge/beads/cmd/bd@latest\", remoteVersion, currentVersion),\n 90, // High priority\n 7 * 24 * time.Hour, // Weekly\n 0.8, // 80% probability (frequent but not always)\n func() bool { return true },\n )\n}\n```\n\n### Example 3: Large Issue Count Suggestion\n```go\n// In bd list code\nif issueCount \u003e 100 {\n InjectTip(\n \"use_filters\",\n \"You have many issues. Use filters: 'bd list --status=open --priority=1'\",\n 50, // Medium priority\n 14 * 24 * time.Hour, // Bi-weekly\n 0.4, // 40% probability (occasional suggestion)\n func() bool { return true },\n )\n}\n```\n\n### Example 4: No Dependencies Used\n```go\n// After analyzing project\nif hasIssues \u0026\u0026 noDependenciesCreated {\n InjectTip(\n \"try_dependencies\",\n \"Try using dependencies: 'bd dep \u003cissue\u003e \u003cblocks-issue\u003e' to track blockers\",\n 30, // Low priority\n 30 * 24 * time.Hour, // Monthly\n 0.3, // 30% probability (low-key suggestion)\n func() bool { return true },\n )\n}\n```\n\n## Probability Guidelines\n\n- **1.0 (100%)**: Critical security, breaking changes, data loss prevention\n- **0.7-0.9 (70-90%)**: Important updates, major new features\n- **0.4-0.6 (40-60%)**: General tips, workflow improvements, feature discovery\n- **0.1-0.3 (10-30%)**: Nice-to-know features, advanced tips, optional optimizations\n\n## Thread Safety\n- Use mutex to protect tip registry\n- Safe for concurrent command execution\n- Deterministic testing via BEADS_TIP_SEED env var","acceptance_criteria":"- InjectTip() API exists and is documented\n- RemoveTip() API exists\n- Thread-safe with mutex protection\n- Can inject tips from any command\n- Injected tips participate in priority/frequency rotation\n- Unit tests for injection API\n- Example usage in code comments","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-11T23:29:46.645583-08:00","updated_at":"2025-11-24T11:28:19.925994-08:00","closed_at":"2025-11-24T11:28:19.925994-08:00","source_repo":".","dependencies":[{"issue_id":"bd-81a","depends_on_id":"bd-d4i","type":"blocks","created_at":"2025-11-11T23:29:46.646327-08:00","created_by":"daemon"}]} {"id":"bd-8wa","content_hash":"4dbf9e3e653a748751b14d4c338643d60a8c6b8703a62bd88962602f9ad14b00","title":"Code Review Sweep: thorough","description":"Perform thorough code review sweep based on accumulated activity.\n\n**AI Reasoning:**\nSignificant code volume added (150,273 lines) across multiple critical areas, including cmd/bd, internal/storage/sqlite, and internal/rpc. High file change count (616) indicates substantial refactoring or new functionality. The metrics suggest potential for subtle architectural or implementation issues that warrant review.\n\n**Scope:** thorough\n**Target Areas:** cmd/bd, internal/storage/sqlite, internal/rpc\n**Estimated Files:** 12\n**Estimated Cost:** $5\n\n**Task:**\nReview files for non-obvious issues that agents miss during focused work:\n- Inefficiencies (algorithmic, resource usage)\n- Subtle bugs (race conditions, off-by-one, copy-paste)\n- Poor patterns (coupling, complexity, duplication)\n- Missing best practices (error handling, docs, tests)\n- Unnamed anti-patterns\n\nFile discovered issues with detailed reasoning and suggestions.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-21T10:25:37.081296-05:00","updated_at":"2025-11-24T00:03:21.246286-08:00","closed_at":"2025-11-23T23:24:19.821439-08:00","source_repo":".","labels":["code-review-sweep","review-area:cmd/bd","review-area:internal/rpc","review-area:internal/storage/sqlite"]} {"id":"bd-98c4e1fa.1","content_hash":"18da5da06505d025d219d9de2e9fe9b7b538725e935efe58ff9463eb11bd1e01","title":"Update AGENTS.md with event-driven mode","description":"Document BEADS_DAEMON_MODE env var. Explain opt-in during Phase 1. Add troubleshooting for watcher failures.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T23:05:13.986452-07:00","updated_at":"2025-11-23T23:52:29.995214-08:00","closed_at":"2025-11-23T23:20:42.790628-08:00","source_repo":"."} {"id":"bd-9cdc","content_hash":"361f598170b06de5743a2bcaade92866135f2d1019cfde4f04daf16e8d817864","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-11-24T01:37:03.441156-08:00","closed_at":"2025-11-24T01:20:17.196828-08:00","source_repo":"."} @@ -63,7 +63,7 @@ {"id":"bd-s0z","content_hash":"b69df0c8664737b3c04b10e3137652e3c8c3d782de0ecd02bfcd648919f8d944","title":"Consider extracting error handling helpers","description":"Evaluate creating FatalError() and WarnError() helpers as suggested in ERROR_HANDLING.md to reduce boilerplate and enforce consistency. Prototype in a few files first to validate the approach.","status":"open","priority":4,"issue_type":"task","created_at":"2025-11-24T00:28:57.248959-08:00","updated_at":"2025-11-24T00:28:57.248959-08:00","source_repo":".","dependencies":[{"issue_id":"bd-s0z","depends_on_id":"bd-1qwo","type":"blocks","created_at":"2025-11-24T00:28:57.249945-08:00","created_by":"daemon"}]} {"id":"bd-t3b","content_hash":"c32a3a0f2f836148033fb330e209ac22e06dbecf18894153c15e2036f5afae1c","title":"Add test coverage for internal/config package","description":"","design":"Config package has 1 test file. Need comprehensive tests. Target: 70% coverage","acceptance_criteria":"- At least 3 test files\n- Package coverage \u003e= 70%","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T21:21:22.91657-05:00","updated_at":"2025-11-20T21:21:22.91657-05:00","source_repo":".","dependencies":[{"issue_id":"bd-t3b","depends_on_id":"bd-ge7","type":"blocks","created_at":"2025-11-20T21:21:31.201036-05:00","created_by":"daemon"}]} {"id":"bd-t4u1","content_hash":"c119816ca9a3fada73b24b145cbc9407560dd72207aa79390d93f9e588810552","title":"False positive detection by Kaspersky Antivirus (Trojan)","description":"Kaspersky Antivirus falsely detects beads (bd.exe v0.23.1) as a Trojan (PDM:Trojan.Win32.Generic) and removes it.\nEvent: Malicious object detected\nComponent: System Watcher\nObject name: bd.exe\n","notes":"## Completed Actions\n\n1. **Root cause analysis**: Documented that this is a known false positive with Go binaries\n2. **Created comprehensive user documentation**: docs/ANTIVIRUS.md with:\n - Explanation of why Go binaries trigger false positives\n - Instructions for adding exclusions (Kaspersky, Windows Defender, etc.)\n - File integrity verification steps\n - False positive reporting procedures\n - FAQ section\n3. **Updated TROUBLESHOOTING.md**: Added quick reference section with link to detailed guide\n\n## Next Steps (Follow-up Issues)\n\nThe following actions require external processes or future releases:\n\n1. **Submit false positive to Kaspersky**: Need to access bd.exe v0.23.1 Windows binary and submit to https://opentip.kaspersky.com/\n2. **Code signing for Windows**: Requires purchasing certificate and configuring GoReleaser (medium-term)\n3. **Apply for Kaspersky whitelist**: Long-term relationship building with vendor\n\n## Current Status\n\nUsers affected by this issue now have:\n- Clear documentation explaining the false positive\n- Step-by-step instructions to work around it\n- Ways to verify file integrity\n- Process to report to their antivirus vendor\n\nBuild configuration already includes recommended optimizations (-s -w flags).","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-20T18:56:12.498187-05:00","updated_at":"2025-11-24T01:37:03.442515-08:00","closed_at":"2025-11-24T01:03:39.166915-08:00","source_repo":"."} -{"id":"bd-tne","content_hash":"2a6596980450714800bddc88e106026743a1a131e96f09198eb7dc2a16d75ca4","title":"Add Claude setup tip with dynamic priority","description":"Add a predefined tip that suggests running `bd setup claude` when Claude Code is detected but not configured. This tip should have higher priority (shown more frequently) until the setup is complete.","design":"## Implementation\n\nAdd to tip registry in `cmd/bd/tips.go`:\n\n```go\n{\n ID: \"claude_setup\",\n Condition: func() bool {\n return isClaudeDetected() \u0026\u0026 !isClaudeSetupComplete()\n },\n Message: \"Run 'bd setup claude' to enable automatic context recovery in Claude Code\",\n Frequency: 24 * time.Hour, // Daily minimum gap\n Priority: 100, // Highest priority\n Probability: 0.6, // 60% chance when eligible\n}\n```\n\n## Detection Logic\n\n```go\nfunc isClaudeDetected() bool {\n // Check environment variables\n if os.Getenv(\"CLAUDE_CODE\") != \"\" || os.Getenv(\"ANTHROPIC_CLI\") != \"\" {\n return true\n }\n // Check if .claude/ directory exists\n if _, err := os.Stat(filepath.Join(os.Getenv(\"HOME\"), \".claude\")); err == nil {\n return true\n }\n return false\n}\n\nfunc isClaudeSetupComplete() bool {\n // Check for global installation\n home, err := os.UserHomeDir()\n if err == nil {\n _, err1 := os.Stat(filepath.Join(home, \".claude/commands/prime_beads.md\"))\n _, err2 := os.Stat(filepath.Join(home, \".claude/hooks/sessionstart\"))\n if err1 == nil \u0026\u0026 err2 == nil {\n return true // Global hooks installed\n }\n }\n \n // Check for project installation\n _, err1 := os.Stat(\".claude/commands/prime_beads.md\")\n _, err2 := os.Stat(\".claude/hooks/sessionstart\")\n return err1 == nil \u0026\u0026 err2 == nil\n}\n```\n\n## Priority and Probability Behavior\n\n**Why 60% probability?**\n- Important message (priority 100) but not critical\n- Daily frequency + 60% = shows ~4 times per week\n- Avoids spam while staying visible\n- Balances persistence with user experience\n\n**Comparison with other probabilities:**\n- 100% probability: Shows EVERY day (annoying)\n- 80% probability: Shows ~6 days per week (too frequent)\n- 60% probability: Shows ~4 days per week (balanced)\n- 40% probability: Shows ~3 days per week (might be missed)\n\n**Auto-stops when setup complete:**\n- Condition becomes false after `bd setup claude`\n- No manual dismissal needed\n- Tip naturally disappears from rotation","acceptance_criteria":"- Claude setup tip added to registry\n- isClaudeDetected() checks environment and filesystem\n- isClaudeSetupComplete() verifies hook installation\n- Tip shows daily until setup complete\n- Tip stops showing after setup\n- Unit tests for detection functions","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-11T23:29:29.871324-08:00","updated_at":"2025-11-11T23:50:29.756454-08:00","source_repo":".","dependencies":[{"issue_id":"bd-tne","depends_on_id":"bd-d4i","type":"blocks","created_at":"2025-11-11T23:29:29.872081-08:00","created_by":"daemon"}]} +{"id":"bd-tne","content_hash":"2a6596980450714800bddc88e106026743a1a131e96f09198eb7dc2a16d75ca4","title":"Add Claude setup tip with dynamic priority","description":"Add a predefined tip that suggests running `bd setup claude` when Claude Code is detected but not configured. This tip should have higher priority (shown more frequently) until the setup is complete.","design":"## Implementation\n\nAdd to tip registry in `cmd/bd/tips.go`:\n\n```go\n{\n ID: \"claude_setup\",\n Condition: func() bool {\n return isClaudeDetected() \u0026\u0026 !isClaudeSetupComplete()\n },\n Message: \"Run 'bd setup claude' to enable automatic context recovery in Claude Code\",\n Frequency: 24 * time.Hour, // Daily minimum gap\n Priority: 100, // Highest priority\n Probability: 0.6, // 60% chance when eligible\n}\n```\n\n## Detection Logic\n\n```go\nfunc isClaudeDetected() bool {\n // Check environment variables\n if os.Getenv(\"CLAUDE_CODE\") != \"\" || os.Getenv(\"ANTHROPIC_CLI\") != \"\" {\n return true\n }\n // Check if .claude/ directory exists\n if _, err := os.Stat(filepath.Join(os.Getenv(\"HOME\"), \".claude\")); err == nil {\n return true\n }\n return false\n}\n\nfunc isClaudeSetupComplete() bool {\n // Check for global installation\n home, err := os.UserHomeDir()\n if err == nil {\n _, err1 := os.Stat(filepath.Join(home, \".claude/commands/prime_beads.md\"))\n _, err2 := os.Stat(filepath.Join(home, \".claude/hooks/sessionstart\"))\n if err1 == nil \u0026\u0026 err2 == nil {\n return true // Global hooks installed\n }\n }\n \n // Check for project installation\n _, err1 := os.Stat(\".claude/commands/prime_beads.md\")\n _, err2 := os.Stat(\".claude/hooks/sessionstart\")\n return err1 == nil \u0026\u0026 err2 == nil\n}\n```\n\n## Priority and Probability Behavior\n\n**Why 60% probability?**\n- Important message (priority 100) but not critical\n- Daily frequency + 60% = shows ~4 times per week\n- Avoids spam while staying visible\n- Balances persistence with user experience\n\n**Comparison with other probabilities:**\n- 100% probability: Shows EVERY day (annoying)\n- 80% probability: Shows ~6 days per week (too frequent)\n- 60% probability: Shows ~4 days per week (balanced)\n- 40% probability: Shows ~3 days per week (might be missed)\n\n**Auto-stops when setup complete:**\n- Condition becomes false after `bd setup claude`\n- No manual dismissal needed\n- Tip naturally disappears from rotation","acceptance_criteria":"- Claude setup tip added to registry\n- isClaudeDetected() checks environment and filesystem\n- isClaudeSetupComplete() verifies hook installation\n- Tip shows daily until setup complete\n- Tip stops showing after setup\n- Unit tests for detection functions","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-11T23:29:29.871324-08:00","updated_at":"2025-11-24T11:28:21.371877-08:00","closed_at":"2025-11-24T11:28:21.371877-08:00","source_repo":".","dependencies":[{"issue_id":"bd-tne","depends_on_id":"bd-d4i","type":"blocks","created_at":"2025-11-11T23:29:29.872081-08:00","created_by":"daemon"}]} {"id":"bd-tru","content_hash":"0de12031088519a3dcd27968d6bf17eb3a92d1853264e5a0dceef3310b3a2b04","title":"Update documentation for bd prime and Claude integration","description":"Update AGENTS.md, README.md, and QUICKSTART.md to document the new `bd prime` command, `bd setup claude` command, and tip system.","design":"## Documentation Updates\n\n### AGENTS.md\nAdd new section \"Context Recovery\":\n```markdown\n## Context Recovery\n\n### The Problem\nAfter context compaction or clearing conversation, AI agents may forget to use Beads and revert to markdown TODOs. Claude Code hooks solve this.\n\n### bd prime Command\nThe `bd prime` command outputs essential Beads workflow context in AI-optimized markdown format (~1-2k tokens).\n\n**When to use:**\n- After context compaction\n- After clearing conversation\n- Starting new session\n- When agent seems to forget bd workflow\n- Manual context refresh\n\n**Usage:**\n```bash\nbd prime # Output workflow context\n```\n\n### Automatic Integration (Recommended)\n\nRun `bd setup claude` to install hooks that auto-refresh bd context:\n- **SessionStart hook**: Loads context in new sessions\n- **PreCompact hook**: Refreshes context before compaction (survives better)\n- **Works with MCP**: Hooks complement MCP server (not replace)\n- **Works without MCP**: bd prime provides workflow via CLI\n\n**Why hooks matter even with MCP:**\n- MCP provides native tools, but agent may forget to use them\n- Hooks keep \"use bd, not markdown\" fresh in context\n- PreCompact refreshes workflow before compaction\n\n### MCP Server vs bd prime\n\n**Not an either/or choice** - they solve different problems:\n\n| Aspect | MCP Server | bd prime | Both |\n|--------|-----------|----------|------|\n| **Purpose** | Native bd tools | Workflow context | Best of both |\n| **Tokens** | 10.5k always loaded | ~1-2k when called | 10.5k + ~2k |\n| **Tool access** | Function calls | CLI via Bash | Function calls |\n| **Context memory** | Can fade after compaction | Hooks keep fresh | Hooks + tools |\n| **Recommended** | Heavy usage | Token optimization | Best experience |\n\n**Setup options:**\n```bash\nbd setup claude # Install hooks (works with or without MCP)\nbd setup claude --local # Per-project only\nbd setup claude --remove # Remove hooks\n```\n```\n\n### README.md\nAdd to \"Getting Started\" section:\n```markdown\n### AI Agent Integration\n\n**Claude Code users:** Run `bd setup claude` to install automatic context recovery hooks.\n\nHooks work with both MCP server and CLI approaches, preventing agents from forgetting bd workflow after compaction.\n\n**MCP vs bd prime:**\n- **With MCP server**: Hooks keep agent using bd tools (prevents markdown TODO reversion)\n- **Without MCP server**: Hooks provide workflow context via `bd prime` (~1-2k tokens)\n```\n\n### QUICKSTART.md\nAdd section on agent integration:\n```markdown\n## For AI Agents\n\n**Context loading:**\n```bash\nbd prime # Load workflow context (~1-2k tokens)\n```\n\n**Automatic setup (Claude Code):**\n```bash\nbd setup claude # Install hooks for automatic context recovery\n```\n\nHooks prevent agents from forgetting bd workflow after compaction.\n```","acceptance_criteria":"- AGENTS.md has Context Recovery section\n- README.md mentions bd setup claude\n- QUICKSTART.md mentions bd prime\n- Examples show when to use bd prime vs MCP\n- Clear comparison of trade-offs","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-11T23:30:22.77349-08:00","updated_at":"2025-11-11T23:45:23.242658-08:00","source_repo":"."} {"id":"bd-v0y","content_hash":"72d91011884bc9e20ac1c18b67f66489dcf49f3fec6da4d9b4dbbe1832d8b72f","title":"Critical: bd sync overwrites git-pulled JSONL instead of importing it","description":"## Problem\n\nWhen a user runs `git pull` (which updates .beads/beads.jsonl) followed by `bd sync`, the sync command fails to detect that the JSONL changed and exports the local database, **overwriting the pulled JSONL** instead of importing it first.\n\nThis causes cleaned-up issues (removed via `bd cleanup` in another repo) to be resurrected and pushed back to the remote.\n\n## Root Cause\n\n`hasJSONLChanged()` in cmd/bd/integrity.go incorrectly returns FALSE when it should return TRUE after git pull updates the JSONL file.\n\nThe function has two code paths:\n1. **Fast-path (line 128-129)**: If mtime unchanged, return false\n2. **Slow-path (line 135-151)**: Compute hash and compare\n\nSuspected issue: The mtime-based fast-path may incorrectly return false if:\n- Git preserves mtime when checking out files, OR\n- The mtime check logic has a race condition, OR\n- The stored last_import_mtime is stale/incorrect\n\n## Reproduction\n\n**Setup:**\n1. Repo A: Has 686 issues (including 630 closed)\n2. Run `bd cleanup -f` in Repo A (removes 630 closed issues → 56 issues remain)\n3. Push to remote\n4. Repo B: Still has 686 issues in database\n\n**Trigger:**\n1. In Repo B: `git pull` (JSONL now has 56 issues)\n2. In Repo B: `bd sync`\n\n**Expected behavior:**\n- hasJSONLChanged() returns TRUE\n- Auto-imports 56-issue JSONL\n- Database updated to 56 issues\n- Exports (no-op, DB and JSONL match)\n- Pushes\n\n**Actual behavior:**\n- hasJSONLChanged() returns FALSE ❌\n- Skips auto-import\n- Exports 686 issues from DB to JSONL\n- Commits and pushes the 686-issue JSONL\n- **Undoes the cleanup from Repo A**\n\n## Evidence\n\nFrom actual session (2025-11-23):\n- Commit 8bc8611 in cino/beads shows: `+640, -14 lines` after bd sync\n- No \"Auto-import complete\" message in output\n- Database had 686 issues, JSONL was pulled with 56 issues\n- Sync exported DB → JSONL instead of importing JSONL → DB\n\n## Impact\n\n**Critical:** This breaks multi-device sync and causes:\n- Lost work (cleanup operations undone)\n- Data resurrection (deleted issues come back)\n- Confusing merge conflicts\n- User trust issues (\"Why does sync keep bringing back deleted issues?\")\n\n## Proposed Solutions\n\n### Option 1: Remove mtime fast-path (safest)\nAlways compute and compare hashes. Eliminates false negatives from mtime issues.\n\n**Pros:** Guaranteed correctness\n**Cons:** Slightly slower (hash computation on every sync)\n\n### Option 2: Fix mtime comparison logic\nInvestigate why mtime check fails and fix it properly.\n\n**Areas to check:**\n- Does git preserve mtime on checkout? (may vary by git version/config)\n- Is last_import_mtime updated correctly after all operations?\n- Race condition between git pull and mtime check?\n\n### Option 3: Add hash-based validation\nKeep mtime fast-path but add hash validation as backup:\n- If mtime unchanged, still spot-check hash occasionally (e.g., 10% of the time)\n- Log warnings when mtime and hash disagree\n- This would catch the bug while maintaining performance\n\n### Option 4: Add `--force-import` flag to bd sync\nShort-term workaround: Allow users to force import even if hasJSONLChanged() returns false.\n\n## Workaround\n\nUntil fixed, after `git pull`:\n```bash\nbd import --force # Force import the pulled JSONL\nbd sync # Then sync normally\n```\n\nOr manually run bd cleanup in affected repos.\n\n## Files\n\n- cmd/bd/integrity.go:101-152 (hasJSONLChanged function)\n- cmd/bd/sync.go:130-143 (auto-import logic)\n- cmd/bd/autoflush.go:698 (export updates last_import_hash)\n\n## Related Issues\n\n- bd-77gm: Import reports misleading counts\n- bd-khnb: Content-based staleness detection (original implementation of hash checking)\n","notes":"## Fix Implemented (Option 1)\n\n**Root Cause Confirmed:**\nGit does NOT update mtime when checking out files. Testing confirmed that after `git checkout HEAD~1`, the file mtime remains unchanged even though content differs.\n\n**Solution:**\nRemoved the mtime-based fast-path in `hasJSONLChanged()`. Now always computes content hash for comparison.\n\n**Changes Made:**\n1. **cmd/bd/integrity.go:107-116** - Removed mtime fast-path, always compute hash\n2. **cmd/bd/sync.go:752** - Removed mtime storage after import\n3. **cmd/bd/import.go:340** - Removed mtime storage after import\n4. **cmd/bd/daemon_sync.go:280-301** - Removed mtime storage and updated comments\n5. **cmd/bd/daemon_sync_test.go:479,607,628** - Removed mtime assertions from tests\n\n**Performance Impact:**\nMinimal. Hash computation takes ~10-50ms even for large databases, which is acceptable for sync operations.\n\n**Testing:**\n- All existing tests pass\n- Test \"mtime changed but content same - git operation scenario\" verifies the fix\n- Full test suite passes (cmd/bd and all internal packages)\n\n**Verification:**\nThe fix ensures that after `git pull` updates the JSONL:\n1. `hasJSONLChanged()` returns TRUE (content hash differs)\n2. Auto-import runs and updates database\n3. No data loss or resurrection of deleted issues","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-23T22:24:53.69901-08:00","updated_at":"2025-11-23T23:52:29.997682-08:00","closed_at":"2025-11-23T22:50:12.611126-08:00","source_repo":"."} {"id":"bd-vfe","content_hash":"c58e7567c9e5d4b789a593e7b5ca40ab109e9dc7b98275262aae09bd1b65650f","title":"Apply wrapDBError to config.go","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-24T00:53:12.771275-08:00","updated_at":"2025-11-24T00:54:15.676618-08:00","closed_at":"2025-11-24T00:54:15.676618-08:00","source_repo":"."} diff --git a/cmd/bd/tips.go b/cmd/bd/tips.go index 6267ef8c..179d5a35 100644 --- a/cmd/bd/tips.go +++ b/cmd/bd/tips.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "os" + "path/filepath" "sort" "strconv" "sync" @@ -152,3 +153,156 @@ func recordTipShown(store storage.Storage, tipID string) { value := time.Now().Format(time.RFC3339) _ = store.SetMetadata(context.Background(), key, value) } + +// InjectTip adds a dynamic tip to the registry at runtime. +// This enables tips to be programmatically added based on detected conditions. +// +// Parameters: +// - id: Unique identifier for the tip (used for frequency tracking) +// - message: The tip message to display to the user +// - priority: Higher values = shown first when eligible (e.g., 100 for critical, 30 for suggestions) +// - frequency: Minimum time between showings (e.g., 24*time.Hour for daily) +// - probability: Chance of showing when eligible (0.0 to 1.0) +// - condition: Function that returns true when tip should be eligible +// +// Example usage: +// +// // Critical security update - always show +// InjectTip("security_update", "CRITICAL: Security update available!", 100, 0, 1.0, func() bool { return true }) +// +// // New version available - frequent but not always +// InjectTip("upgrade_available", "New version available", 90, 7*24*time.Hour, 0.8, func() bool { return true }) +// +// // Feature suggestion - occasional +// InjectTip("try_filters", "Try using filters", 50, 14*24*time.Hour, 0.4, func() bool { return true }) +func InjectTip(id, message string, priority int, frequency time.Duration, probability float64, condition func() bool) { + tipsMutex.Lock() + defer tipsMutex.Unlock() + + // Check if tip with this ID already exists - update it if so + for i, tip := range tips { + if tip.ID == id { + tips[i] = Tip{ + ID: id, + Condition: condition, + Message: message, + Frequency: frequency, + Priority: priority, + Probability: probability, + } + return + } + } + + // Add new tip + tips = append(tips, Tip{ + ID: id, + Condition: condition, + Message: message, + Frequency: frequency, + Priority: priority, + Probability: probability, + }) +} + +// RemoveTip removes a tip from the registry by ID. +// This is useful for removing dynamically injected tips when they are no longer relevant. +// It is safe to call with a non-existent ID (no-op). +func RemoveTip(id string) { + tipsMutex.Lock() + defer tipsMutex.Unlock() + + for i, tip := range tips { + if tip.ID == id { + tips = append(tips[:i], tips[i+1:]...) + return + } + } +} + +// isClaudeDetected checks if the user is running within a Claude Code environment. +// Detection methods: +// - CLAUDE_CODE environment variable (set by Claude Code) +// - ANTHROPIC_CLI environment variable +// - Presence of ~/.claude directory (Claude Code config) +func isClaudeDetected() bool { + // Check environment variables set by Claude Code + if os.Getenv("CLAUDE_CODE") != "" || os.Getenv("ANTHROPIC_CLI") != "" { + return true + } + + // Check if ~/.claude directory exists (Claude Code stores config here) + home, err := os.UserHomeDir() + if err != nil { + return false + } + if _, err := os.Stat(filepath.Join(home, ".claude")); err == nil { + return true + } + + return false +} + +// isClaudeSetupComplete checks if the beads Claude integration is properly configured. +// Checks for either global or project-level installation of the beads hooks. +func isClaudeSetupComplete() bool { + // Check for global installation + home, err := os.UserHomeDir() + if err == nil { + commandFile := filepath.Join(home, ".claude", "commands", "prime_beads.md") + hooksDir := filepath.Join(home, ".claude", "hooks") + + // Check for prime_beads command + if _, err := os.Stat(commandFile); err == nil { + // Check for sessionstart hook (could be a file or directory) + hookPath := filepath.Join(hooksDir, "sessionstart") + if _, err := os.Stat(hookPath); err == nil { + return true // Global hooks installed + } + // Also check PreToolUse hook which is used by beads + preToolUsePath := filepath.Join(hooksDir, "PreToolUse") + if _, err := os.Stat(preToolUsePath); err == nil { + return true // Global hooks installed + } + } + } + + // Check for project-level installation + commandFile := ".claude/commands/prime_beads.md" + hooksDir := ".claude/hooks" + + if _, err := os.Stat(commandFile); err == nil { + hookPath := filepath.Join(hooksDir, "sessionstart") + if _, err := os.Stat(hookPath); err == nil { + return true // Project hooks installed + } + preToolUsePath := filepath.Join(hooksDir, "PreToolUse") + if _, err := os.Stat(preToolUsePath); err == nil { + return true // Project hooks installed + } + } + + return false +} + +// initDefaultTips registers the built-in tips. +// Called during initialization to populate the tip registry. +func initDefaultTips() { + // Claude setup tip - suggest running bd setup claude when Claude is detected + // but the integration is not configured + InjectTip( + "claude_setup", + "Run 'bd setup claude' to enable automatic context recovery in Claude Code", + 100, // Highest priority - this is important for Claude users + 24*time.Hour, // Daily minimum gap + 0.6, // 60% chance when eligible (~4 times per week) + func() bool { + return isClaudeDetected() && !isClaudeSetupComplete() + }, + ) +} + +// init initializes the tip system with default tips +func init() { + initDefaultTips() +} diff --git a/cmd/bd/tips_test.go b/cmd/bd/tips_test.go index 2f2fadeb..ff5d3d3c 100644 --- a/cmd/bd/tips_test.go +++ b/cmd/bd/tips_test.go @@ -272,3 +272,390 @@ func TestTipFrequency(t *testing.T) { t.Error("Expected tip to be selected after frequency window passed") } } + +func TestInjectTip(t *testing.T) { + // Reset tip registry for testing + tipsMutex.Lock() + tips = []Tip{} + tipsMutex.Unlock() + + store := memory.New("") + + // Set deterministic seed for testing + os.Setenv("BEADS_TIP_SEED", "11111") + defer os.Unsetenv("BEADS_TIP_SEED") + tipRandOnce = sync.Once{} + initTipRand() + + // Test 1: Inject a new tip + InjectTip( + "injected_tip_1", + "This is an injected tip", + 80, + 1*time.Hour, + 1.0, // Always show when eligible + func() bool { return true }, + ) + + tipsMutex.RLock() + tipCount := len(tips) + tipsMutex.RUnlock() + + if tipCount != 1 { + t.Errorf("Expected 1 tip, got %d", tipCount) + } + + // Verify tip can be selected + tip := selectNextTip(store) + if tip == nil { + t.Fatal("Expected injected tip to be selected") + } + if tip.ID != "injected_tip_1" { + t.Errorf("Expected tip ID 'injected_tip_1', got %q", tip.ID) + } + if tip.Message != "This is an injected tip" { + t.Errorf("Expected message 'This is an injected tip', got %q", tip.Message) + } + if tip.Priority != 80 { + t.Errorf("Expected priority 80, got %d", tip.Priority) + } + + // Test 2: Inject another tip and verify priority ordering + InjectTip( + "injected_tip_2", + "Higher priority tip", + 100, + 1*time.Hour, + 1.0, + func() bool { return true }, + ) + + tipsMutex.RLock() + tipCount = len(tips) + tipsMutex.RUnlock() + + if tipCount != 2 { + t.Errorf("Expected 2 tips, got %d", tipCount) + } + + // Higher priority tip should be selected first + tip = selectNextTip(store) + if tip == nil { + t.Fatal("Expected tip to be selected") + } + if tip.ID != "injected_tip_2" { + t.Errorf("Expected higher priority tip 'injected_tip_2' to be selected first, got %q", tip.ID) + } + + // Test 3: Update existing tip (same ID) + InjectTip( + "injected_tip_1", + "Updated message", + 50, // Lower priority now + 2*time.Hour, + 0.5, + func() bool { return true }, + ) + + tipsMutex.RLock() + tipCount = len(tips) + var updatedTip *Tip + for i := range tips { + if tips[i].ID == "injected_tip_1" { + updatedTip = &tips[i] + break + } + } + tipsMutex.RUnlock() + + if tipCount != 2 { + t.Errorf("Expected 2 tips after update (no duplicate), got %d", tipCount) + } + if updatedTip == nil { + t.Fatal("Expected to find updated tip") + } + if updatedTip.Message != "Updated message" { + t.Errorf("Expected updated message, got %q", updatedTip.Message) + } + if updatedTip.Priority != 50 { + t.Errorf("Expected updated priority 50, got %d", updatedTip.Priority) + } + if updatedTip.Frequency != 2*time.Hour { + t.Errorf("Expected updated frequency 2h, got %v", updatedTip.Frequency) + } + if updatedTip.Probability != 0.5 { + t.Errorf("Expected updated probability 0.5, got %v", updatedTip.Probability) + } +} + +func TestRemoveTip(t *testing.T) { + // Reset tip registry for testing + tipsMutex.Lock() + tips = []Tip{} + tipsMutex.Unlock() + + // Add some tips + InjectTip("tip_a", "Tip A", 100, time.Hour, 1.0, func() bool { return true }) + InjectTip("tip_b", "Tip B", 90, time.Hour, 1.0, func() bool { return true }) + InjectTip("tip_c", "Tip C", 80, time.Hour, 1.0, func() bool { return true }) + + tipsMutex.RLock() + tipCount := len(tips) + tipsMutex.RUnlock() + + if tipCount != 3 { + t.Fatalf("Expected 3 tips, got %d", tipCount) + } + + // Test 1: Remove middle tip + RemoveTip("tip_b") + + tipsMutex.RLock() + tipCount = len(tips) + var foundB bool + for _, tip := range tips { + if tip.ID == "tip_b" { + foundB = true + break + } + } + tipsMutex.RUnlock() + + if tipCount != 2 { + t.Errorf("Expected 2 tips after removal, got %d", tipCount) + } + if foundB { + t.Error("Expected tip_b to be removed") + } + + // Test 2: Remove non-existent tip (should be no-op) + RemoveTip("tip_nonexistent") + + tipsMutex.RLock() + tipCount = len(tips) + tipsMutex.RUnlock() + + if tipCount != 2 { + t.Errorf("Expected 2 tips after no-op removal, got %d", tipCount) + } + + // Test 3: Remove remaining tips + RemoveTip("tip_a") + RemoveTip("tip_c") + + tipsMutex.RLock() + tipCount = len(tips) + tipsMutex.RUnlock() + + if tipCount != 0 { + t.Errorf("Expected 0 tips after removing all, got %d", tipCount) + } +} + +func TestInjectTipConcurrency(t *testing.T) { + // Reset tip registry for testing + tipsMutex.Lock() + tips = []Tip{} + tipsMutex.Unlock() + + // Test thread safety by injecting and removing tips concurrently + var wg sync.WaitGroup + const numGoroutines = 50 + + // Inject tips concurrently + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + tipID := "concurrent_tip_" + string(rune('a'+id%26)) + InjectTip(tipID, "Message", 50, time.Hour, 0.5, func() bool { return true }) + }(i) + } + wg.Wait() + + // Remove some tips concurrently + for i := 0; i < numGoroutines/2; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + tipID := "concurrent_tip_" + string(rune('a'+id%26)) + RemoveTip(tipID) + }(i) + } + wg.Wait() + + // If we got here without panics or deadlocks, the test passes + // Just verify we can still access the tips + tipsMutex.RLock() + _ = len(tips) + tipsMutex.RUnlock() +} + +func TestIsClaudeDetected(t *testing.T) { + // Save original env vars + origClaudeCode := os.Getenv("CLAUDE_CODE") + origAnthropicCli := os.Getenv("ANTHROPIC_CLI") + defer func() { + os.Setenv("CLAUDE_CODE", origClaudeCode) + os.Setenv("ANTHROPIC_CLI", origAnthropicCli) + }() + + // Clear env vars for clean testing + os.Unsetenv("CLAUDE_CODE") + os.Unsetenv("ANTHROPIC_CLI") + + // Test 1: Detection via CLAUDE_CODE env var + os.Setenv("CLAUDE_CODE", "1") + if !isClaudeDetected() { + t.Error("Expected Claude detected with CLAUDE_CODE env var") + } + os.Unsetenv("CLAUDE_CODE") + + // Test 2: Detection via ANTHROPIC_CLI env var + os.Setenv("ANTHROPIC_CLI", "1") + if !isClaudeDetected() { + t.Error("Expected Claude detected with ANTHROPIC_CLI env var") + } + os.Unsetenv("ANTHROPIC_CLI") + + // Test 3: Detection via ~/.claude directory + // This depends on the test environment - if ~/.claude exists, it should detect + // We can't easily control this without modifying the filesystem + home, err := os.UserHomeDir() + if err == nil { + claudeDir := home + "/.claude" + if _, err := os.Stat(claudeDir); err == nil { + // ~/.claude exists, should detect + if !isClaudeDetected() { + t.Error("Expected Claude detected with ~/.claude directory present") + } + } + } +} + +func TestIsClaudeSetupComplete(t *testing.T) { + // This test checks the logic without modifying the filesystem + // The actual detection depends on the presence of files + + // Test that the function returns a boolean and doesn't panic + result := isClaudeSetupComplete() + // Just verify it returns without error + _ = result + + // If running in an environment with Claude setup, verify detection + // We'll check both global and project paths exist + home, err := os.UserHomeDir() + if err != nil { + return // Skip if we can't get home dir + } + + globalCommand := home + "/.claude/commands/prime_beads.md" + globalHooksSession := home + "/.claude/hooks/sessionstart" + globalHooksPreTool := home + "/.claude/hooks/PreToolUse" + + // Check if global setup exists + if _, err := os.Stat(globalCommand); err == nil { + if _, err := os.Stat(globalHooksSession); err == nil { + if !isClaudeSetupComplete() { + t.Error("Expected Claude setup complete with global hooks (sessionstart)") + } + } else if _, err := os.Stat(globalHooksPreTool); err == nil { + if !isClaudeSetupComplete() { + t.Error("Expected Claude setup complete with global hooks (PreToolUse)") + } + } + } + + // Check project-level setup + projectCommand := ".claude/commands/prime_beads.md" + projectHooksSession := ".claude/hooks/sessionstart" + projectHooksPreTool := ".claude/hooks/PreToolUse" + + if _, err := os.Stat(projectCommand); err == nil { + if _, err := os.Stat(projectHooksSession); err == nil { + if !isClaudeSetupComplete() { + t.Error("Expected Claude setup complete with project hooks (sessionstart)") + } + } else if _, err := os.Stat(projectHooksPreTool); err == nil { + if !isClaudeSetupComplete() { + t.Error("Expected Claude setup complete with project hooks (PreToolUse)") + } + } + } +} + +func TestClaudeSetupTipRegistered(t *testing.T) { + // Reset tip registry with fresh default tips + tipsMutex.Lock() + tips = []Tip{} + tipsMutex.Unlock() + initDefaultTips() + + // Verify that the claude_setup tip is registered + tipsMutex.RLock() + defer tipsMutex.RUnlock() + + var found bool + for _, tip := range tips { + if tip.ID == "claude_setup" { + found = true + // Verify tip properties + if tip.Priority != 100 { + t.Errorf("Expected claude_setup priority 100, got %d", tip.Priority) + } + if tip.Frequency != 24*time.Hour { + t.Errorf("Expected claude_setup frequency 24h, got %v", tip.Frequency) + } + if tip.Probability != 0.6 { + t.Errorf("Expected claude_setup probability 0.6, got %v", tip.Probability) + } + break + } + } + + if !found { + t.Error("Expected claude_setup tip to be registered") + } +} + +func TestClaudeSetupTipCondition(t *testing.T) { + // Save original env vars + origClaudeCode := os.Getenv("CLAUDE_CODE") + defer os.Setenv("CLAUDE_CODE", origClaudeCode) + + // Reset tip registry with fresh default tips + tipsMutex.Lock() + tips = []Tip{} + tipsMutex.Unlock() + initDefaultTips() + + // Find the claude_setup tip + tipsMutex.RLock() + var claudeTip *Tip + for i := range tips { + if tips[i].ID == "claude_setup" { + claudeTip = &tips[i] + break + } + } + tipsMutex.RUnlock() + + if claudeTip == nil { + t.Fatal("claude_setup tip not found") + } + + // Test: When Claude is not detected, condition should be false + os.Unsetenv("CLAUDE_CODE") + os.Unsetenv("ANTHROPIC_CLI") + // Note: This test may pass or fail depending on ~/.claude existence + // The important thing is that the condition function executes without error + _ = claudeTip.Condition() + + // Test: When Claude is detected but setup might be complete + // Set env var to simulate Claude environment + os.Setenv("CLAUDE_CODE", "1") + conditionResult := claudeTip.Condition() + // If setup is complete, should be false; if not complete, should be true + // Just verify it returns a valid boolean + _ = conditionResult +}