worktree.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. from __future__ import annotations
  2. import json
  3. import subprocess
  4. from pathlib import Path
  5. from setup_helpers.base import _git
  6. CALLER_CONSENT_PLAN = """\
  7. # Custom Greeting Implementation Plan
  8. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
  9. > or superpowers:executing-plans to implement this plan task-by-task.
  10. **Goal:** Add a small greeting customization feature to the Node fixture.
  11. ---
  12. ### Task 1: Custom greeting
  13. **Files:**
  14. - Modify: `src/index.js`
  15. - Modify: `src/utils.js`
  16. - Create: `tests/greeting.test.js`
  17. **Acceptance Criteria:**
  18. - The app can greet a provided name instead of always greeting `world`.
  19. - The default behavior remains `Hello, world!`.
  20. - A test covers both the default and custom-name paths.
  21. - [ ] **Step 1: Add tests for default and custom greetings.**
  22. - [ ] **Step 2: Update the greeting implementation.**
  23. - [ ] **Step 3: Run the relevant tests.**
  24. """
  25. def add_worktree(repo_dir: Path, branch: str, worktree_path: str) -> None:
  26. subprocess.run(
  27. ["git", "worktree", "add", "-b", branch, worktree_path],
  28. cwd=repo_dir, check=True, capture_output=True,
  29. )
  30. def detach_head(worktree_path: str) -> None:
  31. result = subprocess.run(
  32. ["git", "rev-parse", "HEAD"], cwd=worktree_path,
  33. capture_output=True, text=True, check=True,
  34. )
  35. commit = result.stdout.strip()
  36. result = subprocess.run(
  37. ["git", "branch", "--show-current"], cwd=worktree_path,
  38. capture_output=True, text=True, check=True,
  39. )
  40. branch = result.stdout.strip()
  41. subprocess.run(
  42. ["git", "checkout", "--detach", commit], cwd=worktree_path,
  43. check=True, capture_output=True,
  44. )
  45. if branch:
  46. subprocess.run(
  47. ["git", "branch", "-D", branch], cwd=worktree_path,
  48. capture_output=True,
  49. )
  50. def add_existing_worktree(workdir: Path) -> None:
  51. """Create an existing worktree (for 'already inside' scenarios)."""
  52. wt_path = workdir.parent / f"{workdir.name}-existing-worktree"
  53. add_worktree(workdir, "existing-feature", str(wt_path))
  54. def detach_worktree_head(workdir: Path) -> None:
  55. """Detach HEAD in the existing worktree."""
  56. wt_path = workdir.parent / f"{workdir.name}-existing-worktree"
  57. detach_head(str(wt_path))
  58. def symlink_superpowers(workdir: Path, superpowers_root: str) -> None:
  59. skills_dir = Path(workdir) / ".agents" / "skills"
  60. skills_dir.mkdir(parents=True, exist_ok=True)
  61. target = Path(superpowers_root) / "skills"
  62. link = skills_dir / "superpowers"
  63. link.symlink_to(target)
  64. def link_gemini_extension(workdir: Path, superpowers_root: str) -> None:
  65. """Link superpowers as a Gemini CLI extension and inject project context.
  66. Extensions are global, but GEMINI.md context loading is project-scoped.
  67. Temp workdirs need a GEMINI.md with absolute paths so Gemini loads
  68. the using-superpowers instructions that tell it to invoke skills.
  69. """
  70. extension_name = "superpowers"
  71. manifest = Path(superpowers_root) / "gemini-extension.json"
  72. if manifest.exists():
  73. try:
  74. extension_name = json.loads(manifest.read_text()).get("name", extension_name)
  75. except json.JSONDecodeError:
  76. pass
  77. # Gemini extensions are global; replace any prior link so this run tests
  78. # the requested SUPERPOWERS_ROOT checkout rather than a stale install.
  79. subprocess.run(
  80. ["gemini", "extensions", "uninstall", extension_name],
  81. capture_output=True,
  82. )
  83. subprocess.run(
  84. ["gemini", "extensions", "link", superpowers_root],
  85. capture_output=True,
  86. input="y\n",
  87. text=True,
  88. check=True,
  89. )
  90. # Create GEMINI.md with absolute @imports so context loads in the temp workdir
  91. skills_root = Path(superpowers_root) / "skills"
  92. gemini_md = workdir / "GEMINI.md"
  93. gemini_md.write_text(
  94. f"@{skills_root}/using-superpowers/SKILL.md\n"
  95. f"@{skills_root}/using-superpowers/references/gemini-tools.md\n"
  96. )
  97. def create_caller_consent_plan(workdir: Path) -> None:
  98. """Add a committed implementation plan that should trigger caller-layer gating."""
  99. plan_path = workdir / "docs" / "superpowers" / "plans" / "custom-greeting.md"
  100. plan_path.parent.mkdir(parents=True, exist_ok=True)
  101. plan_path.write_text(CALLER_CONSENT_PLAN)
  102. _git(["git", "add", str(plan_path.relative_to(workdir))], cwd=workdir)
  103. _git(["git", "commit", "-m", "add caller consent gate plan"], cwd=workdir)