fix: MCP plugin follows .beads/redirect files (bd-7t9a, gt-tnw)

The _find_beads_db_in_tree() function now follows .beads/redirect files
to find shared beads databases. This is essential for polecat/crew
directories that use redirect files to share a central database.

Changes:
- Added _resolve_beads_redirect() helper function
- Updated _find_beads_db_in_tree() to check for redirect files before
  looking for local .db files
- Added comprehensive tests for redirect functionality

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 00:05:17 -08:00
parent 99692b52f0
commit e20de54cb2
3 changed files with 190 additions and 15 deletions

View File

@@ -378,7 +378,7 @@
{"id":"bd-haxi","title":"Restart running daemons","description":"Kill and restart any running bd daemons to pick up new version: pkill -f 'bd daemon' \u0026\u0026 bd daemon --start","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-21T13:52:33.066262-08:00","updated_at":"2025-12-21T13:53:49.757078-08:00","deleted_at":"2025-12-21T13:53:49.757078-08:00","deleted_by":"stevey","delete_reason":"manual delete","original_type":"task"} {"id":"bd-haxi","title":"Restart running daemons","description":"Kill and restart any running bd daemons to pick up new version: pkill -f 'bd daemon' \u0026\u0026 bd daemon --start","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-21T13:52:33.066262-08:00","updated_at":"2025-12-21T13:53:49.757078-08:00","deleted_at":"2025-12-21T13:53:49.757078-08:00","deleted_by":"stevey","delete_reason":"manual delete","original_type":"task"}
{"id":"bd-haze","title":"Fix beads-9yc: pinned column missing from schema. gt mail...","description":"Fix beads-9yc: pinned column missing from schema. gt mail send fails because some beads DBs lack the pinned column. Add migration to ensure it exists.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T15:05:33.394801-08:00","updated_at":"2025-12-21T15:26:35.171757-08:00","closed_at":"2025-12-21T15:26:35.171757-08:00"} {"id":"bd-haze","title":"Fix beads-9yc: pinned column missing from schema. gt mail...","description":"Fix beads-9yc: pinned column missing from schema. gt mail send fails because some beads DBs lack the pinned column. Add migration to ensure it exists.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T15:05:33.394801-08:00","updated_at":"2025-12-21T15:26:35.171757-08:00","closed_at":"2025-12-21T15:26:35.171757-08:00"}
{"id":"bd-hhv3","title":"Test and document molecular chemistry commands","description":"## Context\n\nImplemented the molecular chemistry UX commands per the design docs:\n- gastown/mayor/rig/docs/molecular-chemistry.md\n- gastown/mayor/rig/docs/chemistry-design-changes.md\n\nCommit: cadf798b\n\n## New Commands to Test\n\n| Command | Purpose |\n|---------|---------|\n| `bd pour \u003cproto\u003e` | Instantiate proto as persistent mol |\n| `bd wisp create \u003cproto\u003e` | Instantiate proto as ephemeral wisp |\n| `bd hook [--agent]` | Inspect what's on an agent's hook |\n\n## Enhanced Commands to Test\n\n| Command | Changes |\n|---------|---------|\n| `bd mol spawn --pour` | New flag, `--persistent` deprecated |\n| `bd mol bond --pour` | Force liquid phase on wisp target |\n| `bd pin --for \u003cagent\u003e --start` | Chemistry workflow support |\n\n## Test Scenarios\n\n1. **bd pour**: Create persistent mol from a proto\n - Verify creates in .beads/ (not .beads-wisp/)\n - Verify variable substitution works\n - Verify --dry-run works\n\n2. **bd wisp create**: Create ephemeral wisp from proto\n - Verify creates in .beads-wisp/\n - Verify bd wisp list shows it\n - Verify bd mol squash works\n - Verify bd mol burn works\n\n3. **bd hook**: Inspect pinned work\n - Pin something, verify bd hook shows it\n - Test --agent flag\n - Test --json output\n\n4. **bd pin --for**: Assign work to agent\n - Verify sets pinned=true\n - Verify sets assignee\n - Verify --start sets status=in_progress\n\n5. **bd mol bond --pour**: Force liquid on wisp target\n - Bond a proto to a wisp with --pour\n - Verify spawned issues are in .beads/\n\n## Documentation\n\n- Update CLAUDE.md with new commands\n- Add examples to --help output (already done)\n- Consider adding to docs/CLI_REFERENCE.md\n\n## Code Review\n\n- Check for edge cases\n- Verify error messages are helpful\n- Ensure --json output is consistent","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T02:22:10.906646-08:00","updated_at":"2025-12-22T02:55:37.983703-08:00","closed_at":"2025-12-22T02:55:37.983703-08:00"} {"id":"bd-hhv3","title":"Test and document molecular chemistry commands","description":"## Context\n\nImplemented the molecular chemistry UX commands per the design docs:\n- gastown/mayor/rig/docs/molecular-chemistry.md\n- gastown/mayor/rig/docs/chemistry-design-changes.md\n\nCommit: cadf798b\n\n## New Commands to Test\n\n| Command | Purpose |\n|---------|---------|\n| `bd pour \u003cproto\u003e` | Instantiate proto as persistent mol |\n| `bd wisp create \u003cproto\u003e` | Instantiate proto as ephemeral wisp |\n| `bd hook [--agent]` | Inspect what's on an agent's hook |\n\n## Enhanced Commands to Test\n\n| Command | Changes |\n|---------|---------|\n| `bd mol spawn --pour` | New flag, `--persistent` deprecated |\n| `bd mol bond --pour` | Force liquid phase on wisp target |\n| `bd pin --for \u003cagent\u003e --start` | Chemistry workflow support |\n\n## Test Scenarios\n\n1. **bd pour**: Create persistent mol from a proto\n - Verify creates in .beads/ (not .beads-wisp/)\n - Verify variable substitution works\n - Verify --dry-run works\n\n2. **bd wisp create**: Create ephemeral wisp from proto\n - Verify creates in .beads-wisp/\n - Verify bd wisp list shows it\n - Verify bd mol squash works\n - Verify bd mol burn works\n\n3. **bd hook**: Inspect pinned work\n - Pin something, verify bd hook shows it\n - Test --agent flag\n - Test --json output\n\n4. **bd pin --for**: Assign work to agent\n - Verify sets pinned=true\n - Verify sets assignee\n - Verify --start sets status=in_progress\n\n5. **bd mol bond --pour**: Force liquid on wisp target\n - Bond a proto to a wisp with --pour\n - Verify spawned issues are in .beads/\n\n## Documentation\n\n- Update CLAUDE.md with new commands\n- Add examples to --help output (already done)\n- Consider adding to docs/CLI_REFERENCE.md\n\n## Code Review\n\n- Check for edge cases\n- Verify error messages are helpful\n- Ensure --json output is consistent","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T02:22:10.906646-08:00","updated_at":"2025-12-22T02:55:37.983703-08:00","closed_at":"2025-12-22T02:55:37.983703-08:00"}
{"id":"bd-hj0s","title":"Add 'convoy' issue type with reactive completion","description":"Add convoy as a new issue type with reactive completion semantics.\n\nBehavior:\n- Convoy has list of tracked issues (via 'tracks' relation)\n- When all tracked issues close (including wontfix), convoy auto-closes\n- Supports cross-prefix tracking (convoy in hq-* tracks gt-*, bd-*)\n\nImplementation:\n- New type: convoy\n- Reactive completion trigger on tracked issue closure\n- Query support: 'bd list --type=convoy'\n\nRelated: hq-7h8jx (Convoy System epic in town beads)","status":"hooked","priority":1,"issue_type":"task","assignee":"beads/crew/dave","created_at":"2025-12-29T18:47:02.011011-08:00","created_by":"mayor","updated_at":"2025-12-29T23:56:30.617961-08:00","dependencies":[{"issue_id":"bd-hj0s","depends_on_id":"bd-3roq","type":"blocks","created_at":"2025-12-29T18:47:10.59211-08:00","created_by":"daemon"}]} {"id":"bd-hj0s","title":"Add 'convoy' issue type with reactive completion","description":"Add convoy as a new issue type with reactive completion semantics.\n\nBehavior:\n- Convoy has list of tracked issues (via 'tracks' relation)\n- When all tracked issues close (including wontfix), convoy auto-closes\n- Supports cross-prefix tracking (convoy in hq-* tracks gt-*, bd-*)\n\nImplementation:\n- New type: convoy\n- Reactive completion trigger on tracked issue closure\n- Query support: 'bd list --type=convoy'\n\nRelated: hq-7h8jx (Convoy System epic in town beads)","status":"closed","priority":1,"issue_type":"task","assignee":"beads/crew/dave","created_at":"2025-12-29T18:47:02.011011-08:00","created_by":"mayor","updated_at":"2025-12-30T00:05:13.515078-08:00","closed_at":"2025-12-30T00:05:13.515078-08:00","close_reason":"Implemented convoy type with reactive completion","dependencies":[{"issue_id":"bd-hj0s","depends_on_id":"bd-3roq","type":"blocks","created_at":"2025-12-29T18:47:10.59211-08:00","created_by":"daemon"}]}
{"id":"bd-hkr6","title":"GH#518: Document bd setup command","description":"bd setup is undiscoverable. Add to README/docs. Currently only findable by grepping source. See GitHub issue #518.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:54.664668-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-hkr6","title":"GH#518: Document bd setup command","description":"bd setup is undiscoverable. Add to README/docs. Currently only findable by grepping source. See GitHub issue #518.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:54.664668-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-hlsw","title":"Add sync resilience guardrails for forced pushes and prefix mismatches","description":"Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-14T10:40:14.872875259-07:00","updated_at":"2025-12-14T10:40:14.872875259-07:00"} {"id":"bd-hlsw","title":"Add sync resilience guardrails for forced pushes and prefix mismatches","description":"Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-14T10:40:14.872875259-07:00","updated_at":"2025-12-14T10:40:14.872875259-07:00"}
{"id":"bd-hlsw.3","title":"Auto-recovery mode (bd sync --auto-recover)","description":"Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.599836875-07:00","updated_at":"2025-12-14T10:40:20.599836875-07:00","dependencies":[{"issue_id":"bd-hlsw.3","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.600435888-07:00","created_by":"daemon"}]} {"id":"bd-hlsw.3","title":"Auto-recovery mode (bd sync --auto-recover)","description":"Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.599836875-07:00","updated_at":"2025-12-14T10:40:20.599836875-07:00","dependencies":[{"issue_id":"bd-hlsw.3","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.600435888-07:00","created_by":"daemon"}]}

View File

@@ -64,9 +64,61 @@ def _register_client_for_cleanup(client: BdClientBase) -> None:
pass pass
def _resolve_beads_redirect(beads_dir: str, workspace_root: str) -> str | None:
"""Follow a .beads/redirect file to the actual beads directory.
Args:
beads_dir: Path to the .beads directory that may contain a redirect
workspace_root: The workspace root directory (parent of beads_dir)
Returns:
Resolved workspace root if redirect is valid, None otherwise
"""
import glob
redirect_path = os.path.join(beads_dir, "redirect")
if not os.path.isfile(redirect_path):
return None
try:
with open(redirect_path, 'r') as f:
redirect_target = f.read().strip()
if not redirect_target:
return None
# Resolve relative to workspace_root (the redirect is written from the perspective
# of being inside workspace_root, not inside workspace_root/.beads)
# e.g., redirect contains "../../mayor/rig/.beads"
# from polecats/capable/, this resolves to mayor/rig/.beads
resolved = os.path.normpath(os.path.join(workspace_root, redirect_target))
if not os.path.isdir(resolved):
logger.debug(f"Redirect target {resolved} does not exist")
return None
# Verify the redirected location has a valid database
db_files = glob.glob(os.path.join(resolved, "*.db"))
valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)]
if not valid_dbs:
logger.debug(f"Redirect target {resolved} has no valid .db files")
return None
# Return the workspace root of the redirected location (parent of .beads)
return os.path.dirname(resolved)
except Exception as e:
logger.debug(f"Failed to follow redirect: {e}")
return None
def _find_beads_db_in_tree(start_dir: str | None = None) -> str | None: def _find_beads_db_in_tree(start_dir: str | None = None) -> str | None:
"""Walk up directory tree looking for .beads/*.db (matches Go CLI behavior). """Walk up directory tree looking for .beads/*.db (matches Go CLI behavior).
Also follows .beads/redirect files to shared beads locations, which is
essential for polecat/crew directories that share a central database.
Args: Args:
start_dir: Starting directory (default: current working directory) start_dir: Starting directory (default: current working directory)
@@ -88,7 +140,13 @@ def _find_beads_db_in_tree(start_dir: str | None = None) -> str | None:
while True: while True:
beads_dir = os.path.join(current, ".beads") beads_dir = os.path.join(current, ".beads")
if os.path.isdir(beads_dir): if os.path.isdir(beads_dir):
# Find any .db file in .beads/ (excluding backups) # First, check for redirect file (polecat/crew directories use this)
redirected = _resolve_beads_redirect(beads_dir, current)
if redirected:
logger.debug(f"Followed redirect from {current} to {redirected}")
return redirected
# No redirect, check for local .db files
db_files = glob.glob(os.path.join(beads_dir, "*.db")) db_files = glob.glob(os.path.join(beads_dir, "*.db"))
valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)] valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)]

View File

@@ -154,3 +154,120 @@ async def test_get_client_env_var_over_auto_detect():
assert client is not None assert client is not None
finally: finally:
current_workspace.reset(token) current_workspace.reset(token)
def test_find_beads_db_follows_redirect():
"""Test that _find_beads_db_in_tree follows .beads/redirect files (gt-tnw).
This is essential for polecat/crew directories that use redirect files
to share a central beads database.
"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create main workspace with actual database
main_dir = Path(tmpdir) / "mayor" / "rig"
main_dir.mkdir(parents=True)
main_beads = main_dir / ".beads"
main_beads.mkdir()
(main_beads / "beads.db").touch()
# Create polecat directory with redirect
polecat_dir = Path(tmpdir) / "polecats" / "capable"
polecat_dir.mkdir(parents=True)
polecat_beads = polecat_dir / ".beads"
polecat_beads.mkdir()
# Write redirect file (relative path from polecat dir)
redirect_file = polecat_beads / "redirect"
redirect_file.write_text("../../mayor/rig/.beads")
# Should find workspace via redirect
result = _find_beads_db_in_tree(str(polecat_dir))
assert result == os.path.realpath(str(main_dir))
def test_find_beads_db_redirect_invalid_target():
"""Test that invalid redirect targets are handled gracefully."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create polecat directory with redirect to nonexistent location
polecat_dir = Path(tmpdir) / "polecats" / "capable"
polecat_dir.mkdir(parents=True)
polecat_beads = polecat_dir / ".beads"
polecat_beads.mkdir()
# Write redirect file pointing to nonexistent location
redirect_file = polecat_beads / "redirect"
redirect_file.write_text("../../nonexistent/.beads")
# Should return None (graceful failure)
result = _find_beads_db_in_tree(str(polecat_dir))
assert result is None
def test_find_beads_db_redirect_empty():
"""Test that empty redirect files are handled gracefully."""
with tempfile.TemporaryDirectory() as tmpdir:
polecat_dir = Path(tmpdir) / "polecats" / "capable"
polecat_dir.mkdir(parents=True)
polecat_beads = polecat_dir / ".beads"
polecat_beads.mkdir()
# Write empty redirect file
redirect_file = polecat_beads / "redirect"
redirect_file.write_text("")
# Should return None (graceful failure)
result = _find_beads_db_in_tree(str(polecat_dir))
assert result is None
def test_find_beads_db_redirect_no_db_at_target():
"""Test that redirects to directories without .db files are handled."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create main workspace WITHOUT database
main_dir = Path(tmpdir) / "mayor" / "rig"
main_dir.mkdir(parents=True)
main_beads = main_dir / ".beads"
main_beads.mkdir() # No .db file!
# Create polecat directory with redirect
polecat_dir = Path(tmpdir) / "polecats" / "capable"
polecat_dir.mkdir(parents=True)
polecat_beads = polecat_dir / ".beads"
polecat_beads.mkdir()
redirect_file = polecat_beads / "redirect"
redirect_file.write_text("../../mayor/rig/.beads")
# Should return None (redirect target has no valid db)
result = _find_beads_db_in_tree(str(polecat_dir))
assert result is None
def test_find_beads_db_prefers_redirect_over_parent():
"""Test that redirect in current dir is followed before walking up."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create two beads locations
# 1. Parent directory with its own database
parent_dir = Path(tmpdir)
parent_beads = parent_dir / ".beads"
parent_beads.mkdir()
(parent_beads / "beads.db").touch()
# 2. Remote directory that redirect points to
remote_dir = Path(tmpdir) / "remote"
remote_dir.mkdir()
remote_beads = remote_dir / ".beads"
remote_beads.mkdir()
(remote_beads / "beads.db").touch()
# Create child directory with redirect to remote
child_dir = Path(tmpdir) / "child"
child_dir.mkdir()
child_beads = child_dir / ".beads"
child_beads.mkdir()
redirect_file = child_beads / "redirect"
redirect_file.write_text("../remote/.beads")
# Should follow redirect (to remote), not walk up to parent
result = _find_beads_db_in_tree(str(child_dir))
assert result == os.path.realpath(str(remote_dir))