Compare commits

..

4 Commits

Author SHA1 Message Date
Jesse Vincent
0da121fa05 formatting 2026-04-16 12:33:48 -07:00
Jesse Vincent
68d2b1bee8 formatting 2026-04-16 12:29:58 -07:00
Jesse Vincent
b5744e2b67 reorder installs 2026-04-16 12:25:50 -07:00
Jesse Vincent
bfa3a1a5b1 README updates for Codex, other cleanup 2026-04-16 12:24:40 -07:00
8 changed files with 121 additions and 840 deletions

View File

@@ -1,44 +0,0 @@
{
"name": "superpowers",
"version": "5.0.7",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com",
"url": "https://github.com/obra"
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
"developerName": "Jesse Vincent",
"category": "Coding",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"defaultPrompt": [
"I've got an idea for something I'd like to build.",
"Let's add a feature to this project."
],
"brandColor": "#F59E0B",
"composerIcon": "./assets/superpowers-small.svg",
"logo": "./assets/app-icon.png",
"screenshots": []
}
}

View File

@@ -50,45 +50,6 @@ of human involvement will be closed without review.
|-------------------------------------|-----------------|-------|------------------| |-------------------------------------|-----------------|-------|------------------|
| | | | | | | | | |
## New harness support (required if this PR adds a new harness)
<!-- If this PR adds support for a new harness (IDE, CLI tool, agent
runner), you MUST include a session transcript proving the
integration actually works.
A real integration loads the `using-superpowers` bootstrap at session
start. The bootstrap is what causes skills to auto-trigger. Without
it, the skills are dead weight — present on disk but never invoked
at the right moments.
ACCEPTANCE TEST: Open a clean session in the new harness and send
exactly this user message:
Let's make a react todo list
A working integration auto-triggers the `brainstorming` skill before
any code is written. Paste the complete transcript below.
These are NOT real integrations and PRs that ship them will be closed:
- Manually copying skill files into the harness
- Wrapping with `npx skills` or similar at-runtime shims
- Anything that requires the user to opt in to skills per-session
- Anything where brainstorming does not auto-trigger on the test above
If you are not sure whether your integration loads the bootstrap at
session start, it does not.
-->
<details>
<summary>Clean-session transcript for "Let's make a react todo list"</summary>
```
paste the complete transcript here
```
</details>
## Evaluation ## Evaluation
- What was the initial prompt you (or your human partner) used to start - What was the initial prompt you (or your human partner) used to start
the session that led to this change? the session that led to this change?

View File

@@ -3,7 +3,6 @@
{ "path": "package.json", "field": "version" }, { "path": "package.json", "field": "version" },
{ "path": ".claude-plugin/plugin.json", "field": "version" }, { "path": ".claude-plugin/plugin.json", "field": "version" },
{ "path": ".cursor-plugin/plugin.json", "field": "version" }, { "path": ".cursor-plugin/plugin.json", "field": "version" },
{ "path": ".codex-plugin/plugin.json", "field": "version" },
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" }, { "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
{ "path": "gemini-extension.json", "field": "version" } { "path": "gemini-extension.json", "field": "version" }
], ],

View File

@@ -64,27 +64,6 @@ PRs containing invented claims, fabricated problem descriptions, or hallucinated
PRs containing multiple unrelated changes will be closed. Split them into separate PRs. PRs containing multiple unrelated changes will be closed. Split them into separate PRs.
## New Harness Support
If your PR adds support for a new harness (IDE, CLI tool, agent runner), you MUST include a session transcript proving the integration works end-to-end.
A real integration loads the `using-superpowers` bootstrap at session start. The bootstrap is what causes skills to auto-trigger at the right moments. Without it, the skills are dead weight — present on disk but never invoked.
**The acceptance test.** Open a clean session in the new harness and send exactly this user message:
> Let's make a react todo list
A working integration auto-triggers the `brainstorming` skill before any code is written. Paste the complete transcript in the PR.
**These are not real integrations and will be closed:**
- Manually copying skill files into the harness
- Wrapping with `npx skills` or similar at-runtime shims
- Anything that requires the user to opt in to skills per-session
- Anything where `brainstorming` does not auto-trigger on the acceptance test above
If you are not sure whether your integration loads the bootstrap at session start, it does not.
## Skill Changes Require Evaluation ## Skill Changes Require Evaluation
Skills are not prose — they are code that shapes agent behavior. If you modify skill content: Skills are not prose — they are code that shapes agent behavior. If you modify skill content:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M394.28,207.8c.81,2.41,1.39,4.78,1.8,7.07,1.61,9.03-.93,17.78-5.99,21.74-22.6,17.7-49.85,29.35-75.34,38.6-.59.22-1.09.28-1.4.34-2.22.47-4.95,1.04-7.25,0-1.46-.66-2.25-1.74-2.66-2.3-1.56-2.1-1.59-4.31-1.56-5.13.1-2.67-.01-4.69,0-4.82.45-3.52.91-10.66,1.41-21.28.6-3.87,2.16-9.63,6.94-13.96,4.01-3.62,8.33-4.6,14.59-5.87,10.76-2.19,37.21-8.22,47.42-16.56,1.63-1.33,2.97-2.65,4.19-3.96,3.72-3.99,6.39-7.92,7.93-10.36,3.22,3.22,7.25,8.48,9.92,16.47Z"/><path d="M428.67,185.28c-2.33,11.99-8.91,22.32-15.88,30.38.27-5.5-.05-12.11-1.86-19.08-5.04-19.36-19.74-34.7-37.78-37.78-32.21-9.74-70.59,3.79-99.08,18.29-3.87,1.95-9.52-2.77-11.84-8.16-3.32-7.71-1.63-6.28,2.61-8.49,38.31-20.03,82.01-39.61,123.91-29.7,8.26,1.95,15.96,5.26,23.48,10.54,11.32,7.96,20.21,24.74,16.44,44Z"/><path d="M117.72,304.2c-.81-2.41-1.39-4.78-1.8-7.07-1.61-9.03.93-17.78,5.99-21.74,22.6-17.7,49.85-29.35,75.34-38.6.59-.22,1.09-.28,1.4-.34,2.22-.47,4.95-1.04,7.25,0,1.46.66,2.25,1.74,2.66,2.3,1.56,2.1,1.59,4.31,1.56,5.13-.1,2.67.01,4.69,0,4.82-.45,3.52-.91,10.66-1.41,21.28-.6,3.87-2.16,9.63-6.94,13.96-4.01,3.62-8.33,4.6-14.59,5.87-10.76,2.19-37.21,8.22-47.42,16.56-1.63,1.33-2.97,2.65-4.19,3.96-3.72,3.99-6.39,7.92-7.93,10.36-3.22-3.22-7.25-8.48-9.92-16.47Z"/><path d="M83.33,326.72c2.33-11.99,8.91-22.32,15.88-30.38-.27,5.5.05,12.11,1.86,19.08,5.04,19.36,19.74,34.7,37.78,37.78,32.21,9.74,70.59-3.79,99.08-18.29,3.87-1.95,9.52,2.77,11.84,8.16,3.32,7.71,1.63,6.28-2.61,8.49-38.31,20.03-82.01,39.61-123.91,29.7-8.26-1.95-15.96-5.26-23.48-10.54-11.32-7.96-20.21-24.74-16.44-44Z"/><ellipse cx="255.16" cy="258.86" rx="28.95" ry="28.76"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -3,9 +3,9 @@
# sync-to-codex-plugin.sh # sync-to-codex-plugin.sh
# #
# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins. # Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins.
# Clones the fork fresh into a temp dir, rsyncs tracked upstream plugin content # Clones the fork fresh into a temp dir, rsyncs upstream content, regenerates
# (including committed Codex files under .codex-plugin/ and assets/), commits, # the Codex overlay file (.codex-plugin/plugin.json) inline, commits, pushes a
# pushes a sync branch, and opens a PR. # sync branch, and opens a PR.
# Path/user agnostic — auto-detects upstream from script location. # Path/user agnostic — auto-detects upstream from script location.
# #
# Deterministic: running twice against the same upstream SHA produces PRs with # Deterministic: running twice against the same upstream SHA produces PRs with
@@ -17,11 +17,13 @@
# ./scripts/sync-to-codex-plugin.sh -y # skip confirm # ./scripts/sync-to-codex-plugin.sh -y # skip confirm
# ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout # ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout
# ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main # ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main
# ./scripts/sync-to-codex-plugin.sh --bootstrap # create plugin dir if missing # ./scripts/sync-to-codex-plugin.sh --bootstrap --assets-src DIR # create initial plugin
# #
# Bootstrap mode: skips the "plugin must exist on base" requirement and creates # Bootstrap mode: skips the "plugin must exist on base" check and seeds
# plugins/superpowers/ when absent, then copies the tracked plugin files from # plugins/superpowers/assets/ from --assets-src <dir> which must contain
# upstream just like a normal sync. # PrimeRadiant_Favicon.svg and PrimeRadiant_Favicon.png. Run once by one
# team member to create the initial PR; every subsequent run is a normal
# (non-bootstrap) sync.
# #
# Requires: bash, rsync, git, gh (authenticated), python3. # Requires: bash, rsync, git, gh (authenticated), python3.
@@ -36,6 +38,9 @@ DEFAULT_BASE="main"
DEST_REL="plugins/superpowers" DEST_REL="plugins/superpowers"
# Paths in upstream that should NOT land in the embedded plugin. # Paths in upstream that should NOT land in the embedded plugin.
# The Codex-only paths are here too — they're managed by generate/bootstrap
# steps, not by rsync.
#
# All patterns use a leading "/" to anchor them to the source root. # All patterns use a leading "/" to anchor them to the source root.
# Unanchored patterns like "scripts/" would match any directory named # Unanchored patterns like "scripts/" would match any directory named
# "scripts" at any depth — including legitimate nested dirs like # "scripts" at any depth — including legitimate nested dirs like
@@ -73,57 +78,68 @@ EXCLUDES=(
"/scripts/" "/scripts/"
"/tests/" "/tests/"
"/tmp/" "/tmp/"
# Codex-only paths — managed outside rsync
"/.codex-plugin/"
"/assets/"
) )
# ============================================================================= # =============================================================================
# Ignored-path helpers # Generated overlay file
# ============================================================================= # =============================================================================
IGNORED_DIR_EXCLUDES=() # Writes the Codex plugin manifest to "$1" with the given upstream version.
# Args: dest_path, version
path_has_directory_exclude() { generate_plugin_json() {
local path="$1" local dest="$1"
local dir local version="$2"
mkdir -p "$(dirname "$dest")"
if [[ ${#IGNORED_DIR_EXCLUDES[@]} -eq 0 ]]; then cat > "$dest" <<EOF
return 1 {
fi "name": "superpowers",
"version": "$version",
for dir in "${IGNORED_DIR_EXCLUDES[@]}"; do "description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
[[ "$path" == "$dir"* ]] && return 0 "author": {
done "name": "Jesse Vincent",
"email": "jesse@fsck.com",
return 1 "url": "https://github.com/obra"
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
"developerName": "Jesse Vincent",
"category": "Coding",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"defaultPrompt": [
"I've got an idea for something I'd like to build.",
"Let's add a feature to this project."
],
"brandColor": "#F59E0B",
"composerIcon": "./assets/superpowers-small.svg",
"logo": "./assets/app-icon.png",
"screenshots": []
} }
ignored_directory_has_tracked_descendants() {
local path="$1"
[[ -n "$(git -C "$UPSTREAM" ls-files --cached -- "$path/")" ]]
} }
EOF
append_git_ignored_directory_excludes() {
local path
local lookup_path
while IFS= read -r -d '' path; do
[[ "$path" == */ ]] || continue
lookup_path="${path%/}"
if ! ignored_directory_has_tracked_descendants "$lookup_path"; then
IGNORED_DIR_EXCLUDES+=("$path")
RSYNC_ARGS+=(--exclude="/$path")
fi
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard --directory -z)
}
append_git_ignored_file_excludes() {
local path
while IFS= read -r -d '' path; do
path_has_directory_exclude "$path" && continue
RSYNC_ARGS+=(--exclude="/$path")
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard -z)
} }
# ============================================================================= # =============================================================================
@@ -137,9 +153,10 @@ DRY_RUN=0
YES=0 YES=0
LOCAL_CHECKOUT="" LOCAL_CHECKOUT=""
BOOTSTRAP=0 BOOTSTRAP=0
ASSETS_SRC=""
usage() { usage() {
sed -n '/^# Usage:/,/^# Requires:/s/^# \{0,1\}//p' "$0" sed -n 's/^# \{0,1\}//;2,27p' "$0"
exit "${1:-0}" exit "${1:-0}"
} }
@@ -150,6 +167,7 @@ while [[ $# -gt 0 ]]; do
--local) LOCAL_CHECKOUT="$2"; shift 2 ;; --local) LOCAL_CHECKOUT="$2"; shift 2 ;;
--base) BASE="$2"; shift 2 ;; --base) BASE="$2"; shift 2 ;;
--bootstrap) BOOTSTRAP=1; shift ;; --bootstrap) BOOTSTRAP=1; shift ;;
--assets-src) ASSETS_SRC="$2"; shift 2 ;;
-h|--help) usage 0 ;; -h|--help) usage 0 ;;
*) echo "Unknown arg: $1" >&2; usage 2 ;; *) echo "Unknown arg: $1" >&2; usage 2 ;;
esac esac
@@ -169,11 +187,19 @@ command -v python3 >/dev/null || die "python3 not found in PATH"
gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'" gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'"
[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout" [[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
[[ -f "$UPSTREAM/.codex-plugin/plugin.json" ]] || die "committed Codex manifest missing at $UPSTREAM/.codex-plugin/plugin.json" [[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version"
# Read the upstream version from the committed Codex manifest. # Bootstrap-mode validation
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/.codex-plugin/plugin.json")" if [[ $BOOTSTRAP -eq 1 ]]; then
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from committed Codex manifest" [[ -n "$ASSETS_SRC" ]] || die "--bootstrap requires --assets-src <path>"
ASSETS_SRC="$(cd "$ASSETS_SRC" 2>/dev/null && pwd)" || die "assets source '$ASSETS_SRC' is not a directory"
[[ -f "$ASSETS_SRC/PrimeRadiant_Favicon.svg" ]] || die "assets source missing PrimeRadiant_Favicon.svg"
[[ -f "$ASSETS_SRC/PrimeRadiant_Favicon.png" ]] || die "assets source missing PrimeRadiant_Favicon.png"
fi
# Read the upstream version from package.json
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/package.json")"
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from upstream package.json"
UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)" UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)" UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
@@ -204,9 +230,7 @@ fi
CLEANUP_DIR="" CLEANUP_DIR=""
cleanup() { cleanup() {
if [[ -n "$CLEANUP_DIR" ]]; then [[ -n "$CLEANUP_DIR" ]] && rm -rf "$CLEANUP_DIR"
rm -rf "$CLEANUP_DIR"
fi
} }
trap cleanup EXIT trap cleanup EXIT
@@ -221,84 +245,22 @@ else
fi fi
DEST="$DEST_REPO/$DEST_REL" DEST="$DEST_REPO/$DEST_REL"
PREVIEW_REPO="$DEST_REPO"
PREVIEW_DEST="$DEST"
overlay_destination_paths() { # Checkout base branch
local repo="$1" cd "$DEST_REPO"
local path git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
local source_path
local preview_path
while IFS= read -r -d '' path; do # Plugin-existence check depends on mode
source_path="$repo/$path"
preview_path="$PREVIEW_REPO/$path"
if [[ -e "$source_path" ]]; then
mkdir -p "$(dirname "$preview_path")"
cp -R "$source_path" "$preview_path"
else
rm -rf "$preview_path"
fi
done
}
copy_local_destination_overlay() {
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" diff --name-only -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" diff --cached --name-only -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" ls-files --others --exclude-standard -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" ls-files --others --ignored --exclude-standard -z -- "$DEST_REL"
)
}
local_checkout_has_uncommitted_destination_changes() {
[[ -n "$(git -C "$DEST_REPO" status --porcelain=1 --untracked-files=all --ignored=matching -- "$DEST_REL")" ]]
}
prepare_preview_checkout() {
if [[ -n "$LOCAL_CHECKOUT" ]]; then
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
PREVIEW_REPO="$CLEANUP_DIR/preview"
git clone -q --no-local "$DEST_REPO" "$PREVIEW_REPO"
PREVIEW_DEST="$PREVIEW_REPO/$DEST_REL"
fi
git -C "$PREVIEW_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
if [[ -n "$LOCAL_CHECKOUT" ]]; then
copy_local_destination_overlay
fi
if [[ $BOOTSTRAP -ne 1 ]]; then
[[ -d "$PREVIEW_DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
fi
}
prepare_apply_checkout() {
git -C "$DEST_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
if [[ $BOOTSTRAP -ne 1 ]]; then
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
fi
}
apply_to_preview_checkout() {
if [[ $BOOTSTRAP -eq 1 ]]; then if [[ $BOOTSTRAP -eq 1 ]]; then
mkdir -p "$PREVIEW_DEST" [[ ! -d "$DEST" ]] || die "--bootstrap but base branch '$BASE' already has '$DEST_REL/' — use normal sync instead"
mkdir -p "$DEST"
else
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap + --assets-src, or pass --base <branch>"
fi fi
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$PREVIEW_DEST/" # =============================================================================
} # Create sync branch
# =============================================================================
preview_checkout_has_changes() {
[[ -n "$(git -C "$PREVIEW_REPO" status --porcelain "$DEST_REL")" ]]
}
prepare_preview_checkout
TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)" TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
if [[ $BOOTSTRAP -eq 1 ]]; then if [[ $BOOTSTRAP -eq 1 ]]; then
@@ -306,15 +268,14 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
else else
SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
fi fi
git checkout -q -b "$SYNC_BRANCH"
# ============================================================================= # =============================================================================
# Build rsync args # Build rsync args
# ============================================================================= # =============================================================================
RSYNC_ARGS=(-av --delete --delete-excluded) RSYNC_ARGS=(-av --delete)
for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done
append_git_ignored_directory_excludes
append_git_ignored_file_excludes
# ============================================================================= # =============================================================================
# Dry run preview (always shown) # Dry run preview (always shown)
@@ -327,13 +288,20 @@ echo "Fork: $FORK"
echo "Base: $BASE" echo "Base: $BASE"
echo "Branch: $SYNC_BRANCH" echo "Branch: $SYNC_BRANCH"
if [[ $BOOTSTRAP -eq 1 ]]; then if [[ $BOOTSTRAP -eq 1 ]]; then
echo "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)" echo "Mode: BOOTSTRAP (creating initial plugin from scratch)"
echo "Assets: $ASSETS_SRC"
fi fi
echo "" echo ""
echo "=== Preview (rsync --dry-run) ===" echo "=== Preview (rsync --dry-run) ==="
rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$PREVIEW_DEST/" rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/"
echo "=== End preview ===" echo "=== End preview ==="
echo "" echo ""
echo "Overlay file (.codex-plugin/plugin.json) will be regenerated with"
echo "version $UPSTREAM_VERSION regardless of rsync output."
if [[ $BOOTSTRAP -eq 1 ]]; then
echo "Assets (superpowers-small.svg, app-icon.png) will be seeded from:"
echo " $ASSETS_SRC"
fi
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "" echo ""
@@ -349,27 +317,19 @@ echo ""
confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; } confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
echo "" echo ""
if [[ -n "$LOCAL_CHECKOUT" ]]; then
if local_checkout_has_uncommitted_destination_changes; then
die "local checkout has uncommitted changes under '$DEST_REL' — commit, stash, or discard them before syncing"
fi
apply_to_preview_checkout
if ! preview_checkout_has_changes; then
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
exit 0
fi
fi
prepare_apply_checkout
cd "$DEST_REPO"
git checkout -q -b "$SYNC_BRANCH"
echo "Syncing upstream content..." echo "Syncing upstream content..."
if [[ $BOOTSTRAP -eq 1 ]]; then
mkdir -p "$DEST"
fi
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/"
if [[ $BOOTSTRAP -eq 1 ]]; then
echo "Seeding brand assets..."
mkdir -p "$DEST/assets"
cp "$ASSETS_SRC/PrimeRadiant_Favicon.svg" "$DEST/assets/superpowers-small.svg"
cp "$ASSETS_SRC/PrimeRadiant_Favicon.png" "$DEST/assets/app-icon.png"
fi
echo "Regenerating overlay file..."
generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION"
# Bail early if nothing actually changed # Bail early if nothing actually changed
cd "$DEST_REPO" cd "$DEST_REPO"
if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then
@@ -387,18 +347,16 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION). PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`. Creates \`plugins/superpowers/\` from scratch: upstream content via rsync, \`.codex-plugin/plugin.json\` regenerated inline, brand assets seeded from a local Brand Assets directory.
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\` Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap --assets-src <path>\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files." This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs and will not touch the \`assets/\` directory."
else else
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION). PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
Run via: \`scripts/sync-to-codex-plugin.sh\` Run via: \`scripts/sync-to-codex-plugin.sh\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA

View File

@@ -1,571 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SYNC_SCRIPT_SOURCE="$REPO_ROOT/scripts/sync-to-codex-plugin.sh"
BASH_UNDER_TEST="/bin/bash"
PACKAGE_VERSION="1.2.3"
MANIFEST_VERSION="9.8.7"
FAILURES=0
TEST_ROOT=""
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
assert_equals() {
local actual="$1"
local expected="$2"
local description="$3"
if [[ "$actual" == "$expected" ]]; then
pass "$description"
else
fail "$description"
echo " expected: $expected"
echo " actual: $actual"
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
pass "$description"
else
fail "$description"
echo " expected to find: $needle"
fi
}
assert_not_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
fail "$description"
echo " did not expect to find: $needle"
else
pass "$description"
fi
}
assert_matches() {
local haystack="$1"
local pattern="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Eq -- "$pattern"; then
pass "$description"
else
fail "$description"
echo " expected to match: $pattern"
fi
}
assert_path_absent() {
local path="$1"
local description="$2"
if [[ ! -e "$path" ]]; then
pass "$description"
else
fail "$description"
echo " did not expect path to exist: $path"
fi
}
assert_branch_absent() {
local repo="$1"
local pattern="$2"
local description="$3"
local branches
branches="$(git -C "$repo" branch --list "$pattern")"
if [[ -z "$branches" ]]; then
pass "$description"
else
fail "$description"
echo " did not expect matching branches:"
echo "$branches" | sed 's/^/ /'
fi
}
assert_current_branch() {
local repo="$1"
local expected="$2"
local description="$3"
local actual
actual="$(git -C "$repo" branch --show-current)"
assert_equals "$actual" "$expected" "$description"
}
assert_file_equals() {
local path="$1"
local expected="$2"
local description="$3"
local actual
actual="$(cat "$path")"
assert_equals "$actual" "$expected" "$description"
}
cleanup() {
if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then
rm -rf "$TEST_ROOT"
fi
}
configure_git_identity() {
local repo="$1"
git -C "$repo" config user.name "Test Bot"
git -C "$repo" config user.email "test@example.com"
}
init_repo() {
local repo="$1"
git init -q -b main "$repo"
configure_git_identity "$repo"
}
commit_fixture() {
local repo="$1"
local message="$2"
git -C "$repo" commit -q -m "$message"
}
checkout_fixture_branch() {
local repo="$1"
local branch="$2"
git -C "$repo" checkout -q -b "$branch"
}
write_upstream_fixture() {
local repo="$1"
local with_pure_ignored="${2:-1}"
mkdir -p \
"$repo/.codex-plugin" \
"$repo/.private-journal" \
"$repo/assets" \
"$repo/scripts" \
"$repo/skills/example"
if [[ "$with_pure_ignored" == "1" ]]; then
mkdir -p "$repo/ignored-cache/tmp"
fi
cp "$SYNC_SCRIPT_SOURCE" "$repo/scripts/sync-to-codex-plugin.sh"
cat > "$repo/package.json" <<EOF
{
"name": "fixture-upstream",
"version": "$PACKAGE_VERSION"
}
EOF
cat > "$repo/.gitignore" <<'EOF'
.private-journal/
EOF
if [[ "$with_pure_ignored" == "1" ]]; then
cat >> "$repo/.gitignore" <<'EOF'
ignored-cache/
EOF
fi
cat > "$repo/.codex-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/assets/superpowers-small.svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>
EOF
printf 'png fixture\n' > "$repo/assets/app-icon.png"
cat > "$repo/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
printf 'tracked keep\n' > "$repo/.private-journal/keep.txt"
printf 'ignored leak\n' > "$repo/.private-journal/leak.txt"
if [[ "$with_pure_ignored" == "1" ]]; then
printf 'ignored cache state\n' > "$repo/ignored-cache/tmp/state.json"
fi
git -C "$repo" add \
.codex-plugin/plugin.json \
.gitignore \
assets/app-icon.png \
assets/superpowers-small.svg \
package.json \
scripts/sync-to-codex-plugin.sh \
skills/example/SKILL.md
git -C "$repo" add -f .private-journal/keep.txt
commit_fixture "$repo" "Initial upstream fixture"
}
write_destination_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/skills/example"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
git -C "$repo" add plugins/superpowers/.fixture-keep
git -C "$repo" add plugins/superpowers/skills/example/SKILL.md
commit_fixture "$repo" "Initial destination fixture"
}
dirty_tracked_destination_skill() {
local repo="$1"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Locally modified fixture content.
EOF
}
write_synced_destination_fixture() {
local repo="$1"
mkdir -p \
"$repo/plugins/superpowers/.codex-plugin" \
"$repo/plugins/superpowers/.private-journal" \
"$repo/plugins/superpowers/assets" \
"$repo/plugins/superpowers/skills/example"
cat > "$repo/plugins/superpowers/.codex-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/plugins/superpowers/assets/superpowers-small.svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>
EOF
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
printf 'tracked keep\n' > "$repo/plugins/superpowers/.private-journal/keep.txt"
git -C "$repo" add \
plugins/superpowers/.codex-plugin/plugin.json \
plugins/superpowers/assets/app-icon.png \
plugins/superpowers/assets/superpowers-small.svg \
plugins/superpowers/skills/example/SKILL.md \
plugins/superpowers/.private-journal/keep.txt
commit_fixture "$repo" "Initial synced destination fixture"
}
write_stale_ignored_destination_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/.private-journal"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
git -C "$repo" add plugins/superpowers/.fixture-keep
commit_fixture "$repo" "Initial stale ignored destination fixture"
}
write_fake_gh() {
local bin_dir="$1"
mkdir -p "$bin_dir"
cat > "$bin_dir/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "auth" && "${2:-}" == "status" ]]; then
exit 0
fi
echo "unexpected gh invocation: $*" >&2
exit 1
EOF
chmod +x "$bin_dir/gh"
}
run_preview() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_bootstrap_preview() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --bootstrap --local "$dest" 2>&1
}
run_preview_without_manifest() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
rm -f "$upstream/.codex-plugin/plugin.json"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_preview_with_stale_ignored_destination() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_apply() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
}
run_help() {
local upstream="$1"
local fake_bin="$2"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" --help 2>&1
}
write_bootstrap_destination_fixture() {
local repo="$1"
printf 'bootstrap fixture\n' > "$repo/README.md"
git -C "$repo" add README.md
commit_fixture "$repo" "Initial bootstrap destination fixture"
}
main() {
local upstream
local mixed_only_upstream
local dest
local dest_branch
local mixed_only_dest
local stale_dest
local dirty_apply_dest
local dirty_apply_dest_branch
local noop_apply_dest
local noop_apply_dest_branch
local fake_bin
local bootstrap_dest
local bootstrap_dest_branch
local preview_status
local preview_output
local preview_section
local bootstrap_status
local bootstrap_output
local missing_manifest_status
local missing_manifest_output
local mixed_only_status
local mixed_only_output
local stale_preview_status
local stale_preview_output
local stale_preview_section
local dirty_apply_status
local dirty_apply_output
local noop_apply_status
local noop_apply_output
local help_output
local script_source
local dirty_skill_path
echo "=== Test: sync-to-codex-plugin dry-run regression ==="
TEST_ROOT="$(mktemp -d)"
trap cleanup EXIT
upstream="$TEST_ROOT/upstream"
mixed_only_upstream="$TEST_ROOT/mixed-only-upstream"
dest="$TEST_ROOT/destination"
mixed_only_dest="$TEST_ROOT/mixed-only-destination"
stale_dest="$TEST_ROOT/stale-destination"
dirty_apply_dest="$TEST_ROOT/dirty-apply-destination"
dirty_apply_dest_branch="fixture/dirty-apply-target"
noop_apply_dest="$TEST_ROOT/noop-apply-destination"
noop_apply_dest_branch="fixture/noop-apply-target"
bootstrap_dest="$TEST_ROOT/bootstrap-destination"
dest_branch="fixture/preview-target"
bootstrap_dest_branch="fixture/bootstrap-preview-target"
fake_bin="$TEST_ROOT/bin"
init_repo "$upstream"
write_upstream_fixture "$upstream"
init_repo "$mixed_only_upstream"
write_upstream_fixture "$mixed_only_upstream" 0
init_repo "$dest"
write_destination_fixture "$dest"
checkout_fixture_branch "$dest" "$dest_branch"
dirty_tracked_destination_skill "$dest"
init_repo "$mixed_only_dest"
write_destination_fixture "$mixed_only_dest"
init_repo "$stale_dest"
write_stale_ignored_destination_fixture "$stale_dest"
init_repo "$dirty_apply_dest"
write_synced_destination_fixture "$dirty_apply_dest"
checkout_fixture_branch "$dirty_apply_dest" "$dirty_apply_dest_branch"
dirty_tracked_destination_skill "$dirty_apply_dest"
init_repo "$noop_apply_dest"
write_synced_destination_fixture "$noop_apply_dest"
checkout_fixture_branch "$noop_apply_dest" "$noop_apply_dest_branch"
init_repo "$bootstrap_dest"
write_bootstrap_destination_fixture "$bootstrap_dest"
checkout_fixture_branch "$bootstrap_dest" "$bootstrap_dest_branch"
write_fake_gh "$fake_bin"
# This regression test is about dry-run content, so capture the preview
# output even if the current script exits nonzero in --local mode.
set +e
preview_output="$(run_preview "$upstream" "$dest" "$fake_bin")"
preview_status=$?
bootstrap_output="$(run_bootstrap_preview "$upstream" "$bootstrap_dest" "$fake_bin")"
bootstrap_status=$?
mixed_only_output="$(run_preview "$mixed_only_upstream" "$mixed_only_dest" "$fake_bin")"
mixed_only_status=$?
stale_preview_output="$(run_preview_with_stale_ignored_destination "$upstream" "$stale_dest" "$fake_bin")"
stale_preview_status=$?
dirty_apply_output="$(run_apply "$upstream" "$dirty_apply_dest" "$fake_bin")"
dirty_apply_status=$?
noop_apply_output="$(run_apply "$upstream" "$noop_apply_dest" "$fake_bin")"
noop_apply_status=$?
missing_manifest_output="$(run_preview_without_manifest "$upstream" "$dest" "$fake_bin")"
missing_manifest_status=$?
set -e
help_output="$(run_help "$upstream" "$fake_bin")"
script_source="$(cat "$upstream/scripts/sync-to-codex-plugin.sh")"
preview_section="$(printf '%s\n' "$preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
stale_preview_section="$(printf '%s\n' "$stale_preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
dirty_skill_path="$dirty_apply_dest/plugins/superpowers/skills/example/SKILL.md"
echo ""
echo "Preview assertions..."
assert_equals "$preview_status" "0" "Preview exits successfully"
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" ".private-journal/keep.txt" "Preview includes tracked ignored file"
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"
assert_not_contains "$preview_output" "Overlay file (.codex-plugin/plugin.json) will be regenerated" "Preview omits overlay regeneration note"
assert_not_contains "$preview_output" "Assets (superpowers-small.svg, app-icon.png) will be seeded from" "Preview omits assets seeding note"
assert_contains "$preview_section" "skills/example/SKILL.md" "Preview reflects dirty tracked destination file"
assert_current_branch "$dest" "$dest_branch" "Preview leaves destination checkout on its original branch"
assert_branch_absent "$dest" "sync/superpowers-*" "Preview does not create sync branch in destination checkout"
echo ""
echo "Mixed-directory assertions..."
assert_equals "$mixed_only_status" "0" "Mixed ignored directory preview exits successfully under /bin/bash"
assert_contains "$mixed_only_output" ".private-journal/keep.txt" "Mixed ignored directory preview still includes tracked ignored file"
assert_not_contains "$mixed_only_output" "ignored-cache/" "Mixed ignored directory preview has no pure ignored directory fixture"
echo ""
echo "Convergence assertions..."
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
echo ""
echo "Bootstrap assertions..."
assert_equals "$bootstrap_status" "0" "Bootstrap preview exits successfully"
assert_contains "$bootstrap_output" "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)" "Bootstrap preview describes directory creation"
assert_not_contains "$bootstrap_output" "Assets:" "Bootstrap preview omits external assets path"
assert_contains "$bootstrap_output" "Dry run only. Nothing was changed or pushed." "Bootstrap preview remains dry-run only"
assert_path_absent "$bootstrap_dest/plugins/superpowers" "Bootstrap preview does not create destination plugin directory"
assert_current_branch "$bootstrap_dest" "$bootstrap_dest_branch" "Bootstrap preview leaves destination checkout on its original branch"
assert_branch_absent "$bootstrap_dest" "bootstrap/superpowers-*" "Bootstrap preview does not create bootstrap branch in destination checkout"
echo ""
echo "Apply assertions..."
assert_equals "$dirty_apply_status" "1" "Dirty local apply exits with failure"
assert_contains "$dirty_apply_output" "ERROR: local checkout has uncommitted changes under 'plugins/superpowers'" "Dirty local apply reports protected destination path"
assert_current_branch "$dirty_apply_dest" "$dirty_apply_dest_branch" "Dirty local apply leaves destination checkout on its original branch"
assert_branch_absent "$dirty_apply_dest" "sync/superpowers-*" "Dirty local apply does not create sync branch in destination checkout"
assert_file_equals "$dirty_skill_path" "# Example Skill
Locally modified fixture content." "Dirty local apply preserves tracked working-tree file content"
assert_equals "$noop_apply_status" "0" "Clean no-op local apply exits successfully"
assert_contains "$noop_apply_output" "No changes — embedded plugin was already in sync with upstream" "Clean no-op local apply reports no changes"
assert_current_branch "$noop_apply_dest" "$noop_apply_dest_branch" "Clean no-op local apply leaves destination checkout on its original branch"
assert_branch_absent "$noop_apply_dest" "sync/superpowers-*" "Clean no-op local apply does not create sync branch in destination checkout"
echo ""
echo "Missing manifest assertions..."
assert_equals "$missing_manifest_status" "1" "Missing manifest exits with failure"
assert_contains "$missing_manifest_output" "ERROR: committed Codex manifest missing at" "Missing manifest reports committed manifest path"
echo ""
echo "Help assertions..."
assert_not_contains "$help_output" "--assets-src" "Help omits --assets-src"
echo ""
echo "Source assertions..."
assert_not_contains "$script_source" "regenerated inline" "Source drops regenerated inline phrasing"
assert_not_contains "$script_source" "Brand Assets directory" "Source drops Brand Assets directory phrasing"
assert_not_contains "$script_source" "--assets-src" "Source drops --assets-src"
if [[ $FAILURES -ne 0 ]]; then
echo ""
echo "FAILED: $FAILURES assertion(s) failed."
exit 1
fi
echo ""
echo "PASS"
}
main "$@"