Skills › Superpowers › subagent-driven-development › sdd-workspace
sdd-workspace
87 lines · skills/subagent-driven-development/scripts/sdd-workspace at 2ce8aa1
1#!/usr/bin/env bash
2# Resolve and ensure the working-tree directory SDD uses for one plan's
3# short-lived artifacts: task briefs, implementer reports, review packages,
4# and the progress ledger. Print the plan directory's absolute path.
5#
6# One directory per plan (.superpowers/sdd/<plan-basename>/) so a follow-up
7# plan in the same working tree can never read or overwrite another plan's
8# artifacts. A stale ledger misread as current progress makes controllers
9# skip whole task sequences — plan-scoping removes that failure structurally.
10#
11# Basename slugs collide when two plans share a filename (docs/alpha/plan.md
12# vs docs/beta/plan.md), so each workspace records its owning plan's path in
13# a plan-path marker (repo-relative in-repo, absolute outside). A workspace
14# owned by a different plan is skipped and the slug disambiguated with the
15# plan's parent-directory name, then a counter. A workspace with no marker
16# predates the marker scheme and is adopted for the current plan so in-flight
17# workspaces keep resolving — which means the first collision on such a
18# legacy workspace adopts instead of detecting; acceptable, marker-less
19# workspaces age out as plans finish.
20#
21# The workspace lives in the working tree (not under .git/) because Claude Code
22# treats .git/ as a protected path and denies agent writes there — which blocks
23# an implementer subagent from writing its report file. A self-ignoring
24# .gitignore at .superpowers/sdd/ keeps every plan's workspace out of
25# `git status` and out of accidental commits without modifying any tracked file.
26#
27# Single source of truth for the workspace location, so task-brief and
28# review-package cannot drift to different directories.
29#
30# Usage: sdd-workspace PLAN_FILE
31set -euo pipefail
33if [ $# -ne 1 ]; then
34 echo "usage: sdd-workspace PLAN_FILE" >&2
35 exit 2
36fi
38plan=$1
39[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
41slug=$(basename "$plan" .md)
42[ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
43 || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
45# Physical path, spelled the way pwd -P spells the plan path below: on Git
46# Bash, rev-parse prints C:/Users/... while pwd prints /c/Users/..., and the
47# repo-relative marker check needs both in the same form.
48root=$(CDPATH='' cd -- "$(git rev-parse --show-toplevel)" && pwd -P)
49base="$root/.superpowers/sdd"
51# Normalize the plan path (physical directory, so relative/absolute/../
52# spellings of one plan compare equal) and express it as the marker value:
53# repo-relative when the plan lives under the repo root, absolute otherwise.
54plan_dir=$(CDPATH='' cd -- "$(dirname "$plan")" && pwd -P)
55plan_abs="$plan_dir/$(basename "$plan")"
56case "$plan_abs" in
57 "$root"/*) plan_id=${plan_abs#"$root"/} ;;
58 *) plan_id=$plan_abs ;;
59esac
61# True when the workspace at $1 is (or becomes) this plan's: an existing
62# marker must name this plan; a missing marker means a new workspace or a
63# pre-marker legacy one, and either way the plan claims it by writing one.
64owns() {
65 if [ -e "$1/plan-path" ]; then
66 [ "$(cat "$1/plan-path")" = "$plan_id" ]
67 else
68 mkdir -p "$1"
69 printf '%s\n' "$plan_id" > "$1/plan-path"
70 fi
71}
73dir="$base/$slug"
74if ! owns "$dir"; then
75 parent=$(basename "$plan_dir")
76 dir="$base/$slug-$parent"
77 if ! owns "$dir"; then
78 n=2
79 while ! owns "$base/$slug-$parent-$n"; do n=$((n + 1)); done
80 dir="$base/$slug-$parent-$n"
81 fi
82fi
84# Create-only: a repo may commit its own .gitignore here (e.g. to version
85# progress.md ledgers); overwriting it would revert that on every run.
86[ -e "$base/.gitignore" ] || printf '*\n' > "$base/.gitignore"
87CDPATH='' cd -- "$dir" && pwd