11#! /bin/bash
22# ─────────────────────────────────────────────────────────
3- # jules-handoff .sh
4- # Pulls a Jules branch and launches Claude Code with full context
3+ # jules_handoff .sh
4+ # Checks out a Jules scout branch and launches Claude Code with context
55#
66# Usage:
7- # ./jules-handoff.sh # interactive branch picker
8- # ./jules-handoff.sh <branch-name> # direct branch name
9- # ./jules-handoff.sh <github-pr-url> # paste a Jules PR URL
7+ # ./jules_handoff.sh # interactive branch picker
8+ # ./jules_handoff.sh <branch-name> # direct branch name
9+ # ─────────────────────────────────────────────────────────
10+
11+ # ── Config — only thing to change when reusing across repos
12+ DEFAULT_BRANCH=" master"
13+ UPSTREAM_FALLBACK=" https://github.com/RocketPy-Team/RocketPy.git"
1014# ─────────────────────────────────────────────────────────
1115
1216set -e
1317
1418REPO_DIR=$( git rev-parse --show-toplevel 2> /dev/null)
1519if [ -z " $REPO_DIR " ]; then
16- echo " ❌ Not inside a git repo. Run this from your astropy fork directory. "
20+ echo " ❌ Not inside a git repo."
1721 exit 1
1822fi
1923
@@ -23,34 +27,22 @@ cd "$REPO_DIR"
2327
2428UPSTREAM_URL=$( git remote get-url upstream 2> /dev/null || echo " " )
2529if [ -z " $UPSTREAM_URL " ]; then
26- echo " ❌ No upstream remote found. Run: git remote add upstream <original-repo-url> "
30+ echo " ❌ No upstream remote found. Run: git remote add upstream $UPSTREAM_FALLBACK "
2731 exit 1
2832fi
2933
30- # Parse owner/repo from URL (handles both https and ssh)
31- UPSTREAM_REPO=$( echo " $UPSTREAM_URL " | sed ' s|https://github.com/||' | sed ' s|git@github.com:||' | sed ' s|[.]git$||' )
34+ UPSTREAM_REPO=$( echo " $UPSTREAM_URL " \
35+ | sed ' s|https://github.com/||' \
36+ | sed ' s|git@github.com:||' \
37+ | sed ' s|[.]git$||' )
3238
3339echo " 📡 Upstream: $UPSTREAM_REPO "
3440
35- # ── Step 1: Determine the Jules branch ───────────────────
41+ # ── Step 1: Determine the branch ────── ───────────────────
3642
3743if [ -n " $1 " ]; then
38- INPUT=" $1 "
39-
40- # If it's a GitHub PR URL, extract the branch from the API
41- if [[ " $INPUT " == https://github.com/* ]]; then
42- echo " 🔍 Fetching branch from PR URL..."
43- PR_NUMBER=$( echo " $INPUT " | grep -oE ' /pull/[0-9]+' | grep -oE ' [0-9]+' )
44- REPO_SLUG=$( echo " $INPUT " | sed ' s|https://github.com/||' | cut -d' /' -f1-2)
45- BRANCH=$( curl -s " https://api.github.com/repos/$REPO_SLUG /pulls/$PR_NUMBER " \
46- | python3 -c " import sys,json; print(json.load(sys.stdin)['head']['ref'])" )
47- echo " 📌 Branch: $BRANCH "
48- else
49- BRANCH=" $INPUT "
50- fi
51-
44+ BRANCH=" $1 "
5245else
53- # Interactive: list recent Jules branches
5446 echo " "
5547 echo " 🔍 Fetching recent branches from origin..."
5648 git fetch origin --quiet
6153
6254 BRANCHES=$( git branch -r --sort=-committerdate \
6355 | grep ' origin/' \
64- | grep -v ' origin/main ' \
56+ | grep -v " origin/$DEFAULT_BRANCH " \
6557 | sed ' s|origin/||' \
6658 | head -20)
6759
6860 if [ -z " $BRANCHES " ]; then
69- echo " No branches found other than main ."
70- echo " Jules may not have created a branch yet — check jules.google.com "
61+ echo " No branches found other than $DEFAULT_BRANCH ."
62+ echo " Jules may not have created a branch yet. "
7163 exit 1
7264 fi
7365
8981 fi
9082fi
9183
92- # ── Step 2: Pull the branch locally ──────────────────────
84+ # ── Step 2: Check out the branch ─── ──────────────────────
9385
9486echo " "
9587echo " 📥 Checking out branch: $BRANCH "
@@ -105,167 +97,136 @@ fi
10597
10698echo " ✅ On branch: $BRANCH "
10799
108- # ── Step 3: Read and extract selected issue from scout_report.md ──
100+ # ── Step 3: Read scout_report.md ─────────────────────── ──
109101
110102echo " "
111- echo " 📋 Reading Jules scout report..."
103+ echo " 📋 Reading scout report..."
112104
113105SCOUT_REPORT=" "
106+ SELECTED=" "
107+
114108if [ -f " scout_report.md" ]; then
115- # Read the selected issue number from the SELECTED ISSUE line
116109 SELECTED=$( grep " SELECTED ISSUE:" scout_report.md | grep -oE ' [0-9]+' | head -1)
117110
118111 if [ -z " $SELECTED " ]; then
119112 echo " "
120- echo " 👉 Open scout_report.md and fill in the SELECTED ISSUE number (1, 2, or 3)"
121- echo " Then re-run this script."
113+ echo " 👉 Open scout_report.md and fill in the SELECTED ISSUE number, then re-run."
122114 exit 0
123115 fi
124116
125117 echo " ✅ Selected issue: #$SELECTED "
126118
127- # Extract only the selected issue section
128- SCOUT_REPORT=$( python3 - " $SELECTED " << 'PYEOF '
119+ SCOUT_REPORT=$( python3 << PYEOF
129120import sys, re
130121
131- selected = sys.argv[1]
122+ selected = " $SELECTED "
132123with open("scout_report.md") as f:
133124 text = f.read()
134125
135- # Find the section matching "## Issue N"
136- pattern = rf"(## Issue {selected} —.*?)(?=
137- ## Issue \d|$)"
126+ pattern = r"(## Issue " + selected + r" \u2014.*?)(?=## Issue \d|\Z)"
138127match = re.search(pattern, text, re.DOTALL)
139128if match:
140129 print(match.group(1).strip())
141130else:
142- print(f"Could not find Issue {selected} in scout_report.md" )
131+ print(text )
143132PYEOF
144133)
145- echo " ✅ Extracted Issue $SELECTED from scout_report.md"
134+ echo " ✅ Extracted issue $SELECTED from scout_report.md"
146135else
147- echo " ⚠️ No scout_report.md found — will use PR description only "
136+ echo " ⚠️ No scout_report.md found on this branch "
148137fi
149138
150- # ── Step 4: Get PR info and extract issue number ──────────
139+ # ── Step 4: Check for competing PRs ──────────── ──────────
151140
152- PR_BODY=" "
153- PR_TITLE=" "
154- ISSUE_NUMBER=" "
141+ ISSUE_NUMBER=" $SELECTED "
155142ISSUE_STATUS=" "
143+ ISSUE_TITLE=" "
156144ISSUE_BODY=" "
157145
158- if command -v gh & > /dev/null; then
159- PR_JSON=$( gh pr list --head " $BRANCH " --json body,title,url 2> /dev/null || echo " []" )
160- PR_TITLE=$( echo " $PR_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d[0]['title'] if d else '')" 2> /dev/null || echo " " )
161- PR_BODY=$( echo " $PR_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d[0]['body'] if d else '')" 2> /dev/null || echo " " )
162-
163- # Try to extract issue number from PR body or branch name
164- ISSUE_NUMBER=$( echo " $PR_BODY $BRANCH " | grep -oE ' #[0-9]+|issues/[0-9]+' | grep -oE ' [0-9]+' | head -1)
165-
166- if [ -n " $ISSUE_NUMBER " ]; then
167- echo " 🔍 Checking upstream issue #$ISSUE_NUMBER ..."
168- ISSUE_JSON=$( gh issue view " $ISSUE_NUMBER " --repo " $UPSTREAM_REPO " --json state,title,body 2> /dev/null || echo " " )
169-
170- if [ -n " $ISSUE_JSON " ]; then
171- ISSUE_STATUS=$( echo " $ISSUE_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))" )
172- ISSUE_TITLE=$( echo " $ISSUE_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d.get('title',''))" )
173- ISSUE_BODY=$( echo " $ISSUE_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])" )
174- echo " 📌 Issue #$ISSUE_NUMBER is: $ISSUE_STATUS — $ISSUE_TITLE "
175-
176- if [ " $ISSUE_STATUS " = " closed" ]; then
177- echo " "
178- echo " ⛔ Issue #$ISSUE_NUMBER is already CLOSED — someone already fixed it."
179- echo " Aborted. Pick a different Jules branch."
180- exit 0
181- fi
146+ if command -v gh & > /dev/null && [ -n " $ISSUE_NUMBER " ]; then
147+ echo " 🔍 Checking upstream issue #$ISSUE_NUMBER ..."
148+ ISSUE_JSON=$( gh issue view " $ISSUE_NUMBER " --repo " $UPSTREAM_REPO " --json state,title,body 2> /dev/null || echo " " )
149+
150+ if [ -n " $ISSUE_JSON " ]; then
151+ ISSUE_STATUS=$( echo " $ISSUE_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))" )
152+ ISSUE_TITLE=$( echo " $ISSUE_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d.get('title',''))" )
153+ ISSUE_BODY=$( echo " $ISSUE_JSON " | python3 -c " import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])" )
154+ echo " 📌 Issue #$ISSUE_NUMBER : $ISSUE_STATUS — $ISSUE_TITLE "
155+
156+ if [ " $ISSUE_STATUS " = " closed" ]; then
157+ echo " "
158+ echo " ⛔ Issue #$ISSUE_NUMBER is already closed. Pick a different issue."
159+ exit 0
182160 fi
161+ fi
183162
184- # Check for competing open PRs
185- echo " 🔍 Checking for competing PRs on issue #$ISSUE_NUMBER ..."
186- COMPETING=$( gh pr list --repo " $UPSTREAM_REPO " --search " fixes #$ISSUE_NUMBER " --json number,title,state,isDraft,url 2> /dev/null || echo " []" )
163+ echo " 🔍 Checking for competing PRs..."
164+ COMPETING=$( gh pr list \
165+ --repo " $UPSTREAM_REPO " \
166+ --search " fixes #$ISSUE_NUMBER " \
167+ --json number,title,isDraft,url \
168+ 2> /dev/null || echo " []" )
187169
188- PR_COUNT=$( echo " $COMPETING " | python3 -c " import sys,json; print(len(json.load(sys.stdin)))" )
170+ PR_COUNT=$( echo " $COMPETING " | python3 -c " import sys,json; print(len(json.load(sys.stdin)))" )
189171
190- if [ " $PR_COUNT " -gt " 0" ]; then
191- echo " "
192- echo " ⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER :"
193- echo " $COMPETING " | python3 -c "
172+ if [ " $PR_COUNT " -gt " 0" ]; then
173+ echo " "
174+ echo " ⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER :"
175+ echo " $COMPETING " | python3 -c "
194176import sys, json
195- prs = json.load(sys.stdin)
196- for pr in prs:
177+ for pr in json.load(sys.stdin):
197178 draft = ' [DRAFT]' if pr.get('isDraft') else ''
198179 print(' #' + str(pr['number']) + draft + ': ' + pr['title'])
199180 print(' ' + pr['url'])
200181"
201- echo " "
202- echo " Don't waste your time — pick a different Jules branch."
203- exit 0
204- else
205- echo " ✅ No competing PRs found — you're clear to proceed."
206- fi
182+ echo " "
183+ echo " Pick a different issue."
184+ exit 0
185+ else
186+ echo " ✅ No competing PRs — you're clear to proceed."
207187 fi
208188fi
209189
210- # ── Step 5: Write Claude Code context file ──────────── ────
190+ # ── Step 5: Build context file and launch Claude Code ────
211191
212192CONTEXT_FILE=" /tmp/jules-context-$( date +%s) .md"
213193
214194cat > " $CONTEXT_FILE " << CONTEXT
215- # Jules Handoff Context
195+ # Jules Handoff — $UPSTREAM_REPO
216196
217197## Branch
218198$BRANCH
219199
220- ## Jules PR Title
221- ${PR_TITLE:- " (not found)" }
200+ ## Selected Issue
201+ #${ISSUE_NUMBER:- " unknown" } — ${ISSUE_TITLE:- " unknown" }
202+ Status: ${ISSUE_STATUS:- " unknown" }
222203
223204---
224205
225- ## Jules Scout Report (scout.md)
226- ${SCOUT_REPORT:- " (No scout_report.md on this branch — refer to PR description below )" }
206+ ## Scout Report
207+ ${SCOUT_REPORT:- " (no scout_report.md found )" }
227208
228209---
229210
230- ## Jules PR Description
231- ${PR_BODY :- " (No PR found — check jules.google.com )" }
211+ ## Issue Body
212+ ${ISSUE_BODY :- " (could not fetch )" }
232213
233214---
234215
235- ## Upstream Issue #${ISSUE_NUMBER:- " unknown" }
236- Status: ${ISSUE_STATUS:- " unknown" }
237- Title: ${ISSUE_TITLE:- " unknown" }
238-
239- ${ISSUE_BODY:- " (Could not fetch issue body — check https://github.com/astropy/astropy/issues)" }
240-
241- ---
216+ ## Instructions for Claude Code
242217
243- ## Your job (Claude Code)
218+ Read CLAUDE.md before doing anything else — it contains scope rules and
219+ conventions you must follow.
244220
245- The scout report and issue above are your source of truth — ignore any diff noise
246- from main being ahead of this branch due to the daily sync automation.
247-
248- 1. Read scout.md carefully — Jules has already identified the files and approach.
249-
250- 2. Check if the issue is still open and unassigned upstream before starting:
251- https://github.com/astropy/astropy/issues/${ISSUE_NUMBER:- " " }
252-
253- 3. Run the existing tests for the relevant subpackage first to establish a baseline:
254- \` python -m pytest astropy/<subpackage>/tests/ -x -v\`
255-
256- 4. Implement the fix described in scout.md. Follow astropy standards:
257- - Type hints on all new functions
258- - Numpy-style docstrings
259- - Tests in the corresponding tests/ directory
260- - No public API changes unless the issue explicitly requires it
261-
262- 5. Once tests pass, give me a 3-sentence summary of what changed for the PR description.
221+ 1. Read the scout report and issue above carefully.
222+ 2. Run existing tests for the relevant module first to establish a baseline:
223+ \` python -m pytest tests/ -x -q -k <relevant_test_file>\`
224+ 3. Implement the fix. Stay strictly within the scope of the issue.
225+ 4. Write tests using pytest.approx for any float assertions.
226+ 5. Ensure all new public functions have NumPy docstrings.
227+ 6. When done, give me a 3-sentence summary of what changed for the PR description.
263228CONTEXT
264229
265- echo " ✅ Context ready"
266-
267- # ── Step 6: Print context and launch Claude Code ──────────
268-
269230echo " "
270231echo " 🚀 Launching Claude Code..."
271232echo " "
0 commit comments