Files
hermes-relay/scripts/start-issue.sh
T

195 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# scripts/start-issue.sh — pull a GitHub issue into a ready-to-work git worktree,
# pre-briefed with Claude's automated triage.
#
# This is the local end of the dev-loop (see docs/dev-loop.md): CI triages an
# issue (classification + probable cause, and a deeper analysis if you add the
# `triage:deep` label), then this script bootstraps an isolated worktree off
# `dev` named to our branch convention and drops an ISSUE-BRIEF.md inside it —
# the issue body, the bot's triage/deep-dive comments, and the surface-specific
# verification plan — so the agent session you open there (claude / codex / etc.)
# starts already briefed instead of re-deriving context.
#
# Worktrees share this checkout's object store, so they're cheap and you can run
# several issue branches concurrently (see the shared-worktree workflow notes).
#
# Requires: gh (authenticated) + git. Run from anywhere inside the repo.
#
# Usage:
# scripts/start-issue.sh <issue-number> [base-branch] # base defaults to dev
#
# Examples:
# scripts/start-issue.sh 142 # worktree ../hr-issue-142 off origin/dev
# scripts/start-issue.sh 142 main # base off origin/main instead
set -euo pipefail
die() { printf 'start-issue: %s\n' "$*" >&2; exit 1; }
# --- args -------------------------------------------------------------------
N="${1:-}"
BASE="${2:-dev}"
[ -n "$N" ] || die "usage: scripts/start-issue.sh <issue-number> [base-branch]"
case "$N" in (*[!0-9]*|'') die "issue number must be numeric, got '$N'";; esac
command -v gh >/dev/null 2>&1 || die "gh CLI not found on PATH"
command -v git >/dev/null 2>&1 || die "git not found on PATH"
gh auth status >/dev/null 2>&1 || die "gh is not authenticated — run 'gh auth login'"
# Anchor everything to the repo root so the sibling worktree path is stable.
ROOT="$(git rev-parse --show-toplevel)" || die "not inside a git repository"
cd "$ROOT"
# --- read the issue ---------------------------------------------------------
echo "→ reading issue #$N …"
TITLE="$(gh issue view "$N" --json title --jq .title)" || die "could not read issue #$N (does it exist?)"
URL="$(gh issue view "$N" --json url --jq .url)"
LABELS="$(gh issue view "$N" --json labels --jq '[.labels[].name] | join(", ")')"
# Branch prefix from the TYPE label (matches our convention: fix/ feature/ docs/).
case ",$LABELS," in
*,enhancement,*) PREFIX="feature" ;;
*,documentation,*) PREFIX="docs" ;;
*) PREFIX="fix" ;; # bug / question / unlabeled → fix
esac
# Surface area from the area:* label, for the verification plan.
AREA=""
case ",$LABELS," in
*,area:android,*) AREA="android" ;;
*,area:cli,*) AREA="cli" ;;
*,area:plugin,*) AREA="plugin" ;;
*,area:dashboard,*) AREA="dashboard" ;;
*,area:docs,*) AREA="docs" ;;
esac
# Slug from the title: drop a leading "[bug]"-style tag, lowercase, kebab-case,
# collapse repeats, trim, and cap length so branch names stay sane.
SLUG="$(printf '%s' "$TITLE" \
| sed -E 's/^\[[^]]*\][[:space:]]*//' \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \
| cut -c1-40 | sed -E 's/-+$//')"
[ -n "$SLUG" ] || SLUG="issue"
BRANCH="${PREFIX}/issue-${N}-${SLUG}"
WTDIR="../hr-issue-${N}"
# --- guard rails ------------------------------------------------------------
if git show-ref --verify --quiet "refs/heads/${BRANCH}"; then
die "branch '${BRANCH}' already exists — check it out, or delete it first."
fi
if [ -e "$WTDIR" ]; then
die "worktree path '${WTDIR}' already exists — remove it (git worktree remove '${WTDIR}') first."
fi
# --- create the worktree ----------------------------------------------------
echo "→ fetching origin/${BASE} …"
git fetch --quiet origin "$BASE" || die "could not fetch origin/${BASE}"
echo "→ creating worktree ${WTDIR} on branch ${BRANCH} (off origin/${BASE}) …"
git worktree add "$WTDIR" -b "$BRANCH" "origin/${BASE}" >/dev/null || die "git worktree add failed"
# --- verification plan text -------------------------------------------------
verification_plan() {
case "$AREA" in
android)
cat <<'EOF'
**Surface: Android (Kotlin app).**
- Logic (ViewModels / mappers / pure Kotlin): `./gradlew :app:testGooglePlayDebugUnitTest` + `./gradlew :app:lint` — **CI-gateable** (ci-android.yml).
- UI / on-device behavior: build in Android Studio (▶) and verify on a real device. **NOT CI-gateable — this is a human gate.** Do not mark such a fix "done" from CI alone.
- Prefer TDD for logic fixes: add the failing unit test first, then fix.
EOF
;;
cli)
cat <<'EOF'
**Surface: Desktop CLI (Node/TypeScript).**
- `cd desktop && npm run build && npm run smoke` plus the unit tests — **CI-gateable** (ci-desktop.yml).
- Prefer TDD: reproduce as a failing test first, then fix.
EOF
;;
plugin)
cat <<'EOF'
**Surface: Python relay / plugin.**
- `python -m unittest plugin.tests.test_<name>` (avoid bare `pytest` — conftest imports `responses`, which may be absent in the venv) — **CI-gateable** (ci-plugin.yml).
- Syntax check while iterating: `python -m py_compile plugin/<file>.py`.
- Prefer TDD: add the failing test first, then fix.
EOF
;;
dashboard)
cat <<'EOF'
**Surface: Dashboard (React).**
- Build the dashboard bundle — **CI-gateable** (ci-dashboard.yml).
- Verify the committed IIFE bundle is regenerated if you touch `plugin/dashboard/src/`.
EOF
;;
docs)
cat <<'EOF'
**Surface: Docs.**
- Build the docs site — **CI-gateable** (docs.yml).
EOF
;;
*)
cat <<'EOF'
**Surface: not yet classified** (no `area:*` label). Pick the row that matches the files you touch:
- plugin/ (Python) → `python -m unittest plugin.tests.test_<name>` — CI-gateable.
- desktop/ (CLI) → `cd desktop && npm run build && npm run smoke` — CI-gateable.
- app/ logic → `./gradlew :app:testGooglePlayDebugUnitTest` + `:app:lint` — CI-gateable.
- app/ UI/behavior → Android Studio + device — **human gate, NOT CI-gateable**.
- plugin/dashboard/ → dashboard bundle build — CI-gateable.
- docs/, user-docs/ → docs build — CI-gateable.
EOF
;;
esac
}
# --- write the brief --------------------------------------------------------
BRIEF="${WTDIR}/ISSUE-BRIEF.md"
{
echo "# Issue #${N}: ${TITLE}"
echo
echo "- **Link:** ${URL}"
echo "- **Labels:** ${LABELS:-none}"
echo "- **Branch:** \`${BRANCH}\` (off \`origin/${BASE}\`)"
echo "- **Worktree:** \`${WTDIR}\`"
echo
echo "> Auto-generated by scripts/start-issue.sh. Not committed — it's in .gitignore."
echo
echo "## Verification plan"
echo
verification_plan
echo
echo "## Definition of done"
echo
echo "- The fix passes the verification above (write the failing test first where the surface is CI-gateable)."
echo "- Update CHANGELOG \`[Unreleased]\`, and docs/project/DEVLOG.md at end of session; park any follow-ups in docs/project/TODO.md."
echo "- Feature branches target \`dev\` (never straight to \`main\`); merge with --no-ff."
echo
echo "## Issue body"
echo
gh issue view "$N" --json body --jq '.body // "_(no description)_"'
echo
echo "## Automated triage / deep-dive notes"
echo
# Triage/deep-dive/follow-up comments are posted by the Claude GitHub App
# (author "claude"), NOT "github-actions" — so match on our comment signatures
# (identity-proof), with the known bot logins as a fallback.
BOTNOTES="$(gh issue view "$N" --json comments \
--jq '.comments[] | select(((.body // "") | test("automated triage|Deep-dive analysis|automated follow-up")) or ((.author.login // "") | test("^(claude|github-actions)"))) | "_" + .createdAt + "_\n\n" + .body + "\n\n---\n"')"
if [ -n "$BOTNOTES" ]; then
printf '%s\n' "$BOTNOTES"
else
echo "_No automated triage comments yet. Add the \`triage:deep\` label on the issue for a code-level analysis._"
fi
} > "$BRIEF"
# --- done -------------------------------------------------------------------
echo
echo "✓ ready. Worktree: ${WTDIR}"
echo " Branch: ${BRANCH}"
echo " Brief: ${BRIEF}"
echo
echo "Next:"
echo " cd ${WTDIR}"
echo " \$EDITOR ISSUE-BRIEF.md # or open an agent session here (claude / codex)"
echo
echo "When done, from the main checkout: git worktree remove ${WTDIR}"