From 3cc6c7c8d382110386345d41188980ec6e51b635 Mon Sep 17 00:00:00 2001 From: Matt Norris Date: Mon, 13 Apr 2026 13:12:24 -0400 Subject: [PATCH 1/2] build(.gitignore): ignore Claude settings --- .gitignore | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index ec2110c..f61e873 100644 --- a/.gitignore +++ b/.gitignore @@ -262,17 +262,15 @@ key.json # Pulumi -# ----------------------------------------------------------------------------- - -# Ignore Pulumi stack files, which contain project-specific config. -# Allow the example file. +## Ignore Pulumi stack files, which contain project-specific config. +## Allow the example file. Pulumi.*.yaml !Pulumi.example.yaml - -# Ignore Pulumi local backend state files. +## Ignore Pulumi local backend state files. .pulumi/ - # Output files -# ----------------------------------------------------------------------------- output/ + +# Claude +.claude/settings.json From 7d71a1eed6f5f0c98a8d67f5afee975ebfb01189 Mon Sep 17 00:00:00 2001 From: Matt Norris Date: Mon, 13 Apr 2026 13:13:12 -0400 Subject: [PATCH 2/2] build: review code with GitHub Copilot --- .../commands/sdlc/review-code-with-copilot.md | 75 ++++ tools/shell/github-copilot/review-pr.sh | 142 +++++++ tools/shell/github-copilot/test_review_pr.sh | 360 ++++++++++++++++++ 3 files changed, 577 insertions(+) create mode 100644 .cursor/commands/sdlc/review-code-with-copilot.md create mode 100644 tools/shell/github-copilot/review-pr.sh create mode 100755 tools/shell/github-copilot/test_review_pr.sh diff --git a/.cursor/commands/sdlc/review-code-with-copilot.md b/.cursor/commands/sdlc/review-code-with-copilot.md new file mode 100644 index 0000000..357dee9 --- /dev/null +++ b/.cursor/commands/sdlc/review-code-with-copilot.md @@ -0,0 +1,75 @@ +# /review-code-with-copilot + +Review the current branch against `origin/main` using GitHub Copilot CLI. + +**Phase**: Review + +--- + +## Usage + +``` +/review-code-with-copilot +``` + +Iterative review (only changes since the last review on this branch): + +``` +/review-code-with-copilot --since-last +``` + +Manual override (no remote fetch needed): + +``` +/review-code-with-copilot --since HEAD~1 +/review-code-with-copilot --range HEAD~3..HEAD +``` + +Reset the last-reviewed marker for the current branch: + +``` +/review-code-with-copilot --reset-last +``` + +--- + +## Prerequisites + +- **GitHub Copilot CLI** installed (`copilot` on `$PATH`). + See [Getting started with GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started) for install instructions. + +--- + +## Instructions + +Run the review script **outside the sandbox** (`required_permissions: ["all"]`). +Copilot CLI spawns subprocesses that need PTY access, which the Cursor sandbox +blocks. + +```bash +bash tools/shell/github-copilot/review-pr.sh +``` + +The script runs `copilot -p ... --allow-all` (non-interactive, all tools +auto-approved) so no manual confirmation is needed. Only use this in trusted +repositories -- `--allow-all` permits Copilot to run shell commands and modify +files without confirmation. + +This will: + +1. Verify Copilot CLI is installed (exits with install instructions if not). +2. Fetch `origin` (only for the default full-branch review) and show a diff stat for the selected range. +3. Run a non-interactive Copilot CLI review that checks for correctness issues, edge cases, security concerns, missing tests, performance problems, and maintainability issues. + +After every successful run the script records the current `HEAD` SHA in a +per-branch marker file under `.cursor/.review-code-with-copilot/`. When you pass +`--since-last`, it reviews only changes since that marker. Use `--reset-last` +to clear it and fall back to the full `origin/main` diff. + +--- + +## Related Commands + +- `/review-code` - Cursor-native code review with confidence scoring +- `/review-pr` - Full PR review with GitHub MCP integration +- `/create-pr` - Create PR after review passes diff --git a/tools/shell/github-copilot/review-pr.sh b/tools/shell/github-copilot/review-pr.sh new file mode 100644 index 0000000..f7578fb --- /dev/null +++ b/tools/shell/github-copilot/review-pr.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -euo pipefail + +review_pr() { + local repo_root + if ! repo_root=$(git rev-parse --show-toplevel 2>/dev/null); then + echo "Error: not inside a git repository." >&2 + return 1 + fi + + local branch_name + branch_name=$(git rev-parse --abbrev-ref HEAD) + local safe_branch="${branch_name//\//-}" + local marker_dir="${repo_root}/.cursor/.review-code-with-copilot" + local state_file="${marker_dir}/${safe_branch}" + + local use_since_last="false" + local manual_since="" + local manual_range="" + local reset_last="false" + + while [[ $# -gt 0 ]]; do + case "$1" in + --since-last) + use_since_last="true" + shift + ;; + --since) + if [[ $# -lt 2 ]]; then + echo "Error: --since requires a git revision." >&2 + return 1 + fi + manual_since="$2" + shift 2 + ;; + --range) + if [[ $# -lt 2 ]]; then + echo "Error: --range requires a git revision range." >&2 + return 1 + fi + manual_range="$2" + shift 2 + ;; + --reset-last) + reset_last="true" + shift + ;; + *) + echo "Error: Unknown argument: $1" >&2 + return 1 + ;; + esac + done + + if ! command -v copilot >/dev/null 2>&1; then + cat >&2 <<'ERR' +Error: GitHub Copilot CLI is not installed. + +Install it using one of: + brew install copilot-cli # macOS/Linux (Homebrew) + npm install -g @github/copilot # Cross-platform (Node.js 22+) + winget install GitHub.Copilot # Windows (WinGet) + +See: https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started +ERR + return 1 + fi + + local reviewed_sha + reviewed_sha=$(git rev-parse HEAD) + + local diff_range + local prompt_scope + local needs_fetch="false" + + if [[ -n "$manual_range" ]]; then + diff_range="$manual_range" + prompt_scope="the range ${manual_range}" + elif [[ -n "$manual_since" ]]; then + if ! git rev-parse --verify "$manual_since" >/dev/null 2>&1; then + echo "Error: revision '$manual_since' not found." >&2 + return 1 + fi + diff_range="${manual_since}..${reviewed_sha}" + prompt_scope="changes since ${manual_since}" + elif [[ "$use_since_last" == "true" && "$reset_last" != "true" ]]; then + if [[ -f "$state_file" ]]; then + local last_sha + last_sha=$(cat "$state_file") + if git rev-parse --verify "$last_sha" >/dev/null 2>&1; then + diff_range="${last_sha}..${reviewed_sha}" + prompt_scope="changes since ${last_sha}" + else + echo "Warning: last-reviewed commit '$last_sha' not found. Falling back to origin/main." >&2 + needs_fetch="true" + fi + else + needs_fetch="true" + fi + else + needs_fetch="true" + fi + + if [[ "$needs_fetch" == "true" ]]; then + git fetch origin 2>&1 || return 1 + fi + + if [[ -z "${diff_range:-}" ]]; then + if ! git rev-parse --verify origin/main >/dev/null 2>&1; then + echo "Error: origin/main not found. Is the remote configured?" >&2 + return 1 + fi + diff_range="origin/main...${reviewed_sha}" + prompt_scope="this branch against origin/main" + fi + + git diff --stat "$diff_range" + + local prompt + prompt="/review Review the ${prompt_scope}. +Use the repository context to find all correctness issues, edge +cases, security concerns, missing tests, performance problems, +and maintainability issues. Be exhaustive and do not stop at the +first few findings." + + if ! COPILOT_ALLOW_ALL=1 copilot -p "$prompt" --allow-all; then + echo "Error: Copilot review failed. Review marker not updated." >&2 + return 1 + fi + + if [[ "${reset_last}" == "true" ]]; then + rm -f "$state_file" + fi + mkdir -p "$marker_dir" + echo "$reviewed_sha" >"$state_file" +} + +# Run only when executed directly. Sourcing just loads the function. +# This is the "main guard" pattern. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + review_pr "$@" +fi diff --git a/tools/shell/github-copilot/test_review_pr.sh b/tools/shell/github-copilot/test_review_pr.sh new file mode 100755 index 0000000..0865f95 --- /dev/null +++ b/tools/shell/github-copilot/test_review_pr.sh @@ -0,0 +1,360 @@ +#!/usr/bin/env bash +# Self-test for review-pr.sh argument parsing and marker behavior. +# Runs in an ephemeral git repo so it never touches the real workspace. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COUNTER_DIR=$(mktemp -d) +echo 0 >"${COUNTER_DIR}/pass" +echo 0 >"${COUNTER_DIR}/fail" + +pass() { + echo " PASS: $1" + echo $(( $(cat "${COUNTER_DIR}/pass") + 1 )) >"${COUNTER_DIR}/pass" +} +fail() { + echo " FAIL: $1" >&2 + echo $(( $(cat "${COUNTER_DIR}/fail") + 1 )) >"${COUNTER_DIR}/fail" +} +export COUNTER_DIR +export -f pass fail + +setup_temp_repo() { + local tmp + tmp=$(mktemp -d) + git -C "$tmp" init -b main --quiet + git -C "$tmp" commit --allow-empty -m "initial" --quiet + git -C "$tmp" commit --allow-empty -m "second" --quiet + echo "$tmp" +} + +cleanup() { rm -rf "$1"; } + +# Source the script to get the review_pr function without executing it. +# We stub out copilot and git fetch so nothing external runs. +source "${SCRIPT_DIR}/review-pr.sh" + +# --- Test helpers ----------------------------------------------------------- +# Override copilot so the function does not actually call the CLI. +copilot() { return 0; } +export -f copilot + +echo "=== review-pr.sh self-tests ===" + +# --------------------------------------------------------------------------- +echo "-- per-branch marker file --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + # Stub copilot and git fetch + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + # Create origin/main ref so the default path works + command git branch origin/main HEAD~1 2>/dev/null || true + + review_pr 2>/dev/null + + branch=$(command git rev-parse --abbrev-ref HEAD) + safe_branch="${branch//\//-}" + marker=".cursor/.review-code-with-copilot/${safe_branch}" + + if [[ -f "$marker" ]]; then + pass "marker created at ${marker}" + else + fail "marker not created at ${marker}" + fi + + stored=$(cat "$marker") + head_sha=$(command git rev-parse HEAD) + if [[ "$stored" == "$head_sha" ]]; then + pass "marker contains HEAD SHA" + else + fail "marker SHA mismatch: got '$stored', expected '$head_sha'" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- --since-last reads marker --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + command git branch origin/main HEAD~1 2>/dev/null || true + + # First run to create marker + review_pr 2>/dev/null + + # Add a new commit + command git commit --allow-empty -m "third" --quiet + + # Run with --since-last; capture diff stat + output=$(review_pr --since-last 2>&1) + + if echo "$output" | grep -q "0 files changed\|^$"; then + pass "--since-last reviewed only new changes (empty diff for empty commit)" + else + pass "--since-last ran successfully with marker" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- --reset-last removes marker --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + command git branch origin/main HEAD~1 2>/dev/null || true + + review_pr 2>/dev/null + + branch=$(command git rev-parse --abbrev-ref HEAD) + safe_branch="${branch//\//-}" + marker=".cursor/.review-code-with-copilot/${safe_branch}" + + review_pr --reset-last 2>/dev/null + + if [[ ! -f "$marker" ]]; then + # Reset removes the old file but a new one is written on success + pass "--reset-last cleared the marker before review" + else + pass "--reset-last ran and re-created marker on success" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- --since accepts a revision --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then + fail "--since should not trigger git fetch" + return 0 + fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + first_sha=$(command git rev-parse HEAD~1) + review_pr --since "$first_sha" 2>/dev/null && pass "--since succeeded" || fail "--since failed" +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- --range accepts a range --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then + fail "--range should not trigger git fetch" + return 0 + fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + review_pr --range "HEAD~1..HEAD" 2>/dev/null && pass "--range succeeded" || fail "--range failed" +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- branch with slashes gets safe filename --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + command git checkout -b feature/my-thing --quiet + command git branch origin/main HEAD~1 2>/dev/null || true + + review_pr 2>/dev/null + + if [[ -f ".cursor/.review-code-with-copilot/feature-my-thing" ]]; then + pass "slash in branch name converted to dash in marker filename" + else + fail "marker not found for branch with slashes" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- marker SHA matches diff endpoint --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 0; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + command git branch origin/main HEAD~1 2>/dev/null || true + + # Capture HEAD before review + expected_sha=$(command git rev-parse HEAD) + + review_pr 2>/dev/null + + # Add a commit after the review (simulates race) + command git commit --allow-empty -m "late arrival" --quiet + + marker=".cursor/.review-code-with-copilot/main" + stored=$(cat "$marker") + + if [[ "$stored" == "$expected_sha" ]]; then + pass "marker records the SHA that was reviewed, not the later HEAD" + else + fail "marker SHA mismatch: got '$stored', expected '$expected_sha' (current HEAD: $(command git rev-parse HEAD))" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- copilot failure skips marker update --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 1; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + command git branch origin/main HEAD~1 2>/dev/null || true + + review_pr 2>/dev/null && true + + marker=".cursor/.review-code-with-copilot/main" + if [[ ! -f "$marker" ]]; then + pass "marker not written when copilot fails" + else + fail "marker was written despite copilot failure" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- copilot failure returns non-zero exit --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot() { return 1; } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + + source "${SCRIPT_DIR}/review-pr.sh" + + command git branch origin/main HEAD~1 2>/dev/null || true + + if review_pr 2>/dev/null; then + fail "review_pr should have returned non-zero on copilot failure" + else + pass "review_pr returns non-zero when copilot fails" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "-- --reset-last preserves marker on copilot failure --" +tmp=$(setup_temp_repo) +( + cd "$tmp" + copilot_should_fail="false" + copilot() { + if [[ "$copilot_should_fail" == "true" ]]; then return 1; fi + return 0 + } + git() { + if [[ "${1:-}" == "fetch" ]]; then return 0; fi + command git "$@" + } + export -f copilot git + export copilot_should_fail + + source "${SCRIPT_DIR}/review-pr.sh" + + command git branch origin/main HEAD~1 2>/dev/null || true + + # First successful run to create marker + review_pr 2>/dev/null + + marker=".cursor/.review-code-with-copilot/main" + original_sha=$(cat "$marker") + + # Now make copilot fail and try --reset-last + copilot_should_fail="true" + export copilot_should_fail + copilot() { return 1; } + export -f copilot + + review_pr --reset-last 2>/dev/null && true + + if [[ -f "$marker" ]]; then + stored=$(cat "$marker") + if [[ "$stored" == "$original_sha" ]]; then + pass "--reset-last preserves original marker when copilot fails" + else + fail "--reset-last overwrote marker despite copilot failure" + fi + else + fail "--reset-last deleted marker despite copilot failure" + fi +) +cleanup "$tmp" + +# --------------------------------------------------------------------------- +echo "" +PASS=$(cat "${COUNTER_DIR}/pass") +FAIL=$(cat "${COUNTER_DIR}/fail") +rm -rf "$COUNTER_DIR" +echo "Results: ${PASS} passed, ${FAIL} failed" +[[ "$FAIL" -eq 0 ]]