From a04075501f7c91fa8d37f8fcf2e795d1912c7db0 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Sun, 15 Mar 2026 11:03:06 -0700 Subject: [PATCH 1/4] chore: added personal toolkit files --- .coderabbit | 43 +++++ .github/workflows/sync-upstream.yml | 46 +++++ AGENTS.md | 0 CLAUDE.md | 71 ++++++++ jules_handoff.sh | 273 ++++++++++++++++++++++++++++ prep_pr.sh | 64 +++++++ 6 files changed, 497 insertions(+) create mode 100644 .coderabbit create mode 100644 .github/workflows/sync-upstream.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 jules_handoff.sh create mode 100644 prep_pr.sh diff --git a/.coderabbit b/.coderabbit new file mode 100644 index 000000000..717968302 --- /dev/null +++ b/.coderabbit @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json + +language: "en-US" + +reviews: + profile: "assertive" + poem: false + in_progress_fortune: false + + auto_review: + enabled: true + drafts: false + ignore_title_keywords: + - "wip" + - "do not review" + + path_instructions: + - path: "**/*.py" + instructions: | + This is RocketPy, a scientific Python rocket trajectory simulator. + - Docstrings must use NumPy format with parameter types (e.g. `x : float`) but no units in the type field + - Do not add type hints to function signatures — the codebase does not use them + - No magic numbers without a comment explaining what they represent + - Flag any new public function or class missing a NumPy docstring + - Flag any logic that duplicates existing RocketPy utilities + + - path: "tests/**" + instructions: | + Tests use pytest with pytest.approx for float comparisons, not plain == or np.isclose. + Flag any float assertion that does not use pytest.approx. + Test names should describe the scenario being tested, not just the function name. + + finishing_touches: + docstrings: + enabled: true + unit_tests: + enabled: true + +knowledge_base: + web_search: + enabled: true + learnings: + scope: auto \ No newline at end of file diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 000000000..8a51c3194 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,46 @@ +# .github/workflows/sync-upstream.yml +# Syncs your fork's default branch with the upstream repo daily. +# Also runs on manual trigger via workflow_dispatch. +# +# Setup: +# 1. Copy this file to .github/workflows/sync-upstream.yml in your fork +# 2. Set the UPSTREAM_REPO variable below to the upstream owner/repo + +name: Sync fork with upstream + +on: + schedule: + - cron: '0 12 * * *' # 5:00 AM PT daily (runs before Jules scout at 6) + workflow_dispatch: # manual trigger from Actions tab + +env: + UPSTREAM_REPO: RocketPy-Team/RocketPy # change per fork + BRANCH: main # change if upstream uses 'main' + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout fork + uses: actions/checkout@v4 + with: + ref: ${{ env.BRANCH }} + fetch-depth: 0 + + - name: Add upstream remote + run: git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git + + - name: Fetch upstream + run: git fetch upstream ${{ env.BRANCH }} + + - name: Rebase on upstream + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rebase upstream/${{ env.BRANCH }} + + - name: Push to fork + run: git push origin ${{ env.BRANCH }} --force-with-lease \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..e69de29bb diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1b2d9a39a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Install dependencies +make install + +# Run tests +make pytest # standard test suite +make pytest-slow # slow/marked tests with verbose output +make coverage # tests with coverage + +# Lint and format +make format # ruff format + isort +make lint # ruff + pylint +make ruff-lint # ruff only +make pylint # pylint only + +# Docs +make build-docs +``` + +**Run a single test file:** +```bash +pytest tests/unit/test_environment.py -v +``` + +**Run a single test by name:** +```bash +pytest tests/unit/test_environment.py::test_methodname_expectedbehaviour -v +``` + +## Architecture + +RocketPy simulates 6-DOF rocket trajectories. The core workflow is linear: + +``` +Environment → Motor → Rocket → Flight +``` + +**`rocketpy/environment/`** — Atmospheric models, weather data fetching (NOAA, Wyoming soundings, GFS forecasts). The `Environment` class (~116KB) is the entry point for atmospheric conditions. + +**`rocketpy/motors/`** — `Motor` is the base class. `SolidMotor`, `HybridMotor`, and `LiquidMotor` extend it. `Tank`, `TankGeometry`, and `Fluid` support liquid/hybrid propellant modeling. + +**`rocketpy/rocket/`** — `Rocket` aggregates a motor and aerodynamic surfaces. `aero_surface/` contains fins, nose cone, and tail implementations. `Parachute` uses trigger functions for deployment. + +**`rocketpy/simulation/`** — `Flight` (~162KB) is the simulation engine, integrating equations of motion with scipy's LSODA solver. `MonteCarlo` orchestrates many `Flight` runs for dispersion analysis. + +**`rocketpy/stochastic/`** — Wraps any component (Environment, Rocket, Motor, Flight) with uncertainty distributions for Monte Carlo input generation. + +**`rocketpy/mathutils/`** — `Function` class wraps callable data (arrays, lambdas, files) with interpolation and mathematical operations. Heavily used throughout for aerodynamic curves, thrust profiles, etc. + +**`rocketpy/plots/` and `rocketpy/prints/`** — Visualization and text output, each mirroring the module structure of the core classes. + +**`rocketpy/sensors/`** — Simulated sensors (accelerometer, gyroscope, barometer, GNSS) that can be attached to a `Rocket`. + +**`rocketpy/sensitivity/`** — Global sensitivity analysis via `SensitivityModel`. + +## Coding Standards + +- **Docstrings:** NumPy style with `Parameters`, `Returns`, and `Examples` sections. Always include units for physical quantities (e.g., "in meters", "in radians"). +- **No type hints in function signatures** — put types in the docstring `Parameters` section instead. +- **SI units by default** throughout the codebase (meters, kilograms, seconds, radians). +- **No magic numbers** — name constants with `UPPER_SNAKE_CASE` and comment their physical meaning. +- **Performance:** Use vectorized numpy operations. Cache expensive computations with `cached_property`. +- **Test names:** `test_methodname_expectedbehaviour` pattern. Use `pytest.approx` for float comparisons. +- **Tests follow AAA** (Arrange, Act, Assert) with fixtures from `tests/fixtures/`. +- **Backward compatibility:** Use deprecation warnings before removing public API features; document changes in CHANGELOG. diff --git a/jules_handoff.sh b/jules_handoff.sh new file mode 100644 index 000000000..0ff7644e2 --- /dev/null +++ b/jules_handoff.sh @@ -0,0 +1,273 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────── +# jules-handoff.sh +# Pulls a Jules branch and launches Claude Code with full context +# +# Usage: +# ./jules-handoff.sh # interactive branch picker +# ./jules-handoff.sh # direct branch name +# ./jules-handoff.sh # paste a Jules PR URL +# ───────────────────────────────────────────────────────── + +set -e + +REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -z "$REPO_DIR" ]; then + echo "❌ Not inside a git repo. Run this from your astropy fork directory." + exit 1 +fi + +cd "$REPO_DIR" + +# ── Detect upstream repo slug ───────────────────────────── + +UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null || echo "") +if [ -z "$UPSTREAM_URL" ]; then + echo "❌ No upstream remote found. Run: git remote add upstream " + exit 1 +fi + +# Parse owner/repo from URL (handles both https and ssh) +UPSTREAM_REPO=$(echo "$UPSTREAM_URL" | sed 's|https://github.com/||' | sed 's|git@github.com:||' | sed 's|[.]git$||') + +echo "📡 Upstream: $UPSTREAM_REPO" + +# ── Step 1: Determine the Jules branch ─────────────────── + +if [ -n "$1" ]; then + INPUT="$1" + + # If it's a GitHub PR URL, extract the branch from the API + if [[ "$INPUT" == https://github.com/* ]]; then + echo "🔍 Fetching branch from PR URL..." + PR_NUMBER=$(echo "$INPUT" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+') + REPO_SLUG=$(echo "$INPUT" | sed 's|https://github.com/||' | cut -d'/' -f1-2) + BRANCH=$(curl -s "https://api.github.com/repos/$REPO_SLUG/pulls/$PR_NUMBER" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])") + echo "📌 Branch: $BRANCH" + else + BRANCH="$INPUT" + fi + +else + # Interactive: list recent Jules branches + echo "" + echo "🔍 Fetching recent branches from origin..." + git fetch origin --quiet + + echo "" + echo "Recent branches (newest first):" + echo "──────────────────────────────" + + BRANCHES=$(git branch -r --sort=-committerdate \ + | grep 'origin/' \ + | grep -v 'origin/main' \ + | sed 's|origin/||' \ + | head -20) + + if [ -z "$BRANCHES" ]; then + echo "No branches found other than main." + echo "Jules may not have created a branch yet — check jules.google.com" + exit 1 + fi + + i=1 + while IFS= read -r branch; do + echo " $i) $branch" + i=$((i+1)) + done <<< "$BRANCHES" + + echo "" + printf "Enter number of the Jules branch to work on: " + read -r SELECTION + + BRANCH=$(echo "$BRANCHES" | sed -n "${SELECTION}p" | xargs) + + if [ -z "$BRANCH" ]; then + echo "❌ Invalid selection" + exit 1 + fi +fi + +# ── Step 2: Pull the branch locally ────────────────────── + +echo "" +echo "📥 Checking out branch: $BRANCH" + +git fetch origin "$BRANCH" --quiet + +if git show-ref --verify --quiet "refs/heads/$BRANCH"; then + git checkout "$BRANCH" + git pull origin "$BRANCH" --quiet +else + git checkout -b "$BRANCH" "origin/$BRANCH" +fi + +echo "✅ On branch: $BRANCH" + +# ── Step 3: Read and extract selected issue from scout_report.md ── + +echo "" +echo "📋 Reading Jules scout report..." + +SCOUT_REPORT="" +if [ -f "scout_report.md" ]; then + # Read the selected issue number from the SELECTED ISSUE line + SELECTED=$(grep "SELECTED ISSUE:" scout_report.md | grep -oE '[0-9]+' | head -1) + + if [ -z "$SELECTED" ]; then + echo "" + echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number (1, 2, or 3)" + echo " Then re-run this script." + exit 0 + fi + + echo "✅ Selected issue: #$SELECTED" + + # Extract only the selected issue section + SCOUT_REPORT=$(python3 - "$SELECTED" << 'PYEOF' +import sys, re + +selected = sys.argv[1] +with open("scout_report.md") as f: + text = f.read() + +# Find the section matching "## Issue N" +pattern = rf"(## Issue {selected} —.*?)(?= +## Issue \d|$)" +match = re.search(pattern, text, re.DOTALL) +if match: + print(match.group(1).strip()) +else: + print(f"Could not find Issue {selected} in scout_report.md") +PYEOF +) + echo "✅ Extracted Issue $SELECTED from scout_report.md" +else + echo "⚠️ No scout_report.md found — will use PR description only" +fi + +# ── Step 4: Get PR info and extract issue number ────────── + +PR_BODY="" +PR_TITLE="" +ISSUE_NUMBER="" +ISSUE_STATUS="" +ISSUE_BODY="" + +if command -v gh &> /dev/null; then + PR_JSON=$(gh pr list --head "$BRANCH" --json body,title,url 2>/dev/null || echo "[]") + 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 "") + 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 "") + + # Try to extract issue number from PR body or branch name + ISSUE_NUMBER=$(echo "$PR_BODY $BRANCH" | grep -oE '#[0-9]+|issues/[0-9]+' | grep -oE '[0-9]+' | head -1) + + if [ -n "$ISSUE_NUMBER" ]; then + echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." + ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") + + if [ -n "$ISSUE_JSON" ]; then + ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") + ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") + ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") + echo "📌 Issue #$ISSUE_NUMBER is: $ISSUE_STATUS — $ISSUE_TITLE" + + if [ "$ISSUE_STATUS" = "closed" ]; then + echo "" + echo "⛔ Issue #$ISSUE_NUMBER is already CLOSED — someone already fixed it." + echo " Aborted. Pick a different Jules branch." + exit 0 + fi + fi + + # Check for competing open PRs + echo "🔍 Checking for competing PRs on issue #$ISSUE_NUMBER..." + COMPETING=$(gh pr list --repo "$UPSTREAM_REPO" --search "fixes #$ISSUE_NUMBER" --json number,title,state,isDraft,url 2>/dev/null || echo "[]") + + PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") + + if [ "$PR_COUNT" -gt "0" ]; then + echo "" + echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" + echo "$COMPETING" | python3 -c " +import sys, json +prs = json.load(sys.stdin) +for pr in prs: + draft = ' [DRAFT]' if pr.get('isDraft') else '' + print(' #' + str(pr['number']) + draft + ': ' + pr['title']) + print(' ' + pr['url']) +" + echo "" + echo " Don't waste your time — pick a different Jules branch." + exit 0 + else + echo "✅ No competing PRs found — you're clear to proceed." + fi + fi +fi + +# ── Step 5: Write Claude Code context file ──────────────── + +CONTEXT_FILE="/tmp/jules-context-$(date +%s).md" + +cat > "$CONTEXT_FILE" << CONTEXT +# Jules Handoff Context + +## Branch +$BRANCH + +## Jules PR Title +${PR_TITLE:-"(not found)"} + +--- + +## Jules Scout Report (scout.md) +${SCOUT_REPORT:-"(No scout_report.md on this branch — refer to PR description below)"} + +--- + +## Jules PR Description +${PR_BODY:-"(No PR found — check jules.google.com)"} + +--- + +## Upstream Issue #${ISSUE_NUMBER:-"unknown"} +Status: ${ISSUE_STATUS:-"unknown"} +Title: ${ISSUE_TITLE:-"unknown"} + +${ISSUE_BODY:-"(Could not fetch issue body — check https://github.com/astropy/astropy/issues)"} + +--- + +## Your job (Claude Code) + +The scout report and issue above are your source of truth — ignore any diff noise +from main being ahead of this branch due to the daily sync automation. + +1. Read scout.md carefully — Jules has already identified the files and approach. + +2. Check if the issue is still open and unassigned upstream before starting: + https://github.com/astropy/astropy/issues/${ISSUE_NUMBER:-""} + +3. Run the existing tests for the relevant subpackage first to establish a baseline: + \`python -m pytest astropy//tests/ -x -v\` + +4. Implement the fix described in scout.md. Follow astropy standards: + - Type hints on all new functions + - Numpy-style docstrings + - Tests in the corresponding tests/ directory + - No public API changes unless the issue explicitly requires it + +5. Once tests pass, give me a 3-sentence summary of what changed for the PR description. +CONTEXT + +echo "✅ Context ready" + +# ── Step 6: Print context and launch Claude Code ────────── + +echo "" +echo "🚀 Launching Claude Code..." +echo "" + +claude < "$CONTEXT_FILE" \ No newline at end of file diff --git a/prep_pr.sh b/prep_pr.sh new file mode 100644 index 000000000..bac87e26b --- /dev/null +++ b/prep_pr.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# prep_pr.sh — strips toolkit files from the current branch before opening a PR +# Usage: ./prep_pr.sh +# Run after cleaning up commit history. Modifies the current branch in place. + +set -euo pipefail + +TOOLKIT_FILES=( + "AGENTS.md" + "CLAUDE.md" + "jules_handoff.sh" + ".coderabbit.yaml" + "scout_report.md" + ".github/workflows/sync-upstream.yml" + ".github/workflows/scout.yml" +) + +RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m' +die() { echo -e "${RED}error: $1${NC}" >&2; exit 1; } +warn() { echo -e "${YELLOW}warn: $1${NC}"; } +ok() { echo -e "${GREEN}ok: $1${NC}"; } + +command -v git &>/dev/null || die "git not found" +git rev-parse --git-dir &>/dev/null || die "not inside a git repo" + +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) \ + || die "HEAD is detached — check out your working branch first" + +[[ "$BRANCH" == "master" || "$BRANCH" == "main" ]] \ + && die "you're on $BRANCH — check out your working branch first" + +git diff --quiet && git diff --cached --quiet \ + || die "you have uncommitted changes — commit or stash them first" + +echo "" +echo " Branch : $BRANCH" +echo " Will strip toolkit files and add a cleanup commit." +echo "" +read -rp "Proceed? [y/N] " confirm +[[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; } + +STRIPPED=() +for f in "${TOOLKIT_FILES[@]}"; do + if git ls-files --error-unmatch "$f" &>/dev/null 2>&1; then + git rm -f "$f" + STRIPPED+=("$f") + fi +done + +if [[ ${#STRIPPED[@]} -eq 0 ]]; then + warn "no toolkit files found — nothing to strip" + exit 0 +fi + +git commit -m "chore: strip toolkit files before PR + +Local workflow helpers not intended for upstream: +$(printf ' - %s\n' "${STRIPPED[@]}")" + +ok "stripped: ${STRIPPED[*]}" +echo "" +echo " Next:" +echo " git push origin $BRANCH" +echo " open PR from $BRANCH → upstream master" \ No newline at end of file From 2c8bd9140a4b85efa81cbd3199b30d99f4e7f1fb Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Sun, 15 Mar 2026 11:08:08 -0700 Subject: [PATCH 2/4] update branch name in workflow --- .github/workflows/sync-upstream.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 8a51c3194..061f1a812 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -10,12 +10,12 @@ name: Sync fork with upstream on: schedule: - - cron: '0 12 * * *' # 5:00 AM PT daily (runs before Jules scout at 6) + - cron: '0 12 * * *' # 5:00 AM PT daily workflow_dispatch: # manual trigger from Actions tab env: UPSTREAM_REPO: RocketPy-Team/RocketPy # change per fork - BRANCH: main # change if upstream uses 'main' + BRANCH: master # change if upstream uses 'main' jobs: sync: From 8159eda5af9e154dbea7e3e7713c05ec615d4912 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Mon, 16 Mar 2026 22:13:13 -0700 Subject: [PATCH 3/4] chore: update toolkit files --- CLAUDE.md | 41 +++++++++ jules_handoff.sh | 211 +++++++++++++++++++---------------------------- 2 files changed, 127 insertions(+), 125 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1b2d9a39a..d70b2e15c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,3 +69,44 @@ Environment → Motor → Rocket → Flight - **Test names:** `test_methodname_expectedbehaviour` pattern. Use `pytest.approx` for float comparisons. - **Tests follow AAA** (Arrange, Act, Assert) with fixtures from `tests/fixtures/`. - **Backward compatibility:** Use deprecation warnings before removing public API features; document changes in CHANGELOG. + +# RocketPy fork instructions for Claude Code + +## What this repo is +RocketPy is a 6-DOF rocket trajectory simulator in pure Python. The main source +code lives in `rocketpy/`. Tests live in `tests/`. The default branch is `master`. + +## My workflow +- I am a solo contributor. Jules scouts issues and creates a branch. I hand off + to you (Claude Code) to build the implementation. +- Branches follow the pattern `scout/YYYY-MM-DD-brief-description`. +- Read `scout_report.md` in the repo root at the start of every session — it + contains the selected issue and any context Jules gathered. + +## Scope rules — read these carefully +- Only touch files directly relevant to the issue. Do not refactor unrelated code. +- Do not modify existing function signatures unless the issue explicitly requires it. +- Do not add type hints — the codebase does not use them. +- Do not rename variables or reformat code outside the files you are changing. +- If you think something adjacent should be fixed, mention it in a comment to me + instead of changing it. + +## Code conventions +- Docstrings: NumPy format only. Every new public function and class needs one. + Parameter types go in the docstring (e.g. `x : float`), not as type hints. + Do not document units in the type field — just the type name. +- No magic numbers. Any numeric constant needs a comment explaining what it is. +- Follow the style of the surrounding code exactly — indentation, spacing, naming. + +## Before you consider something done +- All new public functions have NumPy docstrings +- Tests written and passing +- No files modified outside the scope of the issue +- No type hints added anywhere +- Diff is clean — no stray whitespace changes, no reformatting of untouched lines + +## What to ask me before doing +- Any change to an existing public API +- Adding a new dependency +- Creating a new file that isn't a test or a direct implementation of the issue +- Anything that feels like it goes beyond the issue scopeß \ No newline at end of file diff --git a/jules_handoff.sh b/jules_handoff.sh index 0ff7644e2..1b2ac5463 100644 --- a/jules_handoff.sh +++ b/jules_handoff.sh @@ -1,19 +1,23 @@ #!/bin/bash # ───────────────────────────────────────────────────────── -# jules-handoff.sh -# Pulls a Jules branch and launches Claude Code with full context +# jules_handoff.sh +# Checks out a Jules scout branch and launches Claude Code with context # # Usage: -# ./jules-handoff.sh # interactive branch picker -# ./jules-handoff.sh # direct branch name -# ./jules-handoff.sh # paste a Jules PR URL +# ./jules_handoff.sh # interactive branch picker +# ./jules_handoff.sh # direct branch name +# ───────────────────────────────────────────────────────── + +# ── Config — only thing to change when reusing across repos +DEFAULT_BRANCH="master" +UPSTREAM_FALLBACK="https://github.com/RocketPy-Team/RocketPy.git" # ───────────────────────────────────────────────────────── set -e REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null) if [ -z "$REPO_DIR" ]; then - echo "❌ Not inside a git repo. Run this from your astropy fork directory." + echo "❌ Not inside a git repo." exit 1 fi @@ -23,34 +27,22 @@ cd "$REPO_DIR" UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null || echo "") if [ -z "$UPSTREAM_URL" ]; then - echo "❌ No upstream remote found. Run: git remote add upstream " + echo "❌ No upstream remote found. Run: git remote add upstream $UPSTREAM_FALLBACK" exit 1 fi -# Parse owner/repo from URL (handles both https and ssh) -UPSTREAM_REPO=$(echo "$UPSTREAM_URL" | sed 's|https://github.com/||' | sed 's|git@github.com:||' | sed 's|[.]git$||') +UPSTREAM_REPO=$(echo "$UPSTREAM_URL" \ + | sed 's|https://github.com/||' \ + | sed 's|git@github.com:||' \ + | sed 's|[.]git$||') echo "📡 Upstream: $UPSTREAM_REPO" -# ── Step 1: Determine the Jules branch ─────────────────── +# ── Step 1: Determine the branch ───────────────────────── if [ -n "$1" ]; then - INPUT="$1" - - # If it's a GitHub PR URL, extract the branch from the API - if [[ "$INPUT" == https://github.com/* ]]; then - echo "🔍 Fetching branch from PR URL..." - PR_NUMBER=$(echo "$INPUT" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+') - REPO_SLUG=$(echo "$INPUT" | sed 's|https://github.com/||' | cut -d'/' -f1-2) - BRANCH=$(curl -s "https://api.github.com/repos/$REPO_SLUG/pulls/$PR_NUMBER" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])") - echo "📌 Branch: $BRANCH" - else - BRANCH="$INPUT" - fi - + BRANCH="$1" else - # Interactive: list recent Jules branches echo "" echo "🔍 Fetching recent branches from origin..." git fetch origin --quiet @@ -61,13 +53,13 @@ else BRANCHES=$(git branch -r --sort=-committerdate \ | grep 'origin/' \ - | grep -v 'origin/main' \ + | grep -v "origin/$DEFAULT_BRANCH" \ | sed 's|origin/||' \ | head -20) if [ -z "$BRANCHES" ]; then - echo "No branches found other than main." - echo "Jules may not have created a branch yet — check jules.google.com" + echo "No branches found other than $DEFAULT_BRANCH." + echo "Jules may not have created a branch yet." exit 1 fi @@ -89,7 +81,7 @@ else fi fi -# ── Step 2: Pull the branch locally ────────────────────── +# ── Step 2: Check out the branch ───────────────────────── echo "" echo "📥 Checking out branch: $BRANCH" @@ -105,167 +97,136 @@ fi echo "✅ On branch: $BRANCH" -# ── Step 3: Read and extract selected issue from scout_report.md ── +# ── Step 3: Read scout_report.md ───────────────────────── echo "" -echo "📋 Reading Jules scout report..." +echo "📋 Reading scout report..." SCOUT_REPORT="" +SELECTED="" + if [ -f "scout_report.md" ]; then - # Read the selected issue number from the SELECTED ISSUE line SELECTED=$(grep "SELECTED ISSUE:" scout_report.md | grep -oE '[0-9]+' | head -1) if [ -z "$SELECTED" ]; then echo "" - echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number (1, 2, or 3)" - echo " Then re-run this script." + echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number, then re-run." exit 0 fi echo "✅ Selected issue: #$SELECTED" - # Extract only the selected issue section - SCOUT_REPORT=$(python3 - "$SELECTED" << 'PYEOF' + SCOUT_REPORT=$(python3 << PYEOF import sys, re -selected = sys.argv[1] +selected = "$SELECTED" with open("scout_report.md") as f: text = f.read() -# Find the section matching "## Issue N" -pattern = rf"(## Issue {selected} —.*?)(?= -## Issue \d|$)" +pattern = r"(## Issue " + selected + r" \u2014.*?)(?=## Issue \d|\Z)" match = re.search(pattern, text, re.DOTALL) if match: print(match.group(1).strip()) else: - print(f"Could not find Issue {selected} in scout_report.md") + print(text) PYEOF ) - echo "✅ Extracted Issue $SELECTED from scout_report.md" + echo "✅ Extracted issue $SELECTED from scout_report.md" else - echo "⚠️ No scout_report.md found — will use PR description only" + echo "⚠️ No scout_report.md found on this branch" fi -# ── Step 4: Get PR info and extract issue number ────────── +# ── Step 4: Check for competing PRs ────────────────────── -PR_BODY="" -PR_TITLE="" -ISSUE_NUMBER="" +ISSUE_NUMBER="$SELECTED" ISSUE_STATUS="" +ISSUE_TITLE="" ISSUE_BODY="" -if command -v gh &> /dev/null; then - PR_JSON=$(gh pr list --head "$BRANCH" --json body,title,url 2>/dev/null || echo "[]") - 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 "") - 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 "") - - # Try to extract issue number from PR body or branch name - ISSUE_NUMBER=$(echo "$PR_BODY $BRANCH" | grep -oE '#[0-9]+|issues/[0-9]+' | grep -oE '[0-9]+' | head -1) - - if [ -n "$ISSUE_NUMBER" ]; then - echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." - ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") - - if [ -n "$ISSUE_JSON" ]; then - ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") - ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") - ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") - echo "📌 Issue #$ISSUE_NUMBER is: $ISSUE_STATUS — $ISSUE_TITLE" - - if [ "$ISSUE_STATUS" = "closed" ]; then - echo "" - echo "⛔ Issue #$ISSUE_NUMBER is already CLOSED — someone already fixed it." - echo " Aborted. Pick a different Jules branch." - exit 0 - fi +if command -v gh &> /dev/null && [ -n "$ISSUE_NUMBER" ]; then + echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." + ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") + + if [ -n "$ISSUE_JSON" ]; then + ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") + ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") + ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") + echo "📌 Issue #$ISSUE_NUMBER: $ISSUE_STATUS — $ISSUE_TITLE" + + if [ "$ISSUE_STATUS" = "closed" ]; then + echo "" + echo "⛔ Issue #$ISSUE_NUMBER is already closed. Pick a different issue." + exit 0 fi + fi - # Check for competing open PRs - echo "🔍 Checking for competing PRs on issue #$ISSUE_NUMBER..." - COMPETING=$(gh pr list --repo "$UPSTREAM_REPO" --search "fixes #$ISSUE_NUMBER" --json number,title,state,isDraft,url 2>/dev/null || echo "[]") + echo "🔍 Checking for competing PRs..." + COMPETING=$(gh pr list \ + --repo "$UPSTREAM_REPO" \ + --search "fixes #$ISSUE_NUMBER" \ + --json number,title,isDraft,url \ + 2>/dev/null || echo "[]") - PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") + PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") - if [ "$PR_COUNT" -gt "0" ]; then - echo "" - echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" - echo "$COMPETING" | python3 -c " + if [ "$PR_COUNT" -gt "0" ]; then + echo "" + echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" + echo "$COMPETING" | python3 -c " import sys, json -prs = json.load(sys.stdin) -for pr in prs: +for pr in json.load(sys.stdin): draft = ' [DRAFT]' if pr.get('isDraft') else '' print(' #' + str(pr['number']) + draft + ': ' + pr['title']) print(' ' + pr['url']) " - echo "" - echo " Don't waste your time — pick a different Jules branch." - exit 0 - else - echo "✅ No competing PRs found — you're clear to proceed." - fi + echo "" + echo " Pick a different issue." + exit 0 + else + echo "✅ No competing PRs — you're clear to proceed." fi fi -# ── Step 5: Write Claude Code context file ──────────────── +# ── Step 5: Build context file and launch Claude Code ──── CONTEXT_FILE="/tmp/jules-context-$(date +%s).md" cat > "$CONTEXT_FILE" << CONTEXT -# Jules Handoff Context +# Jules Handoff — $UPSTREAM_REPO ## Branch $BRANCH -## Jules PR Title -${PR_TITLE:-"(not found)"} +## Selected Issue +#${ISSUE_NUMBER:-"unknown"} — ${ISSUE_TITLE:-"unknown"} +Status: ${ISSUE_STATUS:-"unknown"} --- -## Jules Scout Report (scout.md) -${SCOUT_REPORT:-"(No scout_report.md on this branch — refer to PR description below)"} +## Scout Report +${SCOUT_REPORT:-"(no scout_report.md found)"} --- -## Jules PR Description -${PR_BODY:-"(No PR found — check jules.google.com)"} +## Issue Body +${ISSUE_BODY:-"(could not fetch)"} --- -## Upstream Issue #${ISSUE_NUMBER:-"unknown"} -Status: ${ISSUE_STATUS:-"unknown"} -Title: ${ISSUE_TITLE:-"unknown"} - -${ISSUE_BODY:-"(Could not fetch issue body — check https://github.com/astropy/astropy/issues)"} - ---- +## Instructions for Claude Code -## Your job (Claude Code) +Read CLAUDE.md before doing anything else — it contains scope rules and +conventions you must follow. -The scout report and issue above are your source of truth — ignore any diff noise -from main being ahead of this branch due to the daily sync automation. - -1. Read scout.md carefully — Jules has already identified the files and approach. - -2. Check if the issue is still open and unassigned upstream before starting: - https://github.com/astropy/astropy/issues/${ISSUE_NUMBER:-""} - -3. Run the existing tests for the relevant subpackage first to establish a baseline: - \`python -m pytest astropy//tests/ -x -v\` - -4. Implement the fix described in scout.md. Follow astropy standards: - - Type hints on all new functions - - Numpy-style docstrings - - Tests in the corresponding tests/ directory - - No public API changes unless the issue explicitly requires it - -5. Once tests pass, give me a 3-sentence summary of what changed for the PR description. +1. Read the scout report and issue above carefully. +2. Run existing tests for the relevant module first to establish a baseline: + \`python -m pytest tests/ -x -q -k \` +3. Implement the fix. Stay strictly within the scope of the issue. +4. Write tests using pytest.approx for any float assertions. +5. Ensure all new public functions have NumPy docstrings. +6. When done, give me a 3-sentence summary of what changed for the PR description. CONTEXT -echo "✅ Context ready" - -# ── Step 6: Print context and launch Claude Code ────────── - echo "" echo "🚀 Launching Claude Code..." echo "" From 4924f1d63673b809dff3b206316861c183acfb0b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:28:24 +0000 Subject: [PATCH 4/4] Add scout report with three "good first issues" Added `scout_report.md` identifying three low-effort upstream issues suitable for a new contributor with a physics and statistics background. The report includes implementation briefs for an environment plotting bug, parachute shock force estimation, and custom exception handling. Co-authored-by: khushalkottaru <216251097+khushalkottaru@users.noreply.github.com> --- scout_report.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 scout_report.md diff --git a/scout_report.md b/scout_report.md new file mode 100644 index 000000000..be7594174 --- /dev/null +++ b/scout_report.md @@ -0,0 +1,93 @@ +--- +# Scout Report — 2026-03-17 + +## ✅ SELECTED ISSUE: 1 +*(Fill in 1, 2, or 3 after reviewing the briefs below)* + +--- + +## Issue 1 — BUG: Wind Heading Profile Plots are not that good (https://github.com/RocketPy-Team/RocketPy/issues/253) + +### What's broken +When plotting the wind heading profile, the angle values can wrap around from 359° to 0° (or vice-versa). This causes matplotlib to draw a straight line straight across the plot, creating visual artifacts that look like massive sudden shifts in wind heading, confusing users. The line should either break or visually wrap smoothly. + +### Files to touch +`rocketpy/environment/environment.py` or `rocketpy/plots/environment_analysis_plots.py` (depending on where the plot function is defined, likely where `wind_heading` is being processed for matplotlib plots). +`tests/test_environment.py` (or test_environment_plots.py) + +### Implementation approach +1. Locate the function responsible for plotting the wind heading (often associated with `Environment.wind_heading` plotting or `EnvironmentAnalysis.wind_heading_profile_grid`). +2. Before passing the wind heading arrays to matplotlib, apply `numpy.unwrap` to the data. Note: `numpy.unwrap` expects angles in radians, so convert to radians using `np.deg2rad(angles)`, unwrap, and then convert back with `np.rad2deg(unwrapped_angles)`. Alternatively, use `numpy.unwrap(angles, period=360)`. +3. If unwrapping is difficult because of `Function` class constraints, an alternative is to insert `np.nan` values where the absolute difference between consecutive angles is greater than 180°, which tells matplotlib to break the line instead of connecting the dots across the graph. +4. Add a test case that creates an environment with a wind heading that crosses the 360-degree boundary and assert that the plotting function executes without raising an exception and the output data array (if accessible) contains unwrapped values or NaNs at the boundary. + +### Acceptance criteria +* The plotted line for wind heading no longer crosses the entire plot area horizontally when the wind shifts from ~359° to ~0° or vice-versa. +* Test cases cover a scenario with boundary-crossing wind heading values. +* Ensure `Environment` and `EnvironmentAnalysis` wind heading plots are both addressed. + +### Guardrails +Do not change the underlying physics computations or the mathematical definition of wind heading used in the simulation. This should strictly be a visualization/plotting fix. Do not introduce heavy dependencies outside of the existing scientific Python stack (numpy, matplotlib). + +### Difficulty +1 — Extremely straightforward, suitable for a beginner who understands matplotlib and numpy array manipulation. + +--- + +## Issue 2 — ENH: Implement Parachute Opening Shock Force Estimation (https://github.com/RocketPy-Team/RocketPy/issues/161) + +### What's broken +Currently, RocketPy cannot estimate the peak opening shock force (inflation load) of a parachute. Users cannot properly size their recovery hardware without this peak transient force, which often greatly exceeds the steady-state drag force. This limits the realism and engineering utility of the recovery system simulation. + +### Files to touch +`rocketpy/rocket/parachute.py` +`tests/test_parachute.py` + +### Implementation approach +1. Modify `Parachute.__init__` to accept a new optional parameter `opening_shock_coefficient` (default to a standard value like 1.5 or 1.6). Store it as `self.opening_shock_coefficient`. +2. Create a new method `calculate_opening_shock(self, density, velocity)` in the `Parachute` class. +3. In this method, implement the Knacke formula: `F_o = opening_shock_coefficient * (0.5 * density * velocity**2) * self.cd_s`. +4. Optionally, you can add a post-processing calculation in the `Flight` class to print or store this peak force when a parachute event triggers (capturing the velocity and density at that timestamp), though just adding the method to `Parachute` is a good first step. +5. Write a test in `test_parachute.py` that verifies `calculate_opening_shock` returns the expected value given specific density, velocity, cd_s, and opening shock coefficient inputs. + +### Acceptance criteria +* The `Parachute` class accepts an `opening_shock_coefficient` upon initialization. +* The `calculate_opening_shock` method returns the correct peak force value based on the formula provided in the issue description. +* Appropriate unit tests are added verifying the output. + +### Guardrails +Do not change the actual 6-DOF equation of motion integration logic to include this transient force as a dynamic load yet; the issue specifically asks for this as an *estimation* (post-processing/informational method) rather than integrating the shock load directly into the flight trajectory calculations. + +### Difficulty +2 — Requires understanding object-oriented Python, adding basic class methods, and translating a physics formula into code. + +--- + +## Issue 3 — ENH: Custom Exception errors and messages (https://github.com/RocketPy-Team/RocketPy/issues/285) + +### What's broken +When users make common mistakes (like creating a rocket with a negative static margin or passing incorrect types), RocketPy currently relies on generic Python exceptions (like `ValueError` or `TypeError`) or silently allows physics-breaking setups. This makes it hard for new users to debug their simulations. + +### Files to touch +`rocketpy/exceptions.py` (Create this file if it doesn't exist) +`rocketpy/rocket/rocket.py` +`tests/test_rocket.py` (and potentially other test files) + +### Implementation approach +1. Create a new module (e.g., `exceptions.py`) to hold custom exception classes inheriting from Python's built-in `Exception` or `ValueError`. For example, create `NegativeStaticMarginError`. +2. Locate the static margin calculation in `Rocket` (often accessed via `Rocket.static_margin`). +3. Add a check during rocket assembly or when static margin is accessed: if the static margin evaluates to a negative number, raise the new `NegativeStaticMarginError` with a helpful, descriptive message explaining why this is physically problematic and what the user should check (e.g., center of mass vs. center of pressure). +4. Add tests to ensure that these custom exceptions are raised correctly when a user provides bad inputs. + +### Acceptance criteria +* A new custom exception class is created for at least one common user error (e.g., negative static margin). +* The code raises this specific custom exception instead of a generic error or failing silently. +* Tests use `pytest.raises` to ensure the correct custom exception is triggered with invalid configurations. + +### Guardrails +Do not change the physics calculations for the static margin or other components. Do not break existing public APIs. Be careful to ensure exceptions are raised at the right time (e.g., when the user actually tries to assemble the rocket or run the flight, as static margin might be temporarily negative while components are being added). + +### Difficulty +2 — Involves standard Python exception handling and understanding where validation checks should be inserted without breaking the object initialization flow. + +---