Files
openhuman/scripts/shortcuts/work/start.sh
T

179 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# start.sh <issue-number> [extra-prompt] [--agent <tool>] [--no-checkout]
#
# Pick up a GitHub issue:
# 1. Sync `main` from upstream.
# 2. Create a working branch `<prefix>/<num>-<slug>` (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 <extra-prompt> 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 <issue-number> [extra-prompt] [--agent <tool>] [--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