beads-orchestration 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/SKILL.md +263 -0
  4. package/bootstrap.py +928 -0
  5. package/package.json +37 -0
  6. package/scripts/cli.js +64 -0
  7. package/scripts/postinstall.js +71 -0
  8. package/skills/create-beads-orchestration/SKILL.md +263 -0
  9. package/skills/subagents-discipline/SKILL.md +158 -0
  10. package/templates/CLAUDE.md +326 -0
  11. package/templates/agents/architect.md +121 -0
  12. package/templates/agents/code-reviewer.md +248 -0
  13. package/templates/agents/detective.md +101 -0
  14. package/templates/agents/discovery.md +492 -0
  15. package/templates/agents/merge-supervisor.md +119 -0
  16. package/templates/agents/scout.md +100 -0
  17. package/templates/agents/scribe.md +96 -0
  18. package/templates/beads-workflow-injection-api.md +116 -0
  19. package/templates/beads-workflow-injection-git.md +108 -0
  20. package/templates/beads-workflow-injection.md +111 -0
  21. package/templates/frontend-reviews-requirement.md +61 -0
  22. package/templates/hooks/block-orchestrator-tools.sh +98 -0
  23. package/templates/hooks/clarify-vague-request.sh +39 -0
  24. package/templates/hooks/enforce-bead-for-supervisor.sh +32 -0
  25. package/templates/hooks/enforce-branch-before-edit.sh +47 -0
  26. package/templates/hooks/enforce-concise-response.sh +41 -0
  27. package/templates/hooks/enforce-sequential-dispatch.sh +63 -0
  28. package/templates/hooks/inject-discipline-reminder.sh +28 -0
  29. package/templates/hooks/log-dispatch-prompt.sh +39 -0
  30. package/templates/hooks/memory-capture.sh +104 -0
  31. package/templates/hooks/remind-inprogress.sh +14 -0
  32. package/templates/hooks/session-start.sh +121 -0
  33. package/templates/hooks/validate-completion.sh +131 -0
  34. package/templates/hooks/validate-epic-close.sh +84 -0
  35. package/templates/mcp.json.template +12 -0
  36. package/templates/memory/recall.sh +121 -0
  37. package/templates/settings.json +74 -0
  38. package/templates/skills/react-best-practices/SKILL.md +487 -0
  39. package/templates/skills/subagents-discipline/SKILL.md +127 -0
  40. package/templates/ui-constraints.md +76 -0
package/bootstrap.py ADDED
@@ -0,0 +1,928 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bootstrap script for beads-based orchestration.
4
+
5
+ Creates:
6
+ - .beads/ directory with beads CLI
7
+ - .claude/agents/ with agent templates (copied, not generated)
8
+ - .claude/hooks/ with hook scripts
9
+ - .claude/settings.json with hook configuration
10
+ - .mcp.json with provider-delegator configuration (only with --external-providers)
11
+
12
+ Usage:
13
+ python bootstrap.py [--project-name NAME] [--project-dir DIR] [--with-kanban-ui]
14
+
15
+ Modes:
16
+ Default: All agents use Claude Task() directly (claude-only)
17
+ --external-providers: Sets up provider_delegator MCP for Codex/Gemini delegation
18
+ """
19
+
20
+ import os
21
+ import sys
22
+ import json
23
+ import shutil
24
+ import stat
25
+ import subprocess
26
+ try:
27
+ import tomllib
28
+ except ImportError:
29
+ tomllib = None
30
+ from pathlib import Path
31
+ from datetime import datetime
32
+ import random
33
+
34
+ # Get the directory where this script lives (lean-orchestration repo)
35
+ SCRIPT_DIR = Path(__file__).parent.resolve()
36
+ TEMPLATES_DIR = SCRIPT_DIR / "templates"
37
+
38
+ # ============================================================================
39
+ # CONFIGURATION
40
+ # ============================================================================
41
+
42
+ CORE_AGENTS = ["scout", "detective", "architect", "scribe", "discovery", "merge-supervisor", "code-reviewer"]
43
+
44
+ # NOTE: Supervisors are NOT bootstrapped - they are created dynamically by the
45
+ # discovery agent which fetches specialists from the external agents directory
46
+ # and injects the beads workflow.
47
+
48
+
49
+ # ============================================================================
50
+ # PROJECT NAME INFERENCE
51
+ # ============================================================================
52
+
53
+ def infer_project_name(project_dir: Path) -> str:
54
+ """Auto-infer project name from package files or directory name."""
55
+
56
+ # Try package.json (Node.js)
57
+ package_json = project_dir / "package.json"
58
+ if package_json.exists():
59
+ try:
60
+ data = json.loads(package_json.read_text())
61
+ if name := data.get("name"):
62
+ return name.replace("-", " ").replace("_", " ").title()
63
+ except (json.JSONDecodeError, KeyError):
64
+ pass
65
+
66
+ # Try pyproject.toml (Python)
67
+ if tomllib:
68
+ pyproject = project_dir / "pyproject.toml"
69
+ if pyproject.exists():
70
+ try:
71
+ data = tomllib.loads(pyproject.read_text())
72
+ if name := data.get("project", {}).get("name"):
73
+ return name.replace("-", " ").replace("_", " ").title()
74
+ if name := data.get("tool", {}).get("poetry", {}).get("name"):
75
+ return name.replace("-", " ").replace("_", " ").title()
76
+ except Exception:
77
+ pass
78
+
79
+ # Try Cargo.toml (Rust)
80
+ cargo = project_dir / "Cargo.toml"
81
+ if cargo.exists():
82
+ try:
83
+ data = tomllib.loads(cargo.read_text())
84
+ if name := data.get("package", {}).get("name"):
85
+ return name.replace("-", " ").replace("_", " ").title()
86
+ except Exception:
87
+ pass
88
+
89
+ # Try go.mod (Go)
90
+ go_mod = project_dir / "go.mod"
91
+ if go_mod.exists():
92
+ try:
93
+ content = go_mod.read_text()
94
+ for line in content.splitlines():
95
+ if line.startswith("module "):
96
+ module_path = line.split()[1]
97
+ name = module_path.split("/")[-1]
98
+ return name.replace("-", " ").replace("_", " ").title()
99
+ except Exception:
100
+ pass
101
+
102
+ # Fallback to directory name
103
+ return project_dir.name.replace("-", " ").replace("_", " ").title()
104
+
105
+
106
+ # ============================================================================
107
+ # PLACEHOLDER REPLACEMENT
108
+ # ============================================================================
109
+
110
+ def replace_placeholders(content: str, replacements: dict) -> str:
111
+ """Replace all placeholders in content."""
112
+ for placeholder, value in replacements.items():
113
+ content = content.replace(placeholder, value)
114
+ return content
115
+
116
+
117
+ def copy_and_replace(source: Path, dest: Path, replacements: dict) -> None:
118
+ """Copy file and replace placeholders."""
119
+ content = source.read_text()
120
+ updated = replace_placeholders(content, replacements)
121
+ dest.parent.mkdir(parents=True, exist_ok=True)
122
+ dest.write_text(updated)
123
+
124
+ # Preserve executable permissions for shell scripts
125
+ if source.suffix == '.sh':
126
+ dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
127
+
128
+
129
+ # ============================================================================
130
+ # CODEX DELEGATOR SETUP (SHARED LOCATION)
131
+ # ============================================================================
132
+
133
+ # Shared location for provider-delegator (installed once, used by all projects)
134
+ SHARED_MCP_DIR = Path.home() / ".claude" / "mcp-servers" / "provider-delegator"
135
+
136
+
137
+ def setup_provider_delegator() -> Path:
138
+ """Set up provider-delegator in shared location (~/.claude/mcp-servers/provider-delegator/).
139
+
140
+ This installs once and is reused by all projects.
141
+ Returns path to venv python.
142
+ """
143
+ print("\n[0/8] Setting up provider-delegator (shared)...")
144
+
145
+ source_dir = SCRIPT_DIR / "mcp-provider-delegator"
146
+ venv_dir = SHARED_MCP_DIR / ".venv"
147
+ venv_python = venv_dir / "bin" / "python"
148
+
149
+ # Check if already installed in shared location
150
+ if venv_python.exists():
151
+ print(f" - Already installed at {SHARED_MCP_DIR}")
152
+ return venv_python
153
+
154
+ # Verify source exists
155
+ if not source_dir.exists():
156
+ print(f" ERROR: mcp-provider-delegator not found at {source_dir}")
157
+ print(" Make sure you cloned the full lean-orchestration repo")
158
+ return None
159
+
160
+ # Check if uv is available
161
+ if not shutil.which("uv"):
162
+ print(" ERROR: 'uv' not found. Install with: curl -LsSf https://astral.sh/uv/install.sh | sh")
163
+ return None
164
+
165
+ # Create shared directory
166
+ print(f" - Installing to {SHARED_MCP_DIR}")
167
+ SHARED_MCP_DIR.mkdir(parents=True, exist_ok=True)
168
+
169
+ # Copy source to shared location
170
+ print(" - Copying source files...")
171
+ for item in source_dir.iterdir():
172
+ if item.name == ".venv":
173
+ continue # Skip any existing venv in source
174
+ dest = SHARED_MCP_DIR / item.name
175
+ if item.is_dir():
176
+ if dest.exists():
177
+ shutil.rmtree(dest)
178
+ shutil.copytree(item, dest)
179
+ else:
180
+ shutil.copy2(item, dest)
181
+
182
+ # Create venv using uv
183
+ print(" - Creating venv with uv...")
184
+ result = subprocess.run(
185
+ ["uv", "venv", str(venv_dir)],
186
+ cwd=SHARED_MCP_DIR,
187
+ capture_output=True,
188
+ text=True
189
+ )
190
+ if result.returncode != 0:
191
+ print(f" ERROR: Failed to create venv: {result.stderr}")
192
+ return None
193
+
194
+ # Install dependencies
195
+ print(" - Installing dependencies...")
196
+ result = subprocess.run(
197
+ ["uv", "pip", "install", "-e", "."],
198
+ cwd=SHARED_MCP_DIR,
199
+ capture_output=True,
200
+ text=True,
201
+ env={**os.environ, "VIRTUAL_ENV": str(venv_dir)}
202
+ )
203
+ if result.returncode != 0:
204
+ print(f" ERROR: Failed to install dependencies: {result.stderr}")
205
+ return None
206
+
207
+ print(f" DONE: provider-delegator installed at {SHARED_MCP_DIR}")
208
+ return venv_python
209
+
210
+
211
+ # ============================================================================
212
+ # BEADS INSTALLATION
213
+ # ============================================================================
214
+
215
+ def install_beads(project_dir: Path, claude_only: bool = False) -> bool:
216
+ """Install beads CLI and initialize .beads directory."""
217
+ step = "[1/7]" if claude_only else "[1/8]"
218
+ print(f"\n{step} Installing beads...")
219
+
220
+ beads_dir = project_dir / ".beads"
221
+
222
+ # Check if beads is already installed globally
223
+ beads_installed = shutil.which("bd") is not None
224
+
225
+ if not beads_installed:
226
+ print(" - beads CLI (bd) not found, installing...")
227
+
228
+ # Try installation methods in order of preference
229
+ installed = False
230
+
231
+ # Method 1: Homebrew (macOS)
232
+ if shutil.which("brew") and sys.platform == "darwin":
233
+ print(" - Trying Homebrew...")
234
+ result = subprocess.run(
235
+ ["brew", "install", "steveyegge/beads/bd"],
236
+ capture_output=True,
237
+ text=True
238
+ )
239
+ if result.returncode == 0:
240
+ installed = True
241
+ print(" - Installed via Homebrew")
242
+
243
+ # Method 2: npm (cross-platform)
244
+ if not installed and shutil.which("npm"):
245
+ print(" - Trying npm...")
246
+ result = subprocess.run(
247
+ ["npm", "install", "-g", "@beads/bd"],
248
+ capture_output=True,
249
+ text=True
250
+ )
251
+ if result.returncode == 0:
252
+ installed = True
253
+ print(" - Installed via npm")
254
+
255
+ # Method 3: curl install script (Linux/macOS/FreeBSD)
256
+ if not installed and sys.platform != "win32":
257
+ print(" - Trying curl install script...")
258
+ result = subprocess.run(
259
+ ["bash", "-c", "curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"],
260
+ capture_output=True,
261
+ text=True
262
+ )
263
+ if result.returncode == 0:
264
+ installed = True
265
+ print(" - Installed via curl script")
266
+
267
+ # Method 4: Go install (if Go is available)
268
+ if not installed and shutil.which("go"):
269
+ print(" - Trying go install...")
270
+ result = subprocess.run(
271
+ ["go", "install", "github.com/steveyegge/beads/cmd/bd@latest"],
272
+ capture_output=True,
273
+ text=True
274
+ )
275
+ if result.returncode == 0:
276
+ installed = True
277
+ print(" - Installed via go install")
278
+
279
+ if not installed:
280
+ print("\n ERROR: Could not install beads CLI (bd)")
281
+ print(" The beads workflow requires the bd command.")
282
+ print(" Please install manually: https://github.com/steveyegge/beads#-installation")
283
+ print("\n Installation options:")
284
+ print(" macOS: brew install steveyegge/beads/bd")
285
+ print(" npm: npm install -g @beads/bd")
286
+ print(" Go: go install github.com/steveyegge/beads/cmd/bd@latest")
287
+ return False
288
+ else:
289
+ print(" - beads CLI already installed")
290
+
291
+ beads_installed = True
292
+
293
+ # Initialize .beads in project
294
+ if not beads_dir.exists():
295
+ print(" - Initializing .beads directory...")
296
+
297
+ # Try bd init first
298
+ if shutil.which("bd"):
299
+ result = subprocess.run(
300
+ ["bd", "init"],
301
+ cwd=project_dir,
302
+ capture_output=True,
303
+ text=True
304
+ )
305
+ if result.returncode == 0:
306
+ print(" - Initialized via 'bd init'")
307
+ else:
308
+ # Manual init as fallback
309
+ _manual_beads_init(beads_dir)
310
+ else:
311
+ _manual_beads_init(beads_dir)
312
+ else:
313
+ print(" - .beads already exists")
314
+
315
+ # Configure custom 'inreview' status for parallel work workflow
316
+ if shutil.which("bd"):
317
+ print(" - Configuring custom 'inreview' status...")
318
+ result = subprocess.run(
319
+ ["bd", "config", "set", "status.custom", "inreview"],
320
+ cwd=project_dir,
321
+ capture_output=True,
322
+ text=True
323
+ )
324
+ if result.returncode == 0:
325
+ print(" - Added 'inreview' custom status")
326
+ else:
327
+ print(f" - Warning: Could not add custom status: {result.stderr}")
328
+
329
+ print(" DONE: beads setup complete")
330
+ return True
331
+
332
+
333
+ def _manual_beads_init(beads_dir: Path):
334
+ """Manually create .beads directory structure."""
335
+ beads_dir.mkdir(exist_ok=True)
336
+ (beads_dir / "issues.jsonl").touch()
337
+ # Create minimal config
338
+ config = {
339
+ "version": "1",
340
+ "mode": "normal"
341
+ }
342
+ (beads_dir / "config.json").write_text(json.dumps(config, indent=2))
343
+ print(" - Created .beads manually")
344
+
345
+
346
+ def setup_memory(project_dir: Path) -> None:
347
+ """Create .beads/memory/ directory with knowledge store and recall script."""
348
+ memory_dir = project_dir / ".beads" / "memory"
349
+ memory_dir.mkdir(parents=True, exist_ok=True)
350
+
351
+ # Create empty knowledge store
352
+ knowledge_file = memory_dir / "knowledge.jsonl"
353
+ if not knowledge_file.exists():
354
+ knowledge_file.touch()
355
+ print(" - Created .beads/memory/knowledge.jsonl")
356
+
357
+ # Copy recall script
358
+ recall_src = TEMPLATES_DIR / "memory" / "recall.sh"
359
+ recall_dest = memory_dir / "recall.sh"
360
+ if recall_src.exists():
361
+ shutil.copy2(recall_src, recall_dest)
362
+ recall_dest.chmod(recall_dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
363
+ print(" - Copied .beads/memory/recall.sh")
364
+ else:
365
+ print(" - WARNING: recall.sh template not found")
366
+
367
+
368
+ # ============================================================================
369
+ # RAMS INSTALLATION (Accessibility Review)
370
+ # ============================================================================
371
+
372
+ def install_rams() -> bool:
373
+ """Install RAMS accessibility review tool if not already installed."""
374
+ print("\n Checking RAMS (accessibility review tool)...")
375
+
376
+ # Check if rams is already installed
377
+ if shutil.which("rams"):
378
+ print(" - RAMS already installed")
379
+ return True
380
+
381
+ print(" - RAMS not found, installing...")
382
+
383
+ # Install via curl
384
+ if sys.platform != "win32":
385
+ result = subprocess.run(
386
+ ["bash", "-c", "curl -fsSL https://rams.ai/install | bash"],
387
+ capture_output=True,
388
+ text=True
389
+ )
390
+ if result.returncode == 0:
391
+ print(" - RAMS installed successfully")
392
+ return True
393
+ else:
394
+ print(f" - Warning: Could not install RAMS: {result.stderr}")
395
+ print(" - Frontend supervisors will still work but RAMS review enforcement may fail")
396
+ print(" - Install manually: curl -fsSL https://rams.ai/install | bash")
397
+ return False
398
+
399
+ print(" - Warning: RAMS installation not supported on Windows")
400
+ return False
401
+
402
+
403
+ # ============================================================================
404
+ # WEB INTERFACE GUIDELINES INSTALLATION
405
+ # ============================================================================
406
+
407
+ def install_web_interface_guidelines() -> bool:
408
+ """Install Web Interface Guidelines review tool if not already installed."""
409
+ print("\n Checking Web Interface Guidelines (design review tool)...")
410
+
411
+ # Check if wig is already installed
412
+ if shutil.which("wig"):
413
+ print(" - Web Interface Guidelines already installed")
414
+ return True
415
+
416
+ print(" - Web Interface Guidelines not found, installing...")
417
+
418
+ # Install via curl
419
+ if sys.platform != "win32":
420
+ result = subprocess.run(
421
+ ["bash", "-c", "curl -fsSL https://vercel.com/design/guidelines/install | bash"],
422
+ capture_output=True,
423
+ text=True
424
+ )
425
+ if result.returncode == 0:
426
+ print(" - Web Interface Guidelines installed successfully")
427
+ return True
428
+ else:
429
+ print(f" - Warning: Could not install Web Interface Guidelines: {result.stderr}")
430
+ print(" - Frontend supervisors will still work but WIG review enforcement may fail")
431
+ print(" - Install manually: curl -fsSL https://vercel.com/design/guidelines/install | bash")
432
+ return False
433
+
434
+ print(" - Warning: Web Interface Guidelines installation not supported on Windows")
435
+ return False
436
+
437
+
438
+ # ============================================================================
439
+ # AGENTS (TEMPLATE COPYING)
440
+ # ============================================================================
441
+
442
+ def copy_agents(project_dir: Path, project_name: str, claude_only: bool = False, with_kanban_ui: bool = False) -> list:
443
+ """Copy core agent templates from templates/ directory.
444
+
445
+ NOTE: Supervisors are NOT copied here - they are created dynamically
446
+ by the discovery agent based on detected tech stack.
447
+ """
448
+ step = "[2/7]" if claude_only else "[2/8]"
449
+ print(f"\n{step} Copying core agent templates...")
450
+
451
+ agents_dir = project_dir / ".claude" / "agents"
452
+ agents_dir.mkdir(parents=True, exist_ok=True)
453
+
454
+ agents_template_dir = TEMPLATES_DIR / "agents"
455
+
456
+ copied = []
457
+
458
+ # Replacements for templates
459
+ replacements = {
460
+ "[Project]": project_name,
461
+ }
462
+
463
+ # Copy core agents ONLY (not supervisors)
464
+ for agent_file in agents_template_dir.glob("*.md"):
465
+ dest = agents_dir / agent_file.name
466
+ copy_and_replace(agent_file, dest, replacements)
467
+ copied.append(agent_file.name)
468
+ print(f" - Copied {agent_file.name}")
469
+
470
+ # Copy beads workflow injection snippet (used by discovery agent)
471
+ # Select API version (with git fallback) or git-only version based on flag
472
+ if with_kanban_ui:
473
+ beads_workflow_src = TEMPLATES_DIR / "beads-workflow-injection-api.md"
474
+ workflow_type = "API + git fallback"
475
+ else:
476
+ beads_workflow_src = TEMPLATES_DIR / "beads-workflow-injection-git.md"
477
+ workflow_type = "git only"
478
+ beads_workflow_dest = project_dir / ".claude" / "beads-workflow-injection.md"
479
+ if beads_workflow_src.exists():
480
+ shutil.copy2(beads_workflow_src, beads_workflow_dest)
481
+ print(f" - Copied beads-workflow-injection.md ({workflow_type})")
482
+
483
+ # Copy UI constraints (used by discovery agent for frontend supervisors)
484
+ ui_constraints_src = TEMPLATES_DIR / "ui-constraints.md"
485
+ ui_constraints_dest = project_dir / ".claude" / "ui-constraints.md"
486
+ if ui_constraints_src.exists():
487
+ shutil.copy2(ui_constraints_src, ui_constraints_dest)
488
+ print(" - Copied ui-constraints.md")
489
+
490
+ # Copy frontend reviews requirement (RAMS + Web Interface Guidelines)
491
+ frontend_reviews_src = TEMPLATES_DIR / "frontend-reviews-requirement.md"
492
+ frontend_reviews_dest = project_dir / ".claude" / "frontend-reviews-requirement.md"
493
+ if frontend_reviews_src.exists():
494
+ shutil.copy2(frontend_reviews_src, frontend_reviews_dest)
495
+ print(" - Copied frontend-reviews-requirement.md")
496
+
497
+ print(f" DONE: {len(copied)} core agents copied")
498
+ print(" NOTE: Supervisors will be created by discovery agent based on tech stack")
499
+ return copied
500
+
501
+
502
+ # ============================================================================
503
+ # SKILLS (TEMPLATE COPYING)
504
+ # ============================================================================
505
+
506
+ def copy_skills(project_dir: Path, claude_only: bool = False) -> list:
507
+ """Copy skill templates from templates/ directory.
508
+
509
+ Skills are copied so discovery agent can install them when tech stack is detected.
510
+ """
511
+ step = "[3/7]" if claude_only else "[3/8]"
512
+ print(f"\n{step} Copying skill templates...")
513
+
514
+ skills_template_dir = TEMPLATES_DIR / "skills"
515
+ if not skills_template_dir.exists():
516
+ print(" - No skill templates found, skipping")
517
+ return []
518
+
519
+ skills_dir = project_dir / ".claude" / "skills"
520
+ skills_dir.mkdir(parents=True, exist_ok=True)
521
+
522
+ copied = []
523
+
524
+ for skill_dir in skills_template_dir.iterdir():
525
+ if skill_dir.is_dir():
526
+ dest_dir = skills_dir / skill_dir.name
527
+ if dest_dir.exists():
528
+ shutil.rmtree(dest_dir)
529
+ shutil.copytree(skill_dir, dest_dir)
530
+ copied.append(skill_dir.name)
531
+ print(f" - Copied {skill_dir.name}/ skill")
532
+
533
+ print(f" DONE: {len(copied)} skill templates copied")
534
+ return copied
535
+
536
+
537
+ # ============================================================================
538
+ # HOOKS (TEMPLATE COPYING)
539
+ # ============================================================================
540
+
541
+ def copy_hooks(project_dir: Path, claude_only: bool = False) -> list:
542
+ """Copy hook templates from templates/ directory.
543
+
544
+ Args:
545
+ project_dir: Target project directory
546
+ claude_only: If True, skip provider delegation enforcement hooks
547
+ """
548
+ step = "[4/7]" if claude_only else "[4/8]"
549
+ print(f"\n{step} Copying hook templates...")
550
+
551
+ hooks_dir = project_dir / ".claude" / "hooks"
552
+ hooks_dir.mkdir(parents=True, exist_ok=True)
553
+
554
+ hooks_template_dir = TEMPLATES_DIR / "hooks"
555
+ copied = []
556
+
557
+ # Hooks to skip in claude-only mode (none currently - all hooks apply to both modes)
558
+ skip_in_claude_only = set()
559
+
560
+ for hook_file in hooks_template_dir.glob("*.sh"):
561
+ # Skip provider enforcement hooks in claude-only mode
562
+ if claude_only and hook_file.name in skip_in_claude_only:
563
+ print(f" - Skipped {hook_file.name} (claude-only mode)")
564
+ continue
565
+
566
+ dest = hooks_dir / hook_file.name
567
+ shutil.copy2(hook_file, dest)
568
+ dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
569
+ copied.append(hook_file.name)
570
+ print(f" - Copied {hook_file.name}")
571
+
572
+ print(f" DONE: {len(copied)} hooks copied")
573
+ return copied
574
+
575
+
576
+ # ============================================================================
577
+ # SETTINGS
578
+ # ============================================================================
579
+
580
+ def copy_settings(project_dir: Path, claude_only: bool = False) -> None:
581
+ """Copy settings.json template, optionally removing provider enforcement hooks.
582
+
583
+ Args:
584
+ project_dir: Target project directory
585
+ claude_only: If True, remove provider delegation enforcement from settings
586
+ """
587
+ step = "[5/7]" if claude_only else "[5/8]"
588
+ print(f"\n{step} Copying settings...")
589
+
590
+ settings_template = TEMPLATES_DIR / "settings.json"
591
+ settings_dest = project_dir / ".claude" / "settings.json"
592
+
593
+ # Settings are the same for both modes now (no provider-specific hooks)
594
+ shutil.copy2(settings_template, settings_dest)
595
+ if claude_only:
596
+ print(" - Copied settings.json (claude-only mode)")
597
+ else:
598
+ print(" - Copied settings.json")
599
+
600
+ print(" DONE: settings configured")
601
+
602
+
603
+ # ============================================================================
604
+ # CLAUDE.MD
605
+ # ============================================================================
606
+
607
+ def copy_claude_md(project_dir: Path, project_name: str, claude_only: bool = False) -> None:
608
+ """Copy CLAUDE.md template with project name replacement."""
609
+ step = "[6/7]" if claude_only else "[6/8]"
610
+ print(f"\n{step} Copying CLAUDE.md...")
611
+
612
+ claude_template = TEMPLATES_DIR / "CLAUDE.md"
613
+ claude_dest = project_dir / "CLAUDE.md"
614
+
615
+ replacements = {"[Project]": project_name}
616
+ copy_and_replace(claude_template, claude_dest, replacements)
617
+
618
+ print(" - Copied CLAUDE.md")
619
+ print(" DONE: CLAUDE.md copied")
620
+
621
+
622
+ # ============================================================================
623
+ # GITIGNORE
624
+ # ============================================================================
625
+
626
+ def setup_gitignore(project_dir: Path, claude_only: bool = False) -> None:
627
+ """Ensure .beads is in .gitignore. .claude/ is tracked (not ignored)."""
628
+ step = "[7/7]" if claude_only else "[7/8]"
629
+ print(f"\n{step} Setting up .gitignore...")
630
+
631
+ gitignore_path = project_dir / ".gitignore"
632
+ # Only ignore .beads/ (ephemeral task data) and .mcp.json (user-specific paths)
633
+ # .claude/ is tracked so it survives git operations
634
+ entries_to_add = [".beads/", ".mcp.json"]
635
+
636
+ if gitignore_path.exists():
637
+ content = gitignore_path.read_text()
638
+ lines = content.splitlines()
639
+
640
+ # Check which entries are missing
641
+ missing = []
642
+ for entry in entries_to_add:
643
+ # Check for exact match or without trailing slash
644
+ entry_no_slash = entry.rstrip("/")
645
+ if entry not in lines and entry_no_slash not in lines:
646
+ missing.append(entry)
647
+
648
+ if missing:
649
+ # Append missing entries
650
+ with open(gitignore_path, "a") as f:
651
+ # Add newline if file doesn't end with one
652
+ if content and not content.endswith("\n"):
653
+ f.write("\n")
654
+ f.write("\n# Beads task tracking (ephemeral)\n")
655
+ for entry in missing:
656
+ f.write(f"{entry}\n")
657
+ print(f" - Added {entry} to .gitignore")
658
+ else:
659
+ print(" - .beads/ and .mcp.json already in .gitignore")
660
+ else:
661
+ # Create new .gitignore
662
+ content = """# Beads task tracking (ephemeral)
663
+ .beads/
664
+
665
+ # MCP config (user-specific paths)
666
+ .mcp.json
667
+ """
668
+ gitignore_path.write_text(content)
669
+ print(" - Created .gitignore with .beads/ and .mcp.json")
670
+
671
+ print(" DONE: .gitignore configured")
672
+ print(" NOTE: .claude/ is tracked (not ignored) to prevent accidental loss")
673
+
674
+
675
+ # ============================================================================
676
+ # MCP CONFIG
677
+ # ============================================================================
678
+
679
+ def create_mcp_config(project_dir: Path, venv_python: Path) -> None:
680
+ """Add provider-delegator to .mcp.json, preserving existing servers."""
681
+ print("\n[8/8] Configuring MCP...")
682
+
683
+ mcp_dest = project_dir / ".mcp.json"
684
+
685
+ # Load existing config or start fresh
686
+ if mcp_dest.exists():
687
+ try:
688
+ existing = json.loads(mcp_dest.read_text())
689
+ print(" - Found existing .mcp.json, merging...")
690
+ except json.JSONDecodeError:
691
+ print(" - Warning: Invalid .mcp.json, creating new one")
692
+ existing = {}
693
+ else:
694
+ existing = {}
695
+
696
+ # Ensure mcpServers key exists
697
+ if "mcpServers" not in existing:
698
+ existing["mcpServers"] = {}
699
+
700
+ # Add/update provider_delegator
701
+ existing["mcpServers"]["provider_delegator"] = {
702
+ "type": "stdio",
703
+ "command": str(venv_python),
704
+ "args": ["-m", "mcp_provider_delegator.server"],
705
+ "env": {
706
+ "AGENT_TEMPLATES_PATH": ".claude/agents"
707
+ }
708
+ }
709
+
710
+ mcp_dest.write_text(json.dumps(existing, indent=2))
711
+
712
+ server_count = len(existing["mcpServers"])
713
+ print(f" - Added provider-delegator to .mcp.json ({server_count} total servers)")
714
+ print(f" Command: {venv_python}")
715
+ print(f" Agents: .claude/agents (relative)")
716
+ print(" DONE: MCP config updated")
717
+
718
+
719
+ # ============================================================================
720
+ # VERIFICATION
721
+ # ============================================================================
722
+
723
+ def verify_installation(project_dir: Path, claude_only: bool = False) -> bool:
724
+ """Verify all components were installed correctly."""
725
+ checks = {
726
+ ".claude/hooks": "Hooks directory",
727
+ ".claude/agents": "Agents directory",
728
+ ".claude/settings.json": "Settings file",
729
+ ".beads": "Beads directory",
730
+ "CLAUDE.md": "CLAUDE.md",
731
+ ".gitignore": ".gitignore",
732
+ }
733
+
734
+ # Only check for .mcp.json in external providers mode
735
+ if not claude_only:
736
+ checks[".mcp.json"] = "MCP config"
737
+
738
+ print("\n=== Verification ===")
739
+ all_good = True
740
+
741
+ for path, description in checks.items():
742
+ full_path = project_dir / path
743
+ if full_path.exists():
744
+ print(f" - {description}")
745
+ else:
746
+ print(f" X {description} MISSING")
747
+ all_good = False
748
+
749
+ # Count files
750
+ hooks_dir = project_dir / ".claude/hooks"
751
+ if hooks_dir.exists():
752
+ hook_count = len(list(hooks_dir.glob("*.sh")))
753
+ print(f" - Hooks: {hook_count}")
754
+
755
+ agents_dir = project_dir / ".claude/agents"
756
+ if agents_dir.exists():
757
+ agent_count = len(list(agents_dir.glob("*.md")))
758
+ print(f" - Agents: {agent_count}")
759
+
760
+ skills_dir = project_dir / ".claude/skills"
761
+ if skills_dir.exists():
762
+ skill_count = len(list(skills_dir.iterdir()))
763
+ if skill_count > 0:
764
+ print(f" - Skills: {skill_count}")
765
+
766
+ return all_good
767
+
768
+
769
+ # ============================================================================
770
+ # MAIN
771
+ # ============================================================================
772
+
773
+ def main():
774
+ import argparse
775
+
776
+ parser = argparse.ArgumentParser(description="Bootstrap beads-based orchestration")
777
+ parser.add_argument("--project-name", default=None, help="Project name (auto-inferred if not provided)")
778
+ parser.add_argument("--project-dir", default=".", help="Project directory")
779
+ parser.add_argument("--external-providers", action="store_true",
780
+ help="Use Codex/Gemini for delegation (default: Claude-only)")
781
+ parser.add_argument("--with-kanban-ui", action="store_true",
782
+ help="Use Beads Kanban UI API for worktree creation (with git fallback)")
783
+ args = parser.parse_args()
784
+
785
+ project_dir = Path(args.project_dir).resolve()
786
+ claude_only = not args.external_providers # Default is now claude-only
787
+ with_kanban_ui = args.with_kanban_ui
788
+
789
+ # Ensure project directory exists
790
+ project_dir.mkdir(parents=True, exist_ok=True)
791
+
792
+ # Auto-infer project name if not provided
793
+ if args.project_name:
794
+ project_name = args.project_name
795
+ else:
796
+ project_name = infer_project_name(project_dir)
797
+ print(f"Auto-inferred project name: {project_name}")
798
+
799
+ mode_str = "CLAUDE-ONLY" if claude_only else "EXTERNAL PROVIDERS"
800
+ worktree_str = "API + git fallback" if with_kanban_ui else "git only"
801
+ print(f"\nBootstrapping beads orchestration for: {project_name}")
802
+ print(f"Directory: {project_dir}")
803
+ print(f"Mode: {mode_str}")
804
+ print(f"Worktrees: {worktree_str}")
805
+ print("=" * 60)
806
+
807
+ # Verify templates exist
808
+ if not TEMPLATES_DIR.exists():
809
+ print(f"\nERROR: Templates directory not found: {TEMPLATES_DIR}")
810
+ print("Make sure you cloned the full lean-orchestration repo")
811
+ sys.exit(1)
812
+
813
+ venv_python = None
814
+
815
+ # Step 0: Setup bundled provider-delegator (skip in claude-only mode)
816
+ if not claude_only:
817
+ venv_python = setup_provider_delegator()
818
+ if not venv_python:
819
+ print("\nERROR: Failed to setup provider-delegator. Aborting.")
820
+ sys.exit(1)
821
+
822
+ # Run remaining steps with provider support
823
+ if not install_beads(project_dir, claude_only=False):
824
+ print("\nERROR: Beads CLI is required. Aborting bootstrap.")
825
+ sys.exit(1)
826
+
827
+ # Install frontend review tools (optional, won't block)
828
+ install_rams()
829
+ install_web_interface_guidelines()
830
+
831
+ copy_agents(project_dir, project_name, claude_only=False, with_kanban_ui=with_kanban_ui)
832
+ copy_skills(project_dir, claude_only=False)
833
+ copy_hooks(project_dir, claude_only=False)
834
+ copy_settings(project_dir, claude_only=False)
835
+ copy_claude_md(project_dir, project_name, claude_only=False)
836
+ setup_memory(project_dir)
837
+ setup_gitignore(project_dir, claude_only=False)
838
+ create_mcp_config(project_dir, venv_python)
839
+ else:
840
+ # Claude-only mode: skip provider setup
841
+ print("\n[0/7] Skipping provider-delegator setup (claude-only mode)")
842
+
843
+ if not install_beads(project_dir, claude_only=True):
844
+ print("\nERROR: Beads CLI is required. Aborting bootstrap.")
845
+ sys.exit(1)
846
+
847
+ # Install frontend review tools (optional, won't block)
848
+ install_rams()
849
+ install_web_interface_guidelines()
850
+
851
+ copy_agents(project_dir, project_name, claude_only=True, with_kanban_ui=with_kanban_ui)
852
+ copy_skills(project_dir, claude_only=True)
853
+ copy_hooks(project_dir, claude_only=True)
854
+ copy_settings(project_dir, claude_only=True)
855
+ copy_claude_md(project_dir, project_name, claude_only=True)
856
+ setup_memory(project_dir)
857
+ setup_gitignore(project_dir, claude_only=True)
858
+
859
+ # Verify
860
+ if not verify_installation(project_dir, claude_only):
861
+ print("\nWARNING: Installation incomplete - check errors above")
862
+
863
+ print("\n" + "=" * 60)
864
+ print("BOOTSTRAP COMPLETE")
865
+ print("=" * 60)
866
+
867
+ if claude_only:
868
+ print(f"""
869
+ Mode: CLAUDE-ONLY (all agents use Claude Task)
870
+
871
+ Next steps:
872
+
873
+ 1. Restart Claude Code to load new hooks and agents
874
+
875
+ 2. **REQUIRED: Run discovery to create supervisors**
876
+ Discovery will scan your codebase and fetch specialist agents:
877
+
878
+ Task(
879
+ subagent_type="discovery",
880
+ prompt="Detect tech stack and create supervisors for {project_name}"
881
+ )
882
+
883
+ 3. Create your first bead:
884
+ bd create "First task"
885
+
886
+ 4. Dispatch work to supervisors:
887
+ Task(subagent_type="<supervisor-name>", prompt="BEAD_ID: BD-001\\n\\nImplement...")
888
+
889
+ NOTE: All agents (scout, detective, architect, etc.) run via Claude Task().
890
+ No external providers (Codex/Gemini) are configured.
891
+ """)
892
+ else:
893
+ print(f"""
894
+ Mode: EXTERNAL PROVIDERS (Codex → Gemini → Claude fallback)
895
+
896
+ Next steps:
897
+
898
+ 1. Restart Claude Code to load new hooks and agents
899
+
900
+ 2. **REQUIRED: Run discovery to create supervisors**
901
+ Discovery will scan your codebase and fetch specialist agents:
902
+
903
+ Task(
904
+ subagent_type="discovery",
905
+ prompt="Detect tech stack and create supervisors for {project_name}"
906
+ )
907
+
908
+ This will:
909
+ - Scan package.json, requirements.txt, Dockerfile, etc.
910
+ - Fetch matching specialists from external agents directory
911
+ - Inject beads workflow at the beginning of each agent
912
+ - Write supervisors to .claude/agents/
913
+
914
+ 3. Create your first bead:
915
+ bd create "First task"
916
+
917
+ 4. Dispatch work to supervisors:
918
+ Task(subagent_type="<supervisor-name>", prompt="BEAD_ID: BD-001\\n\\nImplement...")
919
+
920
+ NOTE: Read-only agents (scout, detective, architect, scribe, code-reviewer)
921
+ are delegated via provider_delegator MCP (Codex → Gemini fallback).
922
+ Supervisors are sourced from https://github.com/ayush-that/sub-agents.directory
923
+ with beads workflow injected.
924
+ """)
925
+
926
+
927
+ if __name__ == "__main__":
928
+ main()