Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Skills live in [`ai/skills/`](ai/skills). The installer symlinks the same direct
| [`review-fix-cycle`](ai/skills/review-fix-cycle) | One review, fix, simplify, clean comments, commit iteration. |
| [`sessions`](ai/skills/sessions) | Show every live Claude session and background agent grouped by what it needs next, with a one-line next step. `--act` offers to tell the ones that can continue to continue. |
| [`simplify`](ai/codex/skills/simplify) | Simplify recently changed code for clarity and maintainability without changing behavior. Codex only; Claude bundles its own. |
| [`spawn-task`](ai/skills/spawn-task) | Start a task in a new tree-me worktree pinned in Supacode, with its own Claude session in a background tab, working from a brief written out of the current conversation. |
| [`spawn-task`](ai/skills/spawn-task) | Start a task in a new tree-me worktree in Supacode, with its own Claude session in a background tab, working from a brief written out of the current conversation. |
| [`sprint-planning`](ai/skills/sprint-planning) | Bi-weekly sprint planning updates for the Feature Flags Platform team. |
| [`squash`](ai/skills/squash) | Squash each contributor's run of contiguous commits on the branch into one, preserving authorship. |
| [`standup`](ai/skills/standup) | Generate standup notes from your recent GitHub PR activity. |
Expand Down
4 changes: 2 additions & 2 deletions ai/skills/spawn-task/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: spawn-task
description: Start a task in a new git worktree with its own Claude session, pinned in Supacode. Creates the worktree with tree-me, writes a self-contained brief from this conversation, and opens a background Supacode tab that runs claude on the brief. Use when the user asks to spin work off into a new worktree or session, says "use tree-me to create a worktree", "attach a Supacode session", "start a pinned session on this", or runs /spawn-task.
description: Start a task in a new git worktree with its own Claude session in Supacode. Creates the worktree with tree-me, writes a self-contained brief from this conversation, and opens a background Supacode tab that runs claude on the brief. Use when the user asks to spin work off into a new worktree or session, says "use tree-me to create a worktree", "attach a Supacode session", "start a session on this", or runs /spawn-task.
argument-hint: "[<task>] [--stack] [--existing <branch|PR>] [--color <tint>] [--plan|--go]"
---

# /spawn-task

Starts a separate Claude session on a task. The session runs in its own worktree, which Supacode pins, in a background tab the user can switch to. This session stays where it is: do not enter the new worktree, and do not do the task here.
Starts a separate Claude session on a task. The session runs in its own worktree, in a background tab the user can switch to. Supacode pins that worktree only when the worktree this session runs in is pinned. This session stays where it is: do not enter the new worktree, and do not do the task here.

`scripts/spawn-task.sh` does the mechanical steps. This skill decides what goes into them: which repo, which branch and base, and what the brief says.

Expand Down
23 changes: 13 additions & 10 deletions ai/skills/spawn-task/scripts/spawn-task.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
# --go Start the session with /go --plan-file .notes/brief.md
#
# Creates the worktree with tree-me, copies the brief to <worktree>/.notes/brief.md,
# makes Supacode list and pin the worktree, and opens a background tab that runs
# claude with the brief as its first prompt.
# makes Supacode list the worktree, and opens a background tab that runs claude
# with the brief as its first prompt. Supacode pins the new worktree only when
# the worktree the caller runs in is pinned.
#
# --branch refuses a branch that already exists locally or on origin, and refuses
# when origin cannot be reached. Without --base, the branch starts at the remote
Expand Down Expand Up @@ -134,12 +135,12 @@ fi
# Supacode names a worktree by its percent-encoded path with a trailing slash. It
# may record the path with symlinks resolved, so both spellings are accepted.
encode() { jq -rn --arg p "$1/" '$p | @uri'; }
ids=("$(encode "$worktree")" "$(encode "$(cd "$worktree" && pwd -P)")")

listed_id() { # list_filter...
local listing id
listed_id() { # path list_filter...
local path="$1" listing id
shift
listing=$(supacode worktree list "$@" 2>/dev/null) || return 1
for id in "${ids[@]}"; do
for id in "$(encode "$path")" "$(encode "$(cd "$path" && pwd -P)")"; do
if grep -qxF -- "$id" <<<"$listing"; then
echo "$id"
return 0
Expand All @@ -149,21 +150,23 @@ listed_id() { # list_filter...
}

# Opening the repo does not bring back an archived worktree.
if ! worktree_id=$(listed_id --not-archived); then
if archived_id=$(listed_id --status archived); then
if ! worktree_id=$(listed_id "$worktree" --not-archived); then
if archived_id=$(listed_id "$worktree" --status archived); then
supacode worktree unarchive -w "$archived_id" --background >/dev/null ||
die "could not unarchive the worktree in Supacode."
else
supacode repo open "$worktree" >/dev/null || die "\`supacode repo open $worktree\` failed."
fi
deadline=$((SECONDS + ${SPAWN_TASK_LIST_TIMEOUT:-30}))
until worktree_id=$(listed_id --not-archived); do
until worktree_id=$(listed_id "$worktree" --not-archived); do
((SECONDS < deadline)) || die "Supacode did not list the worktree."
sleep 1
done
fi

supacode worktree pin -w "$worktree_id" --background >/dev/null || die "could not pin the worktree in Supacode."
if [ -n "$caller_root" ] && listed_id "$caller_root" --status pinned >/dev/null; then
supacode worktree pin -w "$worktree_id" --background >/dev/null || die "could not pin the worktree in Supacode."
fi
if [ -n "$color" ]; then
supacode worktree appearance -w "$worktree_id" --color "$color" >/dev/null ||
die "could not set the worktree's tint to '$color'."
Expand Down
42 changes: 38 additions & 4 deletions ai/skills/spawn-task/scripts/tests/test-spawn-task.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ chmod +x "${SHIM_BIN}/claude"
# fail subcommands, such as "socket" or "tab new", that exit 1
# listed the IDs that `worktree list --not-archived` prints
# archived the IDs that `worktree list --status archived` prints
# pinned the IDs that `worktree list --status pinned` prints
# list-delay how many lists after `repo open` still leave its worktree out
# never-list present when `repo open` never adds its worktree
# `repo open` adds the ID of the path it gets. `worktree unarchive` moves its -w
Expand Down Expand Up @@ -100,6 +101,7 @@ case "$1 ${2-}" in
fi
case " $* " in
*" --status archived "*) cat "${SHIM_DIR}/archived" ;;
*" --status pinned "*) cat "${SHIM_DIR}/pinned" ;;
*" --not-archived "*) cat "${SHIM_DIR}/listed" ;;
*) cat "${SHIM_DIR}/listed" "${SHIM_DIR}/archived" ;;
esac
Expand Down Expand Up @@ -244,6 +246,7 @@ begin_case() { # name [worktree_root_name]
mkdir -p "${SHIM_DIR}/calls" "$SHIM_CLAUDE_DIR"
: > "${SHIM_DIR}/listed"
: > "${SHIM_DIR}/archived"
: > "${SHIM_DIR}/pinned"
printf '0\n' > "${SHIM_DIR}/list-delay"
printf '%s\n' "$DEFAULT_BRIEF" > "$BRIEF"
build_repos || fail "${CASE_NAME}: the fixture repos build"
Expand Down Expand Up @@ -489,16 +492,15 @@ check_eq "${CASE_NAME}: the branch starts at origin's latest main" "$(branch_sha
check "${CASE_NAME}: the brief is copied to .notes/brief.md" cmp -s "$BRIEF" "${WT}/.notes/brief.md"
check_eq "${CASE_NAME}: repo open runs once" "$(call_count "repo open")" "1"
check_eq "${CASE_NAME}: repo open gets the worktree path" "$(call_last_arg "repo open")" "$WT"
check_eq "${CASE_NAME}: pin gets the worktree ID" "$(call_option "worktree pin" -w --worktree)" "$ID"
check "${CASE_NAME}: pin runs in the background" call_has "worktree pin" --background
check_eq "${CASE_NAME}: a caller outside any worktree means no pin" "$(call_count "worktree pin")" "0"
check_eq "${CASE_NAME}: no --color means no appearance call" "$(call_count "worktree appearance")" "0"
check_eq "${CASE_NAME}: tab new runs once" "$(call_count "tab new")" "1"
check_eq "${CASE_NAME}: tab new gets the worktree ID" "$(call_option "tab new" -w --worktree)" "$ID"
check "${CASE_NAME}: tab new runs in the background" call_has "tab new" --background
check_eq "${CASE_NAME}: the tab title is the worktree's last path component" \
"$(call_option "tab new" --title)" "happy-path"
check_eq "${CASE_NAME}: supacode calls run in order" "$(call_order)" \
"socket, worktree list, repo open, worktree list, worktree pin, tab new"
"socket, worktree list, repo open, worktree list, tab new"
check_eq "${CASE_NAME}: stdout names the worktree" "$(out_value worktree)" "$WT"
check_eq "${CASE_NAME}: stdout names the worktree ID" "$(out_value worktree_id)" "$ID"
check_eq "${CASE_NAME}: stdout names the tab" "$(out_value tab)" "$TAB_UUID"
Expand Down Expand Up @@ -625,6 +627,38 @@ spawn --branch titled --title "my task"
check_eq "${CASE_NAME}: exits 0" "$STATUS" "0"
check_eq "${CASE_NAME}: tab new gets the title" "$(call_option "tab new" --title)" "my task"

# ── Pinning ──────────────────────────────────────────────────────────────────

begin_case "a caller in a pinned worktree"
git -C "$REPO" worktree add -q -b pinned-caller "${CASE_DIR}/pinned-caller"
CALLER=$(worktree_path_for pinned-caller)
mkdir -p "${CALLER}/src"
printf '%s\n' "$(worktree_id "$CALLER")" > "${SHIM_DIR}/pinned"
WT="${WORKTREE_ROOT}/widget/from-pinned"
ID=$(worktree_id "$WT")
CASE_CWD="${CALLER}/src" spawn --branch from-pinned
check_eq "${CASE_NAME}: exits 0" "$STATUS" "0"
check_eq "${CASE_NAME}: pin runs once" "$(call_count "worktree pin")" "1"
check_eq "${CASE_NAME}: pin gets the new worktree's ID" "$(call_option "worktree pin" -w --worktree)" "$ID"
check "${CASE_NAME}: pin runs in the background" call_has "worktree pin" --background
check_eq "${CASE_NAME}: the tab opens after the pin" "$(call_order)" \
"socket, worktree list, repo open, worktree list, worktree pin, tab new"

begin_case "a caller in an unpinned worktree"
git -C "$REPO" worktree add -q -b unpinned-caller "${CASE_DIR}/unpinned-caller"
CALLER=$(worktree_path_for unpinned-caller)
printf '%s\n' "$(worktree_id "${CALLER}/nested")" "$(worktree_id "${CASE_DIR}/other")" > "${SHIM_DIR}/pinned"
CASE_CWD="$CALLER" spawn --branch from-unpinned
check_eq "${CASE_NAME}: exits 0" "$STATUS" "0"
check_eq "${CASE_NAME}: pin does not run" "$(call_count "worktree pin")" "0"
check_eq "${CASE_NAME}: tab new runs once" "$(call_count "tab new")" "1"

begin_case "a caller in the main checkout"
printf '%s\n' "$(worktree_id "${CASE_DIR}/other")" > "${SHIM_DIR}/pinned"
CASE_CWD="$REPO" spawn --branch from-main
check_eq "${CASE_NAME}: exits 0" "$STATUS" "0"
check_eq "${CASE_NAME}: pin does not run" "$(call_count "worktree pin")" "0"

# ── Supacode listing the worktree ────────────────────────────────────────────

begin_case "a worktree Supacode already lists"
Expand Down Expand Up @@ -655,7 +689,7 @@ check_eq "${CASE_NAME}: unarchive gets the worktree ID" "$(call_option "worktree
check "${CASE_NAME}: unarchive runs in the background" call_has "worktree unarchive" --background
check_eq "${CASE_NAME}: repo open does not run" "$(call_count "repo open")" "0"
check_eq "${CASE_NAME}: the tab opens only after the unarchive" "$(call_order)" \
"socket, worktree list, worktree unarchive, worktree list, worktree pin, tab new"
"socket, worktree list, worktree unarchive, worktree list, tab new"
check_eq "${CASE_NAME}: tab new gets the worktree ID" "$(call_option "tab new" -w --worktree)" "$ID"

begin_case "a worktree Supacode lists after two polls"
Expand Down
Loading