|
#!/usr/bin/env bash |
|
set -euo pipefail |
|
|
|
# ez — Batch Linear issue fixer using Claude Code |
|
# |
|
# Takes Linear issue URLs, and for each one (sequentially): |
|
# 1. Fetches the git branch name from Linear |
|
# 2. Creates a worktree (using the same logic as `work`) |
|
# 3. Spawns Claude Code to implement the fix |
|
# 4. Claude commits and creates a PR |
|
# |
|
# With --review, after all workers finish, spawns N reviewers in parallel |
|
# that review each PR, fix any issues found, and push. |
|
# |
|
# Usage: |
|
# ez [--review] <linear-url> [<linear-url> ...] |
|
# ez --review "url1 |
|
# url2 |
|
# url3" |
|
# echo "url1\nurl2" | ez --review |
|
# |
|
# Config (env vars): |
|
# EZ_REPO_ROOT — bare repo root (default: ~/code/futurity/futurity.git) |
|
# EZ_MODEL — model for the fix task (default: unset, uses claude default) |
|
# EZ_REVIEW_MODEL — model for the review task (default: unset, uses claude default) |
|
# EZ_BRANCH_MODEL — model for branch name lookup (default: sonnet) |
|
|
|
# Render basic markdown formatting for terminal output |
|
render_md() { |
|
perl -pe ' |
|
s/\*\*`([^`]+)`\*\*/\e[1;34m$1\e[0m/g; |
|
s/\*\*([^*]+)\*\*/\e[1m$1\e[0m/g; |
|
s/`([^`]+)`/\e[34m$1\e[0m/g; |
|
s/(?<!\*)\*([^*\n]+)\*(?!\*)/\e[3m$1\e[0m/g; |
|
' |
|
} |
|
|
|
REPO_ROOT="${EZ_REPO_ROOT:-$HOME/code/futurity/futurity.git}" |
|
FIX_MODEL="${EZ_MODEL:-}" |
|
REVIEW_MODEL="${EZ_REVIEW_MODEL:-}" |
|
BRANCH_MODEL="${EZ_BRANCH_MODEL:-sonnet}" |
|
|
|
# Parse flags and collect URLs |
|
do_review=false |
|
raw_args=() |
|
|
|
for arg in "$@"; do |
|
if [[ "$arg" == "--review" ]]; then |
|
do_review=true |
|
else |
|
raw_args+=("$arg") |
|
fi |
|
done |
|
|
|
# Collect URLs from remaining args (re-split on newlines) |
|
urls=() |
|
for arg in "${raw_args[@]+${raw_args[@]}}"; do |
|
while IFS= read -r line; do |
|
trimmed=$(echo "$line" | xargs) |
|
[[ -n "$trimmed" ]] && urls+=("$trimmed") |
|
done <<< "$arg" |
|
done |
|
|
|
# Read from stdin if it's not a terminal (piped input) |
|
if [[ ! -t 0 ]]; then |
|
while IFS= read -r line; do |
|
trimmed=$(echo "$line" | xargs) |
|
[[ -n "$trimmed" ]] && urls+=("$trimmed") |
|
done |
|
fi |
|
|
|
if [[ ${#urls[@]} -eq 0 ]]; then |
|
echo "Usage: ez [--review] <linear-url> [<linear-url> ...]" |
|
echo "" |
|
echo "Process Linear issues sequentially: for each issue, create a worktree," |
|
echo "spawn Claude Code to implement the fix, and create a PR." |
|
echo "" |
|
echo "Options:" |
|
echo " --review After all fixes, spawn parallel reviewers that review each PR," |
|
echo " fix any issues found, and push." |
|
echo "" |
|
echo "URLs can be space-separated args, newline-separated in quotes, or piped via stdin." |
|
exit 1 |
|
fi |
|
|
|
cd "$REPO_ROOT" |
|
git fetch --quiet |
|
|
|
total=${#urls[@]} |
|
current=0 |
|
failed=() |
|
|
|
# Track completed issues for the review phase: "worktree_path|issue_id|url" |
|
completed=() |
|
|
|
for url in "${urls[@]}"; do |
|
current=$((current + 1)) |
|
|
|
# Extract issue ID from URL (e.g., PF-254 from .../issue/PF-254/slug) |
|
if [[ "$url" =~ issue/([A-Z]+-[0-9]+) ]]; then |
|
issue_id="${BASH_REMATCH[1]}" |
|
else |
|
echo "WARNING: Could not extract issue ID from: $url — skipping" |
|
failed+=("$url (bad URL)") |
|
continue |
|
fi |
|
|
|
echo "" |
|
echo "================================================================" |
|
echo " [$current/$total] $issue_id" |
|
echo " $url" |
|
echo "================================================================" |
|
|
|
# Get branch name from Linear via Claude Code |
|
echo "-> Fetching branch name from Linear..." |
|
branch_name=$(claude -p --model "$BRANCH_MODEL" \ |
|
"Use the mcp__linear-server__get_issue tool to get issue '$issue_id'. Return ONLY the gitBranchName value. No markdown, no explanation, no quotes — just the raw branch name." \ |
|
2>/dev/null | tail -1 | xargs) || true |
|
|
|
if [[ -z "$branch_name" ]]; then |
|
echo "WARNING: Could not get branch name for $issue_id — skipping" |
|
failed+=("$issue_id (no branch name)") |
|
continue |
|
fi |
|
|
|
echo "-> Branch: $branch_name" |
|
|
|
# Create worktree (mirrors the `work` function) |
|
if [[ -d "$branch_name" ]]; then |
|
echo "-> Worktree already exists" |
|
elif git show-ref --verify --quiet "refs/heads/$branch_name" 2>/dev/null; then |
|
echo "-> Local branch exists — creating worktree" |
|
git worktree add "$branch_name" "$branch_name" |
|
(cd "$branch_name" && bun i) |
|
elif git show-ref --verify --quiet "refs/remotes/origin/$branch_name" 2>/dev/null; then |
|
echo "-> Remote branch exists — creating worktree tracking origin" |
|
git worktree add --track -b "$branch_name" "$branch_name" "origin/$branch_name" |
|
(cd "$branch_name" && bun i) |
|
else |
|
echo "-> New branch — creating worktree" |
|
git worktree add -b "$branch_name" "$branch_name" |
|
(cd "$branch_name" && bun i) |
|
fi |
|
|
|
worktree_path="$REPO_ROOT/$branch_name" |
|
|
|
# Build the claude command |
|
# --dangerously-skip-permissions: no tool approval prompts |
|
# --disallowedTools: block plan mode so claude just executes directly |
|
claude_args=(-p --dangerously-skip-permissions --disallowedTools EnterPlanMode) |
|
if [[ -n "$FIX_MODEL" ]]; then |
|
claude_args+=(--model "$FIX_MODEL") |
|
fi |
|
|
|
# Run Claude Code in the worktree to fix the issue and create a PR |
|
# Pipe prompt via stdin to avoid multi-line arg issues |
|
echo "-> Spawning Claude Code..." |
|
if (cd "$worktree_path" && cat <<PROMPT | claude "${claude_args[@]}" | render_md |
|
You are fixing Linear issue $issue_id. |
|
Linear URL: $url |
|
|
|
Instructions: |
|
1. Use the mcp__linear-server__get_issue tool to read the full issue details for '$issue_id' |
|
2. Explore the codebase to understand the relevant code and architecture |
|
3. Implement the fix |
|
4. Run 'bun run lint' to lint and format your changes |
|
5. Run 'bun run check' to type-check all packages |
|
6. Fix any lint or type errors in files you changed (repeat steps 4-5 until clean) |
|
7. Stage and commit all changes. Prefix the commit message with '[agent]' and end with: |
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
|
8. Push the branch and create a PR with 'gh pr create': |
|
- Prefix the PR title with '[agent]' |
|
- Include a summary of what was changed and why |
|
- Include the Linear issue link: $url |
|
- End the PR body with: Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
|
|
|
Do NOT use plan mode. Go straight to implementation. |
|
Follow all guidelines in CLAUDE.md. Do not over-engineer — make the minimum changes necessary. |
|
PROMPT |
|
); then |
|
echo "" |
|
echo "OK: $issue_id" |
|
completed+=("${worktree_path}|${issue_id}|${url}") |
|
else |
|
echo "" |
|
echo "WARNING: Claude Code exited with an error for $issue_id" |
|
failed+=("$issue_id (claude error)") |
|
fi |
|
done |
|
|
|
echo "" |
|
echo "================================================================" |
|
echo " Fixes done — $total issue(s), ${#completed[@]} succeeded, ${#failed[@]} failed" |
|
if [[ ${#failed[@]} -gt 0 ]]; then |
|
for f in "${failed[@]}"; do |
|
echo " FAIL: $f" |
|
done |
|
fi |
|
echo "================================================================" |
|
|
|
# ── Review phase ────────────────────────────────────────────────────────────── |
|
if $do_review && [[ ${#completed[@]} -gt 0 ]]; then |
|
echo "" |
|
echo "================================================================" |
|
echo " Spawning ${#completed[@]} reviewer(s) in parallel..." |
|
echo "================================================================" |
|
|
|
review_claude_args=(-p --dangerously-skip-permissions --disallowedTools EnterPlanMode) |
|
if [[ -n "$REVIEW_MODEL" ]]; then |
|
review_claude_args+=(--model "$REVIEW_MODEL") |
|
fi |
|
|
|
pids=() |
|
review_issues=() |
|
log_dir=$(mktemp -d) |
|
|
|
for entry in "${completed[@]}"; do |
|
IFS='|' read -r wt_path r_issue_id r_url <<< "$entry" |
|
review_issues+=("$r_issue_id") |
|
|
|
log_file="$log_dir/$r_issue_id.log" |
|
|
|
echo "-> Reviewer for $r_issue_id (log: $log_file)" |
|
|
|
( |
|
cd "$wt_path" |
|
|
|
# Find the PR number for this branch |
|
pr_url=$(gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --json url --jq '.[0].url' 2>/dev/null || echo "") |
|
|
|
cat <<REVIEW | claude "${review_claude_args[@]}" |
|
You are reviewing and fixing a PR for Linear issue $r_issue_id. |
|
Linear URL: $r_url |
|
PR: $pr_url |
|
|
|
Instructions: |
|
1. Use the mcp__linear-server__get_issue tool to read the full issue details for '$r_issue_id' |
|
2. Run 'gh pr diff' or 'git diff main...HEAD' to review all changes in this branch |
|
3. Review the code thoroughly: |
|
- Does it actually fix the issue described in the Linear ticket? |
|
- Are there bugs, logic errors, or security issues? |
|
- Does it follow the project's code style and patterns? |
|
- Are there edge cases not handled? |
|
4. If you find issues: fix them directly in the code. Do NOT post review comments. |
|
Do NOT ask for permission. Just fix the problems. |
|
5. After fixing, run 'bun run lint' and 'bun run check' |
|
6. If you made changes, commit them with a message like: |
|
[agent] review fixes for $r_issue_id |
|
|
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
|
7. Push the fixes: 'git push' |
|
8. If no issues found, just confirm the PR looks good — no need to commit anything. |
|
|
|
Do NOT use plan mode. Go straight to review. |
|
Do NOT post review comments to GitHub. Fix issues directly in the code. |
|
Follow all guidelines in CLAUDE.md. |
|
REVIEW |
|
) > "$log_file" 2>&1 & |
|
|
|
pids+=($!) |
|
done |
|
|
|
echo "" |
|
echo "-> Waiting for all reviewers to finish..." |
|
|
|
review_failed=() |
|
for i in "${!pids[@]}"; do |
|
if wait "${pids[$i]}"; then |
|
echo " OK: ${review_issues[$i]}" |
|
else |
|
echo " FAIL: ${review_issues[$i]}" |
|
review_failed+=("${review_issues[$i]}") |
|
fi |
|
done |
|
|
|
echo "" |
|
echo "================================================================" |
|
echo " Reviews done — ${#completed[@]} reviewed, ${#review_failed[@]} failed" |
|
if [[ ${#review_failed[@]} -gt 0 ]]; then |
|
for f in "${review_failed[@]}"; do |
|
echo " FAIL: $f" |
|
done |
|
fi |
|
echo " Logs: $log_dir" |
|
echo "================================================================" |
|
fi |
|
|
|
echo "" |
|
echo "All done." |