From b42a9ecffad3ca188f74ba0b24d0416eeefb419d Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Mon, 13 Apr 2026 17:50:04 +0100 Subject: [PATCH] jj workspaces skill --- pi/files.macos/agent/settings.json | 10 +- .../agent/skills/jj-issue-workspaces/SKILL.md | 99 ++++++ .../scripts/jj-workspace-fanout.sh | 292 ++++++++++++++++++ zellij/files/config.kdl | 2 +- 4 files changed, 397 insertions(+), 6 deletions(-) create mode 100644 pi/files/agent/skills/jj-issue-workspaces/SKILL.md create mode 100755 pi/files/agent/skills/jj-issue-workspaces/scripts/jj-workspace-fanout.sh diff --git a/pi/files.macos/agent/settings.json b/pi/files.macos/agent/settings.json index b88b452..89bb8d9 100644 --- a/pi/files.macos/agent/settings.json +++ b/pi/files.macos/agent/settings.json @@ -1,13 +1,13 @@ { - "lastChangelogVersion": "0.64.0", - "defaultProvider": "anthropic", - "defaultModel": "claude-opus-4-6", - "defaultThinkingLevel": "xhigh", + "lastChangelogVersion": "0.66.1", + "defaultProvider": "openai-codex", + "defaultModel": "gpt-5.4", + "defaultThinkingLevel": "medium", "theme": "matugen", "lsp": { "hookMode": "edit_write" }, - "hideThinkingBlock": false, + "hideThinkingBlock": true, "slowtool": { "timeoutSeconds": 300, "enabled": true diff --git a/pi/files/agent/skills/jj-issue-workspaces/SKILL.md b/pi/files/agent/skills/jj-issue-workspaces/SKILL.md new file mode 100644 index 0000000..52a58af --- /dev/null +++ b/pi/files/agent/skills/jj-issue-workspaces/SKILL.md @@ -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/` +- 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 ` or `ZELLIJ_SESSION_NAME`. +- If the repo uses `main` instead of `master`, pass `--base main`. diff --git a/pi/files/agent/skills/jj-issue-workspaces/scripts/jj-workspace-fanout.sh b/pi/files/agent/skills/jj-issue-workspaces/scripts/jj-workspace-fanout.sh new file mode 100755 index 0000000..8c01b33 --- /dev/null +++ b/pi/files/agent/skills/jj-issue-workspaces/scripts/jj-workspace-fanout.sh @@ -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/ 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 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 diff --git a/zellij/files/config.kdl b/zellij/files/config.kdl index 4825bab..d8135d3 100644 --- a/zellij/files/config.kdl +++ b/zellij/files/config.kdl @@ -328,7 +328,7 @@ default_layout "compact" // The folder in which Zellij will look for themes // (Requires restart) // -theme_dir "/home/thomasgl/.config/zellij/themes" +// theme_dir "/home/thomasgl/.config/zellij/themes" // Toggle enabling the mouse mode. // On certain configurations, or terminals this could