928 lines
34 KiB
Python
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()
|