From 8c8c5e87ce7baa85ad59d5bffc6b901c32b7badc Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 12:03:59 -0700 Subject: [PATCH 1/7] adds tooling to mirror superpowers as a codex plugin with the appropriate metadata changes --- scripts/sync-to-codex-plugin.sh | 212 ++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100755 scripts/sync-to-codex-plugin.sh diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh new file mode 100755 index 00000000..bc66c9f6 --- /dev/null +++ b/scripts/sync-to-codex-plugin.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# +# sync-to-codex-plugin.sh +# +# Syncs this superpowers checkout into a Codex plugin mirror directory. +# Pulls every file except the EXCLUDES list, never touches the PROTECTS list. +# Leaves changes unstaged in the destination so a human can review before committing. +# +# Usage: +# ./scripts/sync-to-codex-plugin.sh # sync with confirmation +# ./scripts/sync-to-codex-plugin.sh -n # dry run, show changes only +# ./scripts/sync-to-codex-plugin.sh -y # skip confirmation prompt +# ./scripts/sync-to-codex-plugin.sh --dest /path/to/plugins/superpowers +# +# Environment: +# CODEX_PLUGIN_DEST Destination plugin path (default: sibling openai-codex-plugins checkout) + +set -euo pipefail + +# ============================================================================= +# Config — edit these lists as the upstream or canonical shape evolves +# ============================================================================= + +# Paths in upstream that should NOT land in the embedded plugin. +# Rsync --exclude patterns (trailing slash = directory). +EXCLUDES=( + # Dotfiles and infra + ".claude/" + ".claude-plugin/" + ".codex/" + ".cursor-plugin/" + ".git/" + ".gitattributes" + ".github/" + ".gitignore" + ".opencode/" + ".version-bump.json" + ".worktrees/" + ".DS_Store" + + # Root ceremony files (not part of a canonical Codex plugin) + "AGENTS.md" + "CHANGELOG.md" + "CLAUDE.md" + "CODE_OF_CONDUCT.md" + "GEMINI.md" + "RELEASE-NOTES.md" + "gemini-extension.json" + "package.json" + + # Directories not shipped by canonical Codex plugins + "commands/" + "docs/" + "hooks/" + "lib/" + "scripts/" + "tests/" + "tmp/" +) + +# Paths in the destination that are hand-authored Codex overlays. +# Rsync will never touch these — including when --delete would otherwise +# remove them because they don't exist in upstream. +PROTECTS=( + ".codex-plugin/" + "agents/openai.yaml" +) + +# ============================================================================= +# Paths +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Default dest: sibling openai-codex-plugins checkout, if it exists +DEFAULT_DEST="${CODEX_PLUGIN_DEST:-$(dirname "$UPSTREAM")/openai-codex-plugins/plugins/superpowers}" + +# ============================================================================= +# Args +# ============================================================================= + +DEST="$DEFAULT_DEST" +DRY_RUN=0 +YES=0 + +usage() { + sed -n 's/^# \{0,1\}//;2,20p' "$0" + exit "${1:-0}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --dest) DEST="$2"; shift 2 ;; + -n|--dry-run) DRY_RUN=1; shift ;; + -y|--yes) YES=1; shift ;; + -h|--help) usage 0 ;; + *) echo "Unknown arg: $1" >&2; usage 2 ;; + esac +done + +# ============================================================================= +# Validate environment +# ============================================================================= + +if [[ ! -d "$UPSTREAM/.git" ]]; then + echo "ERROR: Upstream '$UPSTREAM' is not a git checkout." >&2 + exit 1 +fi + +if [[ ! -d "$DEST" ]]; then + echo "ERROR: Destination '$DEST' does not exist." >&2 + echo "Set CODEX_PLUGIN_DEST or pass --dest ." >&2 + exit 1 +fi + +confirm() { + local prompt="$1" + [[ $YES -eq 1 ]] && return 0 + read -rp "$prompt [y/N] " ans + [[ "$ans" == "y" || "$ans" == "Y" ]] +} + +# Check upstream branch +UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)" +UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)" +UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)" + +if [[ "$UPSTREAM_BRANCH" != "main" ]]; then + echo "WARNING: Upstream is on branch '$UPSTREAM_BRANCH', not 'main'." + confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1 +fi + +# Check upstream working tree is clean +UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)" +if [[ -n "$UPSTREAM_STATUS" ]]; then + echo "WARNING: Upstream has uncommitted changes:" + echo "$UPSTREAM_STATUS" | sed 's/^/ /' + echo "Sync will use the working-tree state, not HEAD ($UPSTREAM_SHORT)." + confirm "Continue anyway?" || exit 1 +fi + +# ============================================================================= +# Build rsync args +# ============================================================================= + +RSYNC_ARGS=(-av --delete) + +for pat in "${EXCLUDES[@]}"; do + RSYNC_ARGS+=(--exclude="$pat") +done + +for pat in "${PROTECTS[@]}"; do + RSYNC_ARGS+=(--filter="protect $pat") +done + +# ============================================================================= +# Dry run first, always +# ============================================================================= + +echo "" +echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)" +echo "Dest: $DEST" +echo "" +echo "=== Preview (rsync --dry-run) ===" +rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/" +echo "=== End preview ===" + +if [[ $DRY_RUN -eq 1 ]]; then + echo "" + echo "Dry run only. Nothing was changed." + exit 0 +fi + +# ============================================================================= +# Apply +# ============================================================================= + +echo "" +confirm "Apply these changes?" || { echo "Aborted."; exit 1; } + +echo "" +echo "Syncing..." +rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" +echo "Done." +echo "" + +# ============================================================================= +# Report +# ============================================================================= + +DEST_GIT_ROOT="$(cd "$DEST" && git rev-parse --show-toplevel 2>/dev/null || echo "")" +if [[ -n "$DEST_GIT_ROOT" ]]; then + DEST_REL="${DEST#$DEST_GIT_ROOT/}" + CHANGES="$(cd "$DEST_GIT_ROOT" && git status --porcelain "$DEST_REL")" + if [[ -z "$CHANGES" ]]; then + echo "No changes — destination was already in sync with upstream $UPSTREAM_SHORT." + exit 0 + fi + + echo "Changes pending review:" + echo "$CHANGES" | sed 's/^/ /' + echo "" + echo "Upstream SHA: $UPSTREAM_SHA" + echo "" + echo "Suggested commit message:" + echo " sync superpowers from upstream main @ $UPSTREAM_SHORT" + echo "" + echo "Review with: git -C $DEST_GIT_ROOT diff -- $DEST_REL" +else + echo "Destination is not a git checkout — cannot report changes." +fi From ac1c715ffb1563de58760d815ad10f52fcda0d6b Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 13:18:36 -0700 Subject: [PATCH 2/7] rewrites sync tool to clone the fork, open a PR, and regenerate overlays inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version was a local rsync helper that required a hand-maintained destination path. This rewrite makes it path/user-agnostic and gives every team member the same flow: - Clones prime-radiant-inc/openai-codex-plugins fresh into a temp dir per run (trap EXIT cleans up) - Auto-detects upstream from the script's own location - Preflight: rsync, git, gh auth, python3, upstream package.json - Reads upstream version from package.json and bakes it into the regenerated .codex-plugin/plugin.json, so version bumps flow through - Regenerates both overlay files (.codex-plugin/plugin.json and agents/openai.yaml) inline via heredoc — single source of truth - Pushes a sync/superpowers-- branch and opens a PR via gh pr create; prints PR URL and /files diff URL on completion - --dry-run, --yes, --base BRANCH, --local PATH flags for all the usual modes - Deterministic: two runs against the same upstream SHA produce PRs with identical diffs, so the tool itself can be sanity-checked by running twice Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/sync-to-codex-plugin.sh | 296 +++++++++++++++++++++++--------- 1 file changed, 212 insertions(+), 84 deletions(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index bc66c9f6..656b806f 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -2,27 +2,37 @@ # # sync-to-codex-plugin.sh # -# Syncs this superpowers checkout into a Codex plugin mirror directory. -# Pulls every file except the EXCLUDES list, never touches the PROTECTS list. -# Leaves changes unstaged in the destination so a human can review before committing. +# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins. +# Clones the fork fresh into a temp dir, rsyncs upstream content, regenerates +# the Codex overlay files (.codex-plugin/plugin.json + agents/openai.yaml) +# inline, commits, pushes a sync branch, and opens a PR. +# Path/user agnostic — auto-detects upstream from script location. +# +# Deterministic: running twice against the same upstream SHA produces PRs with +# identical diffs, so two back-to-back runs can verify the tool itself. # # Usage: -# ./scripts/sync-to-codex-plugin.sh # sync with confirmation -# ./scripts/sync-to-codex-plugin.sh -n # dry run, show changes only -# ./scripts/sync-to-codex-plugin.sh -y # skip confirmation prompt -# ./scripts/sync-to-codex-plugin.sh --dest /path/to/plugins/superpowers +# ./scripts/sync-to-codex-plugin.sh # full run with confirm +# ./scripts/sync-to-codex-plugin.sh -n # dry run, no clone/push/PR +# ./scripts/sync-to-codex-plugin.sh -y # skip confirmation +# ./scripts/sync-to-codex-plugin.sh --local PATH # use existing checkout +# ./scripts/sync-to-codex-plugin.sh --base BRANCH # target branch (default: main) # -# Environment: -# CODEX_PLUGIN_DEST Destination plugin path (default: sibling openai-codex-plugins checkout) +# Requires: bash, rsync, git, gh (authenticated), python3. set -euo pipefail # ============================================================================= -# Config — edit these lists as the upstream or canonical shape evolves +# Config — edit as upstream or canonical plugin shape evolves # ============================================================================= +FORK="prime-radiant-inc/openai-codex-plugins" +DEFAULT_BASE="main" +DEST_REL="plugins/superpowers" + # Paths in upstream that should NOT land in the embedded plugin. -# Rsync --exclude patterns (trailing slash = directory). +# Both the Codex-overlay files are here too — they're managed by the +# generate-overlays step, not by rsync. EXCLUDES=( # Dotfiles and infra ".claude/" @@ -38,7 +48,7 @@ EXCLUDES=( ".worktrees/" ".DS_Store" - # Root ceremony files (not part of a canonical Codex plugin) + # Root ceremony files "AGENTS.md" "CHANGELOG.md" "CLAUDE.md" @@ -56,33 +66,93 @@ EXCLUDES=( "scripts/" "tests/" "tmp/" -) -# Paths in the destination that are hand-authored Codex overlays. -# Rsync will never touch these — including when --delete would otherwise -# remove them because they don't exist in upstream. -PROTECTS=( + # Codex-overlay files — regenerated below, not synced ".codex-plugin/" "agents/openai.yaml" ) # ============================================================================= -# Paths +# Generated overlay files # ============================================================================= -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)" +# Writes the Codex plugin manifest to "$1" with the given upstream version. +# Args: dest_path, version +generate_plugin_json() { + local dest="$1" + local version="$2" + mkdir -p "$(dirname "$dest")" + cat > "$dest" < "$dest" <<'EOF' +interface: + display_name: "Superpowers" + short_description: "Planning, TDD, debugging, and delivery workflows for coding agents" + default_prompt: "Use Superpowers to brainstorm a design, write an implementation plan, run test-driven development, debug bugs systematically, or finish and ship a development branch." +EOF +} # ============================================================================= # Args # ============================================================================= -DEST="$DEFAULT_DEST" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)" +BASE="$DEFAULT_BASE" DRY_RUN=0 YES=0 +LOCAL_CHECKOUT="" usage() { sed -n 's/^# \{0,1\}//;2,20p' "$0" @@ -91,84 +161,122 @@ usage() { while [[ $# -gt 0 ]]; do case "$1" in - --dest) DEST="$2"; shift 2 ;; -n|--dry-run) DRY_RUN=1; shift ;; -y|--yes) YES=1; shift ;; + --local) LOCAL_CHECKOUT="$2"; shift 2 ;; + --base) BASE="$2"; shift 2 ;; -h|--help) usage 0 ;; *) echo "Unknown arg: $1" >&2; usage 2 ;; esac done # ============================================================================= -# Validate environment +# Preflight # ============================================================================= -if [[ ! -d "$UPSTREAM/.git" ]]; then - echo "ERROR: Upstream '$UPSTREAM' is not a git checkout." >&2 - exit 1 -fi +die() { echo "ERROR: $*" >&2; exit 1; } -if [[ ! -d "$DEST" ]]; then - echo "ERROR: Destination '$DEST' does not exist." >&2 - echo "Set CODEX_PLUGIN_DEST or pass --dest ." >&2 - exit 1 -fi +command -v rsync >/dev/null || die "rsync not found in PATH" +command -v git >/dev/null || die "git not found in PATH" +command -v gh >/dev/null || die "gh not found — install GitHub CLI" +command -v python3 >/dev/null || die "python3 not found in PATH" -confirm() { - local prompt="$1" - [[ $YES -eq 1 ]] && return 0 - read -rp "$prompt [y/N] " ans - [[ "$ans" == "y" || "$ans" == "Y" ]] -} +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" +[[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version" + +# 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" -# Check upstream branch UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)" UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)" UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)" +confirm() { + [[ $YES -eq 1 ]] && return 0 + read -rp "$1 [y/N] " ans + [[ "$ans" == "y" || "$ans" == "Y" ]] +} + if [[ "$UPSTREAM_BRANCH" != "main" ]]; then - echo "WARNING: Upstream is on branch '$UPSTREAM_BRANCH', not 'main'." + echo "WARNING: upstream is on '$UPSTREAM_BRANCH', not 'main'" confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1 fi -# Check upstream working tree is clean UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)" if [[ -n "$UPSTREAM_STATUS" ]]; then - echo "WARNING: Upstream has uncommitted changes:" + echo "WARNING: upstream has uncommitted changes:" echo "$UPSTREAM_STATUS" | sed 's/^/ /' - echo "Sync will use the working-tree state, not HEAD ($UPSTREAM_SHORT)." + echo "Sync will use working-tree state, not HEAD ($UPSTREAM_SHORT)." confirm "Continue anyway?" || exit 1 fi # ============================================================================= -# Build rsync args +# Prepare destination (clone fork fresh, or use --local) +# ============================================================================= + +CLEANUP_DIR="" +cleanup() { + [[ -n "$CLEANUP_DIR" ]] && rm -rf "$CLEANUP_DIR" +} +trap cleanup EXIT + +if [[ -n "$LOCAL_CHECKOUT" ]]; then + DEST_REPO="$(cd "$LOCAL_CHECKOUT" && pwd)" + [[ -d "$DEST_REPO/.git" ]] || die "--local path '$DEST_REPO' is not a git checkout" +else + echo "Cloning $FORK..." + CLEANUP_DIR="$(mktemp -d)" + DEST_REPO="$CLEANUP_DIR/openai-codex-plugins" + gh repo clone "$FORK" "$DEST_REPO" >/dev/null +fi + +DEST="$DEST_REPO/$DEST_REL" + +# Checkout base branch +cd "$DEST_REPO" +git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK" + +[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — merge the bootstrap PR first, or pass --base " + +# ============================================================================= +# Create sync branch +# ============================================================================= + +TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)" +SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" +git checkout -q -b "$SYNC_BRANCH" + +# ============================================================================= +# Build rsync args (excludes only — no protects, overlays are regenerated) # ============================================================================= RSYNC_ARGS=(-av --delete) - -for pat in "${EXCLUDES[@]}"; do - RSYNC_ARGS+=(--exclude="$pat") -done - -for pat in "${PROTECTS[@]}"; do - RSYNC_ARGS+=(--filter="protect $pat") -done +for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done # ============================================================================= -# Dry run first, always +# Dry run preview (always shown) # ============================================================================= echo "" echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)" -echo "Dest: $DEST" +echo "Version: $UPSTREAM_VERSION" +echo "Fork: $FORK" +echo "Base: $BASE" +echo "Branch: $SYNC_BRANCH" echo "" echo "=== Preview (rsync --dry-run) ===" rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/" echo "=== End preview ===" +echo "" +echo "Overlay files (.codex-plugin/plugin.json, agents/openai.yaml) will be" +echo "regenerated with version $UPSTREAM_VERSION regardless of rsync output." if [[ $DRY_RUN -eq 1 ]]; then echo "" - echo "Dry run only. Nothing was changed." + echo "Dry run only. Nothing was changed or pushed." exit 0 fi @@ -177,36 +285,56 @@ fi # ============================================================================= echo "" -confirm "Apply these changes?" || { echo "Aborted."; exit 1; } +confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; } echo "" -echo "Syncing..." +echo "Syncing upstream content..." rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" -echo "Done." -echo "" -# ============================================================================= -# Report -# ============================================================================= +echo "Regenerating overlay files..." +generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION" +generate_agents_openai_yaml "$DEST/agents/openai.yaml" -DEST_GIT_ROOT="$(cd "$DEST" && git rev-parse --show-toplevel 2>/dev/null || echo "")" -if [[ -n "$DEST_GIT_ROOT" ]]; then - DEST_REL="${DEST#$DEST_GIT_ROOT/}" - CHANGES="$(cd "$DEST_GIT_ROOT" && git status --porcelain "$DEST_REL")" - if [[ -z "$CHANGES" ]]; then - echo "No changes — destination was already in sync with upstream $UPSTREAM_SHORT." - exit 0 - fi - - echo "Changes pending review:" - echo "$CHANGES" | sed 's/^/ /' - echo "" - echo "Upstream SHA: $UPSTREAM_SHA" - echo "" - echo "Suggested commit message:" - echo " sync superpowers from upstream main @ $UPSTREAM_SHORT" - echo "" - echo "Review with: git -C $DEST_GIT_ROOT diff -- $DEST_REL" -else - echo "Destination is not a git checkout — cannot report changes." +# Bail early if nothing actually changed +cd "$DEST_REPO" +if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then + echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)." + exit 0 fi + +# ============================================================================= +# Commit, push, open PR +# ============================================================================= + +git add "$DEST_REL" +git commit --quiet -m "sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT + +Automated sync via scripts/sync-to-codex-plugin.sh +Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA +Branch: $SYNC_BRANCH" + +echo "Pushing $SYNC_BRANCH to $FORK..." +git push -u origin "$SYNC_BRANCH" --quiet + +PR_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" +PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION). + +Run via: \`scripts/sync-to-codex-plugin.sh\` +Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA + +Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving." + +echo "Opening PR..." +PR_URL="$(gh pr create \ + --repo "$FORK" \ + --base "$BASE" \ + --head "$SYNC_BRANCH" \ + --title "$PR_TITLE" \ + --body "$PR_BODY")" + +PR_NUM="${PR_URL##*/}" +DIFF_URL="https://github.com/$FORK/pull/$PR_NUM/files" + +echo "" +echo "PR opened: $PR_URL" +echo "Diff view: $DIFF_URL" From da283df0582dcf55257b93340c3e432e3c88769f Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 13:23:17 -0700 Subject: [PATCH 3/7] remove things we dont need --- scripts/sync-to-codex-plugin.sh | 35 ++++++++++----------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index 656b806f..8c478048 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -4,8 +4,8 @@ # # Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins. # Clones the fork fresh into a temp dir, rsyncs upstream content, regenerates -# the Codex overlay files (.codex-plugin/plugin.json + agents/openai.yaml) -# inline, commits, pushes a sync branch, and opens a PR. +# the Codex overlay file (.codex-plugin/plugin.json) inline, commits, pushes a +# sync branch, and opens a PR. # Path/user agnostic — auto-detects upstream from script location. # # Deterministic: running twice against the same upstream SHA produces PRs with @@ -31,8 +31,8 @@ DEFAULT_BASE="main" DEST_REL="plugins/superpowers" # Paths in upstream that should NOT land in the embedded plugin. -# Both the Codex-overlay files are here too — they're managed by the -# generate-overlays step, not by rsync. +# The Codex-overlay file is here too — it's managed by the generate step, +# not by rsync. EXCLUDES=( # Dotfiles and infra ".claude/" @@ -67,13 +67,12 @@ EXCLUDES=( "tests/" "tmp/" - # Codex-overlay files — regenerated below, not synced + # Codex-overlay file — regenerated below, not synced ".codex-plugin/" - "agents/openai.yaml" ) # ============================================================================= -# Generated overlay files +# Generated overlay file # ============================================================================= # Writes the Codex plugin manifest to "$1" with the given upstream version. @@ -130,19 +129,6 @@ generate_plugin_json() { EOF } -# Writes the plugin-level agents/openai.yaml to "$1". -# Args: dest_path -generate_agents_openai_yaml() { - local dest="$1" - mkdir -p "$(dirname "$dest")" - cat > "$dest" <<'EOF' -interface: - display_name: "Superpowers" - short_description: "Planning, TDD, debugging, and delivery workflows for coding agents" - default_prompt: "Use Superpowers to brainstorm a design, write an implementation plan, run test-driven development, debug bugs systematically, or finish and ship a development branch." -EOF -} - # ============================================================================= # Args # ============================================================================= @@ -250,7 +236,7 @@ SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" git checkout -q -b "$SYNC_BRANCH" # ============================================================================= -# Build rsync args (excludes only — no protects, overlays are regenerated) +# Build rsync args (excludes only — overlay is regenerated separately) # ============================================================================= RSYNC_ARGS=(-av --delete) @@ -271,8 +257,8 @@ echo "=== Preview (rsync --dry-run) ===" rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/" echo "=== End preview ===" echo "" -echo "Overlay files (.codex-plugin/plugin.json, agents/openai.yaml) will be" -echo "regenerated with version $UPSTREAM_VERSION regardless of rsync output." +echo "Overlay file (.codex-plugin/plugin.json) will be regenerated with" +echo "version $UPSTREAM_VERSION regardless of rsync output." if [[ $DRY_RUN -eq 1 ]]; then echo "" @@ -291,9 +277,8 @@ echo "" echo "Syncing upstream content..." rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" -echo "Regenerating overlay files..." +echo "Regenerating overlay file..." generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION" -generate_agents_openai_yaml "$DEST/agents/openai.yaml" # Bail early if nothing actually changed cd "$DEST_REPO" From 777a9770d8c70cb1eab51f3a64dfa4d7fadbe951 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 13:27:59 -0700 Subject: [PATCH 4/7] sync-to-codex-plugin: mirror CODE_OF_CONDUCT.md, drop agents/openai.yaml overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove CODE_OF_CONDUCT.md from EXCLUDES so it syncs from upstream (per PR #1165 review feedback on the exclude list) - Remove the agents/openai.yaml overlay generator and its exclude entry — the file duplicates fields already in .codex-plugin/plugin.json and only 6 of 28 upstream plugins ship one, so we match the 22-plugin majority shape Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/sync-to-codex-plugin.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index 8c478048..323eb883 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -52,7 +52,6 @@ EXCLUDES=( "AGENTS.md" "CHANGELOG.md" "CLAUDE.md" - "CODE_OF_CONDUCT.md" "GEMINI.md" "RELEASE-NOTES.md" "gemini-extension.json" From 6149f3635ada80182895d586d803c48124524d7d Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 13:48:05 -0700 Subject: [PATCH 5/7] sync-to-codex-plugin: align plugin.json heredoc with current live shape The live .codex-plugin/plugin.json in the downstream fork was cleaned up (websiteURL, privacyPolicyURL, termsOfServiceURL, and defaultPrompt removed) and icon fields were added (composerIcon, logo pointing at assets/superpowers-small.svg and assets/app-icon.png). Update the heredoc to produce the same shape so future sync runs don't wipe the icon fields or reintroduce the removed URL fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/sync-to-codex-plugin.sh | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index 323eb883..e64b83d7 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -113,15 +113,9 @@ generate_plugin_json() { "Read", "Write" ], - "websiteURL": "https://github.com/obra/superpowers", - "privacyPolicyURL": "https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement", - "termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service", - "defaultPrompt": [ - "Use Superpowers to plan this feature before we code", - "Debug this bug with a systematic root-cause workflow", - "Turn this approved design into an implementation plan" - ], "brandColor": "#F59E0B", + "composerIcon": "./assets/superpowers-small.svg", + "logo": "./assets/app-icon.png", "screenshots": [] } } From bcdd7fa24cc0729897462179e073afccf888f7c9 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 13:59:26 -0700 Subject: [PATCH 6/7] sync-to-codex-plugin: exclude assets/, add --bootstrap flag Two coupled changes: 1. Add assets/ to EXCLUDES. A normal sync run was deleting plugins/superpowers/assets/ via --delete because the corresponding directory doesn't exist upstream. Confirmed via dry-run that the previous version would wipe both brand asset files on next sync. 2. Add --bootstrap and --assets-src flags to support creating the initial plugin PR from scratch. Bootstrap mode skips the "plugin must exist on base" preflight, creates the plugin directory, rsyncs upstream content, then copies PrimeRadiant_Favicon.{svg,png} from --assets-src into plugins/superpowers/assets/ as superpowers-small.svg and app-icon.png. Run once by one team member to open the initial PR; every subsequent run is a normal (non-bootstrap) sync. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/sync-to-codex-plugin.sh | 118 ++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 30 deletions(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index e64b83d7..b3dbd1ab 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -12,11 +12,18 @@ # identical diffs, so two back-to-back runs can verify the tool itself. # # Usage: -# ./scripts/sync-to-codex-plugin.sh # full run with confirm -# ./scripts/sync-to-codex-plugin.sh -n # dry run, no clone/push/PR -# ./scripts/sync-to-codex-plugin.sh -y # skip confirmation -# ./scripts/sync-to-codex-plugin.sh --local PATH # use existing checkout -# ./scripts/sync-to-codex-plugin.sh --base BRANCH # target branch (default: main) +# ./scripts/sync-to-codex-plugin.sh # full run +# ./scripts/sync-to-codex-plugin.sh -n # dry run +# ./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 --base BRANCH # default: main +# ./scripts/sync-to-codex-plugin.sh --bootstrap --assets-src DIR # create initial plugin +# +# Bootstrap mode: skips the "plugin must exist on base" check and seeds +# plugins/superpowers/assets/ from --assets-src which must contain +# 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. @@ -31,8 +38,8 @@ DEFAULT_BASE="main" DEST_REL="plugins/superpowers" # Paths in upstream that should NOT land in the embedded plugin. -# The Codex-overlay file is here too — it's managed by the generate step, -# not by rsync. +# The Codex-only paths are here too — they're managed by generate/bootstrap +# steps, not by rsync. EXCLUDES=( # Dotfiles and infra ".claude/" @@ -66,8 +73,9 @@ EXCLUDES=( "tests/" "tmp/" - # Codex-overlay file — regenerated below, not synced + # Codex-only paths — managed outside rsync ".codex-plugin/" + "assets/" ) # ============================================================================= @@ -132,20 +140,24 @@ BASE="$DEFAULT_BASE" DRY_RUN=0 YES=0 LOCAL_CHECKOUT="" +BOOTSTRAP=0 +ASSETS_SRC="" usage() { - sed -n 's/^# \{0,1\}//;2,20p' "$0" + sed -n 's/^# \{0,1\}//;2,27p' "$0" exit "${1:-0}" } while [[ $# -gt 0 ]]; do case "$1" in - -n|--dry-run) DRY_RUN=1; shift ;; - -y|--yes) YES=1; shift ;; - --local) LOCAL_CHECKOUT="$2"; shift 2 ;; - --base) BASE="$2"; shift 2 ;; - -h|--help) usage 0 ;; - *) echo "Unknown arg: $1" >&2; usage 2 ;; + -n|--dry-run) DRY_RUN=1; shift ;; + -y|--yes) YES=1; shift ;; + --local) LOCAL_CHECKOUT="$2"; shift 2 ;; + --base) BASE="$2"; shift 2 ;; + --bootstrap) BOOTSTRAP=1; shift ;; + --assets-src) ASSETS_SRC="$2"; shift 2 ;; + -h|--help) usage 0 ;; + *) echo "Unknown arg: $1" >&2; usage 2 ;; esac done @@ -162,8 +174,16 @@ 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'" -[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout" -[[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version" +[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout" +[[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version" + +# Bootstrap-mode validation +if [[ $BOOTSTRAP -eq 1 ]]; then + [[ -n "$ASSETS_SRC" ]] || die "--bootstrap requires --assets-src " + 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")" @@ -218,18 +238,28 @@ DEST="$DEST_REPO/$DEST_REL" cd "$DEST_REPO" git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK" -[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — merge the bootstrap PR first, or pass --base " +# Plugin-existence check depends on mode +if [[ $BOOTSTRAP -eq 1 ]]; then + [[ ! -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 " +fi # ============================================================================= # Create sync branch # ============================================================================= TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)" -SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" +if [[ $BOOTSTRAP -eq 1 ]]; then + SYNC_BRANCH="bootstrap/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" +else + SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" +fi git checkout -q -b "$SYNC_BRANCH" # ============================================================================= -# Build rsync args (excludes only — overlay is regenerated separately) +# Build rsync args # ============================================================================= RSYNC_ARGS=(-av --delete) @@ -245,6 +275,10 @@ echo "Version: $UPSTREAM_VERSION" echo "Fork: $FORK" echo "Base: $BASE" echo "Branch: $SYNC_BRANCH" +if [[ $BOOTSTRAP -eq 1 ]]; then + echo "Mode: BOOTSTRAP (creating initial plugin from scratch)" + echo "Assets: $ASSETS_SRC" +fi echo "" echo "=== Preview (rsync --dry-run) ===" rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/" @@ -252,6 +286,10 @@ echo "=== End preview ===" 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 echo "" @@ -270,6 +308,13 @@ echo "" echo "Syncing upstream content..." 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" @@ -285,7 +330,28 @@ fi # ============================================================================= git add "$DEST_REL" -git commit --quiet -m "sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT + +if [[ $BOOTSTRAP -eq 1 ]]; then + 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). + +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 --assets-src \` +Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA + +This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs and will not touch the \`assets/\` directory." +else + 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). + +Run via: \`scripts/sync-to-codex-plugin.sh\` +Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA + +Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving." +fi + +git commit --quiet -m "$COMMIT_TITLE Automated sync via scripts/sync-to-codex-plugin.sh Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA @@ -294,20 +360,12 @@ Branch: $SYNC_BRANCH" echo "Pushing $SYNC_BRANCH to $FORK..." git push -u origin "$SYNC_BRANCH" --quiet -PR_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" -PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION). - -Run via: \`scripts/sync-to-codex-plugin.sh\` -Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA - -Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving." - echo "Opening PR..." PR_URL="$(gh pr create \ --repo "$FORK" \ --base "$BASE" \ --head "$SYNC_BRANCH" \ - --title "$PR_TITLE" \ + --title "$COMMIT_TITLE" \ --body "$PR_BODY")" PR_NUM="${PR_URL##*/}" From bc25777c6a144f6b2f595cf0de12b20d2d97ffc0 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 14:03:56 -0700 Subject: [PATCH 7/7] sync-to-codex-plugin: anchor EXCLUDES patterns to source root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rsync exclude patterns without a leading "/" match any directory of the given name at any depth. The previous "scripts/" pattern was meant to exclude upstream's top-level scripts/ dir (which contains sync-to-codex-plugin.sh itself, bump-version.sh, etc.) but also incorrectly excluded skills/brainstorming/scripts/ — a legitimate skill-adjacent dir with 5 files (frame-template.html, helper.js, server.cjs, start-server.sh, stop-server.sh). Found during a determinism check: comparing the hand-crafted add-superpowers-plugin bootstrap PR against an automated bootstrap PR produced a diff showing those 5 files were missing from the automated version. Fix: anchor every top-level-only exclude with a leading "/". .DS_Store stays unanchored because Finder creates them anywhere. This also prevents future drift if anyone adds a tests/, hooks/, docs/, lib/, etc. subdir inside a skill. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/sync-to-codex-plugin.sh | 62 ++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index b3dbd1ab..c57eeb82 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -40,42 +40,48 @@ DEST_REL="plugins/superpowers" # 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. +# Unanchored patterns like "scripts/" would match any directory named +# "scripts" at any depth — including legitimate nested dirs like +# skills/brainstorming/scripts/. Anchoring prevents that. +# (.DS_Store is intentionally unanchored — Finder creates them everywhere.) EXCLUDES=( - # Dotfiles and infra - ".claude/" - ".claude-plugin/" - ".codex/" - ".cursor-plugin/" - ".git/" - ".gitattributes" - ".github/" - ".gitignore" - ".opencode/" - ".version-bump.json" - ".worktrees/" + # Dotfiles and infra — top-level only + "/.claude/" + "/.claude-plugin/" + "/.codex/" + "/.cursor-plugin/" + "/.git/" + "/.gitattributes" + "/.github/" + "/.gitignore" + "/.opencode/" + "/.version-bump.json" + "/.worktrees/" ".DS_Store" # Root ceremony files - "AGENTS.md" - "CHANGELOG.md" - "CLAUDE.md" - "GEMINI.md" - "RELEASE-NOTES.md" - "gemini-extension.json" - "package.json" + "/AGENTS.md" + "/CHANGELOG.md" + "/CLAUDE.md" + "/GEMINI.md" + "/RELEASE-NOTES.md" + "/gemini-extension.json" + "/package.json" # Directories not shipped by canonical Codex plugins - "commands/" - "docs/" - "hooks/" - "lib/" - "scripts/" - "tests/" - "tmp/" + "/commands/" + "/docs/" + "/hooks/" + "/lib/" + "/scripts/" + "/tests/" + "/tmp/" # Codex-only paths — managed outside rsync - ".codex-plugin/" - "assets/" + "/.codex-plugin/" + "/assets/" ) # =============================================================================