diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1f958048..95b48d7b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -13,6 +13,7 @@ {"id":"bd-0zp7","title":"Add missing hook calls in mail reply and ack","description":"The mail commands are missing hook calls:\n\n1. runMailReply (mail.go:525-672) creates a message but doesn't call hookRunner.Run(hooks.EventMessage, ...) after creating the reply in direct mode (around line 640)\n\n2. runMailAck (mail.go:432-523) closes messages but doesn't call hookRunner.Run(hooks.EventClose, ...) after closing each message (around line 487 for daemon mode, 493 for direct mode)\n\nThis means GGT hooks won't fire for replies or message acknowledgments.","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T20:52:53.069412-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-14ie","title":"Work on beads-2vn: Add simple built-in beads viewer (GH#6...","description":"Work on beads-2vn: Add simple built-in beads viewer (GH#654). Add bd list --pretty with --watch flag, tree view with priority/status symbols. When done, submit MR (not PR) to integration branch for Refinery.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:56:47.305831-08:00","updated_at":"2025-12-19T23:28:32.429492-08:00","closed_at":"2025-12-19T23:23:13.928323-08:00","close_reason":"Implemented --pretty flag with tree view and symbols. Tests pass."} {"id":"bd-14v0","title":"Add Windows code signing for bd.exe releases","description":"## Context\n\nGo binaries (including bd.exe) are commonly flagged by antivirus software as false positives due to heuristic detection. See docs/ANTIVIRUS.md for full details.\n\n## Problem\n\nKaspersky and other AV software flag bd.exe as PDM:Trojan.Win32.Generic, causing it to be quarantined or deleted.\n\n## Solution\n\nImplement code signing for Windows releases using:\n1. An EV (Extended Validation) code certificate\n2. Integration with GoReleaser to sign Windows binaries during release\n\n## Benefits\n\n- Reduces false positive rates over time as the certificate builds reputation\n- Provides tamper verification for users\n- Improves SmartScreen trust rating on Windows\n- Professional appearance for enterprise users\n\n## Implementation Steps\n\n1. Acquire EV code signing certificate (annual cost ~$300-500)\n2. Set up signtool or osslsigncode in release pipeline\n3. Update .goreleaser.yml to sign Windows binaries\n4. Update checksums to include signed binary hashes\n5. Document signing verification in ANTIVIRUS.md\n\n## References\n\n- docs/ANTIVIRUS.md - Current documentation\n- bd-t4u1 - Original Kaspersky false positive report\n- https://github.com/golang/go/issues/16292 - Go project discussion","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-23T23:46:48.459177-08:00","updated_at":"2025-12-23T23:54:41.912141-08:00","closed_at":"2025-12-23T23:54:41.912141-08:00","close_reason":"Implemented Windows code signing infrastructure. Added signing script, GoReleaser hook, updated release workflow and documentation. Signing is gracefully degraded when certificate secrets are not configured - releases continue as unsigned. Certificate acquisition (EV cert) is still required to actually enable signing.","dependencies":[{"issue_id":"bd-14v0","depends_on_id":"bd-t4u1","type":"discovered-from","created_at":"2025-12-23T23:47:02.024159-08:00","created_by":"daemon"}]} +{"id":"bd-1ban","title":"Test actor direct","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-26T20:46:02.423367-08:00","updated_at":"2025-12-26T20:46:02.423367-08:00"} {"id":"bd-1dez","title":"Mol Mall: Formula marketplace using GitHub as backend","description":"Create a marketplace for sharing molecule formulas using GitHub repos as the hosting backend.\n\n## Architecture Update (Dec 2025)\n\n**Formulas are the sharing layer.** With ephemeral protos (bd-rciw), the architecture is:\n\n```\nFormulas ──cook──→ [ephemeral proto] ──pour/wisp──→ Mol/Wisp\n ↑ │\n └────────────────── distill ─────────────────────────┘\n```\n\n- **Formulas**: JSON source files (.formula.json) - the thing you share\n- **Protos**: Transient compilation artifacts - auto-deleted after use\n- **Mols/Wisps**: Execution instances - not shared directly\n\n**Key operations:**\n- `bd distill \u003cmol-id\u003e` → Extract formula from completed work\n- `bd mol publish \u003cformula\u003e` → Share to GitHub\n- `bd mol install \u003curl\u003e` → Fetch from GitHub\n- `bd pour \u003cformula\u003e` → Cook and spawn (proto is ephemeral)\n\n## Why GitHub?\n\nGitHub solves multiple problems at once:\n- **Hosting**: Raw file URLs for formula.json\n- **Versioning**: Git tags (v1.0.0, v1.2.0)\n- **Auth**: GitHub tokens for private formulas\n- **Discovery**: GitHub search, topics, stars\n- **Collaboration**: PRs for contributions, issues for bugs\n- **Organizations**: Natural scoping (@anthropic/, @gastown/)\n\n## URL Scheme\n\n```bash\n# Direct GitHub URL\nbd mol install github.com/anthropics/mol-code-review\n\n# With version tag\nbd mol install github.com/anthropics/mol-code-review@v1.2.0\n\n# Shorthand (via registry lookup)\nbd mol install @anthropic/mol-code-review\n```\n\n## Architecture\n\nEach formula lives in its own repo (like Go modules):\n```\ngithub.com/anthropics/mol-code-review/\n├── formula.json # The formula\n├── README.md # Documentation\n└── CHANGELOG.md # Version history\n```\n\n## ID Namespace\n\n| Entity | ID Format | Example |\n|--------|-----------|---------|\n| Formula (GitHub) | `github.com/org/repo` | `github.com/anthropics/mol-code-review` |\n| Installed formula | `mol-name` | `mol-code-review` |\n| Poured instance | `\u003cdb\u003e-mol-xxx` | `bd-mol-b8c` |","notes":"Deferred - focusing on Christmas launch first","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-25T12:05:17.666574-08:00","updated_at":"2025-12-25T21:53:13.415431-08:00","closed_at":"2025-12-25T21:53:13.415431-08:00","close_reason":"Migrated to gastown rig as gt-uzf2l (Mol Mall is Gas Town infrastructure)"} {"id":"bd-1dez.1","title":"bd distill: Extract formula from mol/epic","description":"Extract a formula from completed work (mol, wisp, or epic).\n\n**Key change**: Distill works on execution artifacts (mols/wisps/epics), not protos.\nProtos are ephemeral - they don't persist. Distillation extracts patterns from\nactual executed work.\n\n## Usage\n```bash\nbd distill bd-mol-xyz -o my-workflow.formula.json\nbd distill bd-epic-abc -o feature-workflow.formula.json\n```\n\n## Use Cases\n- **Emergent patterns**: Structured work manually, want to templatize it\n- **Modified execution**: Poured a formula, added custom steps, want to capture\n- **Learning from success**: Extract what made a complex mol succeed\n\n## Implementation\n1. Load mol/wisp/epic subgraph (root + all children)\n2. Convert to formula JSON structure\n3. Extract variables from patterns (titles, descriptions)\n4. Generate step IDs from issue titles (slugify)\n5. Write .formula.json file\n\n## Output Format\n```json\n{\n \"formula\": \"my-workflow\",\n \"description\": \"...\",\n \"version\": 1,\n \"vars\": { ... },\n \"steps\": [ ... ]\n}\n```\n\n## Architecture Note\nThis closes the formula lifecycle loop:\n Formulas ──cook──→ Mols ──distill──→ Formulas\n\nAll sharing happens via formulas. Mols contain execution context and aren't shared.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-25T12:05:47.045105-08:00","updated_at":"2025-12-25T18:54:39.967765-08:00","closed_at":"2025-12-25T18:54:39.967765-08:00","close_reason":"Command already implemented; updated help text, added daemon support, and -o shorthand","dependencies":[{"issue_id":"bd-1dez.1","depends_on_id":"bd-1dez","type":"parent-child","created_at":"2025-12-25T12:05:47.045596-08:00","created_by":"daemon"}]} {"id":"bd-1dez.2","title":"bd formula add: Import formula to local catalog","description":"Import a formula file to the local catalog (search path).\n\n**Replaces**: \"bd mol promote\" (proto-to-proto concept is obsolete with ephemeral protos)\n\n## Usage\n```bash\n# Add a formula file to project catalog\nbd formula add my-workflow.formula.json\n\n# Add to user-level catalog\nbd formula add my-workflow.formula.json --scope user\n\n# Add from URL\nbd formula add https://example.com/workflow.formula.json\n```\n\n## Implementation\n1. Parse the formula file (validate JSON structure)\n2. Determine target directory based on scope:\n - project: .beads/formulas/\n - user: ~/.beads/formulas/\n - town: ~/gt/.beads/formulas/\n3. Copy/download formula to target\n4. Verify it is loadable: bd formula show \u003cname\u003e\n\n## Flags\n- `--scope \u003clevel\u003e` - Where to add (project|user|town, default: project)\n- `--name \u003cname\u003e` - Override formula name (default: from file)\n\n## Note\nThis is for manually adding formulas. For GitHub-hosted formulas, use:\n bd mol install github.com/org/formula-name","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-25T12:05:48.588283-08:00","updated_at":"2025-12-25T19:54:35.242576-08:00","closed_at":"2025-12-25T19:54:35.242576-08:00","close_reason":"Implemented bd formula add command with scope and URL support","dependencies":[{"issue_id":"bd-1dez.2","depends_on_id":"bd-1dez","type":"parent-child","created_at":"2025-12-25T12:05:48.590203-08:00","created_by":"daemon"},{"issue_id":"bd-1dez.2","depends_on_id":"bd-1dez.1","type":"blocks","created_at":"2025-12-25T12:07:06.745686-08:00","created_by":"daemon"}]} @@ -35,7 +36,9 @@ {"id":"bd-28sq.5","title":"Fix JSON errors in epic.go","description":"Replace direct stderr writes with FatalErrorRespectJSON() in epic.go.\n\nCommands affected:\n- epicStatusCmd\n- epicCloseEligibleCmd","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-25T13:32:11.12219-08:00","updated_at":"2025-12-25T13:55:39.738404-08:00","closed_at":"2025-12-25T13:55:39.738404-08:00","close_reason":"Fixed all FatalErrorRespectJSON patterns in epic.go","dependencies":[{"issue_id":"bd-28sq.5","depends_on_id":"bd-28sq","type":"parent-child","created_at":"2025-12-25T13:32:11.124147-08:00","created_by":"daemon"}]} {"id":"bd-29fb","title":"Implement bd close --continue flag","description":"Auto-advance to next step in molecule when closing an issue. Referenced by gt-um6q, gt-lz13. Needed for molecule navigation workflow.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-23T00:17:55.032875-08:00","updated_at":"2025-12-23T01:26:47.255313-08:00","closed_at":"2025-12-23T01:26:47.255313-08:00","close_reason":"Already implemented: --continue flag auto-advances to next step in molecule, --no-auto prevents auto-claiming"} {"id":"bd-2ep8","title":"Update CHANGELOG.md with release notes","description":"Add meaningful release notes to CHANGELOG.md describing what changed in 0.30.7","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:48.649053-08:00","updated_at":"2025-12-19T22:57:31.69559-08:00","closed_at":"2025-12-19T22:57:31.69559-08:00","dependencies":[{"issue_id":"bd-2ep8","depends_on_id":"bd-8pyn","type":"parent-child","created_at":"2025-12-19T22:56:48.650816-08:00","created_by":"stevey"},{"issue_id":"bd-2ep8","depends_on_id":"bd-rupw","type":"blocks","created_at":"2025-12-19T22:56:48.651136-08:00","created_by":"stevey"}]} +{"id":"bd-2fs7","title":"Move pour/ephemeral under bd mol subcommand","description":"For consistency, bd pour and bd ephemeral should become bd mol pour and bd mol ephemeral:\n\nCurrent:\n bd mol list # Available protos\n bd mol show \u003cid\u003e # Proto details\n bd pour \u003cproto\u003e # Create mol ← sticks out\n bd ephemeral \u003cproto\u003e # Create ephemeral ← sticks out \n bd mol bond \u003cproto\u003e \u003cparent\u003e # Attach to existing mol\n bd mol squash \u003cid\u003e # Condense to digest\n bd mol burn \u003cid\u003e # Discard\n\nProposed:\n bd mol list\n bd mol show \u003cid\u003e\n bd mol pour \u003cproto\u003e # Moved under mol\n bd mol ephemeral \u003cproto\u003e # Moved under mol\n bd mol bond \u003cproto\u003e \u003cparent\u003e\n bd mol squash \u003cid\u003e\n bd mol burn \u003cid\u003e\n\nAll molecule operations should be under bd mol for discoverability and consistency.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-26T23:36:23.945902-08:00","created_by":"stevey","updated_at":"2025-12-26T23:41:01.096333-08:00","closed_at":"2025-12-26T23:41:01.096333-08:00","close_reason":"Moved pour and ephemeral under bd mol subcommand for consistency","pinned":true} {"id":"bd-2l03","title":"Implement await type handlers (gh:run, gh:pr, timer, human, mail)","description":"Implement condition checking for each await type.\n\n## Handlers Needed\n- gh:run:\u003cid\u003e - Check GitHub Actions run status via gh CLI\n- gh:pr:\u003cid\u003e - Check PR merged/closed status via gh CLI \n- timer:\u003cduration\u003e - Simple elapsed time check\n- human:\u003cprompt\u003e - Check for human approval (via mail?)\n- mail:\u003cpattern\u003e - Check for mail matching pattern\n\n## Implementation Location\nThis is Deacon logic, so likely in Gas Town (gt) not beads.\n\n## Interface\n```go\ntype AwaitHandler interface {\n Check(awaitID string) (completed bool, result string, err error)\n}\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-23T11:44:38.492837-08:00","updated_at":"2025-12-23T12:19:44.283318-08:00","closed_at":"2025-12-23T12:19:44.283318-08:00","close_reason":"Moved to gastown: gt-ng6g","dependencies":[{"issue_id":"bd-2l03","depends_on_id":"bd-udsi","type":"parent-child","created_at":"2025-12-23T11:44:52.990746-08:00","created_by":"daemon"},{"issue_id":"bd-2l03","depends_on_id":"bd-is6m","type":"blocks","created_at":"2025-12-23T11:44:56.510792-08:00","created_by":"daemon"}]} +{"id":"bd-2nl","title":"Refinery Patrol","description":"Merge queue processor patrol loop with verification gates.","status":"open","priority":2,"issue_type":"molecule","created_at":"2025-12-26T21:20:47.681814-08:00","created_by":"deacon","updated_at":"2025-12-26T21:20:47.681814-08:00"} {"id":"bd-2oo","title":"Edge Schema Consolidation: Unify all edges in dependencies table","description":"Consolidate all edge types into the dependency table per decision 004.\n\n## Changes\n- Add metadata column to dependencies table\n- Add thread_id column for conversation grouping\n- Remove redundant Issue fields: replies_to, relates_to, duplicate_of, superseded_by\n- Update all code to use dependencies API\n- Migration script for existing data\n- JSONL format change (breaking)\n\nReference: ~/gt/hop/decisions/004-edge-schema-consolidation.md","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-12-18T02:01:48.785558-08:00","updated_at":"2025-12-18T02:49:10.61237-08:00","closed_at":"2025-12-18T02:49:10.61237-08:00","close_reason":"Phase 4 complete: all edge fields removed, dependencies API used exclusively"} {"id":"bd-2oo.1","title":"Add metadata and thread_id columns to dependencies table","description":"Schema changes:\n- ALTER TABLE dependencies ADD COLUMN metadata TEXT DEFAULT '{}'\n- ALTER TABLE dependencies ADD COLUMN thread_id TEXT DEFAULT ''\n- CREATE INDEX idx_dependencies_thread ON dependencies(thread_id) WHERE thread_id != ''","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-18T02:02:00.468223-08:00","updated_at":"2025-12-18T02:49:10.575133-08:00","closed_at":"2025-12-18T02:49:10.575133-08:00","close_reason":"Phase 4 complete: all edge fields removed, dependencies API used exclusively","dependencies":[{"issue_id":"bd-2oo.1","depends_on_id":"bd-2oo","type":"parent-child","created_at":"2025-12-18T02:02:00.470012-08:00","created_by":"daemon","metadata":"{}"}]} {"id":"bd-2oo.2","title":"Remove redundant edge fields from Issue struct","description":"Remove from Issue struct:\n- RepliesTo -\u003e dependency with type replies-to\n- RelatesTo -\u003e dependencies with type relates-to \n- DuplicateOf -\u003e dependency with type duplicates\n- SupersededBy -\u003e dependency with type supersedes\n\nKeep: Sender, Ephemeral (these are attributes, not relationships)","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-18T02:02:00.891206-08:00","updated_at":"2025-12-18T02:49:10.584381-08:00","closed_at":"2025-12-18T02:49:10.584381-08:00","close_reason":"Phase 4 complete: all edge fields removed, dependencies API used exclusively","dependencies":[{"issue_id":"bd-2oo.2","depends_on_id":"bd-2oo","type":"parent-child","created_at":"2025-12-18T02:02:00.891655-08:00","created_by":"daemon","metadata":"{}"}]} @@ -100,7 +103,7 @@ {"id":"bd-68bf","title":"Code review: bd mol bond implementation","description":"Review the mol bond command implementation before shipping.\n\nFocus areas:\n1. runMolBond() - polymorphic dispatch logic correctness\n2. bondProtoProto() - compound proto creation, dependency wiring\n3. bondProtoMol() / bondMolProto() - spawn and attach logic\n4. bondMolMol() - joining molecules, lineage tracking\n5. BondRef usage - is lineage tracked correctly?\n6. Error handling - are all failure modes covered?\n7. Edge cases - what could go wrong?\n\nFile: cmd/bd/mol.go (lines 485-859)\nCommit: 386b513e","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T10:13:09.425229-08:00","updated_at":"2025-12-21T11:18:14.206869-08:00","closed_at":"2025-12-21T11:18:14.206869-08:00","close_reason":"Reviewed and fixed label persistence bug","dependencies":[{"issue_id":"bd-68bf","depends_on_id":"bd-o91r","type":"discovered-from","created_at":"2025-12-21T10:13:09.426471-08:00","created_by":"daemon"}]} {"id":"bd-68e4","title":"doctor --fix should export when DB has more issues than JSONL","description":"When 'bd doctor' detects a count mismatch (DB has more issues than JSONL), it currently recommends 'bd sync --import-only', which imports JSONL into DB. But JSONL is the source of truth, not the DB.\n\n**Current behavior:**\n- Doctor detects: DB has 355 issues, JSONL has 292\n- Recommends: 'bd sync --import-only' \n- User runs it: Returns '0 created, 0 updated' (no-op, because JSONL hasn't changed)\n- User is stuck\n\n**Root cause:**\nThe doctor fix is one-directional (JSONL→DB) when it should be bidirectional. If DB has MORE issues, they haven't been exported yet - the fix should be 'bd export' (DB→JSONL), not import.\n\n**Desired fix:**\nIn fix.DBJSONLSync(), detect which has more data:\n- If DB \u003e JSONL: Run 'bd export' to sync JSONL (since DB is the working copy)\n- If JSONL \u003e DB: Run 'bd sync --import-only' to import (JSONL is source of truth)\n- If equal but timestamps differ: Detect based on file mtime\n\nThis makes 'bd doctor --fix' actually fix the problem instead of being a no-op.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-21T11:17:20.994319182-07:00","updated_at":"2025-12-21T11:23:24.38523731-07:00","closed_at":"2025-12-21T11:23:24.38523731-07:00"} {"id":"bd-6a5z","title":"Add stale molecule check to bd doctor","description":"Extend bd doctor to detect stale molecules.\n\n**New check:**\n- Name: 'Stale Molecules'\n- Category: Workflow\n- Severity: Warning (don't fail overall check)\n\n**Detection:**\nReuse logic from bd mol stale command:\n- Find mols where Completed \u003e= Total but root is open\n- Filter to orphaned (not assigned, not pinned)\n- Extra weight if blocking other work\n\n**Output:**\n```\n⚠ Stale Molecules\n Found 2 complete-but-unclosed molecules:\n - bd-xyz: Version bump v0.36.0 (blocking 1 issue)\n - bd-uvw: Old patrol (not blocking)\n Fix: bd close \u003cid\u003e or bd mol squash \u003cid\u003e\n```\n\n**--fix behavior:**\n- Auto-close stale mols (with reason 'Auto-closed by bd doctor')\n- Or prompt interactively with -i flag\n\nDepends on: bd mol stale command","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-24T18:23:24.549941-08:00","updated_at":"2025-12-25T12:42:50.288442-08:00","closed_at":"2025-12-25T12:42:50.288442-08:00","close_reason":"Implemented stale molecules check in bd doctor","dependencies":[{"issue_id":"bd-6a5z","depends_on_id":"bd-anv2","type":"blocks","created_at":"2025-12-24T18:23:48.682552-08:00","created_by":"daemon"}]} -{"id":"bd-6df0","title":"Investigate Claude Code crash logging improvements","description":"## Problem\n\nClaude Code doesn't leave useful crash logs when it terminates unexpectedly. Investigation of a crash on 2025-12-26 showed:\n\n- Debug logs in ~/.claude/debug/ just stop mid-stream with no error/exit message\n- No signal handlers appear to log SIGTERM/SIGKILL/SIGINT\n- No dedicated crash log file exists\n- When Node.js crashes hard or gets killed, there's no record of why\n\n## What we found\n\n- Session debug log (02080b1a-...) stopped at 22:58:40 UTC mid-operation\n- No 'exit', 'error', 'crash', 'signal' entries at end of file\n- macOS DiagnosticReports showed Chrome crashes but no Node crashes\n- System logs showed no relevant kill/OOM events\n\n## Desired improvements\n\n1. Exit handlers that log graceful shutdown\n2. Signal handlers that log SIGTERM/SIGINT before exiting\n3. A dedicated crash log or at least a 'last known state' file\n4. Possibly CLI flags to enable verbose crash debugging\n\n## Investigation paths\n\n- Check if Claude Code has --debug or similar flags\n- Look at Node.js crash handling best practices\n- Consider if we can wrap claude invocations to capture crashes\n\n## Related\n\nThis came up while investigating why a crew worker session crashed at end of a code review task.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-26T15:20:03.578463-08:00","updated_at":"2025-12-26T15:20:03.578463-08:00","comments":[{"id":3,"issue_id":"bd-6df0","author":"stevey","text":"Found existing flags:\n- `--debug [filter]` - Enable debug mode with optional category filtering (e.g., 'api,hooks' or '!statsig,!file')\n- `--verbose` - Override verbose mode setting from config\n\nThese might help with diagnosing issues, but still won't capture hard crashes. The debug output goes to ~/.claude/debug/\u003csession-id\u003e.txt which is what we were already looking at.\n\nNext step: Could wrap claude invocation to capture exit codes and stderr, or look into Node.js --report-on-signal flags.","created_at":"2025-12-26T23:20:20Z"}]} +{"id":"bd-6df0","title":"Investigate Claude Code crash logging improvements","description":"## Problem\n\nClaude Code doesn't leave useful crash logs when it terminates unexpectedly. Investigation of a crash on 2025-12-26 showed:\n\n- Debug logs in ~/.claude/debug/ just stop mid-stream with no error/exit message\n- No signal handlers appear to log SIGTERM/SIGKILL/SIGINT\n- No dedicated crash log file exists\n- When Node.js crashes hard or gets killed, there's no record of why\n\n## What we found\n\n- Session debug log (02080b1a-...) stopped at 22:58:40 UTC mid-operation\n- No 'exit', 'error', 'crash', 'signal' entries at end of file\n- macOS DiagnosticReports showed Chrome crashes but no Node crashes\n- System logs showed no relevant kill/OOM events\n\n## Desired improvements\n\n1. Exit handlers that log graceful shutdown\n2. Signal handlers that log SIGTERM/SIGINT before exiting\n3. A dedicated crash log or at least a 'last known state' file\n4. Possibly CLI flags to enable verbose crash debugging\n\n## Investigation paths\n\n- Check if Claude Code has --debug or similar flags\n- Look at Node.js crash handling best practices\n- Consider if we can wrap claude invocations to capture crashes\n\n## Related\n\nThis came up while investigating why a crew worker session crashed at end of a code review task.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-26T15:20:03.578463-08:00","updated_at":"2025-12-26T15:20:03.578463-08:00","comments":[{"id":1,"issue_id":"bd-6df0","author":"stevey","text":"Found existing flags:\n- `--debug [filter]` - Enable debug mode with optional category filtering (e.g., 'api,hooks' or '!statsig,!file')\n- `--verbose` - Override verbose mode setting from config\n\nThese might help with diagnosing issues, but still won't capture hard crashes. The debug output goes to ~/.claude/debug/\u003csession-id\u003e.txt which is what we were already looking at.\n\nNext step: Could wrap claude invocation to capture exit codes and stderr, or look into Node.js --report-on-signal flags.","created_at":"2025-12-26T23:20:20Z"}]} {"id":"bd-6fe4622f","title":"Remove unreachable utility functions","description":"Several small utility functions are unreachable:\n\nFiles to clean:\n1. `internal/storage/sqlite/hash.go` - `computeIssueContentHash` (line 17)\n - Check if entire file can be deleted if only contains this function\n\n2. `internal/config/config.go` - `FileUsed` (line 151)\n - Delete unused config helper\n\n3. `cmd/bd/git_sync_test.go` - `verifyIssueOpen` (line 300)\n - Delete dead test helper\n\n4. `internal/compact/haiku.go` - `HaikuClient.SummarizeTier2` (line 81)\n - Tier 2 summarization not implemented\n - Options: implement feature OR delete method\n\nImpact: Removes 50-100 LOC depending on decisions","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.434573-07:00","updated_at":"2025-12-25T01:21:01.952723-08:00","close_reason":"Closed","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"} {"id":"bd-6gd","title":"Remove legacy MCP Agent Mail integration","description":"## Summary\n\nRemove the legacy MCP Agent Mail system that requires an external HTTP server. Keep the native `bd mail` system which stores messages as git-synced issues.\n\n## Background\n\nTwo mail systems exist in the codebase:\n1. **Legacy Agent Mail** (`bd message`) - External server dependency, complex setup\n2. **Native bd mail** (`bd mail`) - Built-in, git-synced, no dependencies\n\nThe legacy system causes confusion and is no longer needed. Gas Town's Town Mail will use the native `bd mail` system.\n\n## Files to Delete\n\n### CLI Command\n- [ ] `cmd/bd/message.go` - The `bd message` command implementation\n\n### MCP Integration\n- [ ] `integrations/beads-mcp/src/beads_mcp/mail.py` - HTTP wrapper for Agent Mail server\n- [ ] `integrations/beads-mcp/src/beads_mcp/mail_tools.py` - MCP tool definitions\n- [ ] `integrations/beads-mcp/tests/test_mail.py` - Tests for legacy mail\n\n### Documentation\n- [ ] `docs/AGENT_MAIL.md`\n- [ ] `docs/AGENT_MAIL_QUICKSTART.md`\n- [ ] `docs/AGENT_MAIL_DEPLOYMENT.md`\n- [ ] `docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md`\n- [ ] `docs/adr/002-agent-mail-integration.md`\n\n## Code to Update\n\n- [ ] Remove `message` command registration from `cmd/bd/main.go`\n- [ ] Remove mail tool imports/registration from MCP server `__init__.py` or `server.py`\n- [ ] Check for any other references to Agent Mail in the codebase\n\n## Verification\n\n- [ ] `bd message` command no longer exists\n- [ ] `bd mail` command still works\n- [ ] MCP server starts without errors\n- [ ] Tests pass\n","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-17T23:04:04.099935-08:00","updated_at":"2025-12-25T01:21:01.952723-08:00","close_reason":"Removed legacy MCP Agent Mail integration. Kept native bd mail system.","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"} {"id":"bd-6ns7","title":"test hook pin","status":"tombstone","priority":2,"issue_type":"task","assignee":"stevey","created_at":"2025-12-23T04:39:16.619755-08:00","updated_at":"2025-12-23T04:51:29.436788-08:00","deleted_at":"2025-12-23T04:51:29.436788-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} @@ -110,7 +113,7 @@ {"id":"bd-6sm6","title":"Improve test coverage for internal/export (37.1% → 60%)","description":"The export package has only 37.1% test coverage. Export functionality needs good coverage to ensure data integrity.\n\nCurrent coverage: 37.1%\nTarget coverage: 60%","status":"closed","priority":2,"issue_type":"task","assignee":"beads/alpha","created_at":"2025-12-13T20:43:06.802277-08:00","updated_at":"2025-12-23T22:32:29.16846-08:00","closed_at":"2025-12-23T22:32:29.16846-08:00","close_reason":"Coverage already at 71.8% (target was 60%). Recent commits ba8beb53 and e3e0a044 added tests."} {"id":"bd-6ss","title":"Improve test coverage","description":"The test suite reports less than 45% code coverage. Identify the specific uncovered areas of the codebase, including modules, functions, or features. Rank them by potential impact on system reliability and business value, from most to least, and provide actionable recommendations for improving coverage in each area.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-18T06:54:23.036822442-07:00","updated_at":"2025-12-18T07:17:49.245940799-07:00","closed_at":"2025-12-18T07:17:49.245940799-07:00"} {"id":"bd-70an","title":"test pin","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T11:19:16.760214-08:00","updated_at":"2025-12-21T11:19:46.500688-08:00","closed_at":"2025-12-21T11:19:46.500688-08:00","close_reason":"test issue for pin fix"} -{"id":"bd-70c4","title":"Gate await fields cleared by --no-daemon CLI access (not multi-repo)","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-25T23:30:38.648182-08:00","updated_at":"2025-12-25T23:30:38.648182-08:00","comments":[{"id":1,"issue_id":"bd-70c4","author":"mayor","text":"## Summary\nGate await fields (await_type, await_id, timeout_ns, waiters) are cleared when a CLI command accesses the database directly (--no-daemon) while the daemon is running. This is separate from the multi-repo issue fixed in bd-gr4q.\n\n## Reproduction\n1. Start daemon: bd daemon --start\n2. Create gate: bd gate create --await timer:5s (fields stored correctly)\n3. Verify: sqlite3 .beads/beads.db shows timer|5s\n4. Run CLI with --no-daemon: bd show \u003cid\u003e --no-daemon --no-auto-import --no-auto-flush\n5. Check again: fields are now empty\n\n## Investigation Notes\n- NOT caused by autoImportIfNewer (verified with --no-auto-import flag)\n- NOT caused by HydrateFromMultiRepo (no multi-repo config, returns early)\n- NOT caused by molecule loader (only creates new issues)\n- NOT caused by migrations (gate_columns only adds columns)\n- No database triggers found\n\nThe clearing happens somewhere in sqlite.NewWithTimeout() initialization or command execution path.\n\n## Related\n- bd-gr4q fixed the multi-repo path but this is a different code path\n- The fix pattern (COALESCE/NULLIF) may need to be applied elsewhere","created_at":"2025-12-26T07:30:49Z"}]} +{"id":"bd-70c4","title":"Gate await fields cleared by --no-daemon CLI access (not multi-repo)","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-25T23:30:38.648182-08:00","updated_at":"2025-12-26T23:38:47.972075-08:00","closed_at":"2025-12-26T23:38:47.972075-08:00","close_reason":"Cannot reproduce: tested gate create + --no-daemon show, fields preserved. May have been fixed by recent daemon/routing fixes.","comments":[{"id":2,"issue_id":"bd-70c4","author":"mayor","text":"## Summary\nGate await fields (await_type, await_id, timeout_ns, waiters) are cleared when a CLI command accesses the database directly (--no-daemon) while the daemon is running. This is separate from the multi-repo issue fixed in bd-gr4q.\n\n## Reproduction\n1. Start daemon: bd daemon --start\n2. Create gate: bd gate create --await timer:5s (fields stored correctly)\n3. Verify: sqlite3 .beads/beads.db shows timer|5s\n4. Run CLI with --no-daemon: bd show \u003cid\u003e --no-daemon --no-auto-import --no-auto-flush\n5. Check again: fields are now empty\n\n## Investigation Notes\n- NOT caused by autoImportIfNewer (verified with --no-auto-import flag)\n- NOT caused by HydrateFromMultiRepo (no multi-repo config, returns early)\n- NOT caused by molecule loader (only creates new issues)\n- NOT caused by migrations (gate_columns only adds columns)\n- No database triggers found\n\nThe clearing happens somewhere in sqlite.NewWithTimeout() initialization or command execution path.\n\n## Related\n- bd-gr4q fixed the multi-repo path but this is a different code path\n- The fix pattern (COALESCE/NULLIF) may need to be applied elsewhere","created_at":"2025-12-26T07:30:49Z"}]} {"id":"bd-746","title":"Fix resolvePartialID stub in workflow.go","description":"The resolvePartialID function at workflow.go:921-925 is a stub that just returns the ID unchanged. Should use utils.ResolvePartialID for proper partial ID resolution in direct mode (non-daemon).","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-17T22:22:57.586917-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-74w1","title":"Consolidate duplicate path-finding utilities (findJSONLPath, findBeadsDir, findGitRoot)","description":"Code health review found these functions defined in multiple places:\n\n- findJSONLPath() in autoflush.go:45-73 and doctor/fix/migrate.go\n- findBeadsDir() in autoimport.go:197-239 (with git worktree handling)\n- findGitRoot() in autoimport.go:242-269 (Windows path conversion)\n\nThe beads package has public FindBeadsDir() and FindJSONLPath() APIs that should be used consistently.\n\nImpact: Bug fixes need to be applied in multiple places. Git worktree handling may not be replicated everywhere.\n\nFix: Consolidate all implementations to use the beads package APIs. Remove duplicates.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-16T18:17:16.694293-08:00","updated_at":"2025-12-22T21:13:46.83103-08:00","closed_at":"2025-12-22T21:13:46.83103-08:00","close_reason":"Consolidated duplicate path-finding utilities: findGitRoot() now delegates to git.GetRepoRoot(), findBeadsDir() replaced with beads.FindBeadsDir() across 8 files"} {"id":"bd-754r","title":"Merge: bd-thgk","description":"branch: polecat/Compactor\ntarget: main\nsource_issue: bd-thgk\nrig: beads","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-23T13:41:43.965771-08:00","updated_at":"2025-12-23T19:12:08.345449-08:00","closed_at":"2025-12-23T19:12:08.345449-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} @@ -184,7 +187,7 @@ {"id":"bd-au0.10","title":"Add global verbosity flags (--verbose, --quiet)","description":"Add consistent verbosity controls across all commands.\n\n**Current state:**\n- bd init has --quiet flag\n- No other commands have verbosity controls\n- Debug output controlled by BD_VERBOSE env var\n\n**Proposal:**\nAdd persistent flags:\n- --verbose / -v: Enable debug output\n- --quiet / -q: Suppress non-essential output\n\n**Implementation:**\n- Add to rootCmd.PersistentFlags()\n- Replace BD_VERBOSE checks with flag checks\n- Standardize output levels:\n * Quiet: Errors only\n * Normal: Errors + success messages\n * Verbose: Errors + success + debug info\n\n**Files to modify:**\n- cmd/bd/main.go (add flags)\n- internal/debug/debug.go (respect flags)\n- Update all commands to respect quiet mode\n\n**Testing:**\n- Verify --verbose shows debug output\n- Verify --quiet suppresses normal output\n- Ensure errors always show regardless of mode","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-21T21:08:21.600209-05:00","updated_at":"2025-12-25T22:34:40.197801-08:00","closed_at":"2025-12-25T22:34:40.197801-08:00","close_reason":"Already implemented: --verbose/-v and --quiet/-q persistent flags added, debug package has SetVerbose/SetQuiet/IsQuiet/PrintNormal functions, flags applied in PersistentPreRun","dependencies":[{"issue_id":"bd-au0.10","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:08:21.602557-05:00","created_by":"daemon","metadata":"{}"}]} {"id":"bd-au0.5","title":"Add date and priority filters to bd search","description":"Add date and priority filters to bd search for parity with bd list.\n\n## Current State\nbd search supports: --status, --type, --assignee, --label, --limit\nbd list supports: all of the above PLUS date ranges and priority filters\n\n## Filters to Add\n\n### Priority Filters\n```bash\nbd search \"query\" --priority 1 # Exact priority\nbd search \"query\" --priority-min 0 # P0 and above (higher priority)\nbd search \"query\" --priority-max 2 # P2 and below (lower priority)\n```\n\n### Date Filters\n```bash\nbd search \"query\" --created-after 2025-01-01\nbd search \"query\" --created-before 2025-12-31\nbd search \"query\" --updated-after 2025-01-01\nbd search \"query\" --closed-after 2025-01-01\n```\n\n### Content Filters\n```bash\nbd search \"query\" --desc-contains \"bug\"\nbd search \"query\" --notes-contains \"todo\"\nbd search \"query\" --empty-description # Issues with no description\nbd search \"query\" --no-assignee # Unassigned issues\nbd search \"query\" --no-labels # Issues without labels\n```\n\n## Files to Modify\n\n### 1. cmd/bd/search.go\nAdd flag definitions in init():\n```go\nsearchCmd.Flags().IntP(\"priority\", \"p\", -1, \"Filter by exact priority (0-4)\")\nsearchCmd.Flags().Int(\"priority-min\", -1, \"Filter by minimum priority\")\nsearchCmd.Flags().Int(\"priority-max\", -1, \"Filter by maximum priority\")\nsearchCmd.Flags().String(\"created-after\", \"\", \"Filter by creation date (YYYY-MM-DD)\")\nsearchCmd.Flags().String(\"created-before\", \"\", \"Filter by creation date\")\nsearchCmd.Flags().String(\"updated-after\", \"\", \"Filter by update date\")\nsearchCmd.Flags().String(\"updated-before\", \"\", \"Filter by update date\")\nsearchCmd.Flags().String(\"closed-after\", \"\", \"Filter by close date\")\nsearchCmd.Flags().String(\"closed-before\", \"\", \"Filter by close date\")\nsearchCmd.Flags().String(\"desc-contains\", \"\", \"Filter by description content\")\nsearchCmd.Flags().String(\"notes-contains\", \"\", \"Filter by notes content\")\nsearchCmd.Flags().Bool(\"empty-description\", false, \"Filter issues with empty description\")\nsearchCmd.Flags().Bool(\"no-assignee\", false, \"Filter unassigned issues\")\nsearchCmd.Flags().Bool(\"no-labels\", false, \"Filter issues without labels\")\n```\n\n### 2. internal/rpc/protocol.go\nUpdate SearchArgs struct:\n```go\ntype SearchArgs struct {\n Query string\n Filter types.IssueFilter\n // Already has most fields via IssueFilter\n}\n```\n\nNote: types.IssueFilter already has these fields - just need to wire them up!\n\n### 3. cmd/bd/search.go Run function\nParse flags and populate filter:\n```go\nif priority, _ := cmd.Flags().GetInt(\"priority\"); priority \u003e= 0 {\n filter.Priority = \u0026priority\n}\nif createdAfter, _ := cmd.Flags().GetString(\"created-after\"); createdAfter != \"\" {\n t, err := time.Parse(\"2006-01-02\", createdAfter)\n if err != nil {\n FatalError(\"invalid date format for --created-after: %v\", err)\n }\n filter.CreatedAfter = \u0026t\n}\n// ... similar for other flags\n```\n\n## Implementation Steps\n\n1. **Check types.IssueFilter** - verify all needed fields exist\n2. **Add flags to search.go** init()\n3. **Parse flags** in Run function\n4. **Pass to SearchIssues** via filter\n5. **Test all combinations**\n\n## Testing\n```bash\n# Create test issues\nbd create \"Test P1\" -p 1\nbd create \"Test P2\" -p 2 --description \"Has description\"\n\n# Test filters\nbd search \"\" --priority 1\nbd search \"\" --priority-min 0 --priority-max 1\nbd search \"\" --empty-description\nbd search \"\" --desc-contains \"description\"\n```\n\n## Success Criteria\n- All filters work in both direct and daemon mode\n- Date parsing handles YYYY-MM-DD format\n- --json output includes filtered results\n- Help text documents all new flags","status":"closed","priority":1,"issue_type":"task","assignee":"beads/Searcher","created_at":"2025-11-21T21:07:05.496726-05:00","updated_at":"2025-12-23T13:38:28.475606-08:00","closed_at":"2025-12-23T13:38:28.475606-08:00","close_reason":"Implemented all date, priority, and content filters for bd search","dependencies":[{"issue_id":"bd-au0.5","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:05.497762-05:00","created_by":"daemon","metadata":"{}"},{"issue_id":"bd-au0.5","depends_on_id":"bd-iz5t","type":"parent-child","created_at":"2025-12-23T12:44:07.657303-08:00","created_by":"daemon"}]} {"id":"bd-au0.6","title":"Add comprehensive filters to bd export","description":"Enhance bd export with filtering options for selective exports.\n\n**Currently only has:**\n- --status\n\n**Add filters:**\n- --label, --label-any\n- --assignee\n- --type\n- --priority, --priority-min, --priority-max\n- --created-after, --created-before\n- --updated-after, --updated-before\n\n**Use case:**\n- Export only open issues: bd export --status open\n- Export high-priority bugs: bd export --type bug --priority-max 1\n- Export recent issues: bd export --created-after 2025-01-01\n\n**Files to modify:**\n- cmd/bd/export.go\n- Reuse filter logic from list.go","status":"closed","priority":1,"issue_type":"task","assignee":"beads/dementus","created_at":"2025-11-21T21:07:19.431307-05:00","updated_at":"2025-12-23T23:44:45.602324-08:00","closed_at":"2025-12-23T23:44:45.602324-08:00","close_reason":"All filter flags already implemented and tested","dependencies":[{"issue_id":"bd-au0.6","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:19.432983-05:00","created_by":"daemon","metadata":"{}"}]} -{"id":"bd-au0.7","title":"Audit and standardize JSON output across all commands","description":"Ensure consistent JSON format and error handling when --json flag is used.\n\n**Scope:**\n1. Verify all commands respect --json flag\n2. Standardize success response format\n3. Standardize error response format\n4. Document JSON schemas\n\n**Commands to audit:**\n- Core CRUD: create, update, delete, show, list, search ✓\n- Queries: ready, blocked, stale, count, stats, status\n- Deps: dep add/remove/tree/cycles\n- Labels: label commands\n- Comments: comments add/list/delete\n- Epics: epic status/close-eligible\n- Export/import: already support --json ✓\n\n**Testing:**\n- Success cases return valid JSON\n- Error cases return valid JSON (not plain text)\n- Consistent field naming (snake_case vs camelCase)\n- Array vs object wrapping consistency","notes":"## Audit Complete (2025-12-25)\n\n### Findings\n\n**✓ All commands support --json flag**\n- Query commands: ready, blocked, stale, count, stats, status\n- Dep commands: add, remove, tree, cycles \n- Label commands: add, remove, list, list-all\n- Comments: list, add\n- Epic: status, close-eligible\n\n**✓ Field naming is consistent**\n- All fields use snake_case: created_at, issue_type, dependency_count, etc.\n\n**✗ Error output is INCONSISTENT**\n- Only bd show uses FatalErrorRespectJSON (returns JSON errors)\n- All other commands use fmt.Fprintf(os.Stderr, ...) (returns plain text)\n\n### Files needing fixes\n\n| File | stderr writes | Commands |\n|------|---------------|----------|\n| show.go | 51 | update, close, edit |\n| dep.go | 41 | dep add/remove/tree/cycles |\n| label.go | 19 | label add/remove/list |\n| comments.go | ~10 | comments add/list |\n| epic.go | ~5 | epic status/close-eligible |\n\n### Follow-up\n\nCreated epic bd-28sq to track fixing all error handlers.","status":"closed","priority":1,"issue_type":"task","assignee":"beads/rictus","created_at":"2025-11-21T21:07:35.304424-05:00","updated_at":"2025-12-25T13:32:32.460786-08:00","closed_at":"2025-12-25T13:32:32.460786-08:00","close_reason":"Audit complete. Created bd-28sq epic to fix error output inconsistencies.","dependencies":[{"issue_id":"bd-au0.7","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:35.305663-05:00","created_by":"daemon","metadata":"{}"}],"comments":[{"id":2,"issue_id":"bd-au0.7","author":"stevey","text":"Progress on JSON standardization:\n\n## Completed\n1. **Fixed `bd comments list` null output** - Now returns `[]` instead of `null` for empty comments\n2. **Added `FatalErrorRespectJSON` helper** in errors.go - Pattern for JSON-aware error output\n3. **Fixed flag shadowing** - Removed local `--json` flags from show/update/close that shadowed the global persistent flag\n4. **Updated show command** - Error handlers now use `FatalErrorRespectJSON` as reference implementation\n\n## Audit Results\n- Query commands (ready, blocked, stale, count, stats, status): ✓ All support --json correctly\n- Dep commands (tree, cycles): ✓ All support --json correctly \n- Label commands: ✓ Returns [] for empty\n- Comments: ✓ Fixed null→[]\n- Epic commands (status, close-eligible): ✓ All support --json correctly\n\n## Remaining Work\n- Other commands (list, create, etc.) still use `fmt.Fprintf(os.Stderr, ...)` for errors - could be updated to use `FatalErrorRespectJSON` for JSON error output\n- JSON schema documentation not yet created","created_at":"2025-12-24T07:53:38Z"}]} +{"id":"bd-au0.7","title":"Audit and standardize JSON output across all commands","description":"Ensure consistent JSON format and error handling when --json flag is used.\n\n**Scope:**\n1. Verify all commands respect --json flag\n2. Standardize success response format\n3. Standardize error response format\n4. Document JSON schemas\n\n**Commands to audit:**\n- Core CRUD: create, update, delete, show, list, search ✓\n- Queries: ready, blocked, stale, count, stats, status\n- Deps: dep add/remove/tree/cycles\n- Labels: label commands\n- Comments: comments add/list/delete\n- Epics: epic status/close-eligible\n- Export/import: already support --json ✓\n\n**Testing:**\n- Success cases return valid JSON\n- Error cases return valid JSON (not plain text)\n- Consistent field naming (snake_case vs camelCase)\n- Array vs object wrapping consistency","notes":"## Audit Complete (2025-12-25)\n\n### Findings\n\n**✓ All commands support --json flag**\n- Query commands: ready, blocked, stale, count, stats, status\n- Dep commands: add, remove, tree, cycles \n- Label commands: add, remove, list, list-all\n- Comments: list, add\n- Epic: status, close-eligible\n\n**✓ Field naming is consistent**\n- All fields use snake_case: created_at, issue_type, dependency_count, etc.\n\n**✗ Error output is INCONSISTENT**\n- Only bd show uses FatalErrorRespectJSON (returns JSON errors)\n- All other commands use fmt.Fprintf(os.Stderr, ...) (returns plain text)\n\n### Files needing fixes\n\n| File | stderr writes | Commands |\n|------|---------------|----------|\n| show.go | 51 | update, close, edit |\n| dep.go | 41 | dep add/remove/tree/cycles |\n| label.go | 19 | label add/remove/list |\n| comments.go | ~10 | comments add/list |\n| epic.go | ~5 | epic status/close-eligible |\n\n### Follow-up\n\nCreated epic bd-28sq to track fixing all error handlers.","status":"closed","priority":1,"issue_type":"task","assignee":"beads/rictus","created_at":"2025-11-21T21:07:35.304424-05:00","updated_at":"2025-12-25T13:32:32.460786-08:00","closed_at":"2025-12-25T13:32:32.460786-08:00","close_reason":"Audit complete. Created bd-28sq epic to fix error output inconsistencies.","dependencies":[{"issue_id":"bd-au0.7","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:35.305663-05:00","created_by":"daemon","metadata":"{}"}],"comments":[{"id":3,"issue_id":"bd-au0.7","author":"stevey","text":"Progress on JSON standardization:\n\n## Completed\n1. **Fixed `bd comments list` null output** - Now returns `[]` instead of `null` for empty comments\n2. **Added `FatalErrorRespectJSON` helper** in errors.go - Pattern for JSON-aware error output\n3. **Fixed flag shadowing** - Removed local `--json` flags from show/update/close that shadowed the global persistent flag\n4. **Updated show command** - Error handlers now use `FatalErrorRespectJSON` as reference implementation\n\n## Audit Results\n- Query commands (ready, blocked, stale, count, stats, status): ✓ All support --json correctly\n- Dep commands (tree, cycles): ✓ All support --json correctly \n- Label commands: ✓ Returns [] for empty\n- Comments: ✓ Fixed null→[]\n- Epic commands (status, close-eligible): ✓ All support --json correctly\n\n## Remaining Work\n- Other commands (list, create, etc.) still use `fmt.Fprintf(os.Stderr, ...)` for errors - could be updated to use `FatalErrorRespectJSON` for JSON error output\n- JSON schema documentation not yet created","created_at":"2025-12-24T07:53:38Z"}]} {"id":"bd-au0.8","title":"Improve clean vs cleanup command naming/documentation","description":"Clarify the difference between bd clean and bd cleanup to reduce user confusion.\n\n**Current state:**\n- bd clean: Remove temporary artifacts (.beads/bd.sock, logs, etc.)\n- bd cleanup: Delete old closed issues from database\n\n**Options:**\n1. Rename for clarity:\n - bd clean → bd clean-temp\n - bd cleanup → bd cleanup-issues\n \n2. Keep names but improve help text and documentation\n\n3. Add prominent warnings in help output\n\n**Preferred approach:** Option 2 (improve documentation)\n- Update short/long descriptions in commands\n- Add examples to help text\n- Update README.md\n- Add cross-references in help output\n\n**Files to modify:**\n- cmd/bd/clean.go\n- cmd/bd/cleanup.go\n- README.md or ADVANCED.md","status":"closed","priority":2,"issue_type":"task","assignee":"beads/dementus","created_at":"2025-11-21T21:07:49.960534-05:00","updated_at":"2025-12-23T23:48:00.594734-08:00","closed_at":"2025-12-23T23:48:00.594734-08:00","close_reason":"Documentation already comprehensive: both commands have clear short descriptions, long explanations, cross-references, examples, and explicit disclaimers about what each does NOT do","dependencies":[{"issue_id":"bd-au0.8","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:49.962743-05:00","created_by":"daemon","metadata":"{}"}]} {"id":"bd-au0.9","title":"Review and document rarely-used commands","description":"Document use cases or consider deprecation for infrequently-used commands.\n\n**Commands to review:**\n1. bd rename-prefix - How often is this used? Document use cases\n2. bd detect-pollution - Consider integrating into bd validate\n3. bd migrate-hash-ids - One-time migration, keep but document as legacy\n\n**For each command:**\n- Document typical use cases\n- Add examples to help text\n- Consider if it should be a subcommand instead\n- Add deprecation warning if appropriate\n\n**Not changing:**\n- duplicates ✓ (useful for data quality)\n- repair-deps ✓ (useful for fixing broken refs)\n- restore ✓ (critical for compacted issues)\n- compact ✓ (performance feature)\n\n**Deliverable:**\n- Updated help text\n- Documentation in ADVANCED.md\n- Deprecation plan if needed","status":"closed","priority":3,"issue_type":"task","assignee":"beads/dementus","created_at":"2025-11-21T21:08:05.588275-05:00","updated_at":"2025-12-23T23:50:04.180989-08:00","closed_at":"2025-12-23T23:50:04.180989-08:00","close_reason":"All three commands already have comprehensive docs: USE CASES, EXAMPLES, and appropriate warnings (LEGACY/rare operation notes)","dependencies":[{"issue_id":"bd-au0.9","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:08:05.59003-05:00","created_by":"daemon","metadata":"{}"}]} {"id":"bd-awmf","title":"Merge: bd-dtl8","description":"branch: polecat/dag\ntarget: main\nsource_issue: bd-dtl8\nrig: beads","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-23T20:47:15.147476-08:00","updated_at":"2025-12-23T21:21:57.690692-08:00","closed_at":"2025-12-23T21:21:57.690692-08:00","close_reason":"stale - no code pushed"} @@ -207,7 +210,7 @@ {"id":"bd-bijf","title":"Merge: bd-l13p","description":"branch: polecat/nux\ntarget: main\nsource_issue: bd-l13p\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T16:41:32.467246-08:00","updated_at":"2025-12-23T19:12:08.348252-08:00","closed_at":"2025-12-23T19:12:08.348252-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-bivq","title":"Merge: bd-9usz","description":"branch: polecat/slit\ntarget: main\nsource_issue: bd-9usz\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T20:42:19.995419-08:00","updated_at":"2025-12-23T21:21:57.700579-08:00","closed_at":"2025-12-23T21:21:57.700579-08:00","close_reason":"stale - no code pushed"} {"id":"bd-bkul","title":"Simplify wisp architecture: single DB with ephemeral flag","description":"\n## Problem\n\nThe current wisp architecture uses a separate .beads-wisp/ directory with its own database. This creates unnecessary complexity:\n\n- Separate directory structure and database initialization\n- Parallel routing logic in both beads and gastown\n- Merge queries from two stores for inbox/list operations\n- All this for what is essentially a boolean flag\n\n## Current State\n\nGastown mail code has to:\n- resolveWispDir() vs resolveBeadsDir()\n- Query both, merge results\n- Route deletes to correct store\n\n## Proposed Solution\n\nSingle database with an ephemeral boolean field on Issue. Behavior:\n- bd create --ephemeral sets the flag\n- JSONL export SKIPS ephemeral issues (they never sync)\n- bd list shows all by default, --ephemeral / --persistent filters\n- bd mol squash clears the flag (promotes to permanent, now exports)\n- bd wisp gc deletes old ephemeral issues from the db\n- No separate directory, no separate routing\n\n## Migration\n\n1. Update beads to support ephemeral flag in main db\n2. Update bd wisp commands to work with flag instead of separate dir\n3. Update gastown mail to use simple --ephemeral flag (remove all dual-routing)\n4. Deprecate .beads-wisp/ directory pattern\n\n## Acceptance Criteria\n\n- Single database for all issues (ephemeral and persistent)\n- ephemeral field on Issue type\n- JSONL export skips ephemeral issues\n- bd create --ephemeral works\n- bd mol squash promotes ephemeral to persistent\n- Gastown mail uses simple flag, no dual-routing\n","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-12-24T20:06:27.980055-08:00","updated_at":"2025-12-24T20:43:07.065124-08:00","closed_at":"2025-12-24T20:43:07.065124-08:00","close_reason":"Implemented: single DB with Wisp flag, deprecated wisp storage functions"} -{"id":"bd-blh0","title":"gt nudge doesn't work with crew addresses","description":"## Bug\n\n`gt nudge beads/crew/dave \"message\"` fails because it uses the polecat session manager which produces wrong session names.\n\n## Expected\nSession name: `gt-beads-crew-dave` (hyphen)\n\n## Actual\nSession name: `gt-beads-crew/dave` (slash, from polecat manager)\n\n## Root Cause\n\nIn nudge.go line 46-57:\n```go\nrigName, polecatName, err := parseAddress(target) // beads, crew/dave\nsessionName := mgr.SessionName(polecatName) // gt-beads-crew/dave (WRONG)\n```\n\nShould detect `crew/` prefix in polecatName and use `crewSessionName(rigName, crewName)` instead.\n\n## Fix\n\n```go\nif strings.HasPrefix(polecatName, \"crew/\") {\n crewName := strings.TrimPrefix(polecatName, \"crew/\")\n sessionName = crewSessionName(rigName, crewName)\n} else {\n sessionName = mgr.SessionName(polecatName)\n}\n```","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-26T15:43:32.222784-08:00","updated_at":"2025-12-26T15:43:32.222784-08:00"} +{"id":"bd-blh0","title":"gt nudge doesn't work with crew addresses","description":"## Bug\n\n`gt nudge beads/crew/dave \"message\"` fails because it uses the polecat session manager which produces wrong session names.\n\n## Expected\nSession name: `gt-beads-crew-dave` (hyphen)\n\n## Actual\nSession name: `gt-beads-crew/dave` (slash, from polecat manager)\n\n## Root Cause\n\nIn nudge.go line 46-57:\n```go\nrigName, polecatName, err := parseAddress(target) // beads, crew/dave\nsessionName := mgr.SessionName(polecatName) // gt-beads-crew/dave (WRONG)\n```\n\nShould detect `crew/` prefix in polecatName and use `crewSessionName(rigName, crewName)` instead.\n\n## Fix\n\n```go\nif strings.HasPrefix(polecatName, \"crew/\") {\n crewName := strings.TrimPrefix(polecatName, \"crew/\")\n sessionName = crewSessionName(rigName, crewName)\n} else {\n sessionName = mgr.SessionName(polecatName)\n}\n```","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-26T15:43:32.222784-08:00","updated_at":"2025-12-26T23:36:38.520091-08:00","closed_at":"2025-12-26T23:36:38.520091-08:00","close_reason":"Already fixed in gastown commit 54f0932"} {"id":"bd-bqcc","title":"Consolidate maintenance commands into bd doctor --fix","description":"Per rsnodgrass in GH#692:\n\u003e \"The biggest improvement to beads from an ergonomics perspective would be to prune down commands. We have a lot of 'maintenance' commands that probably should just be folded into 'bd doctor --fix' automatically.\"\n\nCurrent maintenance commands that could be consolidated:\n- clean - Clean up temporary git merge artifacts\n- cleanup - Delete closed issues and prune expired tombstones\n- compact - Compact old closed issues\n- detect-pollution - Detect and clean test issues\n- migrate-* (5 commands) - Various migration utilities\n- repair-deps - Fix orphaned dependency references\n- validate - Database health checks\n\nProposal:\n1. Make `bd doctor` the single entry point for health checks\n2. Add `bd doctor --fix` to auto-fix common issues\n3. Deprecate (but keep working) individual commands\n4. Add `bd doctor --all` for comprehensive maintenance\n\nThis would reduce cognitive load for users - they just need to remember 'bd doctor'.\n\nNote: This is higher impact but also higher risk - needs careful design to avoid breaking existing workflows.","status":"closed","priority":2,"issue_type":"feature","assignee":"beads/capable","created_at":"2025-12-22T14:27:31.466556-08:00","updated_at":"2025-12-23T01:33:25.732363-08:00","closed_at":"2025-12-23T01:33:25.732363-08:00","close_reason":"Merged to main"} {"id":"bd-bw6","title":"Fix G104 errors unhandled in internal/storage/sqlite/queries.go:1181","description":"Linting issue: G104: Errors unhandled (gosec) at internal/storage/sqlite/queries.go:1181:4. Error: rows.Close()","status":"tombstone","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:09.008444133-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-bwk2","title":"Centralize error handling patterns in storage layer","description":"80+ instances of inconsistent error handling across sqlite.go with mix of %w, %v, and no wrapping.\n\nLocation: internal/storage/sqlite/sqlite.go (throughout)\n\nProblem:\n- Some use fmt.Errorf(\"op failed: %w\", err) - correct wrapping\n- Some use fmt.Errorf(\"op failed: %v\", err) - loses error chain\n- Some return err directly - no context\n- Hard to debug production issues\n- Can't distinguish error types\n\nSolution: Create internal/storage/sqlite/errors.go:\n- Define sentinel errors (ErrNotFound, ErrInvalidID, etc.)\n- Create wrapDBError(op string, err error) helper\n- Convert sql.ErrNoRows to ErrNotFound\n- Always wrap with operation context\n\nImpact: Lost error context; inconsistent messages; hard to debug\n\nEffort: 5-7 hours","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-16T14:51:54.974909-08:00","updated_at":"2025-12-21T21:44:37.237175-08:00","closed_at":"2025-12-21T21:44:37.237175-08:00","close_reason":"Already implemented: errors.go exists with sentinel errors (ErrNotFound, ErrInvalidID, ErrConflict, ErrCycle), wrapDBError/wrapDBErrorf helpers that convert sql.ErrNoRows to ErrNotFound, and IsNotFound/IsConflict/IsCycle checkers. 41 uses of wrapDBError, 347 uses of proper %w wrapping, 0 uses of %v. Added one minor fix to CheckpointWAL."} @@ -309,6 +312,8 @@ {"id":"bd-hy9p","title":"Add --body-file flag to bd create for reading descriptions from files","description":"## Problem\n\nCreating issues with long/complex descriptions via CLI requires shell escaping gymnastics:\n\n```bash\n# Current workaround - awkward heredoc quoting\nbd create --title=\"...\" --description=\"$(cat \u003c\u003c'EOF'\n...markdown...\nEOF\n)\"\n\n# Often fails with quote escaping errors in eval context\n# Agents resort to writing temp files then reading them\n```\n\n## Proposed Solution\n\nAdd `--body-file` and `--description-file` flags to read description from a file, matching `gh` CLI pattern.\n\n```bash\n# Natural pattern that aligns with training data\ncat \u003e /tmp/desc.md \u003c\u003c 'EOF'\n...markdown content...\nEOF\n\nbd create --title=\"...\" --body-file=/tmp/desc.md\n```\n\n## Implementation\n\n### 1. Add new flags to `bd create`\n\n```go\ncreateCmd.Flags().String(\"body-file\", \"\", \"Read description from file (use - for stdin)\")\ncreateCmd.Flags().String(\"description-file\", \"\", \"Alias for --body-file\")\n```\n\n### 2. Flag precedence\n\n- If `--body-file` or `--description-file` is provided, read from file\n- If value is `-`, read from stdin\n- Otherwise fall back to `--body` or `--description` flag\n- If neither provided, description is empty (current behavior)\n\n### 3. Error handling\n\n- File doesn't exist → clear error message\n- File not readable → clear error message\n- stdin specified but not available → clear error message\n\n## Benefits\n\n✅ **Matches training data**: `gh issue create --body-file file.txt` is a common pattern\n✅ **No shell escaping issues**: File content is read directly\n✅ **Works with any content**: Markdown, special characters, quotes, etc.\n✅ **Agent-friendly**: Agents already write complex content to temp files\n✅ **User-friendly**: Easier for humans too when pasting long descriptions\n\n## Related Commands\n\nConsider adding similar support to:\n- `bd update --body-file` (for updating descriptions)\n- `bd comment --body-file` (if/when we add comments)\n\n## Examples\n\n```bash\n# From file\nbd create --title=\"Add new feature\" --body-file=feature.md\n\n# From stdin\necho \"Quick description\" | bd create --title=\"Bug fix\" --body-file=-\n\n# With other flags\nbd create \\\n --title=\"Security issue\" \\\n --type=bug \\\n --priority=0 \\\n --body-file=security-report.md \\\n --label=security\n```\n\n## Testing\n\n- Test with normal files\n- Test with stdin (`-`)\n- Test with non-existent files (error handling)\n- Test with binary files (should handle gracefully)\n- Test with empty files (valid - empty description)\n- Test that `--description-file` and `--body-file` are equivalent aliases","status":"tombstone","priority":1,"issue_type":"feature","created_at":"2025-11-22T00:02:08.762684-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-hzvz","title":"Update info.go versionChanges","description":"Add entry to versionChanges in cmd/bd/info.go with agent-actionable changes for 0.30.7","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:48.649359-08:00","updated_at":"2025-12-19T22:57:31.604229-08:00","closed_at":"2025-12-19T22:57:31.604229-08:00","dependencies":[{"issue_id":"bd-hzvz","depends_on_id":"bd-8pyn","type":"parent-child","created_at":"2025-12-19T22:56:48.652068-08:00","created_by":"stevey"},{"issue_id":"bd-hzvz","depends_on_id":"bd-2ep8","type":"blocks","created_at":"2025-12-19T22:56:48.652376-08:00","created_by":"stevey"}]} {"id":"bd-i0rx","title":"Merge: bd-ao0s","description":"branch: polecat/rictus\ntarget: main\nsource_issue: bd-ao0s\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-20T01:13:42.716658-08:00","updated_at":"2025-12-20T23:17:26.993744-08:00","closed_at":"2025-12-20T23:17:26.993744-08:00","close_reason":"Branches nuked, MRs obsolete"} +{"id":"bd-i5l","title":"Witness Patrol","description":"Per-rig worker monitor patrol loop with progressive nudging.","status":"open","priority":2,"issue_type":"molecule","created_at":"2025-12-26T21:20:47.650732-08:00","created_by":"deacon","updated_at":"2025-12-26T21:20:47.650732-08:00"} +{"id":"bd-i7a6","title":"Test actor flag","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-26T20:47:28.470006-08:00","updated_at":"2025-12-26T20:47:28.470006-08:00"} {"id":"bd-ia3g","title":"BondRef.ProtoID field name is misleading for mol+mol bonds","description":"In bondMolMol, the BondRef.ProtoID field is used to store molecule IDs:\n\n```go\nBondedFrom: append(molA.BondedFrom, types.BondRef{\n ProtoID: molB.ID, // This is a molecule, not a proto\n ...\n})\n```\n\nThis is semantically confusing since ProtoID suggests it should only hold proto references.\n\n**Options:**\n1. Rename ProtoID to SourceID (breaking change, needs migration)\n2. Add documentation clarifying ProtoID can hold molecule IDs in bond context\n3. Leave as-is, accept the naming is imprecise\n\nLow priority since it's just naming, not functionality.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T10:23:00.755067-08:00","updated_at":"2025-12-25T14:30:47.455867-08:00"} {"id":"bd-ibl9","title":"Merge: bd-4qfb","description":"branch: polecat/Polish\ntarget: main\nsource_issue: bd-4qfb\nrig: beads","status":"closed","priority":2,"issue_type":"merge-request","created_at":"2025-12-23T13:37:57.255125-08:00","updated_at":"2025-12-23T19:12:08.352249-08:00","closed_at":"2025-12-23T19:12:08.352249-08:00","close_reason":"Stale merge-requests from orphaned polecat branches - refinery not processing"} {"id":"bd-icfe","title":"gt spawn/crew setup should create .beads/redirect for worktrees","description":"Crew clones and polecats need a .beads/redirect file pointing to the shared beads database (../../mayor/rig/.beads). Currently:\n\n- redirect files can get deleted by git clean\n- not auto-created during gt spawn or worktree setup\n- missing redirects cause 'no beads database found' errors\n\nFound missing in: gastown/joe, beads/zoey (after git clean)\n\nFix options:\n1. gt spawn creates redirect during worktree setup\n2. gt prime regenerates missing redirects\n3. bd commands auto-detect worktree and find shared beads\n\nThis should be standard Gas Town rig configuration.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-21T01:30:26.115872-08:00","updated_at":"2025-12-21T17:51:25.740811-08:00","closed_at":"2025-12-21T17:51:25.740811-08:00","close_reason":"Moved to gastown: gt-b6qm"} @@ -357,7 +362,7 @@ {"id":"bd-kwro.7","title":"Identity Configuration","description":"Implement identity system for sender field.\n\nConfiguration sources (in priority order):\n1. --identity flag on commands\n2. BEADS_IDENTITY environment variable\n3. .beads/config.json: {\"identity\": \"worker-name\"}\n4. Default: git user.name or hostname\n\nNew config file support:\n- .beads/config.json for per-repo settings\n- identity field for messaging\n\nHelper function:\n- GetIdentity() string - resolves identity from sources\n\nUpdate bd mail send to use GetIdentity() for sender field.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:02:17.603608-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":"task"} {"id":"bd-kwro.8","title":"Hooks System","description":"Implement hook system for extensibility.\n\nHook directory: .beads/hooks/\nHook files (executable scripts):\n- on_create - runs after bd create\n- on_update - runs after bd update \n- on_close - runs after bd close\n- on_message - runs after bd mail send\n\nHook invocation:\n- Pass issue ID as first argument\n- Pass event type as second argument\n- Pass JSON issue data on stdin\n- Run asynchronously (dont block command)\n\nExample hook (GGT notification):\n #!/bin/bash\n gt notify --event=$2 --issue=$1\n\nThis allows GGT to register notification handlers without Beads knowing about GGT.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:02:23.086393-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":"task"} {"id":"bd-kwro.9","title":"Cleanup: --ephemeral flag","description":"Update bd cleanup to handle ephemeral issues.\n\nNew flag:\n- bd cleanup --ephemeral - deletes all CLOSED issues with ephemeral=true\n\nBehavior:\n- Only deletes if status=closed AND ephemeral=true\n- Respects --dry-run flag\n- Reports count of deleted ephemeral issues\n\nThis allows swarm cleanup to remove transient messages without affecting permanent issues.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T03:02:28.563871-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":"task"} -{"id":"bd-kx1j","title":"Review jordanhubbard chaos testing PR #752","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-12-26T17:22:18.219501-08:00","updated_at":"2025-12-26T17:23:09.56421-08:00"} +{"id":"bd-kx1j","title":"Review jordanhubbard chaos testing PR #752","description":"Review jordanhubbard chaos testing PR #752\n\nFINDINGS:\n- Implementation quality: HIGH\n- Recommendation: MERGE WITH MODIFICATIONS\n- Mods: No hard coverage threshold, chaos tests on releases only\n\nDECISION: User agrees. Next steps:\n1. Add chaos tests to release-bump formula\n2. Merge PR #752\n\nReview doc: docs/pr-752-chaos-testing-review.md","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-26T17:22:18.219501-08:00","updated_at":"2025-12-26T23:14:35.902878-08:00","closed_at":"2025-12-26T17:38:14.904621-08:00"} {"id":"bd-kyll","title":"Add daemon-side delete operation tests","description":"Follow-up epic for PR #626: Add comprehensive test coverage for delete operations at the daemon/RPC layer. PR #626 successfully added storage layer tests but identified gaps in daemon-side delete operations and RPC integration testing.\n\n## Scope\nTests needed for:\n1. deleteViaDaemon (cmd/bd/delete.go:21) - RPC client-side deletion command\n2. Daemon RPC delete handler - Server-side deletion via daemon\n3. createTombstone wrapper (cmd/bd/delete.go:335) - Tombstone creation wrapper\n4. deleteIssue wrapper (cmd/bd/delete.go:349) - Direct deletion wrapper\n\n## Coverage targets\n- Delete via RPC daemon (both success and error paths)\n- Cascade deletion through daemon\n- Force deletion through daemon\n- Dry-run mode validation\n- Tombstone creation and verification\n- Error handling and edge cases","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-18T13:08:26.039663309-07:00","updated_at":"2025-12-25T01:44:03.584007-08:00","closed_at":"2025-12-25T01:44:03.584007-08:00","close_reason":"All child tasks completed"} {"id":"bd-kyo","title":"Run tests and linting","description":"Run the full test suite and linter:\n\n```bash\nTMPDIR=/tmp go test -short ./...\ngolangci-lint run ./...\n```\n\nFix any failures. Linting warnings acceptable (see LINTING.md).","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-18T22:42:59.290588-08:00","updated_at":"2025-12-24T16:25:30.300951-08:00","dependencies":[{"issue_id":"bd-kyo","depends_on_id":"bd-qqc","type":"parent-child","created_at":"2025-12-18T22:43:16.370234-08:00","created_by":"daemon"},{"issue_id":"bd-kyo","depends_on_id":"bd-8hy","type":"blocks","created_at":"2025-12-18T22:43:20.570742-08:00","created_by":"daemon"}],"deleted_at":"2025-12-24T16:25:30.300951-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} {"id":"bd-kzda","title":"Implement conditional bond type for mol bond","description":"The mol bond command accepts 'conditional' as a bond type but doesn't implement any conditional-specific behavior. It currently behaves identically to 'parallel'.\n\n**Expected behavior:**\nConditional bonds should mean 'B runs only if A fails' per the help text (mol.go:318).\n\n**Implementation needed:**\n- Add failure-condition dependency handling\n- Possibly new dependency type or status-based blocking\n- Update bondProtoProto, bondProtoMol, bondMolMol to handle conditional\n\n**Alternative:**\nRemove 'conditional' from valid bond types until implemented.\n\nThis is new functionality, not a regression.","status":"closed","priority":3,"issue_type":"feature","assignee":"beads/toast","created_at":"2025-12-21T10:23:01.966367-08:00","updated_at":"2025-12-23T01:33:25.734264-08:00","closed_at":"2025-12-23T01:33:25.734264-08:00","close_reason":"Merged to main"} @@ -365,6 +370,7 @@ {"id":"bd-l7y3","title":"bd mol bond --pour should set Wisp=false","description":"In mol_bond.go bondProtoMol(), opts.Wisp is hardcoded to true (line 392). This ignores the --pour flag. When user specifies --pour to make an issue persistent, the Wisp field should be false so the issue is not marked for bulk deletion.\n\nCurrent behavior:\n- --pour flag correctly selects regular storage (not wisp storage)\n- But opts.Wisp=true means spawned issues are still marked for cleanup when closed\n\nExpected behavior:\n- --pour should set Wisp=false so persistent issues are not auto-cleaned\n\nComparison with mol_spawn.go (line 204):\n wisp := !pour // Correctly respects --pour flag\n result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, wisp)\n\nFix: Pass pour flag to bondProtoMol and set opts.Wisp = !pour","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-23T15:15:00.562346-08:00","updated_at":"2025-12-23T15:25:22.53144-08:00","closed_at":"2025-12-23T15:25:22.53144-08:00","close_reason":"Fixed - pour parameter now passed through bondProtoMol chain"} {"id":"bd-ldb0","title":"Rename ephemeral → wisp throughout codebase","description":"## The Change\n\nRename 'ephemeral' to 'wisp' throughout the beads codebase.\n\n## Why\n\n**Ephemeral** is:\n- 4 syllables (too long)\n- Greek/academic (doesn't match bond/burn/squash)\n- Overused in tech (K8s, networking, storage)\n- Passive/descriptive\n\n**Wisp** is:\n- 1 syllable (matches bond/burn/squash)\n- Evocative - you can SEE a wisp\n- Steam engine metaphor - Gas Town is engines, steam wisps rise and dissipate\n- Will-o'-the-wisp - transient spirits that guide then vanish\n- Unique - nobody else uses it\n\n## The Steam Engine Metaphor\n\n```\nEngine does work → generates steam\nSteam wisps rise → execution trace\nSteam condenses → digest (distillate)\nSteam dissipates → cleaned up (burned)\n```\n\n## Full Vocabulary\n\n| Term | Meaning |\n|------|---------|\n| bond | Attach proto to work (creates wisps) |\n| wisp | Temporary execution step |\n| squash | Condense wisps into digest |\n| burn | Destroy wisps without record |\n| digest | Permanent condensed record |\n\n## Changes Required\n\n### Code\n- `Ephemeral bool` → `Wisp bool` in types/issue.go\n- `--ephemeral` flag → remove (wisp is default)\n- `--persistent` flag → keep as opt-out\n- `bd cleanup --ephemeral` → `bd cleanup --wisps`\n- Update all references in mol_*.go files\n\n### Docs\n- Update all documentation\n- Update CLAUDE.md examples\n- Update CLI help text\n\n### Database Migration\n- Add migration to rename field (or keep internal name, just change API)\n\n## Example Usage After\n\n```bash\nbd mol bond mol-polecat-work # Creates wisps (default)\nbd mol bond mol-xxx --persistent # Creates permanent issues\nbd mol squash bd-xxx # Condenses wisps → digest\nbd cleanup --wisps # Clean old wisps\nbd list --wisps # Show wisp issues\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-21T14:44:41.576068-08:00","updated_at":"2025-12-22T00:32:31.153738-08:00","closed_at":"2025-12-22T00:32:31.153738-08:00","close_reason":"Renamed ephemeral → wisp throughout codebase"} {"id":"bd-lfak","title":"bd preflight: PR readiness checks for contributors","description":"## Vision\n\nEncode project-specific institutional knowledge into executable checks. CONTRIBUTING.md is documentation that's read once and forgotten; `bd preflight` is documentation that runs at exactly the right moment.\n\n## Problem Statement\n\nContributors face a \"last mile\" problem - they do the work but stumble on project-specific gotchas at PR time:\n- Nix vendorHash gets stale when go.sum changes\n- Beads artifacts leak into PRs (see bd-umbf for namespace solution)\n- Version mismatches between version.go and default.nix\n- Tests/lint not run locally before pushing\n- Other project-specific checks that only surface when CI fails\n\nThese are too obscure to remember, exist in docs nobody reads end-to-end, and waste CI round-trips.\n\n## Why beads?\n\nBeads already has a foothold in the contributor workflow. It knows:\n- Git state (staged files, branch, dirty status)\n- Project structure\n- The specific issue being worked on\n- Project-specific configuration\n\n## Proposed Interface\n\n### Tier 1: Checklist Mode (v1)\n\n $ bd preflight\n PR Readiness Checklist:\n\n [ ] Tests pass: go test -short ./...\n [ ] Lint passes: golangci-lint run ./...\n [ ] No beads pollution: check .beads/issues.jsonl diff\n [ ] Nix hash current: go.sum unchanged or vendorHash updated\n [ ] Version sync: version.go matches default.nix\n\n Run 'bd preflight --check' to validate automatically.\n\n### Tier 2: Check Mode (v2)\n\n $ bd preflight --check\n ✓ Tests pass\n ✓ Lint passes\n ⚠ Beads pollution: 3 issues in diff - are these project issues or personal?\n ✗ Nix hash stale: go.sum changed, vendorHash needs update\n Fix: sha256-KRR6dXzsSw8OmEHGBEVDBOoIgfoZ2p0541T9ayjGHlI=\n ✓ Version sync\n\n 1 error, 1 warning. Run 'bd preflight --fix' to auto-fix where possible.\n\n### Tier 3: Fix Mode (v3)\n\n $ bd preflight --fix\n ✓ Updated vendorHash in default.nix\n ⚠ Cannot auto-fix beads pollution - manual review needed\n\n## Checks to Implement\n\n| Check | Description | Auto-fixable |\n|-------|-------------|--------------|\n| tests | Run go test -short ./... | No |\n| lint | Run golangci-lint | Partial (gofmt) |\n| beads-pollution | Detect personal issues in diff | No (see bd-umbf) |\n| nix-hash | Detect stale vendorHash | Yes (if nix available) |\n| version-sync | version.go matches default.nix | Yes |\n| no-debug | No TODO/FIXME/console.log | Warn only |\n| clean-stage | No unintended files staged | Warn only |\n\n## Future: Configuration\n\nMake checks configurable per-project via .beads/preflight.yaml:\n\n preflight:\n checks:\n - name: tests\n run: go test -short ./...\n required: true\n - name: no-secrets\n pattern: \"**/*.env\"\n staged: deny\n - name: custom-check\n run: ./scripts/validate.sh\n\nThis lets any project using beads define their own preflight checks.\n\n## Implementation Phases\n\n### Phase 1: Static Checklist\n- Implement bd preflight with hardcoded checklist for beads\n- No execution, just prints what to check\n- Update CONTRIBUTING.md to reference it\n\n### Phase 2: Automated Checks\n- Implement bd preflight --check\n- Run tests, lint, detect stale hashes\n- Clear pass/fail/warn output\n\n### Phase 3: Auto-fix\n- Implement bd preflight --fix\n- Fix vendorHash, version sync\n- Integrate with bd-umbf solution for pollution\n\n### Phase 4: Configuration\n- .beads/preflight.yaml support\n- Make it useful for other projects using beads\n- Plugin/hook system for custom checks\n\n## Dependencies\n\n- bd-umbf: Namespace isolation for beads pollution (blocking for full solution)\n\n## Success Metrics\n\n- Fewer CI failures on first PR push\n- Reduced \"fix nix hash\" commits\n- Contributors report preflight caught issues before CI","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-13T18:01:39.587078-08:00","updated_at":"2025-12-13T18:01:39.587078-08:00","dependencies":[{"issue_id":"bd-lfak","depends_on_id":"bd-umbf","type":"blocks","created_at":"2025-12-13T18:01:46.059901-08:00","created_by":"daemon","metadata":"{}"}]} +{"id":"bd-lfiu","title":"bd dep add: Auto-resolve cross-rig IDs using routes.jsonl","description":"Currently, adding a dependency to an issue in another rig requires verbose external reference syntax:\n\n```bash\n# This fails - can't resolve bd-* from gastown context\nbd dep add gt-xyz bd-abc\n\n# This works but is verbose\nbd dep add gt-xyz external:beads:bd-abc\n```\n\nThe town-level routing (~/gt/.beads/routes.jsonl) already knows how to map prefixes to rigs:\n```json\n{\"prefix\": \"gt-\", \"path\": \"gastown/mayor/rig\"}\n{\"prefix\": \"bd-\", \"path\": \"beads/mayor/rig\"}\n```\n\nEnhancement: When `bd dep add` encounters an ID with a foreign prefix, it should:\n1. Check routes.jsonl for the prefix mapping\n2. Auto-resolve to external:\u003cproject\u003e:\u003cid\u003e internally\n3. Allow the simpler `bd dep add gt-xyz bd-abc` syntax\n\nThis would make cross-rig dependencies much more ergonomic.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"beads/dave","created_at":"2025-12-26T20:20:40.814713-08:00","updated_at":"2025-12-26T23:39:47.263248-08:00"} {"id":"bd-likt","title":"Add daemon RPC support for gate commands","description":"Add daemon RPC support for gate commands.\n\n## Current State\nGate commands require --no-daemon flag because they use direct SQLite access:\n- Gate create needs to write await_type, await_id, timeout_ns, waiters fields\n- Gate wait needs to update waiters JSON array\n- Daemon RPC doesnt have methods for these operations\n\n## Implementation\n\n### 1. Add RPC methods to internal/rpc/protocol.go\n\n```go\n// Gate operations\ntype GateCreateArgs struct {\n Title string \\`json:\"title\"\\`\n AwaitType string \\`json:\"await_type\"\\`\n AwaitID string \\`json:\"await_id\"\\`\n Timeout time.Duration \\`json:\"timeout\"\\`\n Waiters []string \\`json:\"waiters\"\\`\n}\n\ntype GateCreateResult struct {\n Issue *types.Issue \\`json:\"issue\"\\`\n}\n\ntype GateListArgs struct {\n All bool \\`json:\"all\"\\` // Include closed gates\n}\n\ntype GateListResult struct {\n Gates []*types.Issue \\`json:\"gates\"\\`\n}\n\ntype GateWaitArgs struct {\n GateID string \\`json:\"gate_id\"\\`\n Waiters []string \\`json:\"waiters\"\\` // Additional waiters to add\n}\n\ntype GateWaitResult struct {\n Gate *types.Issue \\`json:\"gate\"\\`\n AddedCount int \\`json:\"added_count\"\\`\n}\n```\n\n### 2. Add handler methods to internal/daemon/rpc_handler.go\n\n```go\nfunc (h *RPCHandler) GateCreate(ctx context.Context, args *rpc.GateCreateArgs) (*rpc.GateCreateResult, error) {\n now := time.Now()\n gate := \u0026types.Issue{\n Title: args.Title,\n IssueType: types.TypeGate,\n Status: types.StatusOpen,\n Priority: 1,\n Assignee: \"deacon/\",\n Wisp: true,\n AwaitType: args.AwaitType,\n AwaitID: args.AwaitID,\n Timeout: args.Timeout,\n Waiters: args.Waiters,\n CreatedAt: now,\n UpdatedAt: now,\n }\n gate.ContentHash = gate.ComputeContentHash()\n \n if err := h.store.CreateIssue(ctx, gate, h.actor); err != nil {\n return nil, err\n }\n \n return \u0026rpc.GateCreateResult{Issue: gate}, nil\n}\n\nfunc (h *RPCHandler) GateList(ctx context.Context, args *rpc.GateListArgs) (*rpc.GateListResult, error) {\n gateType := types.TypeGate\n filter := types.IssueFilter{IssueType: \u0026gateType}\n if !args.All {\n openStatus := types.StatusOpen\n filter.Status = \u0026openStatus\n }\n \n gates, err := h.store.SearchIssues(ctx, \"\", filter)\n if err != nil {\n return nil, err\n }\n \n return \u0026rpc.GateListResult{Gates: gates}, nil\n}\n\nfunc (h *RPCHandler) GateWait(ctx context.Context, args *rpc.GateWaitArgs) (*rpc.GateWaitResult, error) {\n gate, err := h.store.GetIssue(ctx, args.GateID)\n if err != nil {\n return nil, err\n }\n if gate.IssueType != types.TypeGate {\n return nil, fmt.Errorf(\"%s is not a gate\", args.GateID)\n }\n \n // Merge waiters (dedupe)\n waiterSet := make(map[string]bool)\n for _, w := range gate.Waiters {\n waiterSet[w] = true\n }\n added := 0\n for _, w := range args.Waiters {\n if !waiterSet[w] {\n gate.Waiters = append(gate.Waiters, w)\n waiterSet[w] = true\n added++\n }\n }\n \n if added \u003e 0 {\n // Update via store\n updates := map[string]interface{}{\n \"waiters\": gate.Waiters,\n }\n if err := h.store.UpdateIssue(ctx, args.GateID, updates, h.actor); err != nil {\n return nil, err\n }\n }\n \n return \u0026rpc.GateWaitResult{Gate: gate, AddedCount: added}, nil\n}\n```\n\n### 3. Register methods in daemon\n\nIn internal/daemon/server.go, register the new methods:\n```go\nrpc.RegisterMethod(\"gate.create\", h.GateCreate)\nrpc.RegisterMethod(\"gate.list\", h.GateList)\nrpc.RegisterMethod(\"gate.wait\", h.GateWait)\n```\n\n### 4. Add client methods to internal/rpc/client.go\n\n```go\nfunc (c *Client) GateCreate(ctx context.Context, args *GateCreateArgs) (*GateCreateResult, error) {\n var result GateCreateResult\n err := c.Call(ctx, \"gate.create\", args, \u0026result)\n return \u0026result, err\n}\n\nfunc (c *Client) GateList(ctx context.Context, args *GateListArgs) (*GateListResult, error) {\n var result GateListResult\n err := c.Call(ctx, \"gate.list\", args, \u0026result)\n return \u0026result, err\n}\n\nfunc (c *Client) GateWait(ctx context.Context, args *GateWaitArgs) (*GateWaitResult, error) {\n var result GateWaitResult\n err := c.Call(ctx, \"gate.wait\", args, \u0026result)\n return \u0026result, err\n}\n```\n\n### 5. Update cmd/bd/gate.go to use daemon\n\n```go\n// In gateCreateCmd Run:\nif daemonClient != nil {\n result, err := daemonClient.GateCreate(ctx, \u0026rpc.GateCreateArgs{\n Title: title,\n AwaitType: awaitType,\n AwaitID: awaitID,\n Timeout: timeout,\n Waiters: notifyAddrs,\n })\n if err != nil {\n FatalError(\"gate create: %v\", err)\n }\n gate = result.Issue\n} else {\n // Existing direct store code\n}\n```\n\n## Files to Modify\n\n1. **internal/rpc/protocol.go** - Add Gate*Args/Result types\n2. **internal/daemon/rpc_handler.go** - Add handler methods\n3. **internal/daemon/server.go** - Register methods\n4. **internal/rpc/client.go** - Add client methods\n5. **cmd/bd/gate.go** - Use daemon client when available\n\n## Testing\n\n```bash\n# Start daemon\nbd daemon start\n\n# Test via daemon (should work without --no-daemon)\nbd gate create --await timer:5m --notify beads/dave\nbd gate list\nbd gate wait \u003cid\u003e --notify beads/alice\n\n# Verify daemon handled it\nbd daemons logs . | grep gate\n```\n\n## Success Criteria\n- All gate commands work without --no-daemon\n- Same behavior in daemon vs direct mode\n- Waiters array updates correctly via RPC\n- Tests pass for RPC gate operations","status":"closed","priority":3,"issue_type":"task","assignee":"beads/Gater","created_at":"2025-12-23T12:13:25.778412-08:00","updated_at":"2025-12-23T13:45:58.398604-08:00","closed_at":"2025-12-23T13:45:58.398604-08:00","close_reason":"Implemented daemon RPC support for all gate commands","dependencies":[{"issue_id":"bd-likt","depends_on_id":"bd-udsi","type":"discovered-from","created_at":"2025-12-23T12:13:36.174822-08:00","created_by":"daemon"},{"issue_id":"bd-likt","depends_on_id":"bd-iz5t","type":"parent-child","created_at":"2025-12-23T12:44:07.891992-08:00","created_by":"daemon"}]} {"id":"bd-lk39","title":"Add composite index (issue_id, event_type) on events table","description":"GetCloseReason and GetCloseReasonsForIssues filter by both issue_id and event_type.\n\n**Query (queries.go:355-358):**\n```sql\nSELECT comment FROM events\nWHERE issue_id = ? AND event_type = ?\nORDER BY created_at DESC LIMIT 1\n```\n\n**Problem:** Currently uses idx_events_issue but must filter event_type in memory.\n\n**Solution:** Add migration:\n```sql\nCREATE INDEX IF NOT EXISTS idx_events_issue_type ON events(issue_id, event_type);\n```\n\n**Priority:** Low - events table is typically small relative to issues.","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-22T22:58:54.070587-08:00","updated_at":"2025-12-22T23:15:13.841988-08:00","closed_at":"2025-12-22T23:15:13.841988-08:00","close_reason":"Implemented in migration 026_additional_indexes.go","dependencies":[{"issue_id":"bd-lk39","depends_on_id":"bd-h0we","type":"discovered-from","created_at":"2025-12-22T22:58:54.071286-08:00","created_by":"daemon"}]} {"id":"bd-llfl","title":"Improve test coverage for cmd/bd CLI (26.2% → 50%)","description":"The main CLI package (cmd/bd) has only 26.2% test coverage. CLI commands should have at least 50% coverage to ensure reliability.\n\nKey areas with low/no coverage:\n- daemon_autostart.go (multiple 0% functions)\n- compact.go (several 0% functions)\n- Various command handlers\n\nCurrent coverage: 26.2%\nTarget coverage: 50%","notes":"## Progress Update (2025-12-23)\n\n### Tests Added\nAdded 683 lines of new tests across 3 files:\n- cmd/bd/daemon_config_test.go (144 lines)\n- cmd/bd/utils_test.go (484 lines) \n- cmd/bd/autostart_test.go (55 additional lines)\n\n### Functions Now Tested\n- daemon_config.go: ensureBeadsDir, getPIDFilePath, getLogFilePath, getSocketPathForPID\n- daemon_autostart.go: determineSocketPath, isDaemonRunningQuiet\n- activity.go: printEvent\n- cleanup.go: showCleanupDeprecationHint\n- upgrade.go: pluralize\n- wisp.go: formatTimeAgo\n- list.go: pinIndicator, sortIssues\n- hooks.go: FormatHookWarnings, isRebaseInProgress, hasBeadsJSONL\n- template.go: extractIDSuffix\n- thanks.go: getContributorsSorted\n\n### Coverage Results\n- Before: 22.5%\n- After: 23.1%\n- Delta: +0.6%\n\n### Remaining Work\nMost remaining untested code (77%) involves:\n1. Daemon/RPC operations (runDaemonLoop, tryAutoStartDaemon, etc.)\n2. Command handlers that require database/daemon setup\n3. Git operations (runPreCommitHook, runPostMergeHook, etc.)\n\nTo reach 50%, would need to:\n- Add integration tests with mocked daemon\n- Add scripttest tests for command handlers\n- Add more database-dependent tests\n\nCommit: 4f949c19","status":"in_progress","priority":2,"issue_type":"task","assignee":"beads/charlie","created_at":"2025-12-13T20:43:03.123341-08:00","updated_at":"2025-12-23T22:45:57.860498-08:00"} @@ -409,6 +415,7 @@ {"id":"bd-nqyp","title":"mol-beads-release","description":"Release checklist for beads version {{version}}.\n\nThis molecule ensures all release steps are completed properly.\nVariable: {{version}} - target version (e.g., 0.35.0)\n\n## Step: update-release-notes\nUpdate cmd/bd/info.go with release notes for {{version}}.\n\nAdd a new VersionChange entry at the top of versionChanges slice:\n```go\n{\n Version: \"{{version}}\",\n Date: \"YYYY-MM-DD\",\n Changes: []string{\n \"NEW: Feature description\",\n \"FIX: Bug fix description\",\n \"IMPROVED: Enhancement description\",\n },\n},\n```\n\nRun `git log --oneline v\u003cprevious\u003e..HEAD` to see what changed.\n\n## Step: update-changelog\nUpdate CHANGELOG.md with detailed release notes.\n\nAdd a new section after [Unreleased]:\n```markdown\n## [{{version}}] - YYYY-MM-DD\n\n### Added\n- **Feature name** (issue-id) - Description\n\n### Changed\n- **Change description** (issue-id)\n\n### Fixed\n- **Bug fix** (issue-id) - Description\n```\n\nSort by importance, not chronologically.\nNeeds: update-release-notes\n\n## Step: bump-version\nRun the version bump script.\n\n```bash\n./scripts/bump-version.sh {{version}}\n```\n\nThis updates version in all files:\n- cmd/bd/version.go\n- .claude-plugin/*.json\n- integrations/beads-mcp/pyproject.toml\n- npm-package/package.json\n- Hook templates\n\nNeeds: update-changelog\n\n## Step: run-tests\nRun tests and verify lint passes.\n\n```bash\ngo test -short ./...\n```\n\nCI will run full lint, but fix any obvious issues first.\nNeeds: bump-version\n\n## Step: commit-release\nCommit the release changes.\n\n```bash\ngit add -A\ngit commit -m \"chore: bump version to v{{version}}\"\n```\n\nNeeds: run-tests\n\n## Step: push-and-tag\nPush commit and create release tag.\n\n```bash\ngit push origin main\ngit tag v{{version}}\ngit push origin v{{version}}\n```\n\nThis triggers GitHub Actions release workflow.\nNeeds: commit-release\n\n## Step: wait-for-ci\nWait for GitHub Actions to complete.\n\nMonitor: https://github.com/steveyegge/beads/actions\n\nCI will:\n- Build binaries via GoReleaser\n- Create GitHub Release with assets\n- Publish to npm (@beads/bd)\n- Publish to PyPI (beads-mcp)\n- Update Homebrew tap\n\nWait until all jobs succeed (~5-10 min).\nNeeds: push-and-tag\n\n## Step: verify-release\nVerify the release is complete.\n\n```bash\n# Check GitHub release\ngh release view v{{version}}\n\n# Check Homebrew\nbrew update \u0026\u0026 brew info steveyegge/beads/bd\n\n# Check npm\nnpm view @beads/bd version\n\n# Check PyPI\npip index versions beads-mcp\n```\n\nNeeds: wait-for-ci\n\n## Step: update-local\nUpdate local installations.\n\n```bash\n# Upgrade Homebrew\nbrew upgrade steveyegge/beads/bd\n\n# Or install from source\n./scripts/bump-version.sh {{version}} --install\n\n# Install MCP locally\npip install -e integrations/beads-mcp\n\n# Restart daemons\npkill -f \"bd daemon\" || true\n```\n\nVerify: `bd --version` shows {{version}}\nNeeds: verify-release\n\n## Step: manual-publish\n(Optional) Manual publish if CI failed.\n\n```bash\n# npm (requires npm login)\n./scripts/bump-version.sh {{version}} --publish-npm\n\n# PyPI (requires TWINE credentials)\n./scripts/bump-version.sh {{version}} --publish-pypi\n\n# Or both\n./scripts/bump-version.sh {{version}} --publish-all\n```\n\nOnly needed if CI publishing failed.\nNeeds: wait-for-ci","status":"open","priority":2,"issue_type":"molecule","created_at":"2025-12-23T11:29:39.087936-08:00","updated_at":"2025-12-23T11:29:39.087936-08:00","labels":["template"]} {"id":"bd-nuh1","title":"GH#403: bd doctor --fix circular error message","description":"bd doctor --fix suggests running bd doctor --fix for deletions manifest issue. Fix to provide actual resolution. See GitHub issue #403.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:16.290018-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-nurq","title":"Implement bd mol current command","description":"Show what molecule the agent should currently be working on. Referenced by gt-um6q, gt-lz13. Needed for molecule navigation workflow in templates.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-23T00:17:54.069983-08:00","updated_at":"2025-12-23T01:23:59.523404-08:00","closed_at":"2025-12-23T01:23:59.523404-08:00","close_reason":"Implementation already existed, added tests (TestGetMoleculeProgress, TestFindParentMolecule, TestAdvanceToNextStep*), rebuilt and installed binary"} +{"id":"bd-o18s","title":"Rename 'wisp' back to 'ephemeral' in beads API","description":"The beads API uses 'wisp' terminology (Wisp field, bd wisp command) but the underlying SQLite column is 'ephemeral'. \n\nThis creates cognitive overhead since wisp is a Gas Town concept.\n\nRename to use 'ephemeral' consistently:\n- types.Issue.Wisp → types.Issue.Ephemeral\n- JSON field: wisp → ephemeral \n- CLI: bd wisp → bd ephemeral (or just use flags on existing commands)\n\nThe SQLite column already uses 'ephemeral' so no schema migration needed.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-26T20:16:36.627876-08:00","updated_at":"2025-12-26T21:04:10.212439-08:00","closed_at":"2025-12-26T21:04:10.212439-08:00","close_reason":"Renamed 'wisp' to 'ephemeral' throughout the codebase"} {"id":"bd-o34a","title":"Design auto-squash behavior for wisps","description":"Explore the design space for automatic wisp squashing.\n\n**Context:**\nWisps are ephemeral molecules that should be squashed (digest) or burned (no trace)\nwhen complete. Currently this is manual. Should it be automatic?\n\n**Questions to answer:**\n1. When should auto-squash trigger?\n - On molecule completion?\n - On session end/handoff?\n - On patrol detection?\n \n2. What's the default summary for auto-squash?\n - Generic: 'Auto-squashed on completion'\n - Step-based: List closed steps\n - AI-generated: Require agent to provide\n\n3. Should this be configurable?\n - Per-molecule setting in formula?\n - Global config: auto_squash: true/false\n - Per-wisp flag at creation time?\n\n4. Who decides - Beads or Gas Town?\n - Beads: Provides operators (squash, burn)\n - Gas Town: Makes policy decisions\n - Proposal: GT patrol molecules call bd mol squash\n\n**Constraints:**\n- Don't lose important context (summary matters)\n- Don't create noise in digest history\n- Respect agent's intent (some wisps should burn, not squash)\n\n**Recommendation:**\nGas Town patrol molecules should have explicit squash/burn steps.\nBeads provides primitives, GT makes policy decisions.\nAuto-squash at Beads level is probably wrong layer.","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-24T18:23:24.833877-08:00","updated_at":"2025-12-25T22:56:59.210809-08:00","closed_at":"2025-12-25T22:56:59.210809-08:00","close_reason":"Already resolved: Gas Town handles squash/burn policy via templates, Beads provides primitives. Design matches recommendation in issue."} {"id":"bd-o4qy","title":"Improve CheckStaleness error handling","description":"## Problem\n\nCheckStaleness returns 'false' (not stale) for multiple error conditions instead of returning errors. This masks problems.\n\n**Location:** internal/autoimport/autoimport.go:253-285\n\n## Edge Cases That Return False\n\n1. **Invalid last_import_time format** (line 259-262)\n2. **No JSONL file found** (line 267-277) \n3. **JSONL stat fails** (line 279-282)\n\n## Fix\n\nReturn errors for abnormal conditions:\n\n```go\nlastImportTime, err := time.Parse(time.RFC3339, lastImportStr)\nif err != nil {\n return false, fmt.Errorf(\"corrupted last_import_time: %w\", err)\n}\n\nif jsonlPath == \"\" {\n return false, fmt.Errorf(\"no JSONL file found\")\n}\n\nstat, err := os.Stat(jsonlPath)\nif err != nil {\n return false, fmt.Errorf(\"cannot stat JSONL: %w\", err)\n}\n```\n\n## Impact\nMedium - edge cases are rare but should be handled\n\n## Effort \n30 minutes - requires updating callers in RPC server","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:17:27.606219-05:00","updated_at":"2025-12-25T01:21:01.952723-08:00","dependencies":[{"issue_id":"bd-o4qy","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:26.81065-05:00","created_by":"stevey","metadata":"{}"}],"deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"} {"id":"bd-o55a","title":"GH#509: bd doesn't find .beads when running from nested worktrees","description":"When worktrees are nested under main repo (.worktrees/feature/), bd stops at worktree git root instead of continuing to find .beads in parent. See GitHub issue #509 for detailed fix suggestion.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:20.281591-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"} @@ -452,6 +459,7 @@ {"id":"bd-pdr2","title":"Consider backwards compatibility for ready() and list() return type change","description":"PR #481 changed the return types of `ready()` and `list()` from `list[Issue]` to `list[IssueMinimal] | CompactedResult`. This is a breaking change for MCP clients.\n\n## Impact Assessment\nBreaking change affects:\n- Any MCP client expecting `list[Issue]` from ready()\n- Any MCP client expecting `list[Issue]` from list()\n- Client code that accesses full Issue fields (description, design, acceptance_criteria, timestamps, dependencies, dependents)\n\n## Current Behavior\n- ready() returns `list[IssueMinimal] | CompactedResult`\n- list() returns `list[IssueMinimal] | CompactedResult`\n- show() still returns full `Issue` (good)\n\n## Considerations\n**Pros of current approach:**\n- Forces clients to use show() for full details (good for context efficiency)\n- Simple mental model (always use show for full data)\n- Documentation warns about this\n\n**Cons:**\n- Clients expecting list[Issue] will break\n- No graceful degradation option\n- No migration period\n\n## Potential Solutions\n1. Add optional parameter `full_details=false` to ready/list (would increase payload)\n2. Create separate tools: ready_minimal/list_minimal + ready_full/list_full\n3. Accept breaking change and document upgrade path (current approach)\n4. Version the MCP server and document migration guide\n\n## Recommendation\nCurrent approach (solution 3) is reasonable if:\n- Changelog clearly documents the breaking change\n- Migration guide provided to clients\n- Error handling is graceful for clients expecting specific fields","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:24:56.460465-08:00","updated_at":"2025-12-14T14:24:56.460465-08:00","dependencies":[{"issue_id":"bd-pdr2","depends_on_id":"bd-otf4","type":"discovered-from","created_at":"2025-12-14T14:24:56.461959-08:00","created_by":"stevey","metadata":"{}"}]} {"id":"bd-pe4s","title":"JSON test issue","description":"Line 1\nLine 2\nLine 3","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T16:14:36.969074-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":"task"} {"id":"bd-pgcs","title":"Clean up orphaned child issues (bd-cb64c226.*, bd-cbed9619.*)","description":"## Problem\n\nEvery bd command shows warnings about 12 orphaned child issues:\n- bd-cb64c226.1, .6, .8, .9, .10, .12, .13\n- bd-cbed9619.1, .2, .3, .4, .5\n\nThese are hierarchical IDs (parent.child format) where the parent issues no longer exist.\n\n## Impact\n\n- Clutters output of every bd command\n- Confusing for users\n- Indicates incomplete cleanup of deleted parent issues\n\n## Proposed Solution\n\n1. Delete the orphaned issues since their parents no longer exist:\n ```bash\n bd delete bd-cb64c226.1 bd-cb64c226.6 bd-cb64c226.8 ...\n ```\n\n2. Or convert them to top-level issues if they contain useful content\n\n## Investigation Needed\n\n- What were the parent issues bd-cb64c226 and bd-cbed9619?\n- Why were they deleted without their children?\n- Should bd delete cascade to children automatically?","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T23:06:17.240571-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":"task"} +{"id":"bd-pgh","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-26T21:20:47.62144-08:00","created_by":"deacon","updated_at":"2025-12-26T21:20:47.62144-08:00"} {"id":"bd-phtv","title":"bd pin: pinned field overwritten by subsequent bd commands","description":"## Summary\n\nThe `bd pin` command correctly sets `pinned=1` in SQLite, but any subsequent `bd` command (including read-only commands like `bd show`) resets `pinned` to 0.\n\n## Reproduction Steps\n\n```bash\nbd --no-daemon pin \u003cissue-id\u003e --for=max\nsqlite3 .beads/beads.db \"SELECT id, pinned FROM issues WHERE id=\\\"\u003cissue-id\u003e\\\"\"\n# Shows pinned=1 ✓\n\nbd --no-daemon show \u003cissue-id\u003e --json\nsqlite3 .beads/beads.db \"SELECT id, pinned FROM issues WHERE id=\\\"\u003cissue-id\u003e\\\"\"\n# Shows pinned=0 ✗ WRONG\n```\n\n## Root Cause Investigation\n\n### Prime Suspects\n\n1. **JSONL import overwrites DB** - The `pinned` field has `omitempty` so false values arent in JSONL. When JSONL is imported, it overwrites the DB pinned=1 with default pinned=0.\n\n2. **Files to check:**\n - `internal/importer/importer.go` - ImportIssue() may unconditionally set all fields\n - `internal/storage/sqlite/issues.go` - UpsertIssue() may not preserve pinned\n - `cmd/bd/main.go` - ensureStoreActive() may trigger import\n\n### Debug Steps\n\n```bash\n# Add debug logging to track what is writing pinned=0\ngrep -rn \"pinned\" internal/storage/sqlite/*.go\ngrep -rn \"Pinned\" internal/importer/*.go\n```\n\n## Likely Fix\n\nIn `internal/importer/importer.go` or `internal/storage/sqlite/issues.go`:\n\n```go\n// When upserting from JSONL, preserve pinned field if already set\nfunc (s *SQLiteStorage) UpsertIssue(ctx context.Context, issue *types.Issue) error {\n // Check if issue exists and is pinned\n existing, _ := s.GetIssue(ctx, issue.ID)\n if existing != nil \u0026\u0026 existing.Pinned \u0026\u0026 !issue.Pinned {\n // Preserve existing pinned status\n issue.Pinned = existing.Pinned\n }\n // ... rest of upsert\n}\n```\n\nOR the import should skip fields that are omitempty and not present in JSONL:\n\n```go\n// In importer, only update fields that are explicitly set in JSONL\n// Pinned with omitempty means absent = dont change, not absent = false\n```\n\n## Testing\n\n```bash\n# After fix:\nbd --no-daemon pin \u003cissue-id\u003e --for=max\nbd --no-daemon show \u003cissue-id\u003e --json # Should not reset pinned\nbd list --pinned # Should show the pinned issue\nbd hook --agent max # Should show pinned work\n```\n\n## Files to Modify\n\n1. **internal/importer/importer.go** - Preserve pinned on import\n2. **internal/storage/sqlite/issues.go** - UpsertIssue preserve pinned\n3. **Add test** in internal/importer/importer_test.go\n\n## Success Criteria\n- `bd pin` survives subsequent bd commands\n- `bd list --pinned` shows pinned issues\n- `bd hook --agent X` shows pinned work\n- Existing tests still pass","status":"closed","priority":1,"issue_type":"bug","assignee":"beads/Pinner","created_at":"2025-12-23T12:32:20.046988-08:00","updated_at":"2025-12-23T13:47:49.936021-08:00","closed_at":"2025-12-23T13:47:49.936021-08:00","close_reason":"Fixed two code paths in importer.go and multirepo.go that overwrote pinned field. Tests pass. May need follow-up if bug persists.","labels":["export:pinned-field-fix"],"dependencies":[{"issue_id":"bd-phtv","depends_on_id":"bd-iz5t","type":"parent-child","created_at":"2025-12-23T12:44:07.140151-08:00","created_by":"daemon"}]} {"id":"bd-phwd","title":"Add timeout message for long-running git push operations","description":"When git push hangs waiting for credential/browser auth, show a periodic message to the user instead of appearing frozen. Add timeout messaging after N seconds of inactivity during git operations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-21T11:44:57.318984535-07:00","updated_at":"2025-12-21T11:46:05.218023559-07:00","closed_at":"2025-12-21T11:46:05.218023559-07:00"} {"id":"bd-psg","title":"Add tests for dependency management","description":"Key dependency functions like mergeBidirectionalTrees, GetDependencyTree, and DetectCycles have low or no coverage. These are essential for maintaining data integrity in the dependency graph.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-18T07:00:43.458548462-07:00","updated_at":"2025-12-19T09:54:57.018745301-07:00","closed_at":"2025-12-18T10:24:56.271508339-07:00","dependencies":[{"issue_id":"bd-psg","depends_on_id":"bd-6ss","type":"discovered-from","created_at":"2025-12-18T07:00:43.463910911-07:00","created_by":"matt"}]} @@ -541,6 +549,7 @@ {"id":"bd-uutv","title":"Work on beads-rs0: Namepool configuration for themed pole...","description":"Work on beads-rs0: Namepool configuration for themed polecat names. See bd show beads-rs0 for full details.","status":"closed","priority":2,"issue_type":"task","assignee":"beads/polecat-02","created_at":"2025-12-19T21:49:48.129778-08:00","updated_at":"2025-12-19T21:59:25.565894-08:00","closed_at":"2025-12-19T21:59:25.565894-08:00","close_reason":"Completed work on beads-rs0: Implemented themed namepool feature"} {"id":"bd-uwkp","title":"Phase 2.4: Git merge driver optimization for TOON format","description":"Optimize git 3-way merge for TOON line-oriented format.\n\n## Overview\nTOON is line-oriented (unlike binary formats), enabling smarter git merge strategies. Implement custom merge driver to handle TOON-specific merge patterns.\n\n## Required Work\n\n### 2.4.1 TOON Merge Driver\n- [ ] Create .git/info/attributes entry for *.toon files\n- [ ] Implement custom merge driver script/command\n- [ ] Handle tabular format row merges (line-based 3-way)\n- [ ] Handle YAML-style format merges\n- [ ] Conflict markers for unsolvable conflicts\n\n### 2.4.2 Merge Patterns\n- [ ] Row addition: both branches add different rows → union\n- [ ] Row deletion: one branch deletes, other modifies → conflict (manual review)\n- [ ] Row modification: concurrent field changes → intelligent merge or conflict\n- [ ] Field ordering changes: ignore (TOON format resilient to order)\n\n### 2.4.3 Testing \u0026 Documentation\n- [ ] Unit tests for merge scenarios (3-way merge logic)\n- [ ] Integration tests with actual git merges\n- [ ] Conflict scenario testing\n- [ ] Documentation of merge strategy\n\n## Success Criteria\n- Git merge handles TOON conflicts intelligently\n- Fewer manual merge conflicts than JSONL\n- Round-trip preserved through merges\n- All 70+ tests still passing\n- Git history stays clean (minimal conflict markers)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T14:43:14.339238776-07:00","updated_at":"2025-12-21T14:42:26.434306-08:00","closed_at":"2025-12-21T14:42:26.434306-08:00","close_reason":"TOON approach declined","dependencies":[{"issue_id":"bd-uwkp","depends_on_id":"bd-iic1","type":"discovered-from","created_at":"2025-12-19T14:43:14.34427988-07:00","created_by":"daemon"}]} {"id":"bd-uz8r","title":"Phase 2.3: TOON deletion tracking","description":"Implement deletion tracking in TOON format.\n\n## Overview\nPhase 2.2 switched storage to TOON format. Phase 2.3 adds deletion tracking in TOON format for propagating deletions across clones.\n\n## Required Work\n\n### 2.3.1 Deletion Tracking (TOON Format)\n- [ ] Implement deletions.toon file (tracking deleted issue records)\n- [ ] Add DeleteTracker struct to record deleted issue IDs and metadata\n- [ ] Update bdt delete command to record in deletions.toon\n- [ ] Design deletion record format (ID, timestamp, reason, hash)\n- [ ] Implement auto-prune of old deletion records (configurable TTL)\n\n### 2.3.2 Sync Propagation\n- [ ] Load deletions.toon during import\n- [ ] Remove deleted issues from local database when imported from remote\n- [ ] Handle edge cases (delete same issue in multiple clones)\n- [ ] Deletion ordering and conflict resolution\n\n### 2.3.3 Testing\n- [ ] Unit tests for deletion tracking\n- [ ] Integration tests for deletion propagation\n- [ ] Multi-clone deletion scenarios\n- [ ] TTL expiration tests\n\n## Success Criteria\n- deletions.toon stores deletion records in TOON format\n- Deletions propagate across clones via git sync\n- Old records auto-prune after TTL\n- All 70+ tests still passing\n- bdt delete command works seamlessly","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T14:37:23.722066816-07:00","updated_at":"2025-12-21T14:42:27.491932-08:00","closed_at":"2025-12-21T14:42:27.491932-08:00","close_reason":"TOON approach declined","dependencies":[{"issue_id":"bd-uz8r","depends_on_id":"bd-iic1","type":"discovered-from","created_at":"2025-12-19T14:37:23.726825771-07:00","created_by":"daemon"}]} +{"id":"bd-v8ku","title":"bd: Add town-level activity signal in PersistentPreRun","description":"Add activity signaling to beads so Gas Town daemon can detect bd usage.\n\nIn cmd/bd/main.go PersistentPreRun, add a call to write activity to\nthe Gas Town daemon directory if running inside a Gas Town workspace.\n\nThe signal file is ~/gt/daemon/activity.json (or detected town root).\n\nFormat:\n{\n \"last_command\": \"bd create ...\",\n \"actor\": \"gastown/crew/max\",\n \"timestamp\": \"2025-12-26T19:30:00Z\"\n}\n\nShould be best-effort (silent failure) to avoid breaking bd outside Gas Town.\n\nCross-rig ref: gastown gt-ws8ol (Deacon exponential backoff epic)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-26T19:25:13.537055-08:00","updated_at":"2025-12-26T19:28:44.919491-08:00","closed_at":"2025-12-26T19:28:44.919491-08:00","close_reason":"Implemented activity signaling in PersistentPreRun"} {"id":"bd-vgi5","title":"Push version bump to GitHub","description":"git push origin main - triggers CI but no release yet.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-18T22:43:05.363604-08:00","updated_at":"2025-12-24T16:25:30.019895-08:00","dependencies":[{"issue_id":"bd-vgi5","depends_on_id":"bd-qqc","type":"parent-child","created_at":"2025-12-18T22:43:16.87736-08:00","created_by":"daemon"},{"issue_id":"bd-vgi5","depends_on_id":"bd-3ggb","type":"blocks","created_at":"2025-12-18T22:43:21.078208-08:00","created_by":"daemon"}],"deleted_at":"2025-12-24T16:25:30.019895-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} {"id":"bd-vks2","title":"bd dep tree doesn't display external dependencies","description":"GetDependencyTree (dependencies.go:464-624) uses a recursive CTE that JOINs with the issues table, which means external refs (external:project:capability) are invisible in the tree output.\n\nWhen an issue has an external blocking dependency, running 'bd dep tree \u003cid\u003e' won't show it.\n\nOptions:\n1. Query dependencies table separately for external refs and display them as leaf nodes\n2. Add a synthetic 'external' node type that shows the ref and resolution status\n3. Document that external deps aren't shown in tree view (use bd show for full deps)\n\nLower priority since bd show \u003cid\u003e displays all dependencies including external refs.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-21T23:45:27.121934-08:00","updated_at":"2025-12-22T22:30:19.083652-08:00","closed_at":"2025-12-22T22:30:19.083652-08:00","close_reason":"Implemented: GetDependencyTree now fetches external deps and adds them as synthetic leaf nodes with resolution status. Added test TestGetDependencyTreeExternalDeps. Updated formatTreeNode to display external deps specially.","dependencies":[{"issue_id":"bd-vks2","depends_on_id":"bd-zmmy","type":"discovered-from","created_at":"2025-12-21T23:45:27.122511-08:00","created_by":"daemon"}]} {"id":"bd-vpan","title":"Re: Thread Test 2","description":"Got your message. Testing reply feature.","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:29.144352-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","dependencies":[{"issue_id":"bd-vpan","depends_on_id":"bd-x36g","type":"replies-to","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"} diff --git a/cmd/bd/autoflush.go b/cmd/bd/autoflush.go index 8d3902a9..73f571c8 100644 --- a/cmd/bd/autoflush.go +++ b/cmd/bd/autoflush.go @@ -671,7 +671,7 @@ func flushToJSONLWithState(state flushState) { issues := make([]*types.Issue, 0, len(issueMap)) wispsSkipped := 0 for _, issue := range issueMap { - if issue.Wisp { + if issue.Ephemeral { wispsSkipped++ continue } diff --git a/cmd/bd/cleanup.go b/cmd/bd/cleanup.go index be42c60b..dd7fe371 100644 --- a/cmd/bd/cleanup.go +++ b/cmd/bd/cleanup.go @@ -15,7 +15,7 @@ type CleanupEmptyResponse struct { DeletedCount int `json:"deleted_count"` Message string `json:"message"` Filter string `json:"filter,omitempty"` - Wisp bool `json:"wisp,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` } // Hard delete mode: bypass tombstone TTL safety, use --older-than days directly @@ -56,7 +56,7 @@ Delete issues closed more than 30 days ago: bd cleanup --older-than 30 --force Delete only closed wisps (transient molecules): - bd cleanup --wisp --force + bd cleanup --ephemeral --force Preview what would be deleted/pruned: bd cleanup --dry-run @@ -80,7 +80,7 @@ SEE ALSO: cascade, _ := cmd.Flags().GetBool("cascade") olderThanDays, _ := cmd.Flags().GetInt("older-than") hardDelete, _ := cmd.Flags().GetBool("hard") - wispOnly, _ := cmd.Flags().GetBool("wisp") + wispOnly, _ := cmd.Flags().GetBool("ephemeral") // Calculate custom TTL for --hard mode // When --hard is set, use --older-than days as the tombstone TTL cutoff @@ -129,7 +129,7 @@ SEE ALSO: // Add wisp filter if specified (bd-kwro.9) if wispOnly { wispTrue := true - filter.Wisp = &wispTrue + filter.Ephemeral = &wispTrue } // Get all closed issues matching filter @@ -165,7 +165,7 @@ SEE ALSO: result.Filter = fmt.Sprintf("older than %d days", olderThanDays) } if wispOnly { - result.Wisp = true + result.Ephemeral = true } outputJSON(result) } else { @@ -270,6 +270,6 @@ func init() { cleanupCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues") cleanupCmd.Flags().Int("older-than", 0, "Only delete issues closed more than N days ago (0 = all closed issues)") cleanupCmd.Flags().Bool("hard", false, "Bypass tombstone TTL safety; use --older-than days as cutoff") - cleanupCmd.Flags().Bool("wisp", false, "Only delete closed wisps (transient molecules)") + cleanupCmd.Flags().Bool("ephemeral", false, "Only delete closed wisps (transient molecules)") rootCmd.AddCommand(cleanupCmd) } diff --git a/cmd/bd/cook.go b/cmd/bd/cook.go index c2fa4652..3d19ef68 100644 --- a/cmd/bd/cook.go +++ b/cmd/bd/cook.go @@ -353,7 +353,7 @@ func runCook(cmd *cobra.Command, args []string) { if len(bondPoints) > 0 { fmt.Printf(" Bond points: %s\n", strings.Join(bondPoints, ", ")) } - fmt.Printf("\nTo use: bd pour %s --var =\n", result.ProtoID) + fmt.Printf("\nTo use: bd mol pour %s --var =\n", result.ProtoID) } // cookFormulaResult holds the result of cooking diff --git a/cmd/bd/create.go b/cmd/bd/create.go index 359923b6..eb3a45e0 100644 --- a/cmd/bd/create.go +++ b/cmd/bd/create.go @@ -107,7 +107,7 @@ var createCmd = &cobra.Command{ waitsForGate, _ := cmd.Flags().GetString("waits-for-gate") forceCreate, _ := cmd.Flags().GetBool("force") repoOverride, _ := cmd.Flags().GetString("repo") - wisp, _ := cmd.Flags().GetBool("wisp") + wisp, _ := cmd.Flags().GetBool("ephemeral") // Get estimate if provided var estimatedMinutes *int @@ -222,7 +222,7 @@ var createCmd = &cobra.Command{ Dependencies: deps, WaitsFor: waitsFor, WaitsForGate: waitsForGate, - Wisp: wisp, + Ephemeral: wisp, CreatedBy: getActorWithGit(), } @@ -268,7 +268,7 @@ var createCmd = &cobra.Command{ Assignee: assignee, ExternalRef: externalRefPtr, EstimatedMinutes: estimatedMinutes, - Wisp: wisp, + Ephemeral: wisp, CreatedBy: getActorWithGit(), // GH#748: track who created the issue } @@ -448,7 +448,7 @@ func init() { createCmd.Flags().Bool("force", false, "Force creation even if prefix doesn't match database prefix") createCmd.Flags().String("repo", "", "Target repository for issue (overrides auto-routing)") createCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)") - createCmd.Flags().Bool("wisp", false, "Create as wisp (ephemeral, not exported to JSONL)") + createCmd.Flags().Bool("ephemeral", false, "Create as ephemeral (ephemeral, not exported to JSONL)") // Note: --json flag is defined as a persistent flag in main.go, not here rootCmd.AddCommand(createCmd) } diff --git a/cmd/bd/dep.go b/cmd/bd/dep.go index c2657bdb..fb283d19 100644 --- a/cmd/bd/dep.go +++ b/cmd/bd/dep.go @@ -5,9 +5,11 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "github.com/spf13/cobra" + "github.com/steveyegge/beads/internal/routing" "github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" @@ -15,6 +17,14 @@ import ( "github.com/steveyegge/beads/internal/utils" ) +// getBeadsDir returns the .beads directory path, derived from the global dbPath. +func getBeadsDir() string { + if dbPath != "" { + return filepath.Dir(dbPath) + } + return "" +} + // isChildOf returns true if childID is a hierarchical child of parentID. // For example, "bd-abc.1" is a child of "bd-abc", and "bd-abc.1.2" is a child of "bd-abc.1". func isChildOf(childID, parentID string) bool { @@ -88,9 +98,15 @@ Examples: resolveArgs = &rpc.ResolveIDArgs{ID: args[1]} resp, err = daemonClient.ResolveID(resolveArgs) if err != nil { - FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err) - } - if err := json.Unmarshal(resp.Data, &toID); err != nil { + // Resolution failed - try auto-converting to external ref (bd-lfiu) + beadsDir := getBeadsDir() + if extRef := routing.ResolveToExternalRef(args[1], beadsDir); extRef != "" { + toID = extRef + isExternalRef = true + } else { + FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err) + } + } else if err := json.Unmarshal(resp.Data, &toID); err != nil { FatalErrorRespectJSON("unmarshaling resolved ID: %v", err) } } @@ -111,7 +127,14 @@ Examples: } else { toID, err = utils.ResolvePartialID(ctx, store, args[1]) if err != nil { - FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err) + // Resolution failed - try auto-converting to external ref (bd-lfiu) + beadsDir := getBeadsDir() + if extRef := routing.ResolveToExternalRef(args[1], beadsDir); extRef != "" { + toID = extRef + isExternalRef = true + } else { + FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err) + } } } } diff --git a/cmd/bd/export.go b/cmd/bd/export.go index b669f174..4308fd87 100644 --- a/cmd/bd/export.go +++ b/cmd/bd/export.go @@ -362,7 +362,7 @@ Examples: // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. filtered := make([]*types.Issue, 0, len(issues)) for _, issue := range issues { - if !issue.Wisp { + if !issue.Ephemeral { filtered = append(filtered, issue) } } diff --git a/cmd/bd/gate.go b/cmd/bd/gate.go index e86fb220..eace0e43 100644 --- a/cmd/bd/gate.go +++ b/cmd/bd/gate.go @@ -157,7 +157,7 @@ Examples: Status: types.StatusOpen, Priority: 1, // Gates are typically high priority // Assignee left empty - orchestrator decides who processes gates - Wisp: true, // Gates are wisps (ephemeral) + Ephemeral: true, // Gates are wisps (ephemeral) AwaitType: awaitType, AwaitID: awaitID, Timeout: timeout, diff --git a/cmd/bd/hook.go b/cmd/bd/hook.go index 9e098fa0..99482589 100644 --- a/cmd/bd/hook.go +++ b/cmd/bd/hook.go @@ -87,8 +87,8 @@ func runHook(cmd *cobra.Command, args []string) { for _, issue := range issues { phase := "mol" - if issue.Wisp { - phase = "wisp" + if issue.Ephemeral { + phase = "ephemeral" } fmt.Printf(" 📌 %s (%s) - %s\n", issue.ID, phase, issue.Status) fmt.Printf(" %s\n", issue.Title) diff --git a/cmd/bd/info.go b/cmd/bd/info.go index ff8c1094..ca5b3923 100644 --- a/cmd/bd/info.go +++ b/cmd/bd/info.go @@ -292,6 +292,7 @@ var versionChanges = []VersionChange{ Version: "0.37.0", Date: "2025-12-26", Changes: []string{ + "BREAKING: Ephemeral API rename (bd-o18s) - Wisp→Ephemeral: JSON 'wisp'→'ephemeral', bd wisp→bd ephemeral", "NEW: bd gate create/show/list/close/wait (bd-udsi) - Async coordination primitives for agent workflows", "NEW: bd gate eval (gt-twjr5.2) - Evaluate timer gates and GitHub gates (gh:run, gh:pr, mail)", "NEW: bd gate approve (gt-twjr5.4) - Human gate approval command", diff --git a/cmd/bd/mol.go b/cmd/bd/mol.go index 5cbc673a..18abf012 100644 --- a/cmd/bd/mol.go +++ b/cmd/bd/mol.go @@ -20,8 +20,8 @@ import ( // Usage: // bd mol catalog # List available protos // bd mol show # Show proto/molecule structure -// bd pour --var key=value # Instantiate proto → persistent mol -// bd wisp create --var key=value # Instantiate proto → ephemeral wisp +// bd mol pour --var key=value # Instantiate proto → persistent mol +// bd mol wisp --var key=value # Instantiate proto → ephemeral wisp // MoleculeLabel is the label used to identify molecules (templates) // Molecules use the same label as templates - they ARE templates with workflow semantics @@ -48,14 +48,14 @@ The molecule metaphor: - Distilling extracts a proto from an ad-hoc epic Commands: - catalog List available protos - show Show proto/molecule structure and variables - bond Polymorphic combine: proto+proto, proto+mol, mol+mol - distill Extract proto from ad-hoc epic - -See also: - bd pour # Instantiate as persistent mol (liquid phase) - bd wisp create # Instantiate as ephemeral wisp (vapor phase)`, + catalog List available protos + show Show proto/molecule structure and variables + pour Instantiate proto as persistent mol (liquid phase) + wisp Instantiate proto as ephemeral wisp (vapor phase) + bond Polymorphic combine: proto+proto, proto+mol, mol+mol + squash Condense molecule to digest + burn Discard wisp + distill Extract proto from ad-hoc epic`, } // ============================================================================= @@ -72,7 +72,7 @@ func spawnMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSub Vars: vars, Assignee: assignee, Actor: actorName, - Wisp: ephemeral, + Ephemeral: ephemeral, Prefix: prefix, } return cloneSubgraph(ctx, s, subgraph, opts) diff --git a/cmd/bd/mol_bond.go b/cmd/bd/mol_bond.go index 00776f06..e5d2c1bd 100644 --- a/cmd/bd/mol_bond.go +++ b/cmd/bd/mol_bond.go @@ -40,12 +40,12 @@ Bond types: Phase control: By default, spawned protos follow the target's phase: - - Attaching to mol (Wisp=false) → spawns as persistent (Wisp=false) - - Attaching to wisp (Wisp=true) → spawns as ephemeral (Wisp=true) + - Attaching to mol (Ephemeral=false) → spawns as persistent (Ephemeral=false) + - Attaching to ephemeral issue (Ephemeral=true) → spawns as ephemeral (Ephemeral=true) Override with: - --pour Force spawn as liquid (persistent, Wisp=false) - --wisp Force spawn as vapor (ephemeral, Wisp=true, excluded from JSONL export) + --pour Force spawn as liquid (persistent, Ephemeral=false) + --ephemeral Force spawn as vapor (ephemeral, Ephemeral=true, excluded from JSONL export) Dynamic bonding (Christmas Ornament pattern): Use --ref to specify a custom child reference with variable substitution. @@ -57,7 +57,7 @@ Dynamic bonding (Christmas Ornament pattern): Use cases: - Found important bug during patrol? Use --pour to persist it - - Need ephemeral diagnostic on persistent feature? Use --wisp + - Need ephemeral diagnostic on persistent feature? Use --ephemeral - Spawning per-worker arms on a patrol? Use --ref for readable IDs Examples: @@ -66,7 +66,7 @@ Examples: bd mol bond mol-feature bd-abc123 # Attach proto to molecule bd mol bond bd-abc123 bd-def456 # Join two molecules bd mol bond mol-critical-bug wisp-patrol --pour # Persist found bug - bd mol bond mol-temp-check bd-feature --wisp # Ephemeral diagnostic + bd mol bond mol-temp-check bd-feature --ephemeral # Ephemeral diagnostic bd mol bond mol-arm bd-patrol --ref arm-{{name}} --var name=ace # Dynamic child ID`, Args: cobra.ExactArgs(2), Run: runMolBond, @@ -102,20 +102,20 @@ func runMolBond(cmd *cobra.Command, args []string) { customTitle, _ := cmd.Flags().GetString("as") dryRun, _ := cmd.Flags().GetBool("dry-run") varFlags, _ := cmd.Flags().GetStringSlice("var") - wisp, _ := cmd.Flags().GetBool("wisp") + ephemeral, _ := cmd.Flags().GetBool("ephemeral") pour, _ := cmd.Flags().GetBool("pour") childRef, _ := cmd.Flags().GetString("ref") // Validate phase flags are not both set - if wisp && pour { - fmt.Fprintf(os.Stderr, "Error: cannot use both --wisp and --pour\n") + if ephemeral && pour { + fmt.Fprintf(os.Stderr, "Error: cannot use both --ephemeral and --pour\n") os.Exit(1) } - // All issues go in the main store; wisp vs pour determines the Wisp flag - // --wisp: create with Wisp=true (ephemeral, excluded from JSONL export) - // --pour: create with Wisp=false (persistent, exported to JSONL) - // Default: follow target's phase (wisp if target is wisp, otherwise persistent) + // All issues go in the main store; ephemeral vs pour determines the Wisp flag + // --ephemeral: create with Ephemeral=true (ephemeral, excluded from JSONL export) + // --pour: create with Ephemeral=false (persistent, exported to JSONL) + // Default: follow target's phase (ephemeral if target is ephemeral, otherwise persistent) // Validate bond type if bondType != types.BondTypeSequential && bondType != types.BondTypeParallel && bondType != types.BondTypeConditional { @@ -181,8 +181,8 @@ func runMolBond(cmd *cobra.Command, args []string) { fmt.Printf(" B: %s (%s)\n", issueB.Title, operandType(bIsProto)) } fmt.Printf(" Bond type: %s\n", bondType) - if wisp { - fmt.Printf(" Phase override: vapor (--wisp)\n") + if ephemeral { + fmt.Printf(" Phase override: vapor (--ephemeral)\n") } else if pour { fmt.Printf(" Phase override: liquid (--pour)\n") } @@ -240,16 +240,16 @@ func runMolBond(cmd *cobra.Command, args []string) { case aIsProto && !bIsProto: // Pass subgraph directly if cooked from formula if cookedA { - result, err = bondProtoMolWithSubgraph(ctx, store, subgraphA, issueA, issueB, bondType, vars, childRef, actor, wisp, pour) + result, err = bondProtoMolWithSubgraph(ctx, store, subgraphA, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour) } else { - result, err = bondProtoMol(ctx, store, issueA, issueB, bondType, vars, childRef, actor, wisp, pour) + result, err = bondProtoMol(ctx, store, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour) } case !aIsProto && bIsProto: // Pass subgraph directly if cooked from formula if cookedB { - result, err = bondProtoMolWithSubgraph(ctx, store, subgraphB, issueB, issueA, bondType, vars, childRef, actor, wisp, pour) + result, err = bondProtoMolWithSubgraph(ctx, store, subgraphB, issueB, issueA, bondType, vars, childRef, actor, ephemeral, pour) } else { - result, err = bondMolProto(ctx, store, issueA, issueB, bondType, vars, childRef, actor, wisp, pour) + result, err = bondMolProto(ctx, store, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour) } default: result, err = bondMolMol(ctx, store, issueA, issueB, bondType, actor) @@ -273,10 +273,10 @@ func runMolBond(cmd *cobra.Command, args []string) { if result.Spawned > 0 { fmt.Printf(" Spawned: %d issues\n", result.Spawned) } - if wisp { - fmt.Printf(" Phase: vapor (ephemeral, Wisp=true)\n") + if ephemeral { + fmt.Printf(" Phase: vapor (ephemeral, Ephemeral=true)\n") } else if pour { - fmt.Printf(" Phase: liquid (persistent, Wisp=false)\n") + fmt.Printf(" Phase: liquid (persistent, Ephemeral=false)\n") } } @@ -386,12 +386,12 @@ func bondProtoProto(ctx context.Context, s storage.Storage, protoA, protoB *type // bondProtoMol bonds a proto to an existing molecule by spawning the proto. // If childRef is provided, generates custom IDs like "parent.childref" (dynamic bonding). // protoSubgraph can be nil if proto is from DB (will be loaded), or pre-loaded for formulas. -func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, wispFlag, pourFlag bool) (*BondResult, error) { - return bondProtoMolWithSubgraph(ctx, s, nil, proto, mol, bondType, vars, childRef, actorName, wispFlag, pourFlag) +func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) { + return bondProtoMolWithSubgraph(ctx, s, nil, proto, mol, bondType, vars, childRef, actorName, ephemeralFlag, pourFlag) } // bondProtoMolWithSubgraph is the internal implementation that accepts a pre-loaded subgraph. -func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgraph *TemplateSubgraph, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, wispFlag, pourFlag bool) (*BondResult, error) { +func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgraph *TemplateSubgraph, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) { // Use provided subgraph or load from DB subgraph := protoSubgraph if subgraph == nil { @@ -414,20 +414,20 @@ func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgr return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", ")) } - // Determine wisp flag based on explicit flags or target's phase - // --wisp: force wisp=true, --pour: force wisp=false, neither: follow target - makeWisp := mol.Wisp // Default: follow target's phase - if wispFlag { - makeWisp = true + // Determine ephemeral flag based on explicit flags or target's phase + // --ephemeral: force ephemeral=true, --pour: force ephemeral=false, neither: follow target + makeEphemeral := mol.Ephemeral // Default: follow target's phase + if ephemeralFlag { + makeEphemeral = true } else if pourFlag { - makeWisp = false + makeEphemeral = false } // Build CloneOptions for spawning opts := CloneOptions{ Vars: vars, Actor: actorName, - Wisp: makeWisp, + Ephemeral: makeEphemeral, } // Dynamic bonding: use custom IDs if childRef is provided @@ -482,9 +482,9 @@ func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgr } // bondMolProto bonds a molecule to a proto (symmetric with bondProtoMol) -func bondMolProto(ctx context.Context, s storage.Storage, mol, proto *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, wispFlag, pourFlag bool) (*BondResult, error) { +func bondMolProto(ctx context.Context, s storage.Storage, mol, proto *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) { // Same as bondProtoMol but with arguments swapped - return bondProtoMol(ctx, s, proto, mol, bondType, vars, childRef, actorName, wispFlag, pourFlag) + return bondProtoMol(ctx, s, proto, mol, bondType, vars, childRef, actorName, ephemeralFlag, pourFlag) } // bondMolMol bonds two molecules together @@ -630,8 +630,8 @@ func init() { molBondCmd.Flags().String("as", "", "Custom title for compound proto (proto+proto only)") molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created") molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution for spawned protos (key=value)") - molBondCmd.Flags().Bool("wisp", false, "Force spawn as vapor (ephemeral, Wisp=true)") - molBondCmd.Flags().Bool("pour", false, "Force spawn as liquid (persistent, Wisp=false)") + molBondCmd.Flags().Bool("ephemeral", false, "Force spawn as vapor (ephemeral, Ephemeral=true)") + molBondCmd.Flags().Bool("pour", false, "Force spawn as liquid (persistent, Ephemeral=false)") molBondCmd.Flags().String("ref", "", "Custom child reference with {{var}} substitution (e.g., arm-{{polecat_name}})") molCmd.AddCommand(molBondCmd) diff --git a/cmd/bd/mol_burn.go b/cmd/bd/mol_burn.go index 04da097b..6d053389 100644 --- a/cmd/bd/mol_burn.go +++ b/cmd/bd/mol_burn.go @@ -23,8 +23,8 @@ completely removes the wisp with no trace. Use this for: - Test/debug wisps you don't want to preserve The burn operation: - 1. Verifies the molecule has Wisp=true (is ephemeral) - 2. Deletes the molecule and all its wisp children + 1. Verifies the molecule has Ephemeral=true (is ephemeral) + 2. Deletes the molecule and all its ephemeral children 3. No digest is created (use 'bd mol squash' if you want a digest) CAUTION: This is a destructive operation. The wisp's data will be @@ -81,8 +81,8 @@ func runMolBurn(cmd *cobra.Command, args []string) { } // Verify it's a wisp - if !rootIssue.Wisp { - fmt.Fprintf(os.Stderr, "Error: molecule %s is not a wisp (Wisp=false)\n", resolvedID) + if !rootIssue.Ephemeral { + fmt.Fprintf(os.Stderr, "Error: molecule %s is not a wisp (Ephemeral=false)\n", resolvedID) fmt.Fprintf(os.Stderr, "Hint: mol burn only works with wisp molecules\n") fmt.Fprintf(os.Stderr, " Use 'bd delete' to remove non-wisp issues\n") os.Exit(1) @@ -98,7 +98,7 @@ func runMolBurn(cmd *cobra.Command, args []string) { // Collect wisp issue IDs to delete (only delete wisps, not regular children) var wispIDs []string for _, issue := range subgraph.Issues { - if issue.Wisp { + if issue.Ephemeral { wispIDs = append(wispIDs, issue.ID) } } @@ -120,7 +120,7 @@ func runMolBurn(cmd *cobra.Command, args []string) { fmt.Printf("Root: %s\n", subgraph.Root.Title) fmt.Printf("\nWisp issues to delete (%d total):\n", len(wispIDs)) for _, issue := range subgraph.Issues { - if !issue.Wisp { + if !issue.Ephemeral { continue } status := string(issue.Status) @@ -166,7 +166,7 @@ func runMolBurn(cmd *cobra.Command, args []string) { } fmt.Printf("%s Burned wisp: %d issues deleted\n", ui.RenderPass("✓"), result.DeletedCount) - fmt.Printf(" Wisp: %s\n", resolvedID) + fmt.Printf(" Ephemeral: %s\n", resolvedID) fmt.Printf(" No digest created.\n") } diff --git a/cmd/bd/mol_catalog.go b/cmd/bd/mol_catalog.go index e5e1bf14..1b5f4577 100644 --- a/cmd/bd/mol_catalog.go +++ b/cmd/bd/mol_catalog.go @@ -23,7 +23,7 @@ var molCatalogCmd = &cobra.Command{ Use: "catalog", Aliases: []string{"list", "ls"}, Short: "List available molecule formulas", - Long: `List formulas available for bd pour / bd wisp create. + Long: `List formulas available for bd mol pour / bd mol wisp. Formulas are ephemeral proto definitions stored as .formula.json files. They are cooked inline when pouring, never stored as database beads. @@ -92,12 +92,12 @@ Search paths (in priority order): fmt.Println("\nOr distill from existing work:") fmt.Println(" bd mol distill my-workflow") fmt.Println("\nTo instantiate from formula:") - fmt.Println(" bd pour --var key=value # persistent mol") - fmt.Println(" bd wisp create --var key=value # ephemeral wisp") + fmt.Println(" bd mol pour --var key=value # persistent mol") + fmt.Println(" bd mol wisp --var key=value # ephemeral wisp") return } - fmt.Printf("%s\n\n", ui.RenderPass("Formulas (for bd pour / bd wisp create):")) + fmt.Printf("%s\n\n", ui.RenderPass("Formulas (for bd mol pour / bd mol wisp):")) // Group by type for display byType := make(map[string][]CatalogEntry) diff --git a/cmd/bd/mol_current.go b/cmd/bd/mol_current.go index 95a6651f..75e2f07b 100644 --- a/cmd/bd/mol_current.go +++ b/cmd/bd/mol_current.go @@ -100,7 +100,7 @@ The output shows all steps with status indicators: } fmt.Println(".") fmt.Println("\nTo start work on a molecule:") - fmt.Println(" bd pour # Instantiate a molecule from template") + fmt.Println(" bd mol pour # Instantiate a molecule from template") fmt.Println(" bd update --status in_progress # Claim a step") return } diff --git a/cmd/bd/mol_distill.go b/cmd/bd/mol_distill.go index 49e5f834..e07fbd7d 100644 --- a/cmd/bd/mol_distill.go +++ b/cmd/bd/mol_distill.go @@ -225,7 +225,7 @@ func runMolDistill(cmd *cobra.Command, args []string) { fmt.Printf(" Variables: %s\n", strings.Join(result.Variables, ", ")) } fmt.Printf("\nTo instantiate:\n") - fmt.Printf(" bd pour %s", result.FormulaName) + fmt.Printf(" bd mol pour %s", result.FormulaName) for _, v := range result.Variables { fmt.Printf(" --var %s=", v) } diff --git a/cmd/bd/mol_squash.go b/cmd/bd/mol_squash.go index 6b922a5d..b4236b24 100644 --- a/cmd/bd/mol_squash.go +++ b/cmd/bd/mol_squash.go @@ -18,17 +18,17 @@ import ( var molSquashCmd = &cobra.Command{ Use: "squash ", Short: "Compress molecule execution into a digest", - Long: `Squash a molecule's wisp children into a single digest issue. + Long: `Squash a molecule's ephemeral children into a single digest issue. -This command collects all wisp child issues of a molecule (Wisp=true), +This command collects all ephemeral child issues of a molecule (Ephemeral=true), generates a summary digest, and promotes the wisps to persistent by clearing their Wisp flag (or optionally deletes them). The squash operation: 1. Loads the molecule and all its children - 2. Filters to only wisps (ephemeral issues with Wisp=true) + 2. Filters to only wisps (ephemeral issues with Ephemeral=true) 3. Generates a digest (summary of work done) - 4. Creates a permanent digest issue (Wisp=false) + 4. Creates a permanent digest issue (Ephemeral=false) 5. Clears Wisp flag on children (promotes to persistent) OR deletes them with --delete-children @@ -95,13 +95,13 @@ func runMolSquash(cmd *cobra.Command, args []string) { os.Exit(1) } - // Filter to only wisp children (exclude root) + // Filter to only ephemeral children (exclude root) var wispChildren []*types.Issue for _, issue := range subgraph.Issues { if issue.ID == subgraph.Root.ID { continue // Skip root } - if issue.Wisp { + if issue.Ephemeral { wispChildren = append(wispChildren, issue) } } @@ -113,13 +113,13 @@ func runMolSquash(cmd *cobra.Command, args []string) { SquashedCount: 0, }) } else { - fmt.Printf("No wisp children found for molecule %s\n", moleculeID) + fmt.Printf("No ephemeral children found for molecule %s\n", moleculeID) } return } if dryRun { - fmt.Printf("\nDry run: would squash %d wisp children of %s\n\n", len(wispChildren), moleculeID) + fmt.Printf("\nDry run: would squash %d ephemeral children of %s\n\n", len(wispChildren), moleculeID) fmt.Printf("Root: %s\n", subgraph.Root.Title) fmt.Printf("\nWisp children to squash:\n") for _, issue := range wispChildren { @@ -247,7 +247,7 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c CloseReason: fmt.Sprintf("Squashed from %d wisps", len(children)), Priority: root.Priority, IssueType: types.TypeTask, - Wisp: false, // Digest is permanent, not a wisp + Ephemeral: false, // Digest is permanent, not a wisp ClosedAt: &now, } @@ -283,7 +283,7 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c return nil, err } - // Delete wisp children (outside transaction for better error handling) + // Delete ephemeral children (outside transaction for better error handling) if !keepChildren { deleted, err := deleteWispChildren(ctx, s, childIDs) if err != nil { @@ -319,7 +319,7 @@ func deleteWispChildren(ctx context.Context, s storage.Storage, ids []string) (i func init() { molSquashCmd.Flags().Bool("dry-run", false, "Preview what would be squashed") - molSquashCmd.Flags().Bool("keep-children", false, "Don't delete wisp children after squash") + molSquashCmd.Flags().Bool("keep-children", false, "Don't delete ephemeral children after squash") molSquashCmd.Flags().String("summary", "", "Agent-provided summary (bypasses auto-generation)") molCmd.AddCommand(molSquashCmd) diff --git a/cmd/bd/mol_test.go b/cmd/bd/mol_test.go index cf57a18b..a8d18728 100644 --- a/cmd/bd/mol_test.go +++ b/cmd/bd/mol_test.go @@ -489,7 +489,7 @@ func TestSquashMolecule(t *testing.T) { Status: types.StatusClosed, Priority: 2, IssueType: types.TypeTask, - Wisp: true, + Ephemeral: true, CloseReason: "Completed design", } child2 := &types.Issue{ @@ -498,7 +498,7 @@ func TestSquashMolecule(t *testing.T) { Status: types.StatusClosed, Priority: 2, IssueType: types.TypeTask, - Wisp: true, + Ephemeral: true, CloseReason: "Code merged", } @@ -547,7 +547,7 @@ func TestSquashMolecule(t *testing.T) { if err != nil { t.Fatalf("Failed to get digest: %v", err) } - if digest.Wisp { + if digest.Ephemeral { t.Error("Digest should NOT be ephemeral") } if digest.Status != types.StatusClosed { @@ -595,7 +595,7 @@ func TestSquashMoleculeWithDelete(t *testing.T) { Status: types.StatusClosed, Priority: 2, IssueType: types.TypeTask, - Wisp: true, + Ephemeral: true, } if err := s.CreateIssue(ctx, child, "test"); err != nil { t.Fatalf("Failed to create child: %v", err) @@ -705,7 +705,7 @@ func TestSquashMoleculeWithAgentSummary(t *testing.T) { Status: types.StatusClosed, Priority: 2, IssueType: types.TypeTask, - Wisp: true, + Ephemeral: true, CloseReason: "Done", } if err := s.CreateIssue(ctx, child, "test"); err != nil { @@ -1304,14 +1304,14 @@ func TestWispFilteringFromExport(t *testing.T) { Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, - Wisp: false, + Ephemeral: false, } wispIssue := &types.Issue{ Title: "Wisp Issue", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask, - Wisp: true, + Ephemeral: true, } if err := s.CreateIssue(ctx, normalIssue, "test"); err != nil { @@ -1333,7 +1333,7 @@ func TestWispFilteringFromExport(t *testing.T) { // Filter wisp issues (simulating export behavior) exportableIssues := make([]*types.Issue, 0) for _, issue := range allIssues { - if !issue.Wisp { + if !issue.Ephemeral { exportableIssues = append(exportableIssues, issue) } } diff --git a/cmd/bd/nodb.go b/cmd/bd/nodb.go index 5bbcfd15..012ac5d4 100644 --- a/cmd/bd/nodb.go +++ b/cmd/bd/nodb.go @@ -72,8 +72,11 @@ func initializeNoDbMode() error { debug.Logf("using prefix '%s'", prefix) - // Set global store + // Set global store and mark as active (fixes bd comment --no-db) + storeMutex.Lock() store = memStore + storeActive = true + storeMutex.Unlock() return nil } @@ -218,7 +221,7 @@ func writeIssuesToJSONL(memStore *memory.MemoryStorage, beadsDir string) error { // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. filtered := make([]*types.Issue, 0, len(issues)) for _, issue := range issues { - if !issue.Wisp { + if !issue.Ephemeral { filtered = append(filtered, issue) } } diff --git a/cmd/bd/nodb_test.go b/cmd/bd/nodb_test.go index 56be64e9..0063c58e 100644 --- a/cmd/bd/nodb_test.go +++ b/cmd/bd/nodb_test.go @@ -158,6 +158,90 @@ func TestDetectPrefix(t *testing.T) { }) } +func TestInitializeNoDbMode_SetsStoreActive(t *testing.T) { + // This test verifies the fix for bd comment --no-db not working. + // The bug was that initializeNoDbMode() set `store` but not `storeActive`, + // so ensureStoreActive() would try to find a SQLite database. + + tempDir := t.TempDir() + beadsDir := filepath.Join(tempDir, ".beads") + if err := os.MkdirAll(beadsDir, 0o755); err != nil { + t.Fatalf("Failed to create .beads dir: %v", err) + } + + // Create a minimal JSONL file with one issue + jsonlPath := filepath.Join(beadsDir, "issues.jsonl") + content := `{"id":"bd-1","title":"Test Issue","status":"open"} +` + if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil { + t.Fatalf("Failed to write JSONL: %v", err) + } + + // Save and restore global state + oldStore := store + oldStoreActive := storeActive + oldCwd, _ := os.Getwd() + defer func() { + storeMutex.Lock() + store = oldStore + storeActive = oldStoreActive + storeMutex.Unlock() + _ = os.Chdir(oldCwd) + }() + + // Change to temp dir so initializeNoDbMode finds .beads + if err := os.Chdir(tempDir); err != nil { + t.Fatalf("Failed to chdir: %v", err) + } + + // Reset global state + storeMutex.Lock() + store = nil + storeActive = false + storeMutex.Unlock() + + // Initialize no-db mode + if err := initializeNoDbMode(); err != nil { + t.Fatalf("initializeNoDbMode failed: %v", err) + } + + // Verify storeActive is now true + storeMutex.Lock() + active := storeActive + s := store + storeMutex.Unlock() + + if !active { + t.Error("storeActive should be true after initializeNoDbMode") + } + if s == nil { + t.Fatal("store should not be nil after initializeNoDbMode") + } + + // ensureStoreActive should now return immediately without error + if err := ensureStoreActive(); err != nil { + t.Errorf("ensureStoreActive should succeed after initializeNoDbMode: %v", err) + } + + // Verify comments work (this was the failing case) + ctx := rootCtx + comment, err := s.AddIssueComment(ctx, "bd-1", "testuser", "Test comment") + if err != nil { + t.Fatalf("AddIssueComment failed: %v", err) + } + if comment.Text != "Test comment" { + t.Errorf("Expected 'Test comment', got %s", comment.Text) + } + + comments, err := s.GetIssueComments(ctx, "bd-1") + if err != nil { + t.Fatalf("GetIssueComments failed: %v", err) + } + if len(comments) != 1 { + t.Errorf("Expected 1 comment, got %d", len(comments)) + } +} + func TestWriteIssuesToJSONL(t *testing.T) { tempDir := t.TempDir() beadsDir := filepath.Join(tempDir, ".beads") diff --git a/cmd/bd/pour.go b/cmd/bd/pour.go index bcd05968..06b88305 100644 --- a/cmd/bd/pour.go +++ b/cmd/bd/pour.go @@ -32,9 +32,9 @@ Use pour for: - Anything you might need to reference later Examples: - bd pour mol-feature --var name=auth # Create persistent mol from proto - bd pour mol-release --var version=1.0 # Release workflow - bd pour mol-review --var pr=123 # Code review workflow`, + bd mol pour mol-feature --var name=auth # Create persistent mol from proto + bd mol pour mol-release --var version=1.0 # Release workflow + bd mol pour mol-review --var pr=123 # Code review workflow`, Args: cobra.ExactArgs(1), Run: runPour, } @@ -260,5 +260,5 @@ func init() { pourCmd.Flags().StringSlice("attach", []string{}, "Proto to attach after spawning (repeatable)") pourCmd.Flags().String("attach-type", types.BondTypeSequential, "Bond type for attachments: sequential, parallel, or conditional") - rootCmd.AddCommand(pourCmd) + molCmd.AddCommand(pourCmd) } diff --git a/cmd/bd/show.go b/cmd/bd/show.go index a056ddfc..d80cb9a5 100644 --- a/cmd/bd/show.go +++ b/cmd/bd/show.go @@ -305,20 +305,20 @@ var showCmd = &cobra.Command{ fmt.Printf(" ◊ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status) } } + } - if len(details.Comments) > 0 { - fmt.Printf("\nComments (%d):\n", len(details.Comments)) - for _, comment := range details.Comments { - fmt.Printf(" [%s] %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04")) - commentLines := strings.Split(comment.Text, "\n") - for _, line := range commentLines { - fmt.Printf(" %s\n", line) - } + if len(details.Comments) > 0 { + fmt.Printf("\nComments (%d):\n", len(details.Comments)) + for _, comment := range details.Comments { + fmt.Printf(" [%s] %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04")) + commentLines := strings.Split(comment.Text, "\n") + for _, line := range commentLines { + fmt.Printf(" %s\n", line) } } - } + } - fmt.Println() + fmt.Println() } } diff --git a/cmd/bd/sync_export.go b/cmd/bd/sync_export.go index 606ebe6d..ea73b203 100644 --- a/cmd/bd/sync_export.go +++ b/cmd/bd/sync_export.go @@ -65,7 +65,7 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error { // This prevents "zombie" issues that resurrect after mol squash deletes them. filteredIssues := make([]*types.Issue, 0, len(issues)) for _, issue := range issues { - if issue.Wisp { + if issue.Ephemeral { continue } filteredIssues = append(filteredIssues, issue) diff --git a/cmd/bd/template.go b/cmd/bd/template.go index 24e7c2f9..8d4e6e04 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -42,10 +42,10 @@ type InstantiateResult struct { // CloneOptions controls how the subgraph is cloned during spawn/bond type CloneOptions struct { - Vars map[string]string // Variable substitutions for {{key}} placeholders - Assignee string // Assign the root epic to this agent/user - Actor string // Actor performing the operation - Wisp bool // If true, spawned issues are marked for bulk deletion + Vars map[string]string // Variable substitutions for {{key}} placeholders + Assignee string // Assign the root epic to this agent/user + Actor string // Actor performing the operation + Ephemeral bool // If true, spawned issues are marked for bulk deletion Prefix string // Override prefix for ID generation (bd-hobo: distinct prefixes) // Dynamic bonding fields (for Christmas Ornament pattern) @@ -327,7 +327,7 @@ Example: Vars: vars, Assignee: assignee, Actor: actor, - Wisp: false, + Ephemeral: false, } var result *InstantiateResult if daemonClient != nil { @@ -713,7 +713,7 @@ func cloneSubgraphViaDaemon(client *rpc.Client, subgraph *TemplateSubgraph, opts AcceptanceCriteria: substituteVariables(oldIssue.AcceptanceCriteria, opts.Vars), Assignee: issueAssignee, EstimatedMinutes: oldIssue.EstimatedMinutes, - Wisp: opts.Wisp, + Ephemeral: opts.Ephemeral, IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps } @@ -960,7 +960,7 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub IssueType: oldIssue.IssueType, Assignee: issueAssignee, EstimatedMinutes: oldIssue.EstimatedMinutes, - Wisp: opts.Wisp, // bd-2vh3: mark for cleanup when closed + Ephemeral: opts.Ephemeral, // bd-2vh3: mark for cleanup when closed IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps CreatedAt: time.Now(), UpdatedAt: time.Now(), diff --git a/cmd/bd/thread_test.go b/cmd/bd/thread_test.go index a529796a..9b2881fc 100644 --- a/cmd/bd/thread_test.go +++ b/cmd/bd/thread_test.go @@ -27,7 +27,7 @@ func TestThreadTraversal(t *testing.T) { IssueType: types.TypeMessage, Assignee: "worker", Sender: "manager", - Wisp: true, + Ephemeral: true, CreatedAt: now, UpdatedAt: now, } @@ -43,7 +43,7 @@ func TestThreadTraversal(t *testing.T) { IssueType: types.TypeMessage, Assignee: "manager", Sender: "worker", - Wisp: true, + Ephemeral: true, CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute), } @@ -59,7 +59,7 @@ func TestThreadTraversal(t *testing.T) { IssueType: types.TypeMessage, Assignee: "worker", Sender: "manager", - Wisp: true, + Ephemeral: true, CreatedAt: now.Add(2 * time.Minute), UpdatedAt: now.Add(2 * time.Minute), } @@ -190,7 +190,7 @@ func TestThreadTraversalEmptyThread(t *testing.T) { IssueType: types.TypeMessage, Assignee: "user", Sender: "sender", - Wisp: true, + Ephemeral: true, CreatedAt: now, UpdatedAt: now, } @@ -228,7 +228,7 @@ func TestThreadTraversalBranching(t *testing.T) { IssueType: types.TypeMessage, Assignee: "user", Sender: "sender", - Wisp: true, + Ephemeral: true, CreatedAt: now, UpdatedAt: now, } @@ -245,7 +245,7 @@ func TestThreadTraversalBranching(t *testing.T) { IssueType: types.TypeMessage, Assignee: "sender", Sender: "user", - Wisp: true, + Ephemeral: true, CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute), } @@ -261,7 +261,7 @@ func TestThreadTraversalBranching(t *testing.T) { IssueType: types.TypeMessage, Assignee: "sender", Sender: "another-user", - Wisp: true, + Ephemeral: true, CreatedAt: now.Add(2 * time.Minute), UpdatedAt: now.Add(2 * time.Minute), } @@ -364,7 +364,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) { IssueType: types.TypeMessage, Assignee: "user", Sender: "sender", - Wisp: true, + Ephemeral: true, CreatedAt: now, UpdatedAt: now, } @@ -380,7 +380,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) { IssueType: types.TypeMessage, Assignee: "user", Sender: "sender", - Wisp: true, + Ephemeral: true, CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute), } diff --git a/cmd/bd/wisp.go b/cmd/bd/wisp.go index 2ce28e14..b914924a 100644 --- a/cmd/bd/wisp.go +++ b/cmd/bd/wisp.go @@ -18,33 +18,43 @@ import ( // Wisp commands - manage ephemeral molecules // -// Wisps are ephemeral issues with Wisp=true in the main database. +// Wisps are ephemeral issues with Ephemeral=true in the main database. // They're used for patrol cycles and operational loops that shouldn't // be exported to JSONL (and thus not synced via git). // // Commands: -// bd wisp list - List all wisps in current context -// bd wisp gc - Garbage collect orphaned wisps +// bd mol wisp list - List all wisps in current context +// bd mol wisp gc - Garbage collect orphaned wisps var wispCmd = &cobra.Command{ - Use: "wisp", - Short: "Manage ephemeral molecules (wisps)", - Long: `Manage wisps - ephemeral molecules for operational workflows. + Use: "wisp [proto-id]", + Short: "Create or manage wisps (ephemeral molecules)", + Long: `Create or manage wisps - ephemeral molecules for operational workflows. -Wisps are issues with Wisp=true in the main database. They're stored +When called with a proto-id argument, creates a wisp from that proto. +When called with a subcommand (list, gc), manages existing wisps. + +Wisps are issues with Ephemeral=true in the main database. They're stored locally but NOT exported to JSONL (and thus not synced via git). They're used for patrol cycles, operational loops, and other workflows that shouldn't accumulate in the shared issue database. The wisp lifecycle: - 1. Create: bd wisp create or bd create --wisp - 2. Execute: Normal bd operations work on wisps - 3. Squash: bd mol squash (clears Wisp flag, promotes to persistent) - 4. Or burn: bd mol burn (deletes wisp without creating digest) + 1. Create: bd mol wisp or bd create --ephemeral + 2. Execute: Normal bd operations work on wisp issues + 3. Squash: bd mol squash (clears Ephemeral flag, promotes to persistent) + 4. Or burn: bd mol burn (deletes without creating digest) -Commands: +Examples: + bd mol wisp mol-patrol # Create wisp from proto + bd mol wisp list # List all wisps + bd mol wisp gc # Garbage collect old wisps + +Subcommands: list List all wisps in current context gc Garbage collect orphaned wisps`, + Args: cobra.MaximumNArgs(1), + Run: runWisp, } // WispListItem represents a wisp in list output @@ -68,32 +78,44 @@ type WispListResult struct { // OldThreshold is how old a wisp must be to be flagged as old (time-based, for ephemeral cleanup) const OldThreshold = 24 * time.Hour -// wispCreateCmd instantiates a proto as an ephemeral wisp +// runWisp handles the wisp command when called directly with a proto-id +// It delegates to runWispCreate for the actual work +func runWisp(cmd *cobra.Command, args []string) { + if len(args) == 0 { + // No proto-id provided, show help + cmd.Help() + return + } + // Delegate to the create logic + runWispCreate(cmd, args) +} + +// wispCreateCmd instantiates a proto as an ephemeral wisp (kept for backwards compat) var wispCreateCmd = &cobra.Command{ Use: "create ", - Short: "Instantiate a proto as an ephemeral wisp (solid -> vapor)", + Short: "Instantiate a proto as a wisp (solid -> vapor)", Long: `Create a wisp from a proto - sublimation from solid to vapor. This is the chemistry-inspired command for creating ephemeral work from templates. -The resulting wisp is stored in the main database with Wisp=true and NOT exported to JSONL. +The resulting wisp is stored in the main database with Ephemeral=true and NOT exported to JSONL. Phase transition: Proto (solid) -> Wisp (vapor) -Use wisp create for: +Use wisp for: - Patrol cycles (deacon, witness) - Health checks and monitoring - One-shot orchestration runs - Routine operations with no audit value The wisp will: - - Be stored in main database with Wisp=true flag + - Be stored in main database with Ephemeral=true flag - NOT be exported to JSONL (and thus not synced via git) - Either evaporate (burn) or condense to digest (squash) Examples: - bd wisp create mol-patrol # Ephemeral patrol cycle - bd wisp create mol-health-check # One-time health check - bd wisp create mol-diagnostics --var target=db # Diagnostic run`, + bd mol wisp create mol-patrol # Ephemeral patrol cycle + bd mol wisp create mol-health-check # One-time health check + bd mol wisp create mol-diagnostics --var target=db # Diagnostic run`, Args: cobra.ExactArgs(1), Run: runWispCreate, } @@ -107,7 +129,7 @@ func runWispCreate(cmd *cobra.Command, args []string) { if store == nil { if daemonClient != nil { fmt.Fprintf(os.Stderr, "Error: wisp create requires direct database access\n") - fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon wisp create %s ...\n", args[0]) + fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol wisp %s ...\n", args[0]) } else { fmt.Fprintf(os.Stderr, "Error: no database connection\n") } @@ -215,7 +237,7 @@ func runWispCreate(cmd *cobra.Command, args []string) { if dryRun { fmt.Printf("\nDry run: would create wisp with %d issues from proto %s\n\n", len(subgraph.Issues), protoID) - fmt.Printf("Storage: main database (wisp=true, not exported to JSONL)\n\n") + fmt.Printf("Storage: main database (ephemeral=true, not exported to JSONL)\n\n") for _, issue := range subgraph.Issues { newTitle := substituteVariables(issue.Title, vars) fmt.Printf(" - %s (from %s)\n", newTitle, issue.ID) @@ -223,15 +245,15 @@ func runWispCreate(cmd *cobra.Command, args []string) { return } - // Spawn as wisp in main database (ephemeral=true sets Wisp flag, skips JSONL export) - // bd-hobo: Use "wisp" prefix for distinct visual recognition - result, err := spawnMolecule(ctx, store, subgraph, vars, "", actor, true, "wisp") + // Spawn as ephemeral in main database (Ephemeral=true, skips JSONL export) + // bd-hobo: Use "eph" prefix for distinct visual recognition + result, err := spawnMolecule(ctx, store, subgraph, vars, "", actor, true, "eph") if err != nil { fmt.Fprintf(os.Stderr, "Error creating wisp: %v\n", err) os.Exit(1) } - // Wisps are in main db but don't trigger JSONL export (Wisp flag excludes them) + // Wisp issues are in main db but don't trigger JSONL export (Ephemeral flag excludes them) if jsonOutput { type wispCreateResult struct { @@ -286,9 +308,9 @@ func resolvePartialIDDirect(ctx context.Context, partial string) (string, error) var wispListCmd = &cobra.Command{ Use: "list", Short: "List all wisps in current context", - Long: `List all ephemeral molecules (wisps) in the current context. + Long: `List all wisps (ephemeral molecules) in the current context. -Wisps are issues with Wisp=true in the main database. They are stored +Wisps are issues with Ephemeral=true in the main database. They are stored locally but not exported to JSONL (and thus not synced via git). The list shows: @@ -300,12 +322,12 @@ The list shows: Old wisp detection: - Old wisps haven't been updated in 24+ hours - - Use 'bd wisp gc' to clean up old/abandoned wisps + - Use 'bd mol wisp gc' to clean up old/abandoned wisps Examples: - bd wisp list # List all wisps - bd wisp list --json # JSON output for programmatic use - bd wisp list --all # Include closed wisps`, + bd mol wisp list # List all wisps + bd mol wisp list --json # JSON output for programmatic use + bd mol wisp list --all # Include closed wisps`, Run: runWispList, } @@ -327,15 +349,15 @@ func runWispList(cmd *cobra.Command, args []string) { return } - // Query wisps from main database using Wisp filter - wispFlag := true + // Query wisps from main database using Ephemeral filter + ephemeralFlag := true var issues []*types.Issue var err error if daemonClient != nil { // Use daemon RPC resp, rpcErr := daemonClient.List(&rpc.ListArgs{ - Wisp: &wispFlag, + Ephemeral: &ephemeralFlag, }) if rpcErr != nil { err = rpcErr @@ -347,7 +369,7 @@ func runWispList(cmd *cobra.Command, args []string) { } else { // Direct database access filter := types.IssueFilter{ - Wisp: &wispFlag, + Ephemeral: &ephemeralFlag, } issues, err = store.SearchIssues(ctx, "", filter) } @@ -444,7 +466,7 @@ func runWispList(cmd *cobra.Command, args []string) { if oldCount > 0 { fmt.Printf("\n%s %d old wisp(s) (not updated in 24+ hours)\n", ui.RenderWarn("⚠"), oldCount) - fmt.Println(" Hint: Use 'bd wisp gc' to clean up old wisps") + fmt.Println(" Hint: Use 'bd mol wisp gc' to clean up old wisps") } } @@ -493,10 +515,10 @@ Note: This uses time-based cleanup, appropriate for ephemeral wisps. For graph-pressure staleness detection (blocking other work), see 'bd mol stale'. Examples: - bd wisp gc # Clean abandoned wisps (default: 1h threshold) - bd wisp gc --dry-run # Preview what would be cleaned - bd wisp gc --age 24h # Custom age threshold - bd wisp gc --all # Also clean closed wisps older than threshold`, + bd mol wisp gc # Clean abandoned wisps (default: 1h threshold) + bd mol wisp gc --dry-run # Preview what would be cleaned + bd mol wisp gc --age 24h # Custom age threshold + bd mol wisp gc --all # Also clean closed wisps older than threshold`, Run: runWispGC, } @@ -532,17 +554,17 @@ func runWispGC(cmd *cobra.Command, args []string) { if store == nil { if daemonClient != nil { fmt.Fprintf(os.Stderr, "Error: wisp gc requires direct database access\n") - fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon wisp gc\n") + fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol wisp gc\n") } else { fmt.Fprintf(os.Stderr, "Error: no database connection\n") } os.Exit(1) } - // Query wisps from main database using Wisp filter - wispFlag := true + // Query wisps from main database using Ephemeral filter + ephemeralFlag := true filter := types.IssueFilter{ - Wisp: &wispFlag, + Ephemeral: &ephemeralFlag, } issues, err := store.SearchIssues(ctx, "", filter) if err != nil { @@ -634,7 +656,11 @@ func runWispGC(cmd *cobra.Command, args []string) { } func init() { - // Wisp create command flags + // Wisp command flags (for direct create: bd mol wisp ) + wispCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") + wispCmd.Flags().Bool("dry-run", false, "Preview what would be created") + + // Wisp create command flags (kept for backwards compat: bd mol wisp create ) wispCreateCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") wispCreateCmd.Flags().Bool("dry-run", false, "Preview what would be created") @@ -647,5 +673,5 @@ func init() { wispCmd.AddCommand(wispCreateCmd) wispCmd.AddCommand(wispListCmd) wispCmd.AddCommand(wispGCCmd) - rootCmd.AddCommand(wispCmd) + molCmd.AddCommand(wispCmd) } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7547b967..ce2e4ff8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -275,7 +275,7 @@ open ──▶ in_progress ──▶ closed ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ bd wisp create │───▶│ Wisp Issues │───▶│ bd mol squash │ +│ bd mol wisp │───▶│ Wisp Issues │───▶│ bd mol squash │ │ (from template) │ │ (local-only) │ │ (→ digest) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index 960cd74a..f5b7513c 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -350,8 +350,8 @@ Beads uses a chemistry metaphor for template-based workflows. See [MOLECULES.md] | Phase | State | Storage | Command | |-------|-------|---------|---------| | Solid | Proto | `.beads/` | `bd mol catalog` | -| Liquid | Mol | `.beads/` | `bd pour` | -| Vapor | Wisp | `.beads/` (Wisp=true, not exported) | `bd wisp create` | +| Liquid | Mol | `.beads/` | `bd mol pour` | +| Vapor | Wisp | `.beads/` (Ephemeral=true, not exported) | `bd mol wisp` | ### Proto/Template Commands @@ -370,32 +370,32 @@ bd mol distill --json ```bash # Instantiate proto as persistent mol (solid → liquid) -bd pour --var key=value --json +bd mol pour --var key=value --json # Preview what would be created -bd pour --var key=value --dry-run +bd mol pour --var key=value --dry-run # Assign root issue -bd pour --var key=value --assignee alice --json +bd mol pour --var key=value --assignee alice --json # Attach additional protos during pour -bd pour --attach --json +bd mol pour --attach --json ``` ### Wisp Commands ```bash # Instantiate proto as ephemeral wisp (solid → vapor) -bd wisp create --var key=value --json +bd mol wisp --var key=value --json # List all wisps -bd wisp list --json -bd wisp list --all --json # Include closed +bd mol wisp list --json +bd mol wisp list --all --json # Include closed # Garbage collect orphaned wisps -bd wisp gc --json -bd wisp gc --age 24h --json # Custom age threshold -bd wisp gc --dry-run # Preview what would be cleaned +bd mol wisp gc --json +bd mol wisp gc --age 24h --json # Custom age threshold +bd mol wisp gc --dry-run # Preview what would be cleaned ``` ### Bonding (Combining Work) @@ -424,29 +424,29 @@ bd mol bond --dry-run ```bash # Compress wisp to permanent digest -bd mol squash --json +bd mol squash --json # With agent-provided summary -bd mol squash --summary "Work completed" --json +bd mol squash --summary "Work completed" --json # Preview -bd mol squash --dry-run +bd mol squash --dry-run # Keep wisp children after squash -bd mol squash --keep-children --json +bd mol squash --keep-children --json ``` ### Burn (Discard Wisp) ```bash # Delete wisp without digest (destructive) -bd mol burn --json +bd mol burn --json # Preview -bd mol burn --dry-run +bd mol burn --dry-run # Skip confirmation -bd mol burn --force --json +bd mol burn --force --json ``` **Note:** Most mol commands require `--no-daemon` flag when daemon is running. diff --git a/docs/DELETIONS.md b/docs/DELETIONS.md index 2d067e93..4fd55d97 100644 --- a/docs/DELETIONS.md +++ b/docs/DELETIONS.md @@ -202,7 +202,7 @@ The 1-hour grace period ensures tombstones propagate even with minor clock drift ## Wisps: Intentional Tombstone Bypass -**Wisps** (ephemeral issues created by `bd wisp create`) are intentionally excluded from tombstone tracking. +**Wisps** (ephemeral issues created by `bd mol wisp`) are intentionally excluded from tombstone tracking. ### Why Wisps Don't Need Tombstones diff --git a/docs/MOLECULES.md b/docs/MOLECULES.md index 4dba4cec..172cb178 100644 --- a/docs/MOLECULES.md +++ b/docs/MOLECULES.md @@ -128,8 +128,8 @@ For reusable workflows, beads uses a chemistry metaphor: ### Phase Commands ```bash -bd pour # Proto → Mol (persistent instance) -bd wisp create # Proto → Wisp (ephemeral instance) +bd mol pour # Proto → Mol (persistent instance) +bd mol wisp # Proto → Wisp (ephemeral instance) bd mol squash # Mol/Wisp → Digest (permanent record) bd mol burn # Wisp → nothing (discard) ``` @@ -227,10 +227,10 @@ bd close --reason "Done" Wisps accumulate if not squashed/burned: ```bash -bd wisp list # Check for orphans -bd mol squash # Create digest -bd mol burn # Or discard -bd wisp gc # Garbage collect old wisps +bd mol wisp list # Check for orphans +bd mol squash # Create digest +bd mol burn # Or discard +bd mol wisp gc # Garbage collect old wisps ``` ## Layer Cake Architecture @@ -272,8 +272,8 @@ bd dep tree # Show dependency tree ### Molecules ```bash -bd pour --var k=v # Template → persistent mol -bd wisp create # Template → ephemeral wisp +bd mol pour --var k=v # Template → persistent mol +bd mol wisp # Template → ephemeral wisp bd mol bond A B # Connect work graphs bd mol squash # Compress to digest bd mol burn # Discard without record diff --git a/internal/routing/routes.go b/internal/routing/routes.go index eae2d4ad..59f362ca 100644 --- a/internal/routing/routes.go +++ b/internal/routing/routes.go @@ -67,6 +67,49 @@ func ExtractPrefix(id string) string { return id[:idx+1] // Include the hyphen } +// ExtractProjectFromPath extracts the project name from a route path. +// For "beads/mayor/rig", returns "beads". +// For "gastown/crew/max", returns "gastown". +func ExtractProjectFromPath(path string) string { + // Get the first component of the path + parts := strings.Split(path, "/") + if len(parts) > 0 && parts[0] != "" { + return parts[0] + } + return "" +} + +// ResolveToExternalRef attempts to convert a foreign issue ID to an external reference +// using routes.jsonl for prefix-based routing. +// +// If the ID's prefix matches a route, returns "external::". +// Otherwise, returns empty string (no route found). +// +// Example: If routes.jsonl has {"prefix": "bd-", "path": "beads/mayor/rig"} +// then ResolveToExternalRef("bd-abc", beadsDir) returns "external:beads:bd-abc" +func ResolveToExternalRef(id, beadsDir string) string { + routes, err := LoadRoutes(beadsDir) + if err != nil || len(routes) == 0 { + return "" + } + + prefix := ExtractPrefix(id) + if prefix == "" { + return "" + } + + for _, route := range routes { + if route.Prefix == prefix { + project := ExtractProjectFromPath(route.Path) + if project != "" { + return fmt.Sprintf("external:%s:%s", project, id) + } + } + } + + return "" +} + // ResolveBeadsDirForID determines which beads directory contains the given issue ID. // It first checks the local beads directory, then consults routes.jsonl for prefix-based routing. // diff --git a/internal/routing/routing_test.go b/internal/routing/routing_test.go index 97170e88..13e19906 100644 --- a/internal/routing/routing_test.go +++ b/internal/routing/routing_test.go @@ -88,3 +88,57 @@ func TestDetectUserRole_Fallback(t *testing.T) { t.Errorf("DetectUserRole() = %v, want %v (fallback)", role, Contributor) } } + +func TestExtractPrefix(t *testing.T) { + tests := []struct { + id string + want string + }{ + {"gt-abc123", "gt-"}, + {"bd-xyz", "bd-"}, + {"hq-1234", "hq-"}, + {"abc123", ""}, // No hyphen + {"", ""}, // Empty string + {"-abc", "-"}, // Starts with hyphen + } + + for _, tt := range tests { + t.Run(tt.id, func(t *testing.T) { + got := ExtractPrefix(tt.id) + if got != tt.want { + t.Errorf("ExtractPrefix(%q) = %q, want %q", tt.id, got, tt.want) + } + }) + } +} + +func TestExtractProjectFromPath(t *testing.T) { + tests := []struct { + path string + want string + }{ + {"beads/mayor/rig", "beads"}, + {"gastown/crew/max", "gastown"}, + {"simple", "simple"}, + {"", ""}, + {"/absolute/path", ""}, // Starts with /, first component is empty + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + got := ExtractProjectFromPath(tt.path) + if got != tt.want { + t.Errorf("ExtractProjectFromPath(%q) = %q, want %q", tt.path, got, tt.want) + } + }) + } +} + +func TestResolveToExternalRef(t *testing.T) { + // This test is limited since it requires a routes.jsonl file + // Just test that it returns empty string for nonexistent directory + got := ResolveToExternalRef("bd-abc", "/nonexistent/path") + if got != "" { + t.Errorf("ResolveToExternalRef() = %q, want empty string for nonexistent path", got) + } +} diff --git a/internal/rpc/protocol.go b/internal/rpc/protocol.go index 9e228716..e6099c94 100644 --- a/internal/rpc/protocol.go +++ b/internal/rpc/protocol.go @@ -89,11 +89,11 @@ type CreateArgs struct { WaitsFor string `json:"waits_for,omitempty"` // Spawner issue ID to wait for WaitsForGate string `json:"waits_for_gate,omitempty"` // Gate type: all-children or any-children // Messaging fields (bd-kwro) - Sender string `json:"sender,omitempty"` // Who sent this (for messages) - Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed + Sender string `json:"sender,omitempty"` // Who sent this (for messages) + Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading // ID generation (bd-hobo) - IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, wisp, etc.) + IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.) CreatedBy string `json:"created_by,omitempty"` // Who created the issue } @@ -115,8 +115,8 @@ type UpdateArgs struct { RemoveLabels []string `json:"remove_labels,omitempty"` SetLabels []string `json:"set_labels,omitempty"` // Messaging fields (bd-kwro) - Sender *string `json:"sender,omitempty"` // Who sent this (for messages) - Wisp *bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed + Sender *string `json:"sender,omitempty"` // Who sent this (for messages) + Ephemeral *bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed RepliesTo *string `json:"replies_to,omitempty"` // Issue ID for conversation threading // Graph link fields (bd-fu83) RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs @@ -193,8 +193,8 @@ type ListArgs struct { // Parent filtering (bd-yqhh) ParentID string `json:"parent_id,omitempty"` - // Wisp filtering (bd-bkul) - Wisp *bool `json:"wisp,omitempty"` + // Ephemeral filtering (bd-bkul) + Ephemeral *bool `json:"ephemeral,omitempty"` } // CountArgs represents arguments for the count operation diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 0e40a102..6998f4d6 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -81,8 +81,8 @@ func updatesFromArgs(a UpdateArgs) map[string]interface{} { if a.Sender != nil { u["sender"] = *a.Sender } - if a.Wisp != nil { - u["wisp"] = *a.Wisp + if a.Ephemeral != nil { + u["ephemeral"] = *a.Ephemeral } if a.RepliesTo != nil { u["replies_to"] = *a.RepliesTo @@ -176,8 +176,8 @@ func (s *Server) handleCreate(req *Request) Response { EstimatedMinutes: createArgs.EstimatedMinutes, Status: types.StatusOpen, // Messaging fields (bd-kwro) - Sender: createArgs.Sender, - Wisp: createArgs.Wisp, + Sender: createArgs.Sender, + Ephemeral: createArgs.Ephemeral, // NOTE: RepliesTo now handled via replies-to dependency (Decision 004) // ID generation (bd-hobo) IDPrefix: createArgs.IDPrefix, @@ -844,8 +844,8 @@ func (s *Server) handleList(req *Request) Response { filter.ParentID = &listArgs.ParentID } - // Wisp filtering (bd-bkul) - filter.Wisp = listArgs.Wisp + // Ephemeral filtering (bd-bkul) + filter.Ephemeral = listArgs.Ephemeral // Guard against excessive ID lists to avoid SQLite parameter limits const maxIDs = 1000 @@ -1480,7 +1480,7 @@ func (s *Server) handleGateCreate(req *Request) Response { Status: types.StatusOpen, Priority: 1, // Gates are typically high priority Assignee: "deacon/", - Wisp: true, // Gates are wisps (ephemeral) + Ephemeral: true, // Gates are wisps (ephemeral) AwaitType: args.AwaitType, AwaitID: args.AwaitID, Timeout: args.Timeout, diff --git a/internal/storage/sqlite/dependencies.go b/internal/storage/sqlite/dependencies.go index 282da900..11837cc9 100644 --- a/internal/storage/sqlite/dependencies.go +++ b/internal/storage/sqlite/dependencies.go @@ -885,7 +885,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type issue.Sender = sender.String } if wisp.Valid && wisp.Int64 != 0 { - issue.Wisp = true + issue.Ephemeral = true } // Pinned field (bd-7h5) if pinned.Valid && pinned.Int64 != 0 { @@ -1006,7 +1006,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows * issue.Sender = sender.String } if wisp.Valid && wisp.Int64 != 0 { - issue.Wisp = true + issue.Ephemeral = true } // Pinned field (bd-7h5) if pinned.Valid && pinned.Int64 != 0 { diff --git a/internal/storage/sqlite/graph_links_test.go b/internal/storage/sqlite/graph_links_test.go index 72f85908..b81a58c1 100644 --- a/internal/storage/sqlite/graph_links_test.go +++ b/internal/storage/sqlite/graph_links_test.go @@ -295,7 +295,7 @@ func TestRepliesTo(t *testing.T) { IssueType: types.TypeMessage, Sender: "alice", Assignee: "bob", - Wisp: true, + Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -307,7 +307,7 @@ func TestRepliesTo(t *testing.T) { IssueType: types.TypeMessage, Sender: "bob", Assignee: "alice", - Wisp: true, + Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -363,7 +363,7 @@ func TestRepliesTo_Chain(t *testing.T) { IssueType: types.TypeMessage, Sender: "user", Assignee: "inbox", - Wisp: true, + Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -415,7 +415,7 @@ func TestWispField(t *testing.T) { Status: types.StatusOpen, Priority: 2, IssueType: types.TypeMessage, - Wisp: true, + Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -426,7 +426,7 @@ func TestWispField(t *testing.T) { Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask, - Wisp: false, + Ephemeral: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -443,7 +443,7 @@ func TestWispField(t *testing.T) { if err != nil { t.Fatalf("GetIssue failed: %v", err) } - if !savedWisp.Wisp { + if !savedWisp.Ephemeral { t.Error("Wisp issue should have Wisp=true") } @@ -451,7 +451,7 @@ func TestWispField(t *testing.T) { if err != nil { t.Fatalf("GetIssue failed: %v", err) } - if savedPermanent.Wisp { + if savedPermanent.Ephemeral { t.Error("Permanent issue should have Wisp=false") } } @@ -468,7 +468,7 @@ func TestWispFilter(t *testing.T) { Status: types.StatusClosed, // Closed for cleanup test Priority: 2, IssueType: types.TypeMessage, - Wisp: true, + Ephemeral: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -483,7 +483,7 @@ func TestWispFilter(t *testing.T) { Status: types.StatusClosed, Priority: 2, IssueType: types.TypeTask, - Wisp: false, + Ephemeral: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } @@ -497,7 +497,7 @@ func TestWispFilter(t *testing.T) { closedStatus := types.StatusClosed wispFilter := types.IssueFilter{ Status: &closedStatus, - Wisp: &wispTrue, + Ephemeral: &wispTrue, } wispIssues, err := store.SearchIssues(ctx, "", wispFilter) @@ -512,7 +512,7 @@ func TestWispFilter(t *testing.T) { wispFalse := false nonWispFilter := types.IssueFilter{ Status: &closedStatus, - Wisp: &wispFalse, + Ephemeral: &wispFalse, } permanentIssues, err := store.SearchIssues(ctx, "", nonWispFilter) diff --git a/internal/storage/sqlite/issues.go b/internal/storage/sqlite/issues.go index 41d221f3..7c566165 100644 --- a/internal/storage/sqlite/issues.go +++ b/internal/storage/sqlite/issues.go @@ -28,7 +28,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error } wisp := 0 - if issue.Wisp { + if issue.Ephemeral { wisp = 1 } pinned := 0 @@ -94,7 +94,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er } wisp := 0 - if issue.Wisp { + if issue.Ephemeral { wisp = 1 } pinned := 0 diff --git a/internal/storage/sqlite/migrations/028_tombstone_closed_at.go b/internal/storage/sqlite/migrations/028_tombstone_closed_at.go index 82d9353d..966eff72 100644 --- a/internal/storage/sqlite/migrations/028_tombstone_closed_at.go +++ b/internal/storage/sqlite/migrations/028_tombstone_closed_at.go @@ -3,6 +3,7 @@ package migrations import ( "database/sql" "fmt" + "strings" ) // MigrateTombstoneClosedAt updates the closed_at constraint to allow tombstones @@ -22,8 +23,20 @@ func MigrateTombstoneClosedAt(db *sql.DB) error { // SQLite doesn't support ALTER TABLE to modify CHECK constraints // We must recreate the table with the new constraint + // Idempotency check: see if the new CHECK constraint already exists + // The new constraint contains "status = 'tombstone'" which the old one didn't + var tableSql string + err := db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='issues'`).Scan(&tableSql) + if err != nil { + return fmt.Errorf("failed to get issues table schema: %w", err) + } + // If the schema already has the tombstone clause, migration is already applied + if strings.Contains(tableSql, "status = 'tombstone'") || strings.Contains(tableSql, `status = "tombstone"`) { + return nil + } + // Step 0: Drop views that depend on the issues table - _, err := db.Exec(`DROP VIEW IF EXISTS ready_issues`) + _, err = db.Exec(`DROP VIEW IF EXISTS ready_issues`) if err != nil { return fmt.Errorf("failed to drop ready_issues view: %w", err) } @@ -48,6 +61,7 @@ func MigrateTombstoneClosedAt(db *sql.DB) error { assignee TEXT, estimated_minutes INTEGER, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by TEXT DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, closed_at DATETIME, external_ref TEXT, @@ -81,20 +95,73 @@ func MigrateTombstoneClosedAt(db *sql.DB) error { } // Step 2: Copy data from old table to new table - // List all columns explicitly to handle cases where old table has fewer columns - // Note: created_by is added in migration 029, so don't reference it here - _, err = db.Exec(` - INSERT INTO issues_new - SELECT - id, content_hash, title, description, design, acceptance_criteria, notes, - status, priority, issue_type, assignee, estimated_minutes, - created_at, updated_at, closed_at, external_ref, - source_repo, compaction_level, compacted_at, compacted_at_commit, original_size, - deleted_at, deleted_by, delete_reason, original_type, - sender, ephemeral, close_reason, pinned, is_template, - await_type, await_id, timeout_ns, waiters - FROM issues - `) + // We need to check if created_by column exists in the old table + // If not, we insert a default empty string for it + var hasCreatedBy bool + rows, err := db.Query(`PRAGMA table_info(issues)`) + if err != nil { + return fmt.Errorf("failed to get table info: %w", err) + } + for rows.Next() { + var cid int + var name, ctype string + var notnull, pk int + var dflt interface{} + if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { + rows.Close() + return fmt.Errorf("failed to scan table info: %w", err) + } + if name == "created_by" { + hasCreatedBy = true + break + } + } + rows.Close() + + var insertSQL string + if hasCreatedBy { + // Old table has created_by, copy all columns directly + insertSQL = ` + INSERT INTO issues_new ( + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + created_by, updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + ) + SELECT + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + created_by, updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + FROM issues + ` + } else { + // Old table doesn't have created_by, use empty string default + insertSQL = ` + INSERT INTO issues_new ( + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + created_by, updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + ) + SELECT + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + '', updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + FROM issues + ` + } + + _, err = db.Exec(insertSQL) if err != nil { return fmt.Errorf("failed to copy issues data: %w", err) } diff --git a/internal/storage/sqlite/multirepo.go b/internal/storage/sqlite/multirepo.go index 34f37fdb..c2509ff7 100644 --- a/internal/storage/sqlite/multirepo.go +++ b/internal/storage/sqlite/multirepo.go @@ -282,7 +282,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue * err := tx.QueryRowContext(ctx, `SELECT id FROM issues WHERE id = ?`, issue.ID).Scan(&existingID) wisp := 0 - if issue.Wisp { + if issue.Ephemeral { wisp = 1 } pinned := 0 diff --git a/internal/storage/sqlite/multirepo_export.go b/internal/storage/sqlite/multirepo_export.go index 48d1943e..0d79741f 100644 --- a/internal/storage/sqlite/multirepo_export.go +++ b/internal/storage/sqlite/multirepo_export.go @@ -54,7 +54,7 @@ func (s *SQLiteStorage) ExportToMultiRepo(ctx context.Context) (map[string]int, // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. filtered := make([]*types.Issue, 0, len(allIssues)) for _, issue := range allIssues { - if !issue.Wisp { + if !issue.Ephemeral { filtered = append(filtered, issue) } } diff --git a/internal/storage/sqlite/multirepo_test.go b/internal/storage/sqlite/multirepo_test.go index 5741fd60..18d229b4 100644 --- a/internal/storage/sqlite/multirepo_test.go +++ b/internal/storage/sqlite/multirepo_test.go @@ -909,7 +909,7 @@ func TestUpsertPreservesGateFields(t *testing.T) { Status: types.StatusOpen, Priority: 1, IssueType: types.TypeGate, - Wisp: true, + Ephemeral: true, AwaitType: "gh:run", AwaitID: "123456789", Timeout: 30 * 60 * 1000000000, // 30 minutes in nanoseconds diff --git a/internal/storage/sqlite/queries.go b/internal/storage/sqlite/queries.go index 07325aaf..f11fe85f 100644 --- a/internal/storage/sqlite/queries.go +++ b/internal/storage/sqlite/queries.go @@ -349,7 +349,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue, issue.Sender = sender.String } if wisp.Valid && wisp.Int64 != 0 { - issue.Wisp = true + issue.Ephemeral = true } // Pinned field (bd-7h5) if pinned.Valid && pinned.Int64 != 0 { @@ -562,7 +562,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s issue.Sender = sender.String } if wisp.Valid && wisp.Int64 != 0 { - issue.Wisp = true + issue.Ephemeral = true } // Pinned field (bd-7h5) if pinned.Valid && pinned.Int64 != 0 { @@ -1652,8 +1652,8 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t } // Wisp filtering (bd-kwro.9) - if filter.Wisp != nil { - if *filter.Wisp { + if filter.Ephemeral != nil { + if *filter.Ephemeral { whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral' } else { whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)") diff --git a/internal/storage/sqlite/ready.go b/internal/storage/sqlite/ready.go index 29604142..840d3d24 100644 --- a/internal/storage/sqlite/ready.go +++ b/internal/storage/sqlite/ready.go @@ -17,7 +17,8 @@ import ( // Excludes pinned issues which are persistent anchors, not actionable work (bd-92u) func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilter) ([]*types.Issue, error) { whereClauses := []string{ - "i.pinned = 0", // Exclude pinned issues (bd-92u) + "i.pinned = 0", // Exclude pinned issues (bd-92u) + "(i.ephemeral = 0 OR i.ephemeral IS NULL)", // Exclude wisps (hq-t15s) } args := []interface{}{} @@ -399,7 +400,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi issue.Sender = sender.String } if ephemeral.Valid && ephemeral.Int64 != 0 { - issue.Wisp = true + issue.Ephemeral = true } // Pinned field (bd-7h5) if pinned.Valid && pinned.Int64 != 0 { diff --git a/internal/storage/sqlite/schema.go b/internal/storage/sqlite/schema.go index 898b13f4..3eb19f45 100644 --- a/internal/storage/sqlite/schema.go +++ b/internal/storage/sqlite/schema.go @@ -230,6 +230,7 @@ WITH RECURSIVE SELECT i.* FROM issues i WHERE i.status = 'open' + AND (i.ephemeral = 0 OR i.ephemeral IS NULL) AND NOT EXISTS ( SELECT 1 FROM blocked_transitively WHERE issue_id = i.id ); diff --git a/internal/storage/sqlite/transaction.go b/internal/storage/sqlite/transaction.go index b7e4067b..82607f4a 100644 --- a/internal/storage/sqlite/transaction.go +++ b/internal/storage/sqlite/transaction.go @@ -1089,8 +1089,8 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter } // Wisp filtering (bd-kwro.9) - if filter.Wisp != nil { - if *filter.Wisp { + if filter.Ephemeral != nil { + if *filter.Ephemeral { whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral' } else { whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)") @@ -1244,7 +1244,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) { issue.Sender = sender.String } if wisp.Valid && wisp.Int64 != 0 { - issue.Wisp = true + issue.Ephemeral = true } // Pinned field (bd-7h5) if pinned.Valid && pinned.Int64 != 0 { diff --git a/internal/types/types.go b/internal/types/types.go index 105ca94b..5f7beed8 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -44,8 +44,8 @@ type Issue struct { OriginalType string `json:"original_type,omitempty"` // Issue type before deletion (for tombstones) // Messaging fields (bd-kwro): inter-agent communication support - Sender string `json:"sender,omitempty"` // Who sent this (for messages) - Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed + Sender string `json:"sender,omitempty"` // Who sent this (for messages) + Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed // NOTE: RepliesTo, RelatesTo, DuplicateOf, SupersededBy moved to dependencies table // per Decision 004 (Edge Schema Consolidation). Use dependency API instead. @@ -598,8 +598,8 @@ type IssueFilter struct { // Tombstone filtering (bd-1bu) IncludeTombstones bool // If false (default), exclude tombstones from results - // Wisp filtering (bd-kwro.9) - Wisp *bool // Filter by wisp flag (nil = any, true = only wisps, false = only non-wisps) + // Ephemeral filtering (bd-kwro.9) + Ephemeral *bool // Filter by ephemeral flag (nil = any, true = only ephemeral, false = only persistent) // Pinned filtering (bd-7h5) Pinned *bool // Filter by pinned flag (nil = any, true = only pinned, false = only non-pinned) diff --git a/skills/beads/references/MOLECULES.md b/skills/beads/references/MOLECULES.md index 9484b832..4312511d 100644 --- a/skills/beads/references/MOLECULES.md +++ b/skills/beads/references/MOLECULES.md @@ -83,8 +83,8 @@ bd mol spawn mol-release --var version=2.0 # With variable substitution **Chemistry shortcuts:** ```bash -bd pour mol-feature # Shortcut for spawn --pour -bd wisp create mol-patrol # Explicit wisp creation +bd mol pour mol-feature # Shortcut for spawn --pour +bd mol wisp mol-patrol # Explicit wisp creation ``` ### Spawn with Immediate Execution @@ -164,7 +164,7 @@ bd mol bond mol-feature mol-deploy --as "Feature with Deploy" ### Creating Wisps ```bash -bd wisp create mol-patrol # From proto +bd mol wisp mol-patrol # From proto bd mol spawn mol-patrol # Same (spawn defaults to wisp) bd mol spawn mol-check --var target=db # With variables ``` @@ -172,8 +172,8 @@ bd mol spawn mol-check --var target=db # With variables ### Listing Wisps ```bash -bd wisp list # List all wisps -bd wisp list --json # Machine-readable +bd mol wisp list # List all wisps +bd mol wisp list --json # Machine-readable ``` ### Ending Wisps @@ -198,7 +198,7 @@ Use burn for routine work with no archival value. ### Garbage Collection ```bash -bd wisp gc # Clean up orphaned wisps +bd mol wisp gc # Clean up orphaned wisps ``` --- @@ -289,7 +289,7 @@ bd mol spawn mol-weekly-review --pour ```bash # Patrol proto exists -bd wisp create mol-patrol +bd mol wisp mol-patrol # Execute patrol work... @@ -327,10 +327,10 @@ bd mol distill bd-release-epic --as "Release Process" --var version=X.Y.Z | `bd mol distill ` | Extract proto from ad-hoc work | | `bd mol squash ` | Compress wisp children to digest | | `bd mol burn ` | Delete wisp without trace | -| `bd pour ` | Shortcut for `spawn --pour` | -| `bd wisp create ` | Create ephemeral wisp | -| `bd wisp list` | List all wisps | -| `bd wisp gc` | Garbage collect orphaned wisps | +| `bd mol pour ` | Shortcut for `spawn --pour` | +| `bd mol wisp ` | Create ephemeral wisp | +| `bd mol wisp list` | List all wisps | +| `bd mol wisp gc` | Garbage collect orphaned wisps | | `bd ship ` | Publish capability for cross-project deps | --- @@ -347,7 +347,7 @@ bd mol distill bd-release-epic --as "Release Process" --var version=X.Y.Z **"Wisp commands fail"** - Wisps stored in `.beads-wisp/` (separate from `.beads/`) -- Check `bd wisp list` for active wisps +- Check `bd mol wisp list` for active wisps **"External dependency not satisfied"** - Target project must have closed issue with `provides:` label