jj workspaces skill
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: jj-issue-workspaces
|
||||
description: Create one Jujutsu workspace per issue, base them on an updated mainline bookmark like master, optionally create feature bookmarks, and open a zellij tab running pi in each workspace. Use when the user wants to fan out work across multiple issues, especially from a screenshot, Linear board, or issue list.
|
||||
---
|
||||
|
||||
# JJ Issue Workspaces
|
||||
|
||||
This skill sets up a parallel issue workflow with `jj workspaces`.
|
||||
|
||||
Use it when the user wants any of the following:
|
||||
- one workspace per issue
|
||||
- multiple issues opened side by side
|
||||
- a zellij tab for each issue
|
||||
- `pi` opened in each issue workspace with a task-specific prompt
|
||||
- issue fan-out from a screenshot, Linear board, or manually listed issues
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the target repo and verify it is a `jj` repo.
|
||||
2. If the user gave a screenshot path, use the `read` tool on the screenshot first and extract the issue keys and titles.
|
||||
3. Decide the base bookmark/revision, usually `master` or `main`.
|
||||
4. Run the helper script to:
|
||||
- fetch the base bookmark from `origin`
|
||||
- create sibling workspaces like `../Phoenix-spa-748`
|
||||
- create bookmarks like `feature/spa-748`
|
||||
- optionally open one zellij tab per workspace and launch `pi`
|
||||
5. Tell the user which workspaces and tabs were created.
|
||||
|
||||
## Helper script
|
||||
|
||||
Use the helper script in this skill:
|
||||
|
||||
```bash
|
||||
./scripts/jj-workspace-fanout.sh --help
|
||||
```
|
||||
|
||||
Run it from anywhere. Pass absolute paths when convenient.
|
||||
|
||||
## Common usage
|
||||
|
||||
### Create workspaces and bookmarks only
|
||||
|
||||
```bash
|
||||
./scripts/jj-workspace-fanout.sh \
|
||||
--repo /path/to/repo \
|
||||
--base master \
|
||||
--issue "SPA-748=Wrap text in credits line items" \
|
||||
--issue "SPA-428=Implement \"Downgrade\" Mimir modal (maximalist)" \
|
||||
--issue "SPA-754=Resize seat count picker"
|
||||
```
|
||||
|
||||
### Create workspaces, bookmarks, zellij tabs, and launch pi
|
||||
|
||||
```bash
|
||||
./scripts/jj-workspace-fanout.sh \
|
||||
--repo /path/to/repo \
|
||||
--base master \
|
||||
--session attio \
|
||||
--open-pi \
|
||||
--issue "SPA-748=Wrap text in credits line items" \
|
||||
--issue "SPA-428=Implement \"Downgrade\" Mimir modal (maximalist)" \
|
||||
--issue "SPA-754=Resize seat count picker"
|
||||
```
|
||||
|
||||
### Recreate existing workspaces from scratch
|
||||
|
||||
```bash
|
||||
./scripts/jj-workspace-fanout.sh \
|
||||
--repo /path/to/repo \
|
||||
--base master \
|
||||
--session attio \
|
||||
--open-pi \
|
||||
--reset-existing \
|
||||
--issue "SPA-748=Wrap text in credits line items"
|
||||
```
|
||||
|
||||
## Defaults and conventions
|
||||
|
||||
- Workspace names use the lowercased issue key, for example `spa-748`
|
||||
- Workspace directories are created beside the repo, for example `../Phoenix-spa-748`
|
||||
- Bookmark names default to `feature/<issue-key-lowercase>`
|
||||
- Base revision defaults to `master`
|
||||
- Remote defaults to `origin`
|
||||
- If `--open-pi` is used, the script launches `pi` in each workspace with a task-specific prompt
|
||||
|
||||
## Recommended agent behavior
|
||||
|
||||
When using this skill:
|
||||
- Prefer `jj` over `git`
|
||||
- Check `jj workspace list` before changing anything
|
||||
- If the user says to update `master` or `main` first, let the script fetch that base revision before creating workspaces
|
||||
- If the user wants an existing set recreated, use `--reset-existing`
|
||||
- If zellij tabs already exist and the user wants a clean retry, close those tabs first or recreate the session
|
||||
|
||||
## Notes
|
||||
|
||||
- The script does not delete existing workspaces unless `--reset-existing` is provided.
|
||||
- `--open-pi` requires a zellij session name, either via `--session <name>` or `ZELLIJ_SESSION_NAME`.
|
||||
- If the repo uses `main` instead of `master`, pass `--base main`.
|
||||
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Create one jj workspace per issue, optionally create bookmarks, and optionally open zellij tabs running pi.
|
||||
|
||||
Usage:
|
||||
jj-workspace-fanout.sh [options] --issue "KEY=Title" [--issue "KEY=Title" ...]
|
||||
|
||||
Options:
|
||||
--repo PATH Repo root (default: current directory)
|
||||
--base REV Base revision/bookmark (default: master)
|
||||
--remote NAME Git remote to fetch from (default: origin)
|
||||
--issue KEY=TITLE Issue key and title (repeatable)
|
||||
--session NAME Zellij session name (defaults to ZELLIJ_SESSION_NAME if set)
|
||||
--open-pi Open a zellij tab per workspace and launch pi
|
||||
--no-fetch Skip jj git fetch
|
||||
--no-bookmarks Do not create feature/<issue> bookmarks
|
||||
--keep-existing Skip creation for existing workspaces instead of failing
|
||||
--reset-existing Forget and delete existing workspaces before recreating them
|
||||
--prompt-suffix TEXT Extra text appended to each pi prompt
|
||||
--pi-cmd CMD pi command to launch (default: pi)
|
||||
--dry-run Print planned actions without making changes
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
jj-workspace-fanout.sh \
|
||||
--repo /path/to/Phoenix \
|
||||
--base master \
|
||||
--issue "SPA-748=Wrap text in credits line items" \
|
||||
--issue "SPA-754=Resize seat count picker"
|
||||
|
||||
jj-workspace-fanout.sh \
|
||||
--repo /path/to/Phoenix \
|
||||
--base master \
|
||||
--session attio \
|
||||
--open-pi \
|
||||
--issue "SPA-748=Wrap text in credits line items"
|
||||
EOF
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "error: missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
shell_escape() {
|
||||
printf '%q' "$1"
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[jj-issue-workspaces] %s\n' "$*"
|
||||
}
|
||||
|
||||
run() {
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
printf '[dry-run] '
|
||||
printf '%q ' "$@"
|
||||
printf '\n'
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
workspace_exists() {
|
||||
local workspace_name="$1"
|
||||
jj -R "$REPO" workspace list | awk -F: '{print $1}' | grep -Fxq "$workspace_name"
|
||||
}
|
||||
|
||||
bookmark_exists() {
|
||||
local workspace_dir="$1"
|
||||
local bookmark_name="$2"
|
||||
jj -R "$workspace_dir" bookmark list "$bookmark_name" 2>/dev/null | grep -Eq "^${bookmark_name}:"
|
||||
}
|
||||
|
||||
close_tab_if_exists() {
|
||||
local session_name="$1"
|
||||
local tab_name="$2"
|
||||
local tabs
|
||||
|
||||
tabs=$(zellij --session "$session_name" action query-tab-names 2>/dev/null || true)
|
||||
if printf '%s\n' "$tabs" | grep -Fxq "$tab_name"; then
|
||||
log "closing existing zellij tab $tab_name"
|
||||
run zellij --session "$session_name" action go-to-tab-name "$tab_name"
|
||||
run zellij --session "$session_name" action close-tab
|
||||
fi
|
||||
}
|
||||
|
||||
launch_pi_tab() {
|
||||
local session_name="$1"
|
||||
local tab_name="$2"
|
||||
local workspace_dir="$3"
|
||||
local prompt="$4"
|
||||
local cmd
|
||||
|
||||
cmd="cd $(shell_escape "$workspace_dir") && pwd && $PI_CMD $(shell_escape "$prompt")"
|
||||
|
||||
close_tab_if_exists "$session_name" "$tab_name"
|
||||
run zellij --session "$session_name" action new-tab --name "$tab_name"
|
||||
run zellij --session "$session_name" action write-chars "$cmd"
|
||||
run zellij --session "$session_name" action write 10
|
||||
}
|
||||
|
||||
REPO="$(pwd)"
|
||||
BASE="master"
|
||||
REMOTE="origin"
|
||||
SESSION="${ZELLIJ_SESSION_NAME:-}"
|
||||
OPEN_PI=0
|
||||
FETCH=1
|
||||
CREATE_BOOKMARKS=1
|
||||
KEEP_EXISTING=0
|
||||
RESET_EXISTING=0
|
||||
DRY_RUN=0
|
||||
PROMPT_SUFFIX=""
|
||||
PI_CMD="pi"
|
||||
declare -a ISSUES=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo)
|
||||
REPO="$2"
|
||||
shift 2
|
||||
;;
|
||||
--base)
|
||||
BASE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote)
|
||||
REMOTE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--issue)
|
||||
ISSUES+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
SESSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--open-pi)
|
||||
OPEN_PI=1
|
||||
shift
|
||||
;;
|
||||
--no-fetch)
|
||||
FETCH=0
|
||||
shift
|
||||
;;
|
||||
--no-bookmarks)
|
||||
CREATE_BOOKMARKS=0
|
||||
shift
|
||||
;;
|
||||
--keep-existing)
|
||||
KEEP_EXISTING=1
|
||||
shift
|
||||
;;
|
||||
--reset-existing)
|
||||
RESET_EXISTING=1
|
||||
shift
|
||||
;;
|
||||
--prompt-suffix)
|
||||
PROMPT_SUFFIX="$2"
|
||||
shift 2
|
||||
;;
|
||||
--pi-cmd)
|
||||
PI_CMD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "error: unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#ISSUES[@]} -eq 0 ]]; then
|
||||
echo "error: at least one --issue KEY=TITLE is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$KEEP_EXISTING" -eq 1 && "$RESET_EXISTING" -eq 1 ]]; then
|
||||
echo "error: --keep-existing and --reset-existing cannot be combined" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO="$(cd "$REPO" && pwd)"
|
||||
PARENT_DIR="$(dirname "$REPO")"
|
||||
REPO_BASENAME="$(basename "$REPO")"
|
||||
|
||||
require_cmd jj
|
||||
if [[ "$OPEN_PI" -eq 1 ]]; then
|
||||
require_cmd zellij
|
||||
if [[ -z "$SESSION" ]]; then
|
||||
echo "error: --open-pi requires --session <name> or ZELLIJ_SESSION_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -d "$REPO/.jj" ]]; then
|
||||
echo "error: repo is not a jj repository: $REPO" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FETCH" -eq 1 ]]; then
|
||||
log "fetching $BASE from $REMOTE"
|
||||
run jj -R "$REPO" git fetch --remote "$REMOTE" --branch "$BASE"
|
||||
fi
|
||||
|
||||
log "validating base revision $BASE"
|
||||
run jj -R "$REPO" log -r "$BASE" --no-pager
|
||||
|
||||
created_workspaces=()
|
||||
|
||||
for issue in "${ISSUES[@]}"; do
|
||||
if [[ "$issue" != *=* ]]; then
|
||||
echo "error: issue must be formatted as KEY=TITLE: $issue" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
issue_key="${issue%%=*}"
|
||||
issue_title="${issue#*=}"
|
||||
issue_slug="$(printf '%s' "$issue_key" | tr '[:upper:]' '[:lower:]')"
|
||||
workspace_name="$issue_slug"
|
||||
workspace_dir="$PARENT_DIR/${REPO_BASENAME}-${issue_slug}"
|
||||
bookmark_name="feature/$issue_slug"
|
||||
prompt="Work on ${issue_key}: ${issue_title}. You are in the dedicated jj workspace for this issue. First inspect the relevant code, identify the main components involved, and propose a short plan before editing."
|
||||
|
||||
if [[ -n "$PROMPT_SUFFIX" ]]; then
|
||||
prompt+=" ${PROMPT_SUFFIX}"
|
||||
fi
|
||||
|
||||
if workspace_exists "$workspace_name" || [[ -e "$workspace_dir" ]]; then
|
||||
if [[ "$RESET_EXISTING" -eq 1 ]]; then
|
||||
log "resetting existing workspace $workspace_name"
|
||||
if workspace_exists "$workspace_name"; then
|
||||
run jj -R "$REPO" workspace forget "$workspace_name"
|
||||
fi
|
||||
run rm -rf "$workspace_dir"
|
||||
elif [[ "$KEEP_EXISTING" -eq 1 ]]; then
|
||||
log "keeping existing workspace $workspace_name at $workspace_dir"
|
||||
else
|
||||
echo "error: workspace already exists: $workspace_name ($workspace_dir). Use --keep-existing or --reset-existing." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! workspace_exists "$workspace_name"; then
|
||||
log "creating workspace $workspace_name at $workspace_dir"
|
||||
run jj -R "$REPO" workspace add --name "$workspace_name" -r "$BASE" "$workspace_dir"
|
||||
fi
|
||||
|
||||
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
|
||||
log "ensuring bookmark $bookmark_name exists"
|
||||
if bookmark_exists "$workspace_dir" "$bookmark_name"; then
|
||||
run jj -R "$workspace_dir" bookmark set "$bookmark_name" -r @
|
||||
else
|
||||
run jj -R "$workspace_dir" bookmark create "$bookmark_name"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$OPEN_PI" -eq 1 ]]; then
|
||||
log "opening zellij tab $workspace_name in session $SESSION"
|
||||
run launch_pi_tab "$SESSION" "$workspace_name" "$workspace_dir" "$prompt"
|
||||
fi
|
||||
|
||||
created_workspaces+=("$workspace_name:$workspace_dir:$bookmark_name")
|
||||
done
|
||||
|
||||
printf '\nCreated/updated workspaces:\n'
|
||||
for item in "${created_workspaces[@]}"; do
|
||||
IFS=':' read -r workspace_name workspace_dir bookmark_name <<<"$item"
|
||||
printf ' - %s -> %s' "$workspace_name" "$workspace_dir"
|
||||
if [[ "$CREATE_BOOKMARKS" -eq 1 ]]; then
|
||||
printf ' [%s]' "$bookmark_name"
|
||||
fi
|
||||
printf '\n'
|
||||
done
|
||||
|
||||
if [[ "$OPEN_PI" -eq 1 ]]; then
|
||||
printf '\nZellij session: %s\n' "$SESSION"
|
||||
fi
|
||||
Reference in New Issue
Block a user