Compare commits

..

1 Commits

Author SHA1 Message Date
Drew Ritter
a0ebbf5ba0 fix: exclude repo metadata from Codex sync (PRI-1168) 2026-06-16 12:14:40 -07:00
24 changed files with 55 additions and 275 deletions

View File

@@ -9,7 +9,7 @@
{ {
"name": "superpowers", "name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "6.0.2", "version": "6.0.0",
"source": "./", "source": "./",
"author": { "author": {
"name": "Jesse Vincent", "name": "Jesse Vincent",

View File

@@ -1,7 +1,7 @@
{ {
"name": "superpowers", "name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "6.0.2", "version": "6.0.0",
"author": { "author": {
"name": "Jesse Vincent", "name": "Jesse Vincent",
"email": "jesse@fsck.com" "email": "jesse@fsck.com"

View File

@@ -1,6 +1,6 @@
{ {
"name": "superpowers", "name": "superpowers",
"version": "6.0.2", "version": "6.0.0",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.", "description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
"author": { "author": {
"name": "Jesse Vincent", "name": "Jesse Vincent",

View File

@@ -2,7 +2,7 @@
"name": "superpowers", "name": "superpowers",
"displayName": "Superpowers", "displayName": "Superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "6.0.2", "version": "6.0.0",
"author": { "author": {
"name": "Jesse Vincent", "name": "Jesse Vincent",
"email": "jesse@fsck.com" "email": "jesse@fsck.com"

9
.gitignore vendored
View File

@@ -7,7 +7,8 @@ node_modules/
inspo inspo
triage/ triage/
# Eval harness lives in its own repository, cloned into evals/ for local # Eval harness — drill ships its own gitignore at evals/.gitignore;
# development (see CLAUDE.md / README.md). It is not part of the published # these are belt-and-suspenders entries for tools that don't recurse.
# plugin, so the whole directory is ignored here. evals/results/
evals/ evals/.venv/
evals/.env

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "evals"]
path = evals
url = git@github.com:prime-radiant-inc/superpowers-evals.git

View File

@@ -1,6 +1,6 @@
{ {
"name": "superpowers", "name": "superpowers",
"version": "6.0.2", "version": "6.0.0",
"description": "An agentic skills framework and software development methodology.", "description": "An agentic skills framework and software development methodology.",
"author": { "author": {
"name": "Jesse Vincent", "name": "Jesse Vincent",

View File

@@ -101,7 +101,7 @@ Skills are not prose — they are code that shapes agent behavior. If you modify
## Eval harness ## Eval harness
Skill-behavior evals live in [superpowers-evals](https://github.com/prime-radiant-inc/superpowers-evals/), cloned into `evals/` see `evals/README.md` for setup. Drill (the harness) drives real tmux sessions of Claude Code / Codex / Gemini CLI and judges skill compliance with an LLM verifier. Plugin-infrastructure tests still live at `tests/`. Skill-behavior evals live in the `evals/` submodule — after cloning, run `git submodule update --init evals`, then see `evals/README.md`. Drill (the harness) drives real tmux sessions of Claude Code / Codex / Gemini CLI and judges skill compliance with an LLM verifier. Plugin-infrastructure tests still live at `tests/`.
## Understand the Project Before Contributing ## Understand the Project Before Contributing

View File

@@ -262,7 +262,7 @@ The general contribution process for Superpowers is below. Keep in mind that we
4. Follow the `writing-skills` skill for creating and testing new and modified skills 4. Follow the `writing-skills` skill for creating and testing new and modified skills
5. Submit a PR, being sure to fill in the pull request template. 5. Submit a PR, being sure to fill in the pull request template.
Skill-behavior tests use the drill eval harness from [superpowers-evals](https://github.com/prime-radiant-inc/superpowers-evals/), cloned into `evals/` see `evals/README.md` for setup. Plugin-infrastructure tests live at `tests/` and run via the relevant `run-*.sh` or `npm test`. Skill-behavior tests use the eval harness submodule at `evals/`. After cloning this repo, run `git submodule update --init evals`, then see `evals/README.md` for setup. Plugin-infrastructure tests live at `tests/` and run via the relevant `run-*.sh` or `npm test`.
See `skills/writing-skills/SKILL.md` for the complete guide. See `skills/writing-skills/SKILL.md` for the complete guide.

View File

@@ -1,24 +1,5 @@
# 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)
### Install Fixes
- **We no longer ship the `evals` submodule.** It broke plugin installs for some users, so the eval harness now lives in its own repo, separate from the published plugin. (#1778, #1774)
## v6.0.1 (2026-06-16)
### Codex Fixes
- **Version display in the brainstorm companion** — packaged Codex plugins ship without a root `package.json`, so the visual companion reported its version as "unknown". `readSuperpowersVersion()` now falls back to `.codex-plugin/plugin.json` when `package.json` is absent.
- **Cleaner Codex plugin sync** — the sync-to-codex script now excludes `.gitmodules` and `.pre-commit-config.yaml`, keeping repo metadata out of the packaged Codex plugin.
## v6.0.0 (2026-06-16) ## v6.0.0 (2026-06-16)
Superpowers 6.0 is a big release. The headline is a rewrite of how `subagent-driven-development` reviews each task — cheaper, stricter, and harder to game. Superpowers 6.0 is a big release. The headline is a rewrite of how `subagent-driven-development` reviews each task — cheaper, stricter, and harder to game.

1
evals Submodule

Submodule evals added at 70a245c36c

View File

@@ -1,6 +1,6 @@
{ {
"name": "superpowers", "name": "superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "6.0.2", "version": "6.0.0",
"contextFileName": "GEMINI.md" "contextFileName": "GEMINI.md"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "superpowers", "name": "superpowers",
"version": "6.0.2", "version": "6.0.0",
"description": "Superpowers skills and runtime bootstrap for coding agents", "description": "Superpowers skills and runtime bootstrap for coding agents",
"type": "module", "type": "module",
"main": ".opencode/plugins/superpowers.js", "main": ".opencode/plugins/superpowers.js",

View File

@@ -206,22 +206,14 @@ const helperInjection = '<script>\n' + helperScript + '\n</script>';
// ========== Helper Functions ========== // ========== Helper Functions ==========
function readSuperpowersVersion() { function readSuperpowersVersion() {
const root = path.join(__dirname, '../../..'); try {
const manifests = [ const packageJson = JSON.parse(
path.join(root, 'package.json'), fs.readFileSync(path.join(__dirname, '../../..', 'package.json'), 'utf-8')
path.join(root, '.codex-plugin/plugin.json') );
]; return String(packageJson.version || 'unknown');
} catch (e) {
for (const manifest of manifests) { return 'unknown';
try {
const data = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
if (data.version) return String(data.version);
} catch (e) {
// Packaged Codex plugins omit package.json; try the next manifest.
}
} }
return 'unknown';
} }
function isTruthyEnv(value) { function isTruthyEnv(value) {

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 --show-toplevel)/.superpowers/sdd/progress.md"`. Tasks listed there `cat "$(git rev-parse --git-path 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,8 +260,6 @@ 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,8 +5,9 @@
# tasks intact. # tasks intact.
# #
# Usage: review-package BASE HEAD [OUTFILE] # Usage: review-package BASE HEAD [OUTFILE]
# Default OUTFILE: <repo-root>/.superpowers/sdd/review-<base7>..<head7>.diff # Default OUTFILE: <git-dir>/sdd/review-<base7>..<head7>.diff — unique per
# (named per range, so a re-review after fixes gets a distinct fresh file). # repo instance and per range, so concurrent sessions cannot collide and a
# 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
@@ -23,7 +24,9 @@ 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=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace") dir=$(git rev-parse --git-path sdd)
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

@@ -1,22 +0,0 @@
#!/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: <repo-root>/.superpowers/sdd/task-<N>-brief.md # Default OUTFILE: <git-dir>/sdd/task-<N>-brief.md — unique per repo
# (per worktree; concurrent runs in the same working tree share it). # instance, so concurrent sessions cannot collide.
set -euo pipefail set -euo pipefail
if [ $# -lt 2 ] || [ $# -gt 3 ]; then if [ $# -lt 2 ] || [ $# -gt 3 ]; then
@@ -20,7 +20,9 @@ n=$2
if [ $# -eq 3 ]; then if [ $# -eq 3 ]; then
out=$3 out=$3
else else
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace") dir=$(git rev-parse --git-path sdd)
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 { execFileSync } = require('child_process'); const { execSync } = 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 execFileSync('dot', ['-Tsvg'], { return execSync('dot -Tsvg', {
input: dotContent, input: dotContent,
encoding: 'utf-8', encoding: 'utf-8',
maxBuffer: 10 * 1024 * 1024 maxBuffer: 10 * 1024 * 1024
@@ -107,10 +107,9 @@ function main() {
process.exit(1); process.exit(1);
} }
// Check if dot is available. Run the binary directly rather than probing // Check if dot is available
// with `which`, which is not a command on Windows.
try { try {
execFileSync('dot', ['-V'], { stdio: 'ignore' }); execSync('which dot', { encoding: 'utf-8' });
} 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

@@ -26,9 +26,9 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
function startServer({ port, dir, env = {}, serverPath = SERVER_PATH }) { function startServer({ port, dir, env = {} }) {
cleanup(dir); cleanup(dir);
return spawn('node', [serverPath], { return spawn('node', [SERVER_PATH], {
env: { env: {
...process.env, ...process.env,
BRAINSTORM_PORT: String(port), BRAINSTORM_PORT: String(port),
@@ -74,21 +74,6 @@ function writeFragment(dir) {
fs.writeFileSync(path.join(contentDir, 'screen.html'), '<h2>Pick a layout</h2>'); fs.writeFileSync(path.join(contentDir, 'screen.html'), '<h2>Pick a layout</h2>');
} }
function createPackagedServerFixture(version) {
const root = fs.mkdtempSync(path.join('/tmp', 'superpowers-packaged-server-'));
const scriptDir = path.join(root, 'skills/brainstorming/scripts');
fs.cpSync(path.join(REPO_ROOT, 'skills/brainstorming/scripts'), scriptDir, { recursive: true });
fs.mkdirSync(path.join(root, '.codex-plugin'), { recursive: true });
fs.writeFileSync(
path.join(root, '.codex-plugin/plugin.json'),
JSON.stringify({ name: 'superpowers', version }, null, 2)
);
return {
root,
serverPath: path.join(scriptDir, 'server.cjs')
};
}
async function withServer(options, fn) { async function withServer(options, fn) {
const server = startServer(options); const server = startServer(options);
try { try {
@@ -119,13 +104,13 @@ async function test(name, fn) {
} }
} }
function assertBrandedWithLogo(html, version = PACKAGE_VERSION) { function assertBrandedWithLogo(html) {
assert( assert(
html.includes(`Superpowers v${version}`), html.includes(`Superpowers v${PACKAGE_VERSION}`),
'branding text should include dynamic package version' 'branding text should include dynamic package version'
); );
assert( assert(
!html.includes(`Superpowers v${version} by`), !html.includes(`Superpowers v${PACKAGE_VERSION} by`),
'branding text should not include "by" when the logo is visible' 'branding text should not include "by" when the logo is visible'
); );
assert( assert(
@@ -154,15 +139,15 @@ function assertBrandedWithLogo(html, version = PACKAGE_VERSION) {
); );
} }
function assertBrandedFallbackText(html, version = PACKAGE_VERSION) { function assertBrandedFallbackText(html) {
assert( assert(
html.includes(`Prime Radiant Superpowers v${version}`), html.includes(`Prime Radiant Superpowers v${PACKAGE_VERSION}`),
'disabled telemetry should keep plain text Prime Radiant/Superpowers branding' 'disabled telemetry should keep plain text Prime Radiant/Superpowers branding'
); );
} }
function assertTelemetryImage(html, version = PACKAGE_VERSION) { function assertTelemetryImage(html) {
const expectedUrl = `${ASSET_URL}?v=${encodeURIComponent(version)}`; const expectedUrl = `${ASSET_URL}?v=${encodeURIComponent(PACKAGE_VERSION)}`;
assert(html.includes(`src="${expectedUrl}"`), 'remote image should use the dedicated main-domain asset with only v='); assert(html.includes(`src="${expectedUrl}"`), 'remote image should use the dedicated main-domain asset with only v=');
assert(!html.includes('event='), 'remote image URL must not include event='); assert(!html.includes('event='), 'remote image URL must not include event=');
assert(!html.includes('surface='), 'remote image URL must not include surface='); assert(!html.includes('surface='), 'remote image URL must not include surface=');
@@ -270,26 +255,6 @@ async function main() {
}); });
}); });
await test('packaged Codex plugin reads version from .codex-plugin manifest', async () => {
const port = 3457;
const dir = '/tmp/brainstorm-branding-packaged-codex';
const packagedVersion = '7.8.9';
const fixture = createPackagedServerFixture(packagedVersion);
try {
await withServer({ port, dir, serverPath: fixture.serverPath }, async () => {
writeFragment(dir);
await sleep(300);
const html = await fetchHtml(port);
assertBrandedWithLogo(html, packagedVersion);
assertTelemetryImage(html, packagedVersion);
assert(!html.includes('Superpowers vunknown'), 'packaged plugin should not fall back to unknown version');
});
} finally {
cleanup(fixture.root);
}
});
await test('SUPERPOWERS_DISABLE_TELEMETRY=true omits remote image but keeps local branding', async () => { await test('SUPERPOWERS_DISABLE_TELEMETRY=true omits remote image but keeps local branding', async () => {
const port = 3453; const port = 3453;
const dir = '/tmp/brainstorm-branding-disabled'; const dir = '/tmp/brainstorm-branding-disabled';

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.21.0" "ws": "^8.19.0"
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.21.0", "version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"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.21.0" "ws": "^8.19.0"
} }
} }

View File

@@ -74,7 +74,6 @@ 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

@@ -1,142 +0,0 @@
#!/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 "$@"