Compare commits

..

7 Commits

Author SHA1 Message Date
Jesse Vincent
ffc29fa077 fix(writing-skills): run graphviz without a shell in render-graphs.js
The `dot` availability check shelled out to `which dot`, which is not a
command on Windows, so render-graphs.js reported graphviz as missing on
Windows even when it was installed. Replace it with a direct `dot -V`
probe via execFileSync.

Also switch the SVG render call from execSync to execFileSync('dot',
['-Tsvg']). Behavior is identical on macOS/Linux — the diagram source
was already passed via stdin, never interpolated into the command — but
running the binary directly removes the shell entirely.
2026-06-18 15:15:54 -07:00
Jesse Vincent
81f860a993 test(deps): bump ws to ^8.21.0 in brainstorm-server tests
Clears two dependabot alerts on the test harness's ws dependency:
GHSA-96hv-2xvq-fx4p (high, memory-exhaustion DoS, fixed 8.21.0) and
GHSA-58qx-3vcg-4xpx (medium, uninitialized memory disclosure, fixed
8.20.1). Test-only — the shipped brainstorm server hand-rolls its
WebSocket framing and does not depend on ws. Suite passes (57/57).
2026-06-18 14:57:28 -07:00
Jesse Vincent
80f810a4c2 docs: add v6.0.3 release notes for the SDD .git/ workspace fix 2026-06-18 14:47:05 -07:00
Jesse Vincent
528be9f974 test(sdd): wire test-sdd-workspace.sh into the runner; note git clean -fdx
The per-worktree workspace test was added but never registered in
run-skill-tests.sh, so it only ran when invoked by hand. Add it to the
fast unit-test array alongside the other pure-shell test.

Also document, in the Durable Progress section, that the ledger now
lives in git-ignored working-tree scratch, so `git clean -fdx` deletes
it — recover from `git log` if that happens.
2026-06-18 14:46:21 -07:00
Jesse Vincent
ce8fd59f33 test(sdd): lock in per-worktree workspace isolation (#1780) 2026-06-18 11:49:56 -07:00
Jesse Vincent
f660e9e9d6 fix(sdd): write artifacts to working-tree .superpowers/sdd, not .git/ (#1780) 2026-06-18 11:49:56 -07:00
Jesse Vincent
78ec255a2d feat(sdd): add sdd-workspace helper for a self-ignoring artifact dir 2026-06-18 11:49:56 -07:00
10 changed files with 190 additions and 21 deletions

View File

@@ -1,5 +1,11 @@
# Superpowers Release Notes # Superpowers Release Notes
## v6.0.3 (2026-06-18)
### Subagent-Driven Development
- **SDD scratch files moved out of `.git/`.** Claude Code treats `.git/` as a protected path and denies agent writes there, so an implementer subagent writing its report into `.git/sdd/` got blocked mid-run. Task briefs, implementer reports, review diffs, and the progress ledger now live in a self-ignoring `.superpowers/sdd/` directory in the working tree — kept out of `git status` and out of commits, and resolved per worktree by a shared `sdd-workspace` helper. One caveat: because the workspace is git-ignored working-tree scratch, `git clean -fdx` will delete the progress ledger; recover from `git log` if that happens. (#1780)
## v6.0.2 (2026-06-16) ## v6.0.2 (2026-06-16)
### Install Fixes ### Install Fixes

View File

@@ -251,7 +251,7 @@ sequences — the single most expensive failure observed. Track progress in
a ledger file, not only in todos. a ledger file, not only in todos.
- At skill start, check for a ledger: - At skill start, check for a ledger:
`cat "$(git rev-parse --git-path sdd)/progress.md"`. Tasks listed there `cat "$(git rev-parse --show-toplevel)/.superpowers/sdd/progress.md"`. Tasks listed there
as complete are DONE — do not re-dispatch them; resume at the first task as complete are DONE — do not re-dispatch them; resume at the first task
not marked complete. not marked complete.
- When a task's review comes back clean, append one line to the ledger in - When a task's review comes back clean, append one line to the ledger in
@@ -260,6 +260,8 @@ a ledger file, not only in todos.
- The ledger is your recovery map: the commits it names exist in git even - The ledger is your recovery map: the commits it names exist in git even
when your context no longer remembers creating them. After compaction, when your context no longer remembers creating them. After compaction,
trust the ledger and `git log` over your own recollection. trust the ledger and `git log` over your own recollection.
- `git clean -fdx` will destroy the ledger (it's git-ignored scratch); if
that happens, recover from `git log`.
## Prompt Templates ## Prompt Templates

View File

@@ -5,9 +5,8 @@
# tasks intact. # tasks intact.
# #
# Usage: review-package BASE HEAD [OUTFILE] # Usage: review-package BASE HEAD [OUTFILE]
# Default OUTFILE: <git-dir>/sdd/review-<base7>..<head7>.diff — unique per # Default OUTFILE: <repo-root>/.superpowers/sdd/review-<base7>..<head7>.diff
# repo instance and per range, so concurrent sessions cannot collide and a # (named per range, so a re-review after fixes gets a distinct fresh file).
# re-review after fixes always gets a distinctly named fresh file.
set -euo pipefail set -euo pipefail
if [ $# -lt 2 ] || [ $# -gt 3 ]; then if [ $# -lt 2 ] || [ $# -gt 3 ]; then
@@ -24,9 +23,7 @@ git rev-parse --verify --quiet "$head" >/dev/null || { echo "bad HEAD: $head" >&
if [ $# -eq 3 ]; then if [ $# -eq 3 ]; then
out=$3 out=$3
else else
dir=$(git rev-parse --git-path sdd) dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
mkdir -p "$dir"
dir=$(cd "$dir" && pwd)
out="$dir/review-$(git rev-parse --short "$base")..$(git rev-parse --short "$head").diff" out="$dir/review-$(git rev-parse --short "$base")..$(git rev-parse --short "$head").diff"
fi fi

View File

@@ -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

View File

@@ -4,8 +4,8 @@
# through the controller's context. # through the controller's context.
# #
# Usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE] # Usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]
# Default OUTFILE: <git-dir>/sdd/task-<N>-brief.md — unique per repo # Default OUTFILE: <repo-root>/.superpowers/sdd/task-<N>-brief.md
# instance, so concurrent sessions cannot collide. # (per worktree; concurrent runs in the same working tree share it).
set -euo pipefail set -euo pipefail
if [ $# -lt 2 ] || [ $# -gt 3 ]; then if [ $# -lt 2 ] || [ $# -gt 3 ]; then
@@ -20,9 +20,7 @@ n=$2
if [ $# -eq 3 ]; then if [ $# -eq 3 ]; then
out=$3 out=$3
else else
dir=$(git rev-parse --git-path sdd) dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
mkdir -p "$dir"
dir=$(cd "$dir" && pwd)
out="$dir/task-${n}-brief.md" out="$dir/task-${n}-brief.md"
fi fi

View File

@@ -15,7 +15,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { execSync } = require('child_process'); const { execFileSync } = require('child_process');
function extractDotBlocks(markdown) { function extractDotBlocks(markdown) {
const blocks = []; const blocks = [];
@@ -69,7 +69,7 @@ ${bodies.join('\n\n')}
function renderToSvg(dotContent) { function renderToSvg(dotContent) {
try { try {
return execSync('dot -Tsvg', { return execFileSync('dot', ['-Tsvg'], {
input: dotContent, input: dotContent,
encoding: 'utf-8', encoding: 'utf-8',
maxBuffer: 10 * 1024 * 1024 maxBuffer: 10 * 1024 * 1024
@@ -107,9 +107,10 @@ function main() {
process.exit(1); process.exit(1);
} }
// Check if dot is available // Check if dot is available. Run the binary directly rather than probing
// with `which`, which is not a command on Windows.
try { try {
execSync('which dot', { encoding: 'utf-8' }); execFileSync('dot', ['-V'], { stdio: 'ignore' });
} catch { } catch {
console.error('Error: graphviz (dot) not found. Install with:'); console.error('Error: graphviz (dot) not found. Install with:');
console.error(' brew install graphviz # macOS'); console.error(' brew install graphviz # macOS');

View File

@@ -8,13 +8,13 @@
"name": "brainstorm-server-tests", "name": "brainstorm-server-tests",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"ws": "^8.19.0" "ws": "^8.21.0"
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.19.0", "version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@@ -5,6 +5,6 @@
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node branding.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh" "test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node branding.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
}, },
"dependencies": { "dependencies": {
"ws": "^8.19.0" "ws": "^8.21.0"
} }
} }

View File

@@ -74,6 +74,7 @@ done
# List of skill tests to run (fast unit tests) # List of skill tests to run (fast unit tests)
tests=( tests=(
"test-worktree-path-policy.sh" "test-worktree-path-policy.sh"
"test-sdd-workspace.sh"
"test-subagent-driven-development.sh" "test-subagent-driven-development.sh"
) )

View File

@@ -0,0 +1,142 @@
#!/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 <repo-root>/.superpowers/sdd"
else
fail "prints <repo-root>/.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
cat > "$repo/plan.md" <<'PLAN'
# Plan
## Task 1: First thing
Do the first thing.
PLAN
local brief_out brief_path
brief_out="$(cd "$repo" && "$SDD_SCRIPTS/task-brief" plan.md 1)"
brief_path="$(printf '%s\n' "$brief_out" | sed -n 's/^wrote \(.*\): [0-9][0-9]* lines$/\1/p')"
case "$brief_path" in
"$repo/.superpowers/sdd/"*) pass "task-brief writes its brief under the workspace" ;;
*)
fail "task-brief writes its brief under the workspace"
echo " got: $brief_path"
;;
esac
local git_id=(-c user.email=t@example.com -c user.name=t -c commit.gpgsign=false)
( cd "$repo" \
&& git add plan.md \
&& git "${git_id[@]}" commit -qm c1 \
&& printf 'y\n' > f && git add f \
&& git "${git_id[@]}" commit -qm c2 )
local rp_out rp_path
rp_out="$(cd "$repo" && "$SDD_SCRIPTS/review-package" HEAD~1 HEAD)"
rp_path="$(printf '%s\n' "$rp_out" | sed -n 's/^wrote \(.*\): [0-9].*$/\1/p')"
case "$rp_path" in
"$repo/.superpowers/sdd/"*) pass "review-package writes its diff under the workspace" ;;
*)
fail "review-package writes its diff under the workspace"
echo " got: $rp_path"
;;
esac
# --- Worktree isolation: a linked worktree resolves its own workspace ---
local wt="$TEST_ROOT/wt"
( cd "$repo" && git worktree add -q "$wt" -b wt-feature )
local wt_root wt_dir
wt_root="$(cd "$wt" && git rev-parse --show-toplevel)"
wt_dir="$(cd "$wt" && "$SDD_SCRIPTS/sdd-workspace")"
if [[ "$wt_dir" == "$wt_root/.superpowers/sdd" && "$wt_dir" != "$dir" ]]; then
pass "linked worktree resolves its own distinct workspace"
else
fail "linked worktree resolves its own distinct workspace"
echo " main: $dir"
echo " wt: $wt_dir"
fi
printf 'y\n' > "$wt/.superpowers/sdd/artifact.md"
local wt_status
wt_status="$(cd "$wt" && git status --porcelain)"
if [[ -z "$wt_status" ]]; then
pass "worktree workspace invisible to git status"
else
fail "worktree workspace invisible to git status"
echo " status: $wt_status"
fi
echo ""
if [[ "$FAILURES" -ne 0 ]]; then
echo "FAILED: $FAILURES assertion(s)."
exit 1
fi
echo "PASS"
}
main "$@"