jj workspaces skill
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"lastChangelogVersion": "0.64.0",
|
"lastChangelogVersion": "0.66.1",
|
||||||
"defaultProvider": "anthropic",
|
"defaultProvider": "openai-codex",
|
||||||
"defaultModel": "claude-opus-4-6",
|
"defaultModel": "gpt-5.4",
|
||||||
"defaultThinkingLevel": "xhigh",
|
"defaultThinkingLevel": "medium",
|
||||||
"theme": "matugen",
|
"theme": "matugen",
|
||||||
"lsp": {
|
"lsp": {
|
||||||
"hookMode": "edit_write"
|
"hookMode": "edit_write"
|
||||||
},
|
},
|
||||||
"hideThinkingBlock": false,
|
"hideThinkingBlock": true,
|
||||||
"slowtool": {
|
"slowtool": {
|
||||||
"timeoutSeconds": 300,
|
"timeoutSeconds": 300,
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -328,7 +328,7 @@ default_layout "compact"
|
|||||||
// The folder in which Zellij will look for themes
|
// The folder in which Zellij will look for themes
|
||||||
// (Requires restart)
|
// (Requires restart)
|
||||||
//
|
//
|
||||||
theme_dir "/home/thomasgl/.config/zellij/themes"
|
// theme_dir "/home/thomasgl/.config/zellij/themes"
|
||||||
|
|
||||||
// Toggle enabling the mouse mode.
|
// Toggle enabling the mouse mode.
|
||||||
// On certain configurations, or terminals this could
|
// On certain configurations, or terminals this could
|
||||||
|
|||||||
Reference in New Issue
Block a user