beadboard/.agents/skills/create-beads-orchestration/bootstrap.py

928 lines
34 KiB
Python

#!/usr/bin/env python3
"""
Bootstrap script for beads-based orchestration.
Creates:
- .beads/ directory with beads CLI
- .claude/agents/ with agent templates (copied, not generated)
- .claude/hooks/ with hook scripts
- .claude/settings.json with hook configuration
- .mcp.json with provider-delegator configuration (only with --external-providers)
Usage:
python bootstrap.py [--project-name NAME] [--project-dir DIR] [--with-kanban-ui]
Modes:
Default: All agents use Claude Task() directly (claude-only)
--external-providers: Sets up provider_delegator MCP for Codex/Gemini delegation
"""
import os
import sys
import json
import shutil
import stat
import subprocess
try:
import tomllib
except ImportError:
tomllib = None
from pathlib import Path
from datetime import datetime
import random
# Get the directory where this script lives (lean-orchestration repo)
SCRIPT_DIR = Path(__file__).parent.resolve()
TEMPLATES_DIR = SCRIPT_DIR / "templates"
# ============================================================================
# CONFIGURATION
# ============================================================================
CORE_AGENTS = ["scout", "detective", "architect", "scribe", "discovery", "merge-supervisor", "code-reviewer"]
# NOTE: Supervisors are NOT bootstrapped - they are created dynamically by the
# discovery agent which fetches specialists from the external agents directory
# and injects the beads workflow.
# ============================================================================
# PROJECT NAME INFERENCE
# ============================================================================
def infer_project_name(project_dir: Path) -> str:
"""Auto-infer project name from package files or directory name."""
# Try package.json (Node.js)
package_json = project_dir / "package.json"
if package_json.exists():
try:
data = json.loads(package_json.read_text())
if name := data.get("name"):
return name.replace("-", " ").replace("_", " ").title()
except (json.JSONDecodeError, KeyError):
pass
# Try pyproject.toml (Python)
if tomllib:
pyproject = project_dir / "pyproject.toml"
if pyproject.exists():
try:
data = tomllib.loads(pyproject.read_text())
if name := data.get("project", {}).get("name"):
return name.replace("-", " ").replace("_", " ").title()
if name := data.get("tool", {}).get("poetry", {}).get("name"):
return name.replace("-", " ").replace("_", " ").title()
except Exception:
pass
# Try Cargo.toml (Rust)
cargo = project_dir / "Cargo.toml"
if cargo.exists():
try:
data = tomllib.loads(cargo.read_text())
if name := data.get("package", {}).get("name"):
return name.replace("-", " ").replace("_", " ").title()
except Exception:
pass
# Try go.mod (Go)
go_mod = project_dir / "go.mod"
if go_mod.exists():
try:
content = go_mod.read_text()
for line in content.splitlines():
if line.startswith("module "):
module_path = line.split()[1]
name = module_path.split("/")[-1]
return name.replace("-", " ").replace("_", " ").title()
except Exception:
pass
# Fallback to directory name
return project_dir.name.replace("-", " ").replace("_", " ").title()
# ============================================================================
# PLACEHOLDER REPLACEMENT
# ============================================================================
def replace_placeholders(content: str, replacements: dict) -> str:
"""Replace all placeholders in content."""
for placeholder, value in replacements.items():
content = content.replace(placeholder, value)
return content
def copy_and_replace(source: Path, dest: Path, replacements: dict) -> None:
"""Copy file and replace placeholders."""
content = source.read_text()
updated = replace_placeholders(content, replacements)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(updated)
# Preserve executable permissions for shell scripts
if source.suffix == '.sh':
dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
# ============================================================================
# CODEX DELEGATOR SETUP (SHARED LOCATION)
# ============================================================================
# Shared location for provider-delegator (installed once, used by all projects)
SHARED_MCP_DIR = Path.home() / ".claude" / "mcp-servers" / "provider-delegator"
def setup_provider_delegator() -> Path:
"""Set up provider-delegator in shared location (~/.claude/mcp-servers/provider-delegator/).
This installs once and is reused by all projects.
Returns path to venv python.
"""
print("\n[0/8] Setting up provider-delegator (shared)...")
source_dir = SCRIPT_DIR / "mcp-provider-delegator"
venv_dir = SHARED_MCP_DIR / ".venv"
venv_python = venv_dir / "bin" / "python"
# Check if already installed in shared location
if venv_python.exists():
print(f" - Already installed at {SHARED_MCP_DIR}")
return venv_python
# Verify source exists
if not source_dir.exists():
print(f" ERROR: mcp-provider-delegator not found at {source_dir}")
print(" Make sure you cloned the full lean-orchestration repo")
return None
# Check if uv is available
if not shutil.which("uv"):
print(" ERROR: 'uv' not found. Install with: curl -LsSf https://astral.sh/uv/install.sh | sh")
return None
# Create shared directory
print(f" - Installing to {SHARED_MCP_DIR}")
SHARED_MCP_DIR.mkdir(parents=True, exist_ok=True)
# Copy source to shared location
print(" - Copying source files...")
for item in source_dir.iterdir():
if item.name == ".venv":
continue # Skip any existing venv in source
dest = SHARED_MCP_DIR / item.name
if item.is_dir():
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(item, dest)
else:
shutil.copy2(item, dest)
# Create venv using uv
print(" - Creating venv with uv...")
result = subprocess.run(
["uv", "venv", str(venv_dir)],
cwd=SHARED_MCP_DIR,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f" ERROR: Failed to create venv: {result.stderr}")
return None
# Install dependencies
print(" - Installing dependencies...")
result = subprocess.run(
["uv", "pip", "install", "-e", "."],
cwd=SHARED_MCP_DIR,
capture_output=True,
text=True,
env={**os.environ, "VIRTUAL_ENV": str(venv_dir)}
)
if result.returncode != 0:
print(f" ERROR: Failed to install dependencies: {result.stderr}")
return None
print(f" DONE: provider-delegator installed at {SHARED_MCP_DIR}")
return venv_python
# ============================================================================
# BEADS INSTALLATION
# ============================================================================
def install_beads(project_dir: Path, claude_only: bool = False) -> bool:
"""Install beads CLI and initialize .beads directory."""
step = "[1/7]" if claude_only else "[1/8]"
print(f"\n{step} Installing beads...")
beads_dir = project_dir / ".beads"
# Check if beads is already installed globally
beads_installed = shutil.which("bd") is not None
if not beads_installed:
print(" - beads CLI (bd) not found, installing...")
# Try installation methods in order of preference
installed = False
# Method 1: Homebrew (macOS)
if shutil.which("brew") and sys.platform == "darwin":
print(" - Trying Homebrew...")
result = subprocess.run(
["brew", "install", "steveyegge/beads/bd"],
capture_output=True,
text=True
)
if result.returncode == 0:
installed = True
print(" - Installed via Homebrew")
# Method 2: npm (cross-platform)
if not installed and shutil.which("npm"):
print(" - Trying npm...")
result = subprocess.run(
["npm", "install", "-g", "@beads/bd"],
capture_output=True,
text=True
)
if result.returncode == 0:
installed = True
print(" - Installed via npm")
# Method 3: curl install script (Linux/macOS/FreeBSD)
if not installed and sys.platform != "win32":
print(" - Trying curl install script...")
result = subprocess.run(
["bash", "-c", "curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"],
capture_output=True,
text=True
)
if result.returncode == 0:
installed = True
print(" - Installed via curl script")
# Method 4: Go install (if Go is available)
if not installed and shutil.which("go"):
print(" - Trying go install...")
result = subprocess.run(
["go", "install", "github.com/steveyegge/beads/cmd/bd@latest"],
capture_output=True,
text=True
)
if result.returncode == 0:
installed = True
print(" - Installed via go install")
if not installed:
print("\n ERROR: Could not install beads CLI (bd)")
print(" The beads workflow requires the bd command.")
print(" Please install manually: https://github.com/steveyegge/beads#-installation")
print("\n Installation options:")
print(" macOS: brew install steveyegge/beads/bd")
print(" npm: npm install -g @beads/bd")
print(" Go: go install github.com/steveyegge/beads/cmd/bd@latest")
return False
else:
print(" - beads CLI already installed")
beads_installed = True
# Initialize .beads in project
if not beads_dir.exists():
print(" - Initializing .beads directory...")
# Try bd init first
if shutil.which("bd"):
result = subprocess.run(
["bd", "init"],
cwd=project_dir,
capture_output=True,
text=True
)
if result.returncode == 0:
print(" - Initialized via 'bd init'")
else:
# Manual init as fallback
_manual_beads_init(beads_dir)
else:
_manual_beads_init(beads_dir)
else:
print(" - .beads already exists")
# Configure custom 'inreview' status for parallel work workflow
if shutil.which("bd"):
print(" - Configuring custom 'inreview' status...")
result = subprocess.run(
["bd", "config", "set", "status.custom", "inreview"],
cwd=project_dir,
capture_output=True,
text=True
)
if result.returncode == 0:
print(" - Added 'inreview' custom status")
else:
print(f" - Warning: Could not add custom status: {result.stderr}")
print(" DONE: beads setup complete")
return True
def _manual_beads_init(beads_dir: Path):
"""Manually create .beads directory structure."""
beads_dir.mkdir(exist_ok=True)
(beads_dir / "issues.jsonl").touch()
# Create minimal config
config = {
"version": "1",
"mode": "normal"
}
(beads_dir / "config.json").write_text(json.dumps(config, indent=2))
print(" - Created .beads manually")
def setup_memory(project_dir: Path) -> None:
"""Create .beads/memory/ directory with knowledge store and recall script."""
memory_dir = project_dir / ".beads" / "memory"
memory_dir.mkdir(parents=True, exist_ok=True)
# Create empty knowledge store
knowledge_file = memory_dir / "knowledge.jsonl"
if not knowledge_file.exists():
knowledge_file.touch()
print(" - Created .beads/memory/knowledge.jsonl")
# Copy recall script
recall_src = TEMPLATES_DIR / "memory" / "recall.sh"
recall_dest = memory_dir / "recall.sh"
if recall_src.exists():
shutil.copy2(recall_src, recall_dest)
recall_dest.chmod(recall_dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
print(" - Copied .beads/memory/recall.sh")
else:
print(" - WARNING: recall.sh template not found")
# ============================================================================
# RAMS INSTALLATION (Accessibility Review)
# ============================================================================
def install_rams() -> bool:
"""Install RAMS accessibility review tool if not already installed."""
print("\n Checking RAMS (accessibility review tool)...")
# Check if rams is already installed
if shutil.which("rams"):
print(" - RAMS already installed")
return True
print(" - RAMS not found, installing...")
# Install via curl
if sys.platform != "win32":
result = subprocess.run(
["bash", "-c", "curl -fsSL https://rams.ai/install | bash"],
capture_output=True,
text=True
)
if result.returncode == 0:
print(" - RAMS installed successfully")
return True
else:
print(f" - Warning: Could not install RAMS: {result.stderr}")
print(" - Frontend supervisors will still work but RAMS review enforcement may fail")
print(" - Install manually: curl -fsSL https://rams.ai/install | bash")
return False
print(" - Warning: RAMS installation not supported on Windows")
return False
# ============================================================================
# WEB INTERFACE GUIDELINES INSTALLATION
# ============================================================================
def install_web_interface_guidelines() -> bool:
"""Install Web Interface Guidelines review tool if not already installed."""
print("\n Checking Web Interface Guidelines (design review tool)...")
# Check if wig is already installed
if shutil.which("wig"):
print(" - Web Interface Guidelines already installed")
return True
print(" - Web Interface Guidelines not found, installing...")
# Install via curl
if sys.platform != "win32":
result = subprocess.run(
["bash", "-c", "curl -fsSL https://vercel.com/design/guidelines/install | bash"],
capture_output=True,
text=True
)
if result.returncode == 0:
print(" - Web Interface Guidelines installed successfully")
return True
else:
print(f" - Warning: Could not install Web Interface Guidelines: {result.stderr}")
print(" - Frontend supervisors will still work but WIG review enforcement may fail")
print(" - Install manually: curl -fsSL https://vercel.com/design/guidelines/install | bash")
return False
print(" - Warning: Web Interface Guidelines installation not supported on Windows")
return False
# ============================================================================
# AGENTS (TEMPLATE COPYING)
# ============================================================================
def copy_agents(project_dir: Path, project_name: str, claude_only: bool = False, with_kanban_ui: bool = False) -> list:
"""Copy core agent templates from templates/ directory.
NOTE: Supervisors are NOT copied here - they are created dynamically
by the discovery agent based on detected tech stack.
"""
step = "[2/7]" if claude_only else "[2/8]"
print(f"\n{step} Copying core agent templates...")
agents_dir = project_dir / ".claude" / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
agents_template_dir = TEMPLATES_DIR / "agents"
copied = []
# Replacements for templates
replacements = {
"[Project]": project_name,
}
# Copy core agents ONLY (not supervisors)
for agent_file in agents_template_dir.glob("*.md"):
dest = agents_dir / agent_file.name
copy_and_replace(agent_file, dest, replacements)
copied.append(agent_file.name)
print(f" - Copied {agent_file.name}")
# Copy beads workflow injection snippet (used by discovery agent)
# Select API version (with git fallback) or git-only version based on flag
if with_kanban_ui:
beads_workflow_src = TEMPLATES_DIR / "beads-workflow-injection-api.md"
workflow_type = "API + git fallback"
else:
beads_workflow_src = TEMPLATES_DIR / "beads-workflow-injection-git.md"
workflow_type = "git only"
beads_workflow_dest = project_dir / ".claude" / "beads-workflow-injection.md"
if beads_workflow_src.exists():
shutil.copy2(beads_workflow_src, beads_workflow_dest)
print(f" - Copied beads-workflow-injection.md ({workflow_type})")
# Copy UI constraints (used by discovery agent for frontend supervisors)
ui_constraints_src = TEMPLATES_DIR / "ui-constraints.md"
ui_constraints_dest = project_dir / ".claude" / "ui-constraints.md"
if ui_constraints_src.exists():
shutil.copy2(ui_constraints_src, ui_constraints_dest)
print(" - Copied ui-constraints.md")
# Copy frontend reviews requirement (RAMS + Web Interface Guidelines)
frontend_reviews_src = TEMPLATES_DIR / "frontend-reviews-requirement.md"
frontend_reviews_dest = project_dir / ".claude" / "frontend-reviews-requirement.md"
if frontend_reviews_src.exists():
shutil.copy2(frontend_reviews_src, frontend_reviews_dest)
print(" - Copied frontend-reviews-requirement.md")
print(f" DONE: {len(copied)} core agents copied")
print(" NOTE: Supervisors will be created by discovery agent based on tech stack")
return copied
# ============================================================================
# SKILLS (TEMPLATE COPYING)
# ============================================================================
def copy_skills(project_dir: Path, claude_only: bool = False) -> list:
"""Copy skill templates from templates/ directory.
Skills are copied so discovery agent can install them when tech stack is detected.
"""
step = "[3/7]" if claude_only else "[3/8]"
print(f"\n{step} Copying skill templates...")
skills_template_dir = TEMPLATES_DIR / "skills"
if not skills_template_dir.exists():
print(" - No skill templates found, skipping")
return []
skills_dir = project_dir / ".claude" / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
copied = []
for skill_dir in skills_template_dir.iterdir():
if skill_dir.is_dir():
dest_dir = skills_dir / skill_dir.name
if dest_dir.exists():
shutil.rmtree(dest_dir)
shutil.copytree(skill_dir, dest_dir)
copied.append(skill_dir.name)
print(f" - Copied {skill_dir.name}/ skill")
print(f" DONE: {len(copied)} skill templates copied")
return copied
# ============================================================================
# HOOKS (TEMPLATE COPYING)
# ============================================================================
def copy_hooks(project_dir: Path, claude_only: bool = False) -> list:
"""Copy hook templates from templates/ directory.
Args:
project_dir: Target project directory
claude_only: If True, skip provider delegation enforcement hooks
"""
step = "[4/7]" if claude_only else "[4/8]"
print(f"\n{step} Copying hook templates...")
hooks_dir = project_dir / ".claude" / "hooks"
hooks_dir.mkdir(parents=True, exist_ok=True)
hooks_template_dir = TEMPLATES_DIR / "hooks"
copied = []
# Hooks to skip in claude-only mode (none currently - all hooks apply to both modes)
skip_in_claude_only = set()
for hook_file in hooks_template_dir.glob("*.sh"):
# Skip provider enforcement hooks in claude-only mode
if claude_only and hook_file.name in skip_in_claude_only:
print(f" - Skipped {hook_file.name} (claude-only mode)")
continue
dest = hooks_dir / hook_file.name
shutil.copy2(hook_file, dest)
dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
copied.append(hook_file.name)
print(f" - Copied {hook_file.name}")
print(f" DONE: {len(copied)} hooks copied")
return copied
# ============================================================================
# SETTINGS
# ============================================================================
def copy_settings(project_dir: Path, claude_only: bool = False) -> None:
"""Copy settings.json template, optionally removing provider enforcement hooks.
Args:
project_dir: Target project directory
claude_only: If True, remove provider delegation enforcement from settings
"""
step = "[5/7]" if claude_only else "[5/8]"
print(f"\n{step} Copying settings...")
settings_template = TEMPLATES_DIR / "settings.json"
settings_dest = project_dir / ".claude" / "settings.json"
# Settings are the same for both modes now (no provider-specific hooks)
shutil.copy2(settings_template, settings_dest)
if claude_only:
print(" - Copied settings.json (claude-only mode)")
else:
print(" - Copied settings.json")
print(" DONE: settings configured")
# ============================================================================
# CLAUDE.MD
# ============================================================================
def copy_claude_md(project_dir: Path, project_name: str, claude_only: bool = False) -> None:
"""Copy CLAUDE.md template with project name replacement."""
step = "[6/7]" if claude_only else "[6/8]"
print(f"\n{step} Copying CLAUDE.md...")
claude_template = TEMPLATES_DIR / "CLAUDE.md"
claude_dest = project_dir / "CLAUDE.md"
replacements = {"[Project]": project_name}
copy_and_replace(claude_template, claude_dest, replacements)
print(" - Copied CLAUDE.md")
print(" DONE: CLAUDE.md copied")
# ============================================================================
# GITIGNORE
# ============================================================================
def setup_gitignore(project_dir: Path, claude_only: bool = False) -> None:
"""Ensure .beads is in .gitignore. .claude/ is tracked (not ignored)."""
step = "[7/7]" if claude_only else "[7/8]"
print(f"\n{step} Setting up .gitignore...")
gitignore_path = project_dir / ".gitignore"
# Only ignore .beads/ (ephemeral task data) and .mcp.json (user-specific paths)
# .claude/ is tracked so it survives git operations
entries_to_add = [".beads/", ".mcp.json"]
if gitignore_path.exists():
content = gitignore_path.read_text()
lines = content.splitlines()
# Check which entries are missing
missing = []
for entry in entries_to_add:
# Check for exact match or without trailing slash
entry_no_slash = entry.rstrip("/")
if entry not in lines and entry_no_slash not in lines:
missing.append(entry)
if missing:
# Append missing entries
with open(gitignore_path, "a") as f:
# Add newline if file doesn't end with one
if content and not content.endswith("\n"):
f.write("\n")
f.write("\n# Beads task tracking (ephemeral)\n")
for entry in missing:
f.write(f"{entry}\n")
print(f" - Added {entry} to .gitignore")
else:
print(" - .beads/ and .mcp.json already in .gitignore")
else:
# Create new .gitignore
content = """# Beads task tracking (ephemeral)
.beads/
# MCP config (user-specific paths)
.mcp.json
"""
gitignore_path.write_text(content)
print(" - Created .gitignore with .beads/ and .mcp.json")
print(" DONE: .gitignore configured")
print(" NOTE: .claude/ is tracked (not ignored) to prevent accidental loss")
# ============================================================================
# MCP CONFIG
# ============================================================================
def create_mcp_config(project_dir: Path, venv_python: Path) -> None:
"""Add provider-delegator to .mcp.json, preserving existing servers."""
print("\n[8/8] Configuring MCP...")
mcp_dest = project_dir / ".mcp.json"
# Load existing config or start fresh
if mcp_dest.exists():
try:
existing = json.loads(mcp_dest.read_text())
print(" - Found existing .mcp.json, merging...")
except json.JSONDecodeError:
print(" - Warning: Invalid .mcp.json, creating new one")
existing = {}
else:
existing = {}
# Ensure mcpServers key exists
if "mcpServers" not in existing:
existing["mcpServers"] = {}
# Add/update provider_delegator
existing["mcpServers"]["provider_delegator"] = {
"type": "stdio",
"command": str(venv_python),
"args": ["-m", "mcp_provider_delegator.server"],
"env": {
"AGENT_TEMPLATES_PATH": ".claude/agents"
}
}
mcp_dest.write_text(json.dumps(existing, indent=2))
server_count = len(existing["mcpServers"])
print(f" - Added provider-delegator to .mcp.json ({server_count} total servers)")
print(f" Command: {venv_python}")
print(f" Agents: .claude/agents (relative)")
print(" DONE: MCP config updated")
# ============================================================================
# VERIFICATION
# ============================================================================
def verify_installation(project_dir: Path, claude_only: bool = False) -> bool:
"""Verify all components were installed correctly."""
checks = {
".claude/hooks": "Hooks directory",
".claude/agents": "Agents directory",
".claude/settings.json": "Settings file",
".beads": "Beads directory",
"CLAUDE.md": "CLAUDE.md",
".gitignore": ".gitignore",
}
# Only check for .mcp.json in external providers mode
if not claude_only:
checks[".mcp.json"] = "MCP config"
print("\n=== Verification ===")
all_good = True
for path, description in checks.items():
full_path = project_dir / path
if full_path.exists():
print(f" - {description}")
else:
print(f" X {description} MISSING")
all_good = False
# Count files
hooks_dir = project_dir / ".claude/hooks"
if hooks_dir.exists():
hook_count = len(list(hooks_dir.glob("*.sh")))
print(f" - Hooks: {hook_count}")
agents_dir = project_dir / ".claude/agents"
if agents_dir.exists():
agent_count = len(list(agents_dir.glob("*.md")))
print(f" - Agents: {agent_count}")
skills_dir = project_dir / ".claude/skills"
if skills_dir.exists():
skill_count = len(list(skills_dir.iterdir()))
if skill_count > 0:
print(f" - Skills: {skill_count}")
return all_good
# ============================================================================
# MAIN
# ============================================================================
def main():
import argparse
parser = argparse.ArgumentParser(description="Bootstrap beads-based orchestration")
parser.add_argument("--project-name", default=None, help="Project name (auto-inferred if not provided)")
parser.add_argument("--project-dir", default=".", help="Project directory")
parser.add_argument("--external-providers", action="store_true",
help="Use Codex/Gemini for delegation (default: Claude-only)")
parser.add_argument("--with-kanban-ui", action="store_true",
help="Use Beads Kanban UI API for worktree creation (with git fallback)")
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
claude_only = not args.external_providers # Default is now claude-only
with_kanban_ui = args.with_kanban_ui
# Ensure project directory exists
project_dir.mkdir(parents=True, exist_ok=True)
# Auto-infer project name if not provided
if args.project_name:
project_name = args.project_name
else:
project_name = infer_project_name(project_dir)
print(f"Auto-inferred project name: {project_name}")
mode_str = "CLAUDE-ONLY" if claude_only else "EXTERNAL PROVIDERS"
worktree_str = "API + git fallback" if with_kanban_ui else "git only"
print(f"\nBootstrapping beads orchestration for: {project_name}")
print(f"Directory: {project_dir}")
print(f"Mode: {mode_str}")
print(f"Worktrees: {worktree_str}")
print("=" * 60)
# Verify templates exist
if not TEMPLATES_DIR.exists():
print(f"\nERROR: Templates directory not found: {TEMPLATES_DIR}")
print("Make sure you cloned the full lean-orchestration repo")
sys.exit(1)
venv_python = None
# Step 0: Setup bundled provider-delegator (skip in claude-only mode)
if not claude_only:
venv_python = setup_provider_delegator()
if not venv_python:
print("\nERROR: Failed to setup provider-delegator. Aborting.")
sys.exit(1)
# Run remaining steps with provider support
if not install_beads(project_dir, claude_only=False):
print("\nERROR: Beads CLI is required. Aborting bootstrap.")
sys.exit(1)
# Install frontend review tools (optional, won't block)
install_rams()
install_web_interface_guidelines()
copy_agents(project_dir, project_name, claude_only=False, with_kanban_ui=with_kanban_ui)
copy_skills(project_dir, claude_only=False)
copy_hooks(project_dir, claude_only=False)
copy_settings(project_dir, claude_only=False)
copy_claude_md(project_dir, project_name, claude_only=False)
setup_memory(project_dir)
setup_gitignore(project_dir, claude_only=False)
create_mcp_config(project_dir, venv_python)
else:
# Claude-only mode: skip provider setup
print("\n[0/7] Skipping provider-delegator setup (claude-only mode)")
if not install_beads(project_dir, claude_only=True):
print("\nERROR: Beads CLI is required. Aborting bootstrap.")
sys.exit(1)
# Install frontend review tools (optional, won't block)
install_rams()
install_web_interface_guidelines()
copy_agents(project_dir, project_name, claude_only=True, with_kanban_ui=with_kanban_ui)
copy_skills(project_dir, claude_only=True)
copy_hooks(project_dir, claude_only=True)
copy_settings(project_dir, claude_only=True)
copy_claude_md(project_dir, project_name, claude_only=True)
setup_memory(project_dir)
setup_gitignore(project_dir, claude_only=True)
# Verify
if not verify_installation(project_dir, claude_only):
print("\nWARNING: Installation incomplete - check errors above")
print("\n" + "=" * 60)
print("BOOTSTRAP COMPLETE")
print("=" * 60)
if claude_only:
print(f"""
Mode: CLAUDE-ONLY (all agents use Claude Task)
Next steps:
1. Restart Claude Code to load new hooks and agents
2. **REQUIRED: Run discovery to create supervisors**
Discovery will scan your codebase and fetch specialist agents:
Task(
subagent_type="discovery",
prompt="Detect tech stack and create supervisors for {project_name}"
)
3. Create your first bead:
bd create "First task"
4. Dispatch work to supervisors:
Task(subagent_type="<supervisor-name>", prompt="BEAD_ID: BD-001\\n\\nImplement...")
NOTE: All agents (scout, detective, architect, etc.) run via Claude Task().
No external providers (Codex/Gemini) are configured.
""")
else:
print(f"""
Mode: EXTERNAL PROVIDERS (Codex → Gemini → Claude fallback)
Next steps:
1. Restart Claude Code to load new hooks and agents
2. **REQUIRED: Run discovery to create supervisors**
Discovery will scan your codebase and fetch specialist agents:
Task(
subagent_type="discovery",
prompt="Detect tech stack and create supervisors for {project_name}"
)
This will:
- Scan package.json, requirements.txt, Dockerfile, etc.
- Fetch matching specialists from external agents directory
- Inject beads workflow at the beginning of each agent
- Write supervisors to .claude/agents/
3. Create your first bead:
bd create "First task"
4. Dispatch work to supervisors:
Task(subagent_type="<supervisor-name>", prompt="BEAD_ID: BD-001\\n\\nImplement...")
NOTE: Read-only agents (scout, detective, architect, scribe, code-reviewer)
are delegated via provider_delegator MCP (Codex → Gemini fallback).
Supervisors are sourced from https://github.com/ayush-that/sub-agents.directory
with beads workflow injected.
""")
if __name__ == "__main__":
main()