new-lane.sh 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. #!/bin/bash
  2. # new-lane.sh — create an isolated git worktree + branch ("lane") for parallel work, carrying over
  3. # the gitignored env files a fresh worktree won't have. The one-command remedy when the peer-writer
  4. # guard fires, or any time you want to parallelise without two sessions sharing one checkout.
  5. # Part of the git-ops skill (Worktree Operations → "Lane provisioning").
  6. #
  7. # Usage: new-lane.sh [--sibling] <slug> [base-branch]
  8. # <slug> short kebab name -> branch lane/<slug>
  9. # [base-branch] what to branch from (default: the current branch)
  10. # --sibling place the worktree OUTSIDE the repo at <repo>/../<repo>-<slug>
  11. # (structural isolation; use for a repo that can't gitignore
  12. # .claude/worktrees/, or just before destructive cleanups)
  13. #
  14. # Default is IN-REPO: <main>/.claude/worktrees/<slug> — the native Claude Code worktree
  15. # location: tidy (no sibling dirs littering the parent), and gitignored so `git add -A`
  16. # can't stage its gitlinks (the precondition is verified/ensured below). Committed lane work
  17. # lives in the shared object store and survives even directory deletion; only UNCOMMITTED work
  18. # is at risk from `git clean -ff` / `rm -rf`, so land early/often. See rules/worktree-boundaries.md.
  19. #
  20. # Output contract (SKILL-RESOURCE-PROTOCOL): the new worktree's absolute path is the ONLY thing on
  21. # stdout (so a caller can `cd "$(new-lane.sh foo)"`); all human-facing messages go to stderr.
  22. # Exit: 0 ok | 2 usage/precondition | 1 conflict (branch/path exists).
  23. #
  24. # Examples:
  25. # new-lane.sh auth-refactor # lane/auth-refactor in .claude/worktrees/auth-refactor
  26. # new-lane.sh hotfix main # lane/hotfix off main, in-repo
  27. # new-lane.sh --sibling big-migration # lane/big-migration in ../<repo>-big-migration
  28. set -euo pipefail
  29. show_help() { sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//' >&2; }
  30. SIBLING=0
  31. ARGS=()
  32. for a in "$@"; do
  33. case "$a" in
  34. --sibling) SIBLING=1 ;;
  35. -h|--help) show_help; exit 0 ;;
  36. --) : ;;
  37. -*) echo "new-lane: unknown option '$a' (valid: --sibling, -h/--help)" >&2; exit 2 ;;
  38. *) ARGS+=("$a") ;;
  39. esac
  40. done
  41. set -- ${ARGS[@]+"${ARGS[@]}"}
  42. [ -n "${1:-}" ] || { show_help; exit 2; }
  43. SLUG=$(printf '%s' "$1" | tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-')
  44. [ -n "$SLUG" ] || { echo "new-lane: slug empty after sanitising" >&2; exit 2; }
  45. ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "new-lane: not inside a git repo" >&2; exit 2; }
  46. # Anchor at the MAIN worktree root so a lane never nests inside a linked worktree
  47. # (running from .claude/worktrees/<x> would otherwise create worktrees-in-worktrees).
  48. COMMON=$(git -C "$ROOT" rev-parse --git-common-dir 2>/dev/null || echo "$ROOT/.git")
  49. case "$COMMON" in
  50. /*|[A-Za-z]:[\\/]*) ;; # already absolute
  51. *) COMMON="$ROOT/$COMMON" ;; # relative → resolve under ROOT
  52. esac
  53. MAIN=$(cd "$(dirname "$COMMON")" 2>/dev/null && pwd) || MAIN="$ROOT"
  54. REPO=$(basename "$MAIN")
  55. BRANCH="lane/$SLUG"
  56. BASE="${2:-$(git -C "$MAIN" rev-parse --abbrev-ref HEAD 2>/dev/null)}"
  57. # Resolve the lane path (no side effects yet).
  58. if [ "$SIBLING" -eq 1 ]; then
  59. WT="$(dirname "$MAIN")/${REPO}-${SLUG}"
  60. else
  61. WT="$MAIN/.claude/worktrees/$SLUG"
  62. fi
  63. # Validate EVERYTHING before mutating anything — a bad base or a conflict must
  64. # never leave partial state (e.g. a stray .gitignore append, see below).
  65. git -C "$MAIN" rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1 \
  66. || { echo "new-lane: base '$BASE' is not a valid branch/commit" >&2; exit 2; }
  67. git -C "$MAIN" show-ref --verify --quiet "refs/heads/$BRANCH" && { echo "new-lane: branch $BRANCH already exists" >&2; exit 1; }
  68. [ -e "$WT" ] && { echo "new-lane: path $WT already exists" >&2; exit 1; }
  69. # In-repo placement is only safe when the lane dir is gitignored — otherwise
  70. # `git add -A` from the main checkout stages worktree gitlinks (the 2026-04-19
  71. # incident). Ensure the ignore now that all checks have passed, so the in-repo
  72. # default is safe in ANY repo — not just one someone already tidied.
  73. if [ "$SIBLING" -ne 1 ] && ! git -C "$MAIN" check-ignore -q ".claude/worktrees/.probe" 2>/dev/null; then
  74. printf '\n# git worktrees / lanes — never tracked (new-lane.sh)\n.claude/worktrees/\n' >> "$MAIN/.gitignore"
  75. echo "new-lane: '.claude/worktrees/' was not gitignored — added it to .gitignore (commit this)" >&2
  76. fi
  77. git -C "$MAIN" worktree add "$WT" -b "$BRANCH" "$BASE" >&2
  78. # Carry over gitignored env/secret files the new worktree won't have (Cloudflare/Node/etc.).
  79. for f in .dev.vars .env .env.local .env.development .secrets; do
  80. if [ -e "$MAIN/$f" ] && [ ! -e "$WT/$f" ]; then
  81. cp -r "$MAIN/$f" "$WT/$f" 2>/dev/null && echo "new-lane: carried over $f" >&2
  82. fi
  83. done
  84. echo "new-lane: lane ready — branch $BRANCH (base $BASE)" >&2
  85. echo "new-lane: open a Claude session with cwd '$WT' — one writer per tree; land via fleet-ops" >&2
  86. printf '%s\n' "$WT"