mirror of
https://github.com/obra/superpowers.git
synced 2026-08-21 14:16:00 +00:00
0e798e1dc2
sdd-workspace slugged workspaces by basename alone, so docs/alpha/plan.md and docs/beta/plan.md resolved to one directory and task-brief silently overwrote the other plan's brief — the single gitignored source of task requirements, unrecoverable once clobbered. Each workspace now records its owning plan in a plan-path marker (repo-relative in-repo, absolute outside). Lookup keeps basename slugs and existing behavior for the common case: a markerless workspace is adopted in place (no migration break for in-flight plans), a marker naming this plan is a match, and a marker naming a different plan disambiguates with the plan's parent-directory name, then a counter. Plan paths are normalized (CDPATH-guarded physical cd) so relative, absolute, and ../ spellings of one plan share one workspace. task-brief and review-package delegate to sdd-workspace and need no changes. SKILL.md's workspace bullet no longer promises the exact <plan-basename> path, since disambiguated workspaces differ. Reported by @CRGDan; reproduction and test groundwork by @crisnahine in PR #2120. Fixes #2045
83 lines
3.2 KiB
Bash
Executable File
83 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Resolve and ensure the working-tree directory SDD uses for one plan's
|
|
# short-lived artifacts: task briefs, implementer reports, review packages,
|
|
# and the progress ledger. Print the plan directory's absolute path.
|
|
#
|
|
# One directory per plan (.superpowers/sdd/<plan-basename>/) so a follow-up
|
|
# plan in the same working tree can never read or overwrite another plan's
|
|
# artifacts. A stale ledger misread as current progress makes controllers
|
|
# skip whole task sequences — plan-scoping removes that failure structurally.
|
|
#
|
|
# Basename slugs collide when two plans share a filename (docs/alpha/plan.md
|
|
# vs docs/beta/plan.md), so each workspace records its owning plan's path in
|
|
# a plan-path marker (repo-relative in-repo, absolute outside). A workspace
|
|
# owned by a different plan is skipped and the slug disambiguated with the
|
|
# plan's parent-directory name, then a counter. A workspace with no marker
|
|
# predates the marker scheme and is adopted for the current plan so in-flight
|
|
# workspaces keep resolving — which means the first collision on such a
|
|
# legacy workspace adopts instead of detecting; acceptable, marker-less
|
|
# workspaces age out as plans finish.
|
|
#
|
|
# The workspace lives in the working tree (not under .git/) because Claude Code
|
|
# treats .git/ as a protected path and denies agent writes there — which blocks
|
|
# an implementer subagent from writing its report file. A self-ignoring
|
|
# .gitignore at .superpowers/sdd/ keeps every plan's workspace out of
|
|
# `git status` and out of accidental commits without modifying any tracked file.
|
|
#
|
|
# Single source of truth for the workspace location, so task-brief and
|
|
# review-package cannot drift to different directories.
|
|
#
|
|
# Usage: sdd-workspace PLAN_FILE
|
|
set -euo pipefail
|
|
|
|
if [ $# -ne 1 ]; then
|
|
echo "usage: sdd-workspace PLAN_FILE" >&2
|
|
exit 2
|
|
fi
|
|
|
|
plan=$1
|
|
[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
|
|
|
|
slug=$(basename "$plan" .md)
|
|
[ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
|
|
|| { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
|
|
|
|
root=$(git rev-parse --show-toplevel)
|
|
base="$root/.superpowers/sdd"
|
|
|
|
# Normalize the plan path (physical directory, so relative/absolute/../
|
|
# spellings of one plan compare equal) and express it as the marker value:
|
|
# repo-relative when the plan lives under the repo root, absolute otherwise.
|
|
plan_dir=$(CDPATH= cd -- "$(dirname "$plan")" && pwd -P)
|
|
plan_abs="$plan_dir/$(basename "$plan")"
|
|
case "$plan_abs" in
|
|
"$root"/*) plan_id=${plan_abs#"$root"/} ;;
|
|
*) plan_id=$plan_abs ;;
|
|
esac
|
|
|
|
# True when the workspace at $1 is (or becomes) this plan's: an existing
|
|
# marker must name this plan; a missing marker means a new workspace or a
|
|
# pre-marker legacy one, and either way the plan claims it by writing one.
|
|
owns() {
|
|
if [ -e "$1/plan-path" ]; then
|
|
[ "$(cat "$1/plan-path")" = "$plan_id" ]
|
|
else
|
|
mkdir -p "$1"
|
|
printf '%s\n' "$plan_id" > "$1/plan-path"
|
|
fi
|
|
}
|
|
|
|
dir="$base/$slug"
|
|
if ! owns "$dir"; then
|
|
parent=$(basename "$plan_dir")
|
|
dir="$base/$slug-$parent"
|
|
if ! owns "$dir"; then
|
|
n=2
|
|
while ! owns "$base/$slug-$parent-$n"; do n=$((n + 1)); done
|
|
dir="$base/$slug-$parent-$n"
|
|
fi
|
|
fi
|
|
|
|
printf '*\n' > "$base/.gitignore"
|
|
CDPATH= cd -- "$dir" && pwd
|