#!/usr/bin/env bash # start.sh [extra-prompt] [--agent ] [--no-checkout] # # Pick up a GitHub issue: # 1. Sync `main` from upstream. # 2. Create a working branch `/-` (slug from issue title). # 3. Pull the issue (title/body/labels) via gh. # 4. Hand off to the agent CLI with a prompt that includes the issue plus # repo conventions (CLAUDE.md / AGENTS.md pointers). # # --agent picks the CLI that drives the work. Default: claude. # `--agent codex` uses `codex exec --dangerously-bypass-approvals-and-sandbox` # and `--agent cursor` / `cursor-agent` use `cursor-agent --yolo`, so those # sessions start in their equivalent "yolo" mode. # A trailing positional is appended to the agent prompt. # --no-checkout skips git sync/branch creation (use the current branch as-is). set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../review/lib.sh source "$here/../review/lib.sh" require git gh jq if [ -z "${1:-}" ]; then echo "Usage: pnpm work [extra-prompt] [--agent ] [--no-checkout]" >&2 exit 1 fi case "$1" in ''|*[!0-9]*) echo "[work] issue-number must be numeric, got: $1" >&2 exit 1 ;; esac issue="$1" shift agent="claude" extra_prompt="" do_checkout=1 while [ $# -gt 0 ]; do case "$1" in --agent) agent="${2:?--agent requires a value}"; shift 2 ;; --agent=*) agent="${1#*=}"; shift ;; --no-checkout) do_checkout=0; shift ;; *) if [ -n "$extra_prompt" ]; then echo "[work] unexpected extra arg: $1 (extra-prompt already set)" >&2 exit 1 fi extra_prompt="$1"; shift ;; esac done require "$agent" # resolve_repo() lives in scripts/shortcuts/review/lib.sh; honour WORK_REPO override too. repo="${WORK_REPO:-${REVIEW_REPO:-}}" if [ -z "$repo" ]; then repo=$(REVIEW_REPO= resolve_repo) fi branch_prefix="${WORK_BRANCH_PREFIX:-issue}" auto_assign="${WORK_AUTO_ASSIGN:-1}" echo "[work] fetching issue #$issue from $repo..." issue_json=$(gh issue view "$issue" -R "$repo" \ --json number,title,body,labels,state,url,assignees) if [ "$auto_assign" = "1" ]; then gh_assign_self_issue "$issue" "$repo" fi state=$(jq -r '.state' <<<"$issue_json") if [ "$state" != "OPEN" ]; then echo "[work] ! issue #$issue is $state — continuing anyway" >&2 fi title=$(jq -r '.title' <<<"$issue_json") body=$(jq -r '.body // ""' <<<"$issue_json") url=$(jq -r '.url' <<<"$issue_json") labels=$(jq -r '[.labels[].name] | join(", ")' <<<"$issue_json") # Slug: lowercase, alnum + hyphens, max 40 chars, trimmed. slug=$(printf '%s' "$title" \ | tr '[:upper:]' '[:lower:]' \ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \ | cut -c1-40 \ | sed -E 's/-+$//') if [ -z "$slug" ]; then slug="work" fi branch="${branch_prefix}/${issue}-${slug}" if [ "$do_checkout" = "1" ]; then echo "[work] syncing main..." git checkout main if git remote get-url upstream >/dev/null 2>&1; then git fetch upstream git merge --ff-only upstream/main || git merge upstream/main fi if git remote get-url origin >/dev/null 2>&1; then git pull --ff-only origin main fi git submodule update --init --recursive if git show-ref --verify --quiet "refs/heads/$branch"; then echo "[work] branch $branch already exists — checking it out and merging main" git checkout "$branch" if ! git merge main; then echo "[work] merge from main failed on branch $branch; resolve conflicts and re-run." >&2 exit 1 fi else echo "[work] creating branch $branch off main" git checkout -b "$branch" fi else echo "[work] --no-checkout: staying on $(git branch --show-current)" fi current_branch=$(git branch --show-current) labels_display="${labels:-(none)}" template="$here/prompts/start.md" if [ ! -f "$template" ]; then echo "[work] missing prompt template: $template" >&2 exit 1 fi # Use awk for substitution — handles multi-line values (issue body) cleanly. # Pass values via the environment (ENVIRON[]) because BSD awk on macOS rejects # literal newlines in `-v var=value`, and the issue body routinely has them. # Escape backslashes and ampersands so gsub doesn't interpret them in the # replacement text. prompt=$(WORK_ISSUE="$issue" WORK_REPO_NAME="$repo" WORK_BRANCH="$current_branch" \ WORK_URL="$url" WORK_TITLE="$title" WORK_LABELS="$labels_display" \ WORK_BODY="$body" \ awk ' function esc(s) { gsub(/\\/, "\\\\", s); gsub(/&/, "\\\\&", s); return s } BEGIN { issue=esc(ENVIRON["WORK_ISSUE"]); repo=esc(ENVIRON["WORK_REPO_NAME"]); branch=esc(ENVIRON["WORK_BRANCH"]); url=esc(ENVIRON["WORK_URL"]); title=esc(ENVIRON["WORK_TITLE"]); labels=esc(ENVIRON["WORK_LABELS"]); body=esc(ENVIRON["WORK_BODY"]); } { gsub(/__ISSUE__/, issue); gsub(/__REPO__/, repo); gsub(/__BRANCH__/, branch); gsub(/__URL__/, url); gsub(/__TITLE__/, title); gsub(/__LABELS__/, labels); gsub(/__BODY__/, body); print } ' "$template") if [ -n "$extra_prompt" ]; then prompt="${prompt} # Additional instructions from the user ${extra_prompt}" fi echo "[work] handing off to ${agent} on branch ${current_branch}" if [ "$agent" = "codex" ]; then codex exec --dangerously-bypass-approvals-and-sandbox "$prompt" elif [ "$agent" = "cursor" ] || [ "$agent" = "cursor-agent" ]; then cursor-agent --yolo "$prompt" else "$agent" "$prompt" fi