sdd-workspace 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  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
  31. set -euo pipefail
  32. if [ $# -ne 1 ]; then
  33. echo "usage: sdd-workspace PLAN_FILE" >&2
  34. exit 2
  35. fi
  36. plan=$1
  37. [ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
  38. slug=$(basename "$plan" .md)
  39. [ -n "$slug" ] && [ "$slug" != "." ] && [ "$slug" != ".." ] \
  40. || { echo "cannot derive a workspace name from: $plan" >&2; exit 2; }
  41. root=$(git rev-parse --show-toplevel)
  42. base="$root/.superpowers/sdd"
  43. # Normalize the plan path (physical directory, so relative/absolute/../
  44. # spellings of one plan compare equal) and express it as the marker value:
  45. # repo-relative when the plan lives under the repo root, absolute otherwise.
  46. plan_dir=$(CDPATH= cd -- "$(dirname "$plan")" && pwd -P)
  47. plan_abs="$plan_dir/$(basename "$plan")"
  48. case "$plan_abs" in
  49. "$root"/*) plan_id=${plan_abs#"$root"/} ;;
  50. *) plan_id=$plan_abs ;;
  51. esac
  52. # True when the workspace at $1 is (or becomes) this plan's: an existing
  53. # marker must name this plan; a missing marker means a new workspace or a
  54. # pre-marker legacy one, and either way the plan claims it by writing one.
  55. owns() {
  56. if [ -e "$1/plan-path" ]; then
  57. [ "$(cat "$1/plan-path")" = "$plan_id" ]
  58. else
  59. mkdir -p "$1"
  60. printf '%s\n' "$plan_id" > "$1/plan-path"
  61. fi
  62. }
  63. dir="$base/$slug"
  64. if ! owns "$dir"; then
  65. parent=$(basename "$plan_dir")
  66. dir="$base/$slug-$parent"
  67. if ! owns "$dir"; then
  68. n=2
  69. while ! owns "$base/$slug-$parent-$n"; do n=$((n + 1)); done
  70. dir="$base/$slug-$parent-$n"
  71. fi
  72. fi
  73. printf '*\n' > "$base/.gitignore"
  74. CDPATH= cd -- "$dir" && pwd