From 1e912bc7578e7cd70d7b3285abeb392064e97e60 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 17 Jun 2026 13:18:28 -0700 Subject: [PATCH] feat(sdd): add sdd-workspace helper for a self-ignoring artifact dir --- .../scripts/sdd-workspace | 22 +++++ tests/claude-code/test-sdd-workspace.sh | 82 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100755 skills/subagent-driven-development/scripts/sdd-workspace create mode 100755 tests/claude-code/test-sdd-workspace.sh diff --git a/skills/subagent-driven-development/scripts/sdd-workspace b/skills/subagent-driven-development/scripts/sdd-workspace new file mode 100755 index 00000000..ea9bb08f --- /dev/null +++ b/skills/subagent-driven-development/scripts/sdd-workspace @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Resolve and ensure the working-tree directory SDD uses for its short-lived +# artifacts: task briefs, implementer reports, review packages, and the +# progress ledger. Print the directory's absolute path. +# +# 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 keeps the 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 +set -euo pipefail + +root=$(git rev-parse --show-toplevel) +dir="$root/.superpowers/sdd" +mkdir -p "$dir" +printf '*\n' > "$dir/.gitignore" +cd "$dir" && pwd diff --git a/tests/claude-code/test-sdd-workspace.sh b/tests/claude-code/test-sdd-workspace.sh new file mode 100755 index 00000000..06ce2fff --- /dev/null +++ b/tests/claude-code/test-sdd-workspace.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Tests for the SDD workspace: scripts/sdd-workspace resolves a self-ignoring +# working-tree directory for SDD artifacts, and the SDD scripts write into it. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +SDD_SCRIPTS="$REPO_ROOT/skills/subagent-driven-development/scripts" + +FAILURES=0 +TEST_ROOT="" + +pass() { echo " [PASS] $1"; } +fail() { + echo " [FAIL] $1" + FAILURES=$((FAILURES + 1)) +} + +cleanup() { + if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then + rm -rf "$TEST_ROOT" + fi +} + +main() { + echo "=== Test: sdd-workspace ===" + + TEST_ROOT="$(mktemp -d)" + trap cleanup EXIT + + # Resolve repo to its physical path so string comparisons match the + # helper's output (git rev-parse --show-toplevel resolves symlinks; on + # macOS mktemp lives under /var -> /private/var). + git init -q -b main "$TEST_ROOT/repo" + local repo + repo="$(cd "$TEST_ROOT/repo" && git rev-parse --show-toplevel)" + + local dir + dir="$(cd "$repo" && "$SDD_SCRIPTS/sdd-workspace")" + + if [[ "$dir" == "$repo/.superpowers/sdd" ]]; then + pass "prints /.superpowers/sdd" + else + fail "prints /.superpowers/sdd" + echo " got: $dir" + fi + + if [[ -f "$repo/.superpowers/sdd/.gitignore" && "$(cat "$repo/.superpowers/sdd/.gitignore")" == "*" ]]; then + pass "self-ignoring .gitignore created with '*'" + else + fail "self-ignoring .gitignore created with '*'" + fi + + printf 'x\n' > "$repo/.superpowers/sdd/artifact.md" + local status + status="$(cd "$repo" && git status --porcelain)" + if [[ -z "$status" ]]; then + pass "workspace invisible to git status" + else + fail "workspace invisible to git status" + echo " status: $status" + fi + + ( cd "$repo" && git add -A ) + local staged + staged="$(cd "$repo" && git diff --cached --name-only)" + if [[ -z "$staged" ]]; then + pass "git add -A does not stage the workspace" + else + fail "git add -A does not stage the workspace" + echo " staged: $staged" + fi + + echo "" + if [[ "$FAILURES" -ne 0 ]]; then + echo "FAILED: $FAILURES assertion(s)." + exit 1 + fi + echo "PASS" +} + +main "$@"