From 74a0c004eb2b0b2bb9c4e71aba4f3f6319b9d2aa Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 12:30:35 -0700 Subject: [PATCH 01/46] docs: add Codex App compatibility design spec (PRI-823) Design for making using-git-worktrees, finishing-a-development-branch, and subagent-driven-development skills work in the Codex App's sandboxed worktree environment. Read-only environment detection via git-dir vs git-common-dir comparison, ~48 lines across 4 files, zero breaking changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...26-03-23-codex-app-compatibility-design.md | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md diff --git a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md new file mode 100644 index 00000000..4ed559ac --- /dev/null +++ b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md @@ -0,0 +1,193 @@ +# Codex App Compatibility: Worktree and Finishing Skill Adaptation + +Make superpowers skills work in the Codex App's sandboxed worktree environment without breaking existing Claude Code or Codex CLI behavior. + +**Ticket:** PRI-823 + +## Motivation + +The Codex App runs agents inside git worktrees it manages — detached HEAD, located under `$CODEX_HOME/worktrees/`, with a Seatbelt sandbox that blocks `git checkout -b`, `git push`, and network access. Three superpowers skills assume unrestricted git access: `using-git-worktrees` creates manual worktrees with named branches, `finishing-a-development-branch` merges/pushes/PRs by branch name, and `subagent-driven-development` requires both. + +The Codex CLI (open source terminal tool) does NOT have this conflict — it has no built-in worktree management. Our manual worktree approach fills an isolation gap there. The problem is specifically with the Codex App. + +## Empirical Findings + +Tested in the Codex App on 2026-03-23: + +| Operation | workspace-write sandbox | Full access sandbox | +|---|---|---| +| `git add` | Works | Works | +| `git commit` | Works | Works | +| `git checkout -b` | **Blocked** (can't write `.git/refs/heads/`) | Works | +| `git push` | **Blocked** (network + `.git/refs/remotes/`) | Works | +| `gh pr create` | **Blocked** (network) | Works | +| `git status/diff/log` | Works | Works | + +Additional findings: +- `spawn_agent` subagents **share** the parent thread's filesystem (confirmed via marker file test) +- "Create branch" button appears in the App header regardless of which branch the worktree was started from +- The App's native finishing flow: Create branch → Commit modal → Commit and push / Commit and create PR +- `network_access = true` config is silently broken on macOS (issue #10390) + +## Design: Read-Only Environment Detection + +Three read-only git commands detect the environment without side effects: + +```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +``` + +Two signals derived: + +- **IN_LINKED_WORKTREE:** `GIT_DIR != GIT_COMMON` — the agent is in a worktree created by something else (Codex App, Claude Code Agent tool, previous skill run, or the user) +- **ON_DETACHED_HEAD:** `BRANCH` is empty — no named branch exists + +Why `git-dir != git-common-dir` instead of checking `show-toplevel`: +- In a normal repo, both resolve to the same `.git` directory +- In a linked worktree, `git-dir` is `.git/worktrees/` while `git-common-dir` is `.git` +- In a submodule, both are equal — avoiding a false positive that `show-toplevel` would produce +- Resolving via `cd && pwd -P` handles the relative-path problem (`git-common-dir` returns `.git` relative in normal repos but absolute in worktrees) and symlinks (macOS `/tmp` → `/private/tmp`) + +### Decision Matrix + +| Linked Worktree? | Detached HEAD? | Environment | Action | +|---|---|---|---| +| No | No | Claude Code / Codex CLI / normal git | Full skill behavior (unchanged) | +| Yes | Yes | Codex App worktree (workspace-write) | Skip worktree creation; handoff payload at finish | +| Yes | No | Codex App (Full access) or manual worktree | Skip worktree creation; full finishing flow | +| No | Yes | Unusual (manual detached HEAD) | Create worktree normally; warn at finish | + +## Changes + +### 1. `using-git-worktrees/SKILL.md` — Add Step 0 (~12 lines) + +New section between "Overview" and "Directory Selection Process": + +**Step 0: Check if Already in an Isolated Workspace** + +Run the detection commands. If `GIT_DIR != GIT_COMMON`, skip worktree creation entirely. Instead: +1. Skip to Step 3 (Run Project Setup) — `npm install` etc. is idempotent, worth running for safety +2. Then Step 4 (Verify Clean Baseline) — run tests +3. Report with branch state: + - On a branch: "Already in an isolated workspace at `` on branch ``. Tests passing. Ready to implement." + - Detached HEAD: "Already in an isolated workspace at `` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement." + +If `GIT_DIR == GIT_COMMON`, proceed with the full worktree creation flow (unchanged). + +Safety verification (.gitignore check) is skipped when Step 0 fires — irrelevant for externally-created worktrees. + +Update the Integration section description to note the skill ensures a verified workspace (creates one or verifies existing). + +**Everything else unchanged:** Directory Selection, Safety Verification, Creation Steps, Project Setup, Baseline Tests, Quick Reference, Common Mistakes, Red Flags. + +### 2. `finishing-a-development-branch/SKILL.md` — Add Step 1.5 + cleanup guard (~20 lines) + +**Step 1.5: Detect Environment** (after "Verify Tests", before "Present Options") + +Run the detection commands. Three paths: + +**Path A — Externally managed worktree + detached HEAD** (`GIT_DIR != GIT_COMMON` AND `BRANCH` empty): + +Do NOT present the 4-option menu. Emit a handoff payload: + +``` +Implementation complete. All tests passing. + +This workspace is externally managed (detached HEAD). +I cannot create branches, push, or open PRs from here. + +Next steps — use the host application's controls: +- "Create branch" — to name a branch, then commit/push/PR +- "Hand off to local" — to move changes to your local checkout + +Suggested branch name: +Suggested commit message: +``` + +Skip to Step 5 (cleanup is a no-op for externally managed worktrees). + +**Path B — Externally managed worktree + named branch** (`GIT_DIR != GIT_COMMON` AND `BRANCH` exists): + +Present the 4-option menu as normal. Mark worktree as externally managed for cleanup. + +**Path C — Normal environment** (`GIT_DIR == GIT_COMMON`): + +Present the 4-option menu as today (unchanged). + +**Step 5 cleanup guard:** + +If `using-git-worktrees` reported "Already in an isolated workspace" (externally managed), skip `git worktree remove`. The host environment owns this workspace. + +Otherwise, check and remove as today. + +**Everything else unchanged:** Options 1-4 logic, Quick Reference, Common Mistakes, Red Flags. + +### 3. `subagent-driven-development/SKILL.md` — 1 line edit + +Change Integration section line from: +``` +- superpowers:using-git-worktrees - REQUIRED: Set up isolated workspace before starting +``` +To: +``` +- superpowers:using-git-worktrees - REQUIRED: Ensures isolated workspace (creates one or verifies existing) +``` + +**Everything else unchanged:** Dispatch/review loop, prompt templates, model selection, status handling, red flags. + +### 4. `codex-tools.md` — Add environment detection docs (~15 lines) + +Two new sections at the end: + +**Environment Detection:** Documents the `git-dir != git-common-dir` pattern and what each signal means. Skills that create worktrees or finish branches should run this detection. + +**Codex App Finishing:** When the sandbox blocks branch/push operations, the agent commits all work and tells the user to use the App's "Create branch" or "Hand off to local" controls. The agent can still run tests, stage files, and suggest branch names, commit messages, and PR descriptions in its output. + +## What Does NOT Change + +- `implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md` — subagent prompts untouched +- `executing-plans/SKILL.md` — inherits behavior through `using-git-worktrees` and `finishing-a-development-branch` +- `dispatching-parallel-agents/SKILL.md` — no worktree or finishing operations +- `.codex/INSTALL.md` — installation process unchanged +- The 4-option finishing menu — preserved exactly for Claude Code and Codex CLI +- The full worktree creation flow — preserved exactly for non-worktree environments +- Subagent dispatch/review/iterate loop — unchanged (filesystem sharing confirmed) + +## Scope Summary + +| File | Change | +|---|---| +| `skills/using-git-worktrees/SKILL.md` | +12 lines (Step 0) | +| `skills/finishing-a-development-branch/SKILL.md` | +20 lines (Step 1.5 + cleanup guard) | +| `skills/subagent-driven-development/SKILL.md` | 1 line edit | +| `skills/using-superpowers/references/codex-tools.md` | +15 lines | + +~48 lines added/changed across 4 files. Zero new files. Zero breaking changes. + +## Future Considerations + +If a third skill needs the same detection pattern, extract it into a shared `references/environment-detection.md` file (Approach B). Not needed now — only 2 skills use it. + +## Test Plan + +### Automated (run in Claude Code after implementation) + +1. Normal repo detection — assert IN_LINKED_WORKTREE=false +2. Linked worktree detection — `git worktree add` test worktree, assert IN_LINKED_WORKTREE=true +3. Detached HEAD detection — `git checkout --detach`, assert ON_DETACHED_HEAD=true +4. Finishing skill handoff output — verify handoff message (not 4-option menu) in restricted environment + +### Manual Codex App Tests (4 tests) + +1. Detection in Worktree thread (workspace-write) — verify GIT_DIR != GIT_COMMON, empty branch +2. Detection in Worktree thread (Full access) — same detection, different sandbox behavior +3. Finishing skill handoff format — verify agent emits handoff payload, not 4-option menu +4. Full lifecycle — detection → commit → finishing detection → correct behavior → cleanup + +### Regression + +- Existing Claude Code skill-triggering tests still pass +- Existing subagent-driven-development integration tests still pass +- Normal Claude Code session: full worktree creation + 4-option finishing still works From 33e9bea3cce015dceaf56d1449e46dfca6e3b00a Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 12:33:15 -0700 Subject: [PATCH 02/46] docs: address spec review feedback for PRI-823 Fix three Important issues from spec review: - Clarify Step 1.5 placement relative to existing Steps 2/3 - Re-derive environment state at cleanup time instead of relying on earlier skill output - Acknowledge pre-existing Step 5 cleanup inconsistency Also: precise step references, exact codex-tools.md content, clearer Integration section update instructions. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...26-03-23-codex-app-compatibility-design.md | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md index 4ed559ac..90428554 100644 --- a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +++ b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md @@ -68,8 +68,8 @@ New section between "Overview" and "Directory Selection Process": **Step 0: Check if Already in an Isolated Workspace** Run the detection commands. If `GIT_DIR != GIT_COMMON`, skip worktree creation entirely. Instead: -1. Skip to Step 3 (Run Project Setup) — `npm install` etc. is idempotent, worth running for safety -2. Then Step 4 (Verify Clean Baseline) — run tests +1. Skip to "Run Project Setup" subsection under Creation Steps — `npm install` etc. is idempotent, worth running for safety +2. Then "Verify Clean Baseline" — run tests 3. Report with branch state: - On a branch: "Already in an isolated workspace at `` on branch ``. Tests passing. Ready to implement." - Detached HEAD: "Already in an isolated workspace at `` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement." @@ -78,16 +78,19 @@ If `GIT_DIR == GIT_COMMON`, proceed with the full worktree creation flow (unchan Safety verification (.gitignore check) is skipped when Step 0 fires — irrelevant for externally-created worktrees. -Update the Integration section description to note the skill ensures a verified workspace (creates one or verifies existing). +Update the Integration section's "Called by" entries. Change the description on each from context-specific text to: "Ensures isolated workspace (creates one or verifies existing)". For example, the `subagent-driven-development` entry changes from "REQUIRED: Set up isolated workspace before starting" to "REQUIRED: Ensures isolated workspace (creates one or verifies existing)". **Everything else unchanged:** Directory Selection, Safety Verification, Creation Steps, Project Setup, Baseline Tests, Quick Reference, Common Mistakes, Red Flags. ### 2. `finishing-a-development-branch/SKILL.md` — Add Step 1.5 + cleanup guard (~20 lines) -**Step 1.5: Detect Environment** (after "Verify Tests", before "Present Options") +**Step 1.5: Detect Environment** (after Step 1 "Verify Tests", before Step 2 "Determine Base Branch") Run the detection commands. Three paths: +- **Path A** skips Steps 2 and 3 entirely (no base branch or options needed). +- **Paths B and C** proceed through Step 2 (Determine Base Branch) and Step 3 (Present Options) as normal. + **Path A — Externally managed worktree + detached HEAD** (`GIT_DIR != GIT_COMMON` AND `BRANCH` empty): Do NOT present the 4-option menu. Emit a handoff payload: @@ -118,9 +121,9 @@ Present the 4-option menu as today (unchanged). **Step 5 cleanup guard:** -If `using-git-worktrees` reported "Already in an isolated workspace" (externally managed), skip `git worktree remove`. The host environment owns this workspace. +Re-run the `GIT_DIR` vs `GIT_COMMON` detection at cleanup time (do not rely on earlier skill output — the finishing skill may run in a different session). If `GIT_DIR != GIT_COMMON`, skip `git worktree remove` — the host environment owns this workspace. -Otherwise, check and remove as today. +Otherwise, check and remove as today. Note: the existing Step 5 text says "For Options 1, 2, 4" but the Quick Reference table and Common Mistakes section say "Options 1 & 4 only." The new guard is added before this existing logic and does not change which options trigger cleanup. **Everything else unchanged:** Options 1-4 logic, Quick Reference, Common Mistakes, Red Flags. @@ -141,9 +144,42 @@ To: Two new sections at the end: -**Environment Detection:** Documents the `git-dir != git-common-dir` pattern and what each signal means. Skills that create worktrees or finish branches should run this detection. +**Environment Detection:** -**Codex App Finishing:** When the sandbox blocks branch/push operations, the agent commits all work and tells the user to use the App's "Create branch" or "Hand off to local" controls. The agent can still run tests, stage files, and suggest branch names, commit messages, and PR descriptions in its output. +```markdown +## Environment Detection + +Skills that create worktrees or finish branches should detect their +environment with read-only git commands before proceeding: + +\```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +\``` + +- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation) +- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox) + +See `using-git-worktrees` Step 0 and `finishing-a-development-branch` +Step 1.5 for how each skill uses these signals. +``` + +**Codex App Finishing:** + +```markdown +## Codex App Finishing + +When the sandbox blocks branch/push operations (detached HEAD in an +externally managed worktree), the agent commits all work and informs +the user to use the App's native controls: + +- **"Create branch"** — names the branch, then commit/push/PR via App UI +- **"Hand off to local"** — transfers work to the user's local checkout + +The agent can still run tests, stage files, and output suggested branch +names, commit messages, and PR descriptions for the user to copy. +``` ## What Does NOT Change From c28b28ffbd8adc1725e3e25a022d1421e15cab80 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 12:38:25 -0700 Subject: [PATCH 03/46] docs: address team review feedback for PRI-823 spec - Add commit SHA + data loss warning to handoff payload (HIGH) - Add explicit commit step before handoff (HIGH) - Remove misleading "mark as externally managed" from Path B - Add executing-plans 1-line edit (was missing) - Add branch name derivation rules - Add conditional UI language for non-App environments - Add sandbox fallback for permission errors - Add STOP directive after Step 0 reporting Co-Authored-By: Claude Opus 4.6 (1M context) --- ...26-03-23-codex-app-compatibility-design.md | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md index 90428554..32a5c489 100644 --- a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +++ b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md @@ -80,6 +80,10 @@ Safety verification (.gitignore check) is skipped when Step 0 fires — irreleva Update the Integration section's "Called by" entries. Change the description on each from context-specific text to: "Ensures isolated workspace (creates one or verifies existing)". For example, the `subagent-driven-development` entry changes from "REQUIRED: Set up isolated workspace before starting" to "REQUIRED: Ensures isolated workspace (creates one or verifies existing)". +**Sandbox fallback:** If `GIT_DIR == GIT_COMMON` and the skill proceeds to Creation Steps, but `git worktree add -b` fails with a permission error (e.g., Seatbelt sandbox denial), treat this as a late-detected restricted environment. Fall back to the Step 0 "already in workspace" behavior — skip creation, run setup and baseline tests in the current directory, report accordingly. + +After reporting in Step 0, STOP. Do not continue to Directory Selection or Creation Steps. + **Everything else unchanged:** Directory Selection, Safety Verification, Creation Steps, Project Setup, Baseline Tests, Quick Reference, Common Mistakes, Red Flags. ### 2. `finishing-a-development-branch/SKILL.md` — Add Step 1.5 + cleanup guard (~20 lines) @@ -93,27 +97,35 @@ Run the detection commands. Three paths: **Path A — Externally managed worktree + detached HEAD** (`GIT_DIR != GIT_COMMON` AND `BRANCH` empty): -Do NOT present the 4-option menu. Emit a handoff payload: +First, ensure all work is staged and committed (`git add` + `git commit`). The Codex App's finishing controls operate on committed work. + +Then present this to the user (do NOT present the 4-option menu): ``` Implementation complete. All tests passing. +Current HEAD: This workspace is externally managed (detached HEAD). I cannot create branches, push, or open PRs from here. -Next steps — use the host application's controls: +⚠ These commits are on a detached HEAD. If you do not create a branch, +they may be lost when this workspace is cleaned up. + +If your host application provides these controls: - "Create branch" — to name a branch, then commit/push/PR - "Hand off to local" — to move changes to your local checkout -Suggested branch name: +Suggested branch name: Suggested commit message: ``` +Branch name derivation: use the ticket ID if available (e.g., `pri-823/codex-compat`), otherwise slugify the first 5 words of the plan title, otherwise omit the suggestion. Avoid including sensitive content (vulnerability descriptions, customer names) in branch names. + Skip to Step 5 (cleanup is a no-op for externally managed worktrees). **Path B — Externally managed worktree + named branch** (`GIT_DIR != GIT_COMMON` AND `BRANCH` exists): -Present the 4-option menu as normal. Mark worktree as externally managed for cleanup. +Present the 4-option menu as normal. (The Step 5 cleanup guard will re-detect the externally managed state independently.) **Path C — Normal environment** (`GIT_DIR == GIT_COMMON`): @@ -127,9 +139,9 @@ Otherwise, check and remove as today. Note: the existing Step 5 text says "For O **Everything else unchanged:** Options 1-4 logic, Quick Reference, Common Mistakes, Red Flags. -### 3. `subagent-driven-development/SKILL.md` — 1 line edit +### 3. `subagent-driven-development/SKILL.md` and `executing-plans/SKILL.md` — 1 line edit each -Change Integration section line from: +Both skills have an identical Integration section line. Change from: ``` - superpowers:using-git-worktrees - REQUIRED: Set up isolated workspace before starting ``` @@ -184,7 +196,7 @@ names, commit messages, and PR descriptions for the user to copy. ## What Does NOT Change - `implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md` — subagent prompts untouched -- `executing-plans/SKILL.md` — inherits behavior through `using-git-worktrees` and `finishing-a-development-branch` +- `executing-plans/SKILL.md` — same 1-line Integration edit as `subagent-driven-development`; runtime behavior inherited through `using-git-worktrees` and `finishing-a-development-branch` - `dispatching-parallel-agents/SKILL.md` — no worktree or finishing operations - `.codex/INSTALL.md` — installation process unchanged - The 4-option finishing menu — preserved exactly for Claude Code and Codex CLI @@ -198,9 +210,10 @@ names, commit messages, and PR descriptions for the user to copy. | `skills/using-git-worktrees/SKILL.md` | +12 lines (Step 0) | | `skills/finishing-a-development-branch/SKILL.md` | +20 lines (Step 1.5 + cleanup guard) | | `skills/subagent-driven-development/SKILL.md` | 1 line edit | +| `skills/executing-plans/SKILL.md` | 1 line edit | | `skills/using-superpowers/references/codex-tools.md` | +15 lines | -~48 lines added/changed across 4 files. Zero new files. Zero breaking changes. +~50 lines added/changed across 5 files. Zero new files. Zero breaking changes. ## Future Considerations From 80c0a45fcce85e0f1d6851b0d94a7638bdfdc5e9 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 12:39:19 -0700 Subject: [PATCH 04/46] docs: clarify executing-plans in What Does NOT Change section Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-23-codex-app-compatibility-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md index 32a5c489..6f73aa4d 100644 --- a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +++ b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md @@ -196,7 +196,7 @@ names, commit messages, and PR descriptions for the user to copy. ## What Does NOT Change - `implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md` — subagent prompts untouched -- `executing-plans/SKILL.md` — same 1-line Integration edit as `subagent-driven-development`; runtime behavior inherited through `using-git-worktrees` and `finishing-a-development-branch` +- `executing-plans/SKILL.md` — only the 1-line Integration description changes (same as `subagent-driven-development`); all runtime behavior is unchanged - `dispatching-parallel-agents/SKILL.md` — no worktree or finishing operations - `.codex/INSTALL.md` — installation process unchanged - The 4-option finishing menu — preserved exactly for Claude Code and Codex CLI From eb2b44b23f90ebc14d9773332fa6f8663f8fe6e9 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 12:46:39 -0700 Subject: [PATCH 05/46] docs: add cleanup guard test (#5) and sandbox fallback test (#10) to spec Both tests address real risk scenarios: - #5: cleanup guard bug would delete Codex App's own worktree (data loss) - #10: Local thread sandbox fallback needs manual Codex App validation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-23-codex-app-compatibility-design.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md index 6f73aa4d..c3fecc04 100644 --- a/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +++ b/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md @@ -227,13 +227,15 @@ If a third skill needs the same detection pattern, extract it into a shared `ref 2. Linked worktree detection — `git worktree add` test worktree, assert IN_LINKED_WORKTREE=true 3. Detached HEAD detection — `git checkout --detach`, assert ON_DETACHED_HEAD=true 4. Finishing skill handoff output — verify handoff message (not 4-option menu) in restricted environment +5. **Step 5 cleanup guard** — create a linked worktree (`git worktree add /tmp/test-cleanup -b test-cleanup`), `cd` into it, run the Step 5 cleanup detection (`GIT_DIR` vs `GIT_COMMON`), assert it would NOT call `git worktree remove`. Then `cd` back to main repo, run the same detection, assert it WOULD call `git worktree remove`. Clean up test worktree afterward. -### Manual Codex App Tests (4 tests) +### Manual Codex App Tests (5 tests) 1. Detection in Worktree thread (workspace-write) — verify GIT_DIR != GIT_COMMON, empty branch 2. Detection in Worktree thread (Full access) — same detection, different sandbox behavior 3. Finishing skill handoff format — verify agent emits handoff payload, not 4-option menu 4. Full lifecycle — detection → commit → finishing detection → correct behavior → cleanup +5. **Sandbox fallback in Local thread** — Start a Codex App **Local thread** (workspace-write sandbox). Prompt: "Use the superpowers skill `using-git-worktrees` to set up an isolated workspace for implementing a small change." Pre-check: `git checkout -b test-sandbox-check` should fail with `Operation not permitted`. Expected: the skill detects `GIT_DIR == GIT_COMMON` (normal repo), attempts `git worktree add -b`, hits Seatbelt denial, falls back to Step 0 "already in workspace" behavior — runs setup, baseline tests, reports ready from current directory. Pass: agent recovers gracefully without cryptic error messages. Fail: agent prints raw Seatbelt error, retries, or gives up with confusing output. ### Regression From bd080e3cc87a17f78fc53a2347510e410d147ab6 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 13:01:13 -0700 Subject: [PATCH 06/46] docs: add implementation plan for Codex App compatibility (PRI-823) 8 tasks covering: environment detection in using-git-worktrees, Step 1.5 + cleanup guard in finishing-a-development-branch, Integration line updates, codex-tools.md docs, automated tests, and final verification. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-23-codex-app-compatibility.md | 564 ++++++++++++++++++ 1 file changed, 564 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-23-codex-app-compatibility.md diff --git a/docs/superpowers/plans/2026-03-23-codex-app-compatibility.md b/docs/superpowers/plans/2026-03-23-codex-app-compatibility.md new file mode 100644 index 00000000..933cddfd --- /dev/null +++ b/docs/superpowers/plans/2026-03-23-codex-app-compatibility.md @@ -0,0 +1,564 @@ +# Codex App Compatibility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `using-git-worktrees`, `finishing-a-development-branch`, and related skills work in the Codex App's sandboxed worktree environment without breaking existing behavior. + +**Architecture:** Read-only environment detection (`git-dir` vs `git-common-dir`) at the start of two skills. If already in a linked worktree, skip creation. If on detached HEAD, emit a handoff payload instead of the 4-option menu. Sandbox fallback catches permission errors during worktree creation. + +**Tech Stack:** Git, Markdown (skill files are instruction documents, not executable code) + +**Spec:** `docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md` + +--- + +## File Structure + +| File | Responsibility | Action | +|---|---|---| +| `skills/using-git-worktrees/SKILL.md` | Worktree creation + isolation | Add Step 0 detection + sandbox fallback | +| `skills/finishing-a-development-branch/SKILL.md` | Branch finishing workflow | Add Step 1.5 detection + cleanup guard | +| `skills/subagent-driven-development/SKILL.md` | Plan execution with subagents | Update Integration description | +| `skills/executing-plans/SKILL.md` | Plan execution inline | Update Integration description | +| `skills/using-superpowers/references/codex-tools.md` | Codex platform reference | Add detection + finishing docs | + +--- + +### Task 1: Add Step 0 to `using-git-worktrees` + +**Files:** +- Modify: `skills/using-git-worktrees/SKILL.md:14-15` (insert after Overview, before Directory Selection Process) + +- [ ] **Step 1: Read the current skill file** + +Read `skills/using-git-worktrees/SKILL.md` in full. Identify the exact insertion point: after the "Announce at start" line (line 14) and before "## Directory Selection Process" (line 16). + +- [ ] **Step 2: Insert Step 0 section** + +Insert the following between the Overview section and "## Directory Selection Process": + +```markdown +## Step 0: Check if Already in an Isolated Workspace + +Before creating a worktree, check if one already exists: + +```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +``` + +**If `GIT_DIR` differs from `GIT_COMMON`:** You are already inside a linked worktree (created by the Codex App, Claude Code's Agent tool, a previous skill run, or the user). Do NOT create another worktree. Instead: + +1. Run project setup (auto-detect package manager as in "Run Project Setup" below) +2. Verify clean baseline (run tests as in "Verify Clean Baseline" below) +3. Report with branch state: + - On a branch: "Already in an isolated workspace at `` on branch ``. Tests passing. Ready to implement." + - Detached HEAD: "Already in an isolated workspace at `` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement." + +After reporting, STOP. Do not continue to Directory Selection or Creation Steps. + +**If `GIT_DIR` equals `GIT_COMMON`:** Proceed with the full worktree creation flow below. + +**Sandbox fallback:** If you proceed to Creation Steps but `git worktree add -b` fails with a permission error (e.g., "Operation not permitted"), treat this as a late-detected restricted environment. Fall back to the behavior above — run setup and baseline tests in the current directory, report accordingly, and STOP. +``` + +- [ ] **Step 3: Verify the insertion** + +Read the file again. Confirm: +- Step 0 appears between Overview and Directory Selection Process +- The rest of the file (Directory Selection, Safety Verification, Creation Steps, etc.) is unchanged +- No duplicate sections or broken markdown + +- [ ] **Step 4: Commit** + +```bash +git add skills/using-git-worktrees/SKILL.md +git commit -m "feat(using-git-worktrees): add Step 0 environment detection (PRI-823) + +Skip worktree creation when already in a linked worktree. Includes +sandbox fallback for permission errors on git worktree add." +``` + +--- + +### Task 2: Update `using-git-worktrees` Integration section + +**Files:** +- Modify: `skills/using-git-worktrees/SKILL.md:211-215` (Integration > Called by) + +- [ ] **Step 1: Update the three "Called by" entries** + +Change lines 212-214 from: + +```markdown +- **brainstorming** (Phase 4) - REQUIRED when design is approved and implementation follows +- **subagent-driven-development** - REQUIRED before executing any tasks +- **executing-plans** - REQUIRED before executing any tasks +``` + +To: + +```markdown +- **brainstorming** - REQUIRED: Ensures isolated workspace (creates one or verifies existing) +- **subagent-driven-development** - REQUIRED: Ensures isolated workspace (creates one or verifies existing) +- **executing-plans** - REQUIRED: Ensures isolated workspace (creates one or verifies existing) +``` + +- [ ] **Step 2: Verify the Integration section** + +Read the Integration section. Confirm all three entries are updated, "Pairs with" is unchanged. + +- [ ] **Step 3: Commit** + +```bash +git add skills/using-git-worktrees/SKILL.md +git commit -m "docs(using-git-worktrees): update Integration descriptions (PRI-823) + +Clarify that skill ensures a workspace exists, not that it always creates one." +``` + +--- + +### Task 3: Add Step 1.5 to `finishing-a-development-branch` + +**Files:** +- Modify: `skills/finishing-a-development-branch/SKILL.md:38` (insert after Step 1, before Step 2) + +- [ ] **Step 1: Read the current skill file** + +Read `skills/finishing-a-development-branch/SKILL.md` in full. Identify the insertion point: after "**If tests pass:** Continue to Step 2." (line 38) and before "### Step 2: Determine Base Branch" (line 40). + +- [ ] **Step 2: Insert Step 1.5 section** + +Insert the following between Step 1 and Step 2: + +```markdown +### Step 1.5: Detect Environment + +```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +``` + +**Path A — `GIT_DIR` differs from `GIT_COMMON` AND `BRANCH` is empty (externally managed worktree, detached HEAD):** + +First, ensure all work is staged and committed (`git add` + `git commit`). + +Then present this to the user (do NOT present the 4-option menu): + +``` +Implementation complete. All tests passing. +Current HEAD: + +This workspace is externally managed (detached HEAD). +I cannot create branches, push, or open PRs from here. + +⚠ These commits are on a detached HEAD. If you do not create a branch, +they may be lost when this workspace is cleaned up. + +If your host application provides these controls: +- "Create branch" — to name a branch, then commit/push/PR +- "Hand off to local" — to move changes to your local checkout + +Suggested branch name: +Suggested commit message: +``` + +Branch name: use ticket ID if available (e.g., `pri-823/codex-compat`), otherwise slugify the first 5 words of the plan title, otherwise omit. Avoid sensitive content in branch names. + +Skip to Step 5 (cleanup is a no-op — see guard below). + +**Path B — `GIT_DIR` differs from `GIT_COMMON` AND `BRANCH` exists (externally managed worktree, named branch):** + +Proceed to Step 2 and present the 4-option menu as normal. + +**Path C — `GIT_DIR` equals `GIT_COMMON` (normal environment):** + +Proceed to Step 2 and present the 4-option menu as normal. +``` + +- [ ] **Step 3: Verify the insertion** + +Read the file again. Confirm: +- Step 1.5 appears between Step 1 and Step 2 +- Steps 2-5 are unchanged +- Path A handoff includes commit SHA and data loss warning +- Paths B and C proceed to Step 2 normally + +- [ ] **Step 4: Commit** + +```bash +git add skills/finishing-a-development-branch/SKILL.md +git commit -m "feat(finishing-a-development-branch): add Step 1.5 environment detection (PRI-823) + +Detect externally managed worktrees with detached HEAD and emit handoff +payload instead of 4-option menu. Includes commit SHA and data loss warning." +``` + +--- + +### Task 4: Add Step 5 cleanup guard to `finishing-a-development-branch` + +**Files:** +- Modify: `skills/finishing-a-development-branch/SKILL.md` (Step 5: Cleanup Worktree — find by section heading, line numbers will have shifted after Task 3) + +- [ ] **Step 1: Read the current Step 5 section** + +Find the "### Step 5: Cleanup Worktree" section in `skills/finishing-a-development-branch/SKILL.md` (line numbers will have shifted after Task 3's insertion). The current Step 5 is: + +```markdown +### Step 5: Cleanup Worktree + +**For Options 1, 2, 4:** + +Check if in worktree: +```bash +git worktree list | grep $(git branch --show-current) +``` + +If yes: +```bash +git worktree remove +``` + +**For Option 3:** Keep worktree. +``` + +- [ ] **Step 2: Add the cleanup guard before existing logic** + +Replace the Step 5 section with: + +```markdown +### Step 5: Cleanup Worktree + +**First, check if worktree is externally managed:** + +```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +``` + +If `GIT_DIR` differs from `GIT_COMMON`: skip worktree removal — the host environment owns this workspace. + +**Otherwise, for Options 1 and 4:** + +Check if in worktree: +```bash +git worktree list | grep $(git branch --show-current) +``` + +If yes: +```bash +git worktree remove +``` + +**For Option 3:** Keep worktree. +``` + +Note: the original text said "For Options 1, 2, 4" but the Quick Reference table and Common Mistakes section say "Options 1 & 4 only." This edit aligns Step 5 with those sections. + +- [ ] **Step 3: Verify the replacement** + +Read Step 5. Confirm: +- Cleanup guard (re-detection) appears first +- Existing removal logic preserved for non-externally-managed worktrees +- "Options 1 and 4" (not "1, 2, 4") matches Quick Reference and Common Mistakes + +- [ ] **Step 4: Commit** + +```bash +git add skills/finishing-a-development-branch/SKILL.md +git commit -m "feat(finishing-a-development-branch): add Step 5 cleanup guard (PRI-823) + +Re-detect externally managed worktree at cleanup time and skip removal. +Also fixes pre-existing inconsistency: cleanup now correctly says +Options 1 and 4 only, matching Quick Reference and Common Mistakes." +``` + +--- + +### Task 5: Update Integration lines in `subagent-driven-development` and `executing-plans` + +**Files:** +- Modify: `skills/subagent-driven-development/SKILL.md:268` +- Modify: `skills/executing-plans/SKILL.md:68` + +- [ ] **Step 1: Update `subagent-driven-development`** + +Change line 268 from: +``` +- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting +``` +To: +``` +- **superpowers:using-git-worktrees** - REQUIRED: Ensures isolated workspace (creates one or verifies existing) +``` + +- [ ] **Step 2: Update `executing-plans`** + +Change line 68 from: +``` +- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting +``` +To: +``` +- **superpowers:using-git-worktrees** - REQUIRED: Ensures isolated workspace (creates one or verifies existing) +``` + +- [ ] **Step 3: Verify both files** + +Read line 268 of `skills/subagent-driven-development/SKILL.md` and line 68 of `skills/executing-plans/SKILL.md`. Confirm both say "Ensures isolated workspace (creates one or verifies existing)". + +- [ ] **Step 4: Commit** + +```bash +git add skills/subagent-driven-development/SKILL.md skills/executing-plans/SKILL.md +git commit -m "docs(sdd, executing-plans): update worktree Integration descriptions (PRI-823) + +Clarify that using-git-worktrees ensures a workspace exists rather than +always creating one." +``` + +--- + +### Task 6: Add environment detection docs to `codex-tools.md` + +**Files:** +- Modify: `skills/using-superpowers/references/codex-tools.md:25` (append at end) + +- [ ] **Step 1: Read the current file** + +Read `skills/using-superpowers/references/codex-tools.md` in full. Confirm it ends at line 25-26 after the multi_agent section. + +- [ ] **Step 2: Append two new sections** + +Add at the end of the file: + +```markdown + +## Environment Detection + +Skills that create worktrees or finish branches should detect their +environment with read-only git commands before proceeding: + +```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +``` + +- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation) +- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox) + +See `using-git-worktrees` Step 0 and `finishing-a-development-branch` +Step 1.5 for how each skill uses these signals. + +## Codex App Finishing + +When the sandbox blocks branch/push operations (detached HEAD in an +externally managed worktree), the agent commits all work and informs +the user to use the App's native controls: + +- **"Create branch"** — names the branch, then commit/push/PR via App UI +- **"Hand off to local"** — transfers work to the user's local checkout + +The agent can still run tests, stage files, and output suggested branch +names, commit messages, and PR descriptions for the user to copy. +``` + +- [ ] **Step 3: Verify the additions** + +Read the full file. Confirm: +- Two new sections appear after the existing content +- Bash code block renders correctly (not escaped) +- Cross-references to Step 0 and Step 1.5 are present + +- [ ] **Step 4: Commit** + +```bash +git add skills/using-superpowers/references/codex-tools.md +git commit -m "docs(codex-tools): add environment detection and App finishing docs (PRI-823) + +Document the git-dir vs git-common-dir detection pattern and the Codex +App's native finishing flow for skills that need to adapt." +``` + +--- + +### Task 7: Automated test — environment detection + +**Files:** +- Create: `tests/codex-app-compat/test-environment-detection.sh` + +- [ ] **Step 1: Create test directory** + +```bash +mkdir -p tests/codex-app-compat +``` + +- [ ] **Step 2: Write the detection test script** + +Create `tests/codex-app-compat/test-environment-detection.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Test environment detection logic from PRI-823 +# Tests the git-dir vs git-common-dir comparison used by +# using-git-worktrees Step 0 and finishing-a-development-branch Step 1.5 + +PASS=0 +FAIL=0 +TEMP_DIR=$(mktemp -d) +trap "rm -rf $TEMP_DIR" EXIT + +log_pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +log_fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +# Helper: run detection and return "linked" or "normal" +detect_worktree() { + local git_dir git_common + git_dir=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) + git_common=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) + if [ "$git_dir" != "$git_common" ]; then + echo "linked" + else + echo "normal" + fi +} + +echo "=== Test 1: Normal repo detection ===" +cd "$TEMP_DIR" +git init test-repo > /dev/null 2>&1 +cd test-repo +git commit --allow-empty -m "init" > /dev/null 2>&1 +result=$(detect_worktree) +if [ "$result" = "normal" ]; then + log_pass "Normal repo detected as normal" +else + log_fail "Normal repo detected as '$result' (expected 'normal')" +fi + +echo "=== Test 2: Linked worktree detection ===" +git worktree add "$TEMP_DIR/test-wt" -b test-branch > /dev/null 2>&1 +cd "$TEMP_DIR/test-wt" +result=$(detect_worktree) +if [ "$result" = "linked" ]; then + log_pass "Linked worktree detected as linked" +else + log_fail "Linked worktree detected as '$result' (expected 'linked')" +fi + +echo "=== Test 3: Detached HEAD detection ===" +git checkout --detach HEAD > /dev/null 2>&1 +branch=$(git branch --show-current) +if [ -z "$branch" ]; then + log_pass "Detached HEAD: branch is empty" +else + log_fail "Detached HEAD: branch is '$branch' (expected empty)" +fi + +echo "=== Test 4: Linked worktree + detached HEAD (Codex App simulation) ===" +result=$(detect_worktree) +branch=$(git branch --show-current) +if [ "$result" = "linked" ] && [ -z "$branch" ]; then + log_pass "Codex App simulation: linked + detached HEAD" +else + log_fail "Codex App simulation: result='$result', branch='$branch'" +fi + +echo "=== Test 5: Cleanup guard — linked worktree should NOT remove ===" +cd "$TEMP_DIR/test-wt" +result=$(detect_worktree) +if [ "$result" = "linked" ]; then + log_pass "Cleanup guard: linked worktree correctly detected (would skip removal)" +else + log_fail "Cleanup guard: expected 'linked', got '$result'" +fi + +echo "=== Test 6: Cleanup guard — main repo SHOULD remove ===" +cd "$TEMP_DIR/test-repo" +result=$(detect_worktree) +if [ "$result" = "normal" ]; then + log_pass "Cleanup guard: main repo correctly detected (would proceed with removal)" +else + log_fail "Cleanup guard: expected 'normal', got '$result'" +fi + +# Cleanup worktree before temp dir removal +cd "$TEMP_DIR/test-repo" +git worktree remove "$TEMP_DIR/test-wt" > /dev/null 2>&1 || true + +echo "" +echo "=== Results: $PASS passed, $FAIL failed ===" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi +``` + +- [ ] **Step 3: Make it executable and run it** + +```bash +chmod +x tests/codex-app-compat/test-environment-detection.sh +./tests/codex-app-compat/test-environment-detection.sh +``` + +Expected output: 6 passed, 0 failed. + +- [ ] **Step 4: Commit** + +```bash +git add tests/codex-app-compat/test-environment-detection.sh +git commit -m "test: add environment detection tests for Codex App compat (PRI-823) + +Tests git-dir vs git-common-dir comparison in normal repo, linked +worktree, detached HEAD, and cleanup guard scenarios." +``` + +--- + +### Task 8: Final verification + +**Files:** +- Read: all 5 modified skill files + +- [ ] **Step 1: Run the automated detection tests** + +```bash +./tests/codex-app-compat/test-environment-detection.sh +``` + +Expected: 6 passed, 0 failed. + +- [ ] **Step 2: Read each modified file and verify changes** + +Read each file end-to-end: +- `skills/using-git-worktrees/SKILL.md` — Step 0 present, rest unchanged +- `skills/finishing-a-development-branch/SKILL.md` — Step 1.5 present, cleanup guard present, rest unchanged +- `skills/subagent-driven-development/SKILL.md` — line 268 updated +- `skills/executing-plans/SKILL.md` — line 68 updated +- `skills/using-superpowers/references/codex-tools.md` — two new sections at end + +- [ ] **Step 3: Verify no unintended changes** + +```bash +git diff --stat HEAD~7 +``` + +Should show exactly 6 files changed (5 skill files + 1 test file). No other files modified. + +- [ ] **Step 4: Run existing test suite** + +If test runner exists: +```bash +# Run skill-triggering tests +./tests/skill-triggering/run-all.sh 2>/dev/null || echo "Skill triggering tests not available in this environment" + +# Run SDD integration test +./tests/claude-code/test-subagent-driven-development-integration.sh 2>/dev/null || echo "SDD integration test not available in this environment" +``` + +Note: these tests require Claude Code with `--dangerously-skip-permissions`. If not available, document that regression tests should be run manually. From 2b1bfe5db62e0b9bf1566a3c79e87608ea4cc5f5 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 15:42:52 -0700 Subject: [PATCH 07/46] docs(codex-tools): add named agent dispatch mapping for Codex (#647) --- .../references/codex-tools.md | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/skills/using-superpowers/references/codex-tools.md b/skills/using-superpowers/references/codex-tools.md index 86f58fa6..539b2b1c 100644 --- a/skills/using-superpowers/references/codex-tools.md +++ b/skills/using-superpowers/references/codex-tools.md @@ -4,7 +4,7 @@ Skills use Claude Code tool names. When you encounter these in a skill, use your | Skill references | Codex equivalent | |-----------------|------------------| -| `Task` tool (dispatch subagent) | `spawn_agent` | +| `Task` tool (dispatch subagent) | `spawn_agent` (see [Named agent dispatch](#named-agent-dispatch)) | | Multiple `Task` calls (parallel) | Multiple `spawn_agent` calls | | Task returns result | `wait` | | Task completes automatically | `close_agent` to free slot | @@ -23,3 +23,78 @@ multi_agent = true ``` This enables `spawn_agent`, `wait`, and `close_agent` for skills like `dispatching-parallel-agents` and `subagent-driven-development`. + +## Named agent dispatch + +Claude Code skills reference named agent types like `superpowers:code-reviewer`. +Codex does not have a named agent registry — `spawn_agent` creates generic agents +from built-in roles (`default`, `explorer`, `worker`). + +When a skill says to dispatch a named agent type: + +1. Find the agent's prompt file (e.g., `agents/code-reviewer.md` or the skill's + local prompt template like `code-quality-reviewer-prompt.md`) +2. Read the prompt content +3. Fill any template placeholders (`{BASE_SHA}`, `{WHAT_WAS_IMPLEMENTED}`, etc.) +4. Spawn a `worker` agent with the filled content as the `message` + +| Skill instruction | Codex equivalent | +|-------------------|------------------| +| `Task tool (superpowers:code-reviewer)` | `spawn_agent(agent_type="worker", message=...)` with `code-reviewer.md` content | +| `Task tool (general-purpose)` with inline prompt | `spawn_agent(message=...)` with the same prompt | + +### Message framing + +The `message` parameter is user-level input, not a system prompt. Structure it +for maximum instruction adherence: + +``` +Your task is to perform the following. Follow the instructions below exactly. + + +[filled prompt content from the agent's .md file] + + +Execute this now. Output ONLY the structured response following the format +specified in the instructions above. +``` + +- Use task-delegation framing ("Your task is...") rather than persona framing ("You are...") +- Wrap instructions in XML tags — the model treats tagged blocks as authoritative +- End with an explicit execution directive to prevent summarization of the instructions + +### When this workaround can be removed + +This approach compensates for Codex's plugin system not yet supporting an `agents` +field in `plugin.json`. When `RawPluginManifest` gains an `agents` field, the +plugin can symlink to `agents/` (mirroring the existing `skills/` symlink) and +skills can dispatch named agent types directly. + +## Environment Detection + +Skills that create worktrees or finish branches should detect their +environment with read-only git commands before proceeding: + +```bash +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +``` + +- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation) +- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox) + +See `using-git-worktrees` Step 0 and `finishing-a-development-branch` +Step 1 for how each skill uses these signals. + +## Codex App Finishing + +When the sandbox blocks branch/push operations (detached HEAD in an +externally managed worktree), the agent commits all work and informs +the user to use the App's native controls: + +- **"Create branch"** — names the branch, then commit/push/PR via App UI +- **"Hand off to local"** — transfers work to the user's local checkout + +The agent can still run tests, stage files, and output suggested branch +names, commit messages, and PR descriptions for the user to copy. From 4fd9aa2dd5b602a102a1ff4743849642b73655c1 Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Mon, 23 Mar 2026 17:05:42 -0700 Subject: [PATCH 08/46] fix(writing-skills): correct false 'only two fields' frontmatter claim (#882) --- skills/writing-skills/SKILL.md | 4 ++-- skills/writing-skills/anthropic-best-practices.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skills/writing-skills/SKILL.md b/skills/writing-skills/SKILL.md index 4cd8ddfc..c3b73d8b 100644 --- a/skills/writing-skills/SKILL.md +++ b/skills/writing-skills/SKILL.md @@ -93,7 +93,7 @@ skills/ ## SKILL.md Structure **Frontmatter (YAML):** -- Only two fields supported: `name` and `description` +- Two required fields: `name` and `description` (see [agentskills.io/specification](https://agentskills.io/specification) for all supported fields) - Max 1024 characters total - `name`: Use letters, numbers, and hyphens only (no parentheses, special chars) - `description`: Third-person, describes ONLY when to use (NOT what it does) @@ -604,7 +604,7 @@ Deploying untested skills = deploying untested code. It's a violation of quality **GREEN Phase - Write Minimal Skill:** - [ ] Name uses only letters, numbers, hyphens (no parentheses/special chars) -- [ ] YAML frontmatter with only name and description (max 1024 chars) +- [ ] YAML frontmatter with required `name` and `description` fields (max 1024 chars; see [spec](https://agentskills.io/specification)) - [ ] Description starts with "Use when..." and includes specific triggers/symptoms - [ ] Description written in third person - [ ] Keywords throughout for search (errors, symptoms, tools) diff --git a/skills/writing-skills/anthropic-best-practices.md b/skills/writing-skills/anthropic-best-practices.md index a5a7d07a..9f3f6ecf 100644 --- a/skills/writing-skills/anthropic-best-practices.md +++ b/skills/writing-skills/anthropic-best-practices.md @@ -144,7 +144,7 @@ What works perfectly for Opus might need more detail for Haiku. If you plan to u ## Skill structure - **YAML Frontmatter**: The SKILL.md frontmatter supports two fields: + **YAML Frontmatter**: The SKILL.md frontmatter requires two fields: * `name` - Human-readable name of the Skill (64 characters maximum) * `description` - One-line description of what the Skill does and when to use it (1024 characters maximum) @@ -1092,7 +1092,7 @@ reader = PdfReader("file.pdf") ### YAML frontmatter requirements -The SKILL.md frontmatter includes only `name` (64 characters max) and `description` (1024 characters max) fields. See the [Skills overview](/en/docs/agents-and-tools/agent-skills/overview#skill-structure) for complete structure details. +The SKILL.md frontmatter requires `name` (64 characters max) and `description` (1024 characters max) fields. See the [Skills overview](/en/docs/agents-and-tools/agent-skills/overview#skill-structure) for complete structure details. ### Token budgets From e6221a48c54141e0ef04adaea4895a8150fc57a1 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Fri, 20 Mar 2026 13:28:56 -0700 Subject: [PATCH 09/46] Replace subagent review loops with lightweight inline self-review The subagent review loop (dispatching a fresh agent to review plans/specs) doubled execution time (~25 min overhead) without measurably improving plan quality. Regression testing across 5 versions (v3.6.0 through v5.0.4) with 5 trials each showed identical plan sizes, task counts, and quality scores regardless of whether the review loop ran. Changes: - writing-plans: Replace subagent Plan Review Loop with inline Self-Review checklist (spec coverage, placeholder scan, type consistency) - writing-plans: Add explicit "No Placeholders" section listing plan failures (TBD, vague descriptions, undefined references, "similar to Task N") - brainstorming: Replace subagent Spec Review Loop with inline Spec Self-Review (placeholder scan, internal consistency, scope check, ambiguity check) - Both skills now use "look at it with fresh eyes" framing Testing: 5 trials with the new skill show self-review catches 3-5 real bugs per run (spawn positions, API mismatches, seed bugs, grid indexing) in ~30s instead of ~25 min. Remaining defects are comparable to the subagent approach. Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/brainstorming/SKILL.md | 24 ++++++++++++------------ skills/writing-plans/SKILL.md | 31 +++++++++++++++++++------------ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index edbc2b57..06cd0a21 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -27,7 +27,7 @@ You MUST create a task for each of these items and complete them in order: 4. **Propose 2-3 approaches** — with trade-offs and your recommendation 5. **Present design** — in sections scaled to their complexity, get user approval after each section 6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD--design.md` and commit -7. **Spec review loop** — dispatch spec-document-reviewer subagent with precisely crafted review context (never your session history); fix issues and re-dispatch until approved (max 3 iterations, then surface to human) +7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below) 8. **User reviews written spec** — ask user to review the spec file before proceeding 9. **Transition to implementation** — invoke writing-plans skill to create implementation plan @@ -43,8 +43,7 @@ digraph brainstorming { "Present design sections" [shape=box]; "User approves design?" [shape=diamond]; "Write design doc" [shape=box]; - "Spec review loop" [shape=box]; - "Spec review passed?" [shape=diamond]; + "Spec self-review\n(fix inline)" [shape=box]; "User reviews spec?" [shape=diamond]; "Invoke writing-plans skill" [shape=doublecircle]; @@ -57,10 +56,8 @@ digraph brainstorming { "Present design sections" -> "User approves design?"; "User approves design?" -> "Present design sections" [label="no, revise"]; "User approves design?" -> "Write design doc" [label="yes"]; - "Write design doc" -> "Spec review loop"; - "Spec review loop" -> "Spec review passed?"; - "Spec review passed?" -> "Spec review loop" [label="issues found,\nfix and re-dispatch"]; - "Spec review passed?" -> "User reviews spec?" [label="approved"]; + "Write design doc" -> "Spec self-review\n(fix inline)"; + "Spec self-review\n(fix inline)" -> "User reviews spec?"; "User reviews spec?" -> "Write design doc" [label="changes requested"]; "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"]; } @@ -116,12 +113,15 @@ digraph brainstorming { - Use elements-of-style:writing-clearly-and-concisely skill if available - Commit the design document to git -**Spec Review Loop:** -After writing the spec document: +**Spec Self-Review:** +After writing the spec document, look at it with fresh eyes: -1. Dispatch spec-document-reviewer subagent (see spec-document-reviewer-prompt.md) -2. If Issues Found: fix, re-dispatch, repeat until Approved -3. If loop exceeds 3 iterations, surface to human for guidance +1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them. +2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions? +3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition? +4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit. + +Fix any issues inline. No need to re-review — just fix and move on. **User Review Gate:** After the spec review loop passes, ask the user to review the written spec before proceeding: diff --git a/skills/writing-plans/SKILL.md b/skills/writing-plans/SKILL.md index 60f9834f..0d9c00ba 100644 --- a/skills/writing-plans/SKILL.md +++ b/skills/writing-plans/SKILL.md @@ -103,26 +103,33 @@ git commit -m "feat: add specific feature" ``` ```` +## No Placeholders + +Every step must contain the actual content an engineer needs. These are **plan failures** — never write them: +- "TBD", "TODO", "implement later", "fill in details" +- "Add appropriate error handling" / "add validation" / "handle edge cases" +- "Write tests for the above" (without actual test code) +- "Similar to Task N" (repeat the code — the engineer may be reading tasks out of order) +- Steps that describe what to do without showing how (code blocks required for code steps) +- References to types, functions, or methods not defined in any task + ## Remember - Exact file paths always -- Complete code in plan (not "add validation") +- Complete code in every step — if a step changes code, show the code - Exact commands with expected output -- Reference relevant skills with @ syntax - DRY, YAGNI, TDD, frequent commits -## Plan Review Loop +## Self-Review -After writing the complete plan: +After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch. -1. Dispatch a single plan-document-reviewer subagent (see plan-document-reviewer-prompt.md) with precisely crafted review context — never your session history. This keeps the reviewer focused on the plan, not your thought process. - - Provide: path to the plan document, path to spec document -2. If ❌ Issues Found: fix the issues, re-dispatch reviewer for the whole plan -3. If ✅ Approved: proceed to execution handoff +**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps. -**Review loop guidance:** -- Same agent that wrote the plan fixes it (preserves context) -- If loop exceeds 3 iterations, surface to human for guidance -- Reviewers are advisory — explain disagreements if you believe feedback is incorrect +**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them. + +**3. Type consistency:** Do the types, method signatures, and property names you used in later tasks match what you defined in earlier tasks? A function called `clearLayers()` in Task 3 but `clearFullLayers()` in Task 7 is a bug. + +If you find issues, fix them inline. No need to re-review — just fix and move on. If you find a spec requirement with no task, add the task. ## Execution Handoff From 4ae1a3d6a6e1e2171ae9de0eb7daf3e192c2b792 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 10:43:58 -0700 Subject: [PATCH 10/46] Revert "Replace subagent review loops with lightweight inline self-review" This reverts commit bf8f7572eb85d793b73a44913e8039f9a2e83c1e. --- skills/brainstorming/SKILL.md | 24 ++++++++++++------------ skills/writing-plans/SKILL.md | 31 ++++++++++++------------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 06cd0a21..edbc2b57 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -27,7 +27,7 @@ You MUST create a task for each of these items and complete them in order: 4. **Propose 2-3 approaches** — with trade-offs and your recommendation 5. **Present design** — in sections scaled to their complexity, get user approval after each section 6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD--design.md` and commit -7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below) +7. **Spec review loop** — dispatch spec-document-reviewer subagent with precisely crafted review context (never your session history); fix issues and re-dispatch until approved (max 3 iterations, then surface to human) 8. **User reviews written spec** — ask user to review the spec file before proceeding 9. **Transition to implementation** — invoke writing-plans skill to create implementation plan @@ -43,7 +43,8 @@ digraph brainstorming { "Present design sections" [shape=box]; "User approves design?" [shape=diamond]; "Write design doc" [shape=box]; - "Spec self-review\n(fix inline)" [shape=box]; + "Spec review loop" [shape=box]; + "Spec review passed?" [shape=diamond]; "User reviews spec?" [shape=diamond]; "Invoke writing-plans skill" [shape=doublecircle]; @@ -56,8 +57,10 @@ digraph brainstorming { "Present design sections" -> "User approves design?"; "User approves design?" -> "Present design sections" [label="no, revise"]; "User approves design?" -> "Write design doc" [label="yes"]; - "Write design doc" -> "Spec self-review\n(fix inline)"; - "Spec self-review\n(fix inline)" -> "User reviews spec?"; + "Write design doc" -> "Spec review loop"; + "Spec review loop" -> "Spec review passed?"; + "Spec review passed?" -> "Spec review loop" [label="issues found,\nfix and re-dispatch"]; + "Spec review passed?" -> "User reviews spec?" [label="approved"]; "User reviews spec?" -> "Write design doc" [label="changes requested"]; "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"]; } @@ -113,15 +116,12 @@ digraph brainstorming { - Use elements-of-style:writing-clearly-and-concisely skill if available - Commit the design document to git -**Spec Self-Review:** -After writing the spec document, look at it with fresh eyes: +**Spec Review Loop:** +After writing the spec document: -1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them. -2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions? -3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition? -4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit. - -Fix any issues inline. No need to re-review — just fix and move on. +1. Dispatch spec-document-reviewer subagent (see spec-document-reviewer-prompt.md) +2. If Issues Found: fix, re-dispatch, repeat until Approved +3. If loop exceeds 3 iterations, surface to human for guidance **User Review Gate:** After the spec review loop passes, ask the user to review the written spec before proceeding: diff --git a/skills/writing-plans/SKILL.md b/skills/writing-plans/SKILL.md index 0d9c00ba..60f9834f 100644 --- a/skills/writing-plans/SKILL.md +++ b/skills/writing-plans/SKILL.md @@ -103,33 +103,26 @@ git commit -m "feat: add specific feature" ``` ```` -## No Placeholders - -Every step must contain the actual content an engineer needs. These are **plan failures** — never write them: -- "TBD", "TODO", "implement later", "fill in details" -- "Add appropriate error handling" / "add validation" / "handle edge cases" -- "Write tests for the above" (without actual test code) -- "Similar to Task N" (repeat the code — the engineer may be reading tasks out of order) -- Steps that describe what to do without showing how (code blocks required for code steps) -- References to types, functions, or methods not defined in any task - ## Remember - Exact file paths always -- Complete code in every step — if a step changes code, show the code +- Complete code in plan (not "add validation") - Exact commands with expected output +- Reference relevant skills with @ syntax - DRY, YAGNI, TDD, frequent commits -## Self-Review +## Plan Review Loop -After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch. +After writing the complete plan: -**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps. +1. Dispatch a single plan-document-reviewer subagent (see plan-document-reviewer-prompt.md) with precisely crafted review context — never your session history. This keeps the reviewer focused on the plan, not your thought process. + - Provide: path to the plan document, path to spec document +2. If ❌ Issues Found: fix the issues, re-dispatch reviewer for the whole plan +3. If ✅ Approved: proceed to execution handoff -**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them. - -**3. Type consistency:** Do the types, method signatures, and property names you used in later tasks match what you defined in earlier tasks? A function called `clearLayers()` in Task 3 but `clearFullLayers()` in Task 7 is a bug. - -If you find issues, fix them inline. No need to re-review — just fix and move on. If you find a spec requirement with no task, add the task. +**Review loop guidance:** +- Same agent that wrote the plan fixes it (preserves context) +- If loop exceeds 3 iterations, surface to human for guidance +- Reviewers are advisory — explain disagreements if you believe feedback is incorrect ## Execution Handoff From 3f80f1c769d8a172ee9803d049253f15fbe4895b Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 10:44:09 -0700 Subject: [PATCH 11/46] Reapply "Replace subagent review loops with lightweight inline self-review" This reverts commit b045fa3950f4adffd9228d6ddb15aaee7ab2a556. --- skills/brainstorming/SKILL.md | 24 ++++++++++++------------ skills/writing-plans/SKILL.md | 31 +++++++++++++++++++------------ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index edbc2b57..06cd0a21 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -27,7 +27,7 @@ You MUST create a task for each of these items and complete them in order: 4. **Propose 2-3 approaches** — with trade-offs and your recommendation 5. **Present design** — in sections scaled to their complexity, get user approval after each section 6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD--design.md` and commit -7. **Spec review loop** — dispatch spec-document-reviewer subagent with precisely crafted review context (never your session history); fix issues and re-dispatch until approved (max 3 iterations, then surface to human) +7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below) 8. **User reviews written spec** — ask user to review the spec file before proceeding 9. **Transition to implementation** — invoke writing-plans skill to create implementation plan @@ -43,8 +43,7 @@ digraph brainstorming { "Present design sections" [shape=box]; "User approves design?" [shape=diamond]; "Write design doc" [shape=box]; - "Spec review loop" [shape=box]; - "Spec review passed?" [shape=diamond]; + "Spec self-review\n(fix inline)" [shape=box]; "User reviews spec?" [shape=diamond]; "Invoke writing-plans skill" [shape=doublecircle]; @@ -57,10 +56,8 @@ digraph brainstorming { "Present design sections" -> "User approves design?"; "User approves design?" -> "Present design sections" [label="no, revise"]; "User approves design?" -> "Write design doc" [label="yes"]; - "Write design doc" -> "Spec review loop"; - "Spec review loop" -> "Spec review passed?"; - "Spec review passed?" -> "Spec review loop" [label="issues found,\nfix and re-dispatch"]; - "Spec review passed?" -> "User reviews spec?" [label="approved"]; + "Write design doc" -> "Spec self-review\n(fix inline)"; + "Spec self-review\n(fix inline)" -> "User reviews spec?"; "User reviews spec?" -> "Write design doc" [label="changes requested"]; "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"]; } @@ -116,12 +113,15 @@ digraph brainstorming { - Use elements-of-style:writing-clearly-and-concisely skill if available - Commit the design document to git -**Spec Review Loop:** -After writing the spec document: +**Spec Self-Review:** +After writing the spec document, look at it with fresh eyes: -1. Dispatch spec-document-reviewer subagent (see spec-document-reviewer-prompt.md) -2. If Issues Found: fix, re-dispatch, repeat until Approved -3. If loop exceeds 3 iterations, surface to human for guidance +1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them. +2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions? +3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition? +4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit. + +Fix any issues inline. No need to re-review — just fix and move on. **User Review Gate:** After the spec review loop passes, ask the user to review the written spec before proceeding: diff --git a/skills/writing-plans/SKILL.md b/skills/writing-plans/SKILL.md index 60f9834f..0d9c00ba 100644 --- a/skills/writing-plans/SKILL.md +++ b/skills/writing-plans/SKILL.md @@ -103,26 +103,33 @@ git commit -m "feat: add specific feature" ``` ```` +## No Placeholders + +Every step must contain the actual content an engineer needs. These are **plan failures** — never write them: +- "TBD", "TODO", "implement later", "fill in details" +- "Add appropriate error handling" / "add validation" / "handle edge cases" +- "Write tests for the above" (without actual test code) +- "Similar to Task N" (repeat the code — the engineer may be reading tasks out of order) +- Steps that describe what to do without showing how (code blocks required for code steps) +- References to types, functions, or methods not defined in any task + ## Remember - Exact file paths always -- Complete code in plan (not "add validation") +- Complete code in every step — if a step changes code, show the code - Exact commands with expected output -- Reference relevant skills with @ syntax - DRY, YAGNI, TDD, frequent commits -## Plan Review Loop +## Self-Review -After writing the complete plan: +After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch. -1. Dispatch a single plan-document-reviewer subagent (see plan-document-reviewer-prompt.md) with precisely crafted review context — never your session history. This keeps the reviewer focused on the plan, not your thought process. - - Provide: path to the plan document, path to spec document -2. If ❌ Issues Found: fix the issues, re-dispatch reviewer for the whole plan -3. If ✅ Approved: proceed to execution handoff +**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps. -**Review loop guidance:** -- Same agent that wrote the plan fixes it (preserves context) -- If loop exceeds 3 iterations, surface to human for guidance -- Reviewers are advisory — explain disagreements if you believe feedback is incorrect +**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them. + +**3. Type consistency:** Do the types, method signatures, and property names you used in later tasks match what you defined in earlier tasks? A function called `clearLayers()` in Task 3 but `clearFullLayers()` in Task 7 is a bug. + +If you find issues, fix them inline. No need to re-review — just fix and move on. If you find a spec requirement with no task, add the task. ## Execution Handoff From a1155f623fbf9ff321a064fe7a2714ffa68b97fc Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 10:50:38 -0700 Subject: [PATCH 12/46] Add v5.0.6 release notes --- RELEASE-NOTES.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 362a6512..835457a6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,26 @@ # Superpowers Release Notes +## v5.0.6 (2026-03-24) + +### Inline Self-Review Replaces Subagent Review Loops + +The subagent review loop (dispatching a fresh agent to review plans/specs) doubled execution time (~25 min overhead) without measurably improving plan quality. Regression testing across 5 versions with 5 trials each showed identical quality scores regardless of whether the review loop ran. + +- **brainstorming** — replaced Spec Review Loop (subagent dispatch + 3-iteration cap) with inline Spec Self-Review checklist: placeholder scan, internal consistency, scope check, ambiguity check +- **writing-plans** — replaced Plan Review Loop (subagent dispatch + 3-iteration cap) with inline Self-Review checklist: spec coverage, placeholder scan, type consistency +- **writing-plans** — added explicit "No Placeholders" section defining plan failures (TBD, vague descriptions, undefined references, "similar to Task N") +- Self-review catches 3-5 real bugs per run in ~30s instead of ~25 min, with comparable defect rates to the subagent approach + +### Bug Fixes + +- **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) + +### Codex App Compatibility + +- **codex-tools** — added named agent dispatch mapping documenting how to translate Claude Code's named agent types to Codex's `spawn_agent` with worker roles (PR #647 by @arittr) +- **codex-tools** — added environment detection and Codex App finishing sections for worktree-aware skills (by @arittr) +- **Design spec** — added Codex App compatibility design spec (PRI-823) covering read-only environment detection, worktree-safe skill behavior, and sandbox fallback patterns (by @arittr) + ## v5.0.5 (2026-03-17) ### Bug Fixes From 151cfb16a0901275e493b46fe591f391e91924ea Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 10:56:12 -0700 Subject: [PATCH 13/46] Move brainstorm server metadata to .meta/ subdirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Metadata files (.server-info, .events, .server.pid, .server.log, .server-stopped) were stored in the same directory served over HTTP, making them accessible via the /files/ route. They now live in a .meta/ subdirectory that is not web-accessible. Also fixes a stale test assertion ("Waiting for Claude" → "Waiting for the agent"). Reported-By: 吉田仁 --- RELEASE-NOTES.md | 1 + skills/brainstorming/scripts/server.cjs | 12 +++++++----- skills/brainstorming/scripts/start-server.sh | 9 +++++---- skills/brainstorming/scripts/stop-server.sh | 5 +++-- skills/brainstorming/visual-companion.md | 16 ++++++++-------- tests/brainstorm-server/server.test.js | 20 ++++++++++---------- 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 835457a6..9b132652 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -13,6 +13,7 @@ The subagent review loop (dispatching a fresh agent to review plans/specs) doubl ### Bug Fixes +- **Brainstorm server metadata isolation** — metadata files (`.server-info`, `.events`, `.server.pid`, `.server.log`, `.server-stopped`) are now written to a `.meta/` subdirectory within the session directory, so they are not accessible over the web server's `/files/` route. (Reported by 吉田仁) - **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) ### Codex App Compatibility diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index 86c3080f..7a77f3fa 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -77,6 +77,7 @@ const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; +const META_DIR = path.join(SCREEN_DIR, '.meta'); const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; const MIME_TYPES = { @@ -230,7 +231,7 @@ function handleMessage(text) { touchActivity(); console.log(JSON.stringify({ source: 'user-event', ...event })); if (event.choice) { - const eventsFile = path.join(SCREEN_DIR, '.events'); + const eventsFile = path.join(META_DIR, '.events'); fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); } } @@ -259,6 +260,7 @@ const debounceTimers = new Map(); function startServer() { if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true }); + if (!fs.existsSync(META_DIR)) fs.mkdirSync(META_DIR, { recursive: true }); // Track known files to distinguish new screens from updates. // macOS fs.watch reports 'rename' for both new files and overwrites, @@ -283,7 +285,7 @@ function startServer() { if (!knownFiles.has(filename)) { knownFiles.add(filename); - const eventsFile = path.join(SCREEN_DIR, '.events'); + const eventsFile = path.join(META_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); console.log(JSON.stringify({ type: 'screen-added', file: filePath })); } else { @@ -297,10 +299,10 @@ function startServer() { function shutdown(reason) { console.log(JSON.stringify({ type: 'server-stopped', reason })); - const infoFile = path.join(SCREEN_DIR, '.server-info'); + const infoFile = path.join(META_DIR, '.server-info'); if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); fs.writeFileSync( - path.join(SCREEN_DIR, '.server-stopped'), + path.join(META_DIR, '.server-stopped'), JSON.stringify({ reason, timestamp: Date.now() }) + '\n' ); watcher.close(); @@ -327,7 +329,7 @@ function startServer() { screen_dir: SCREEN_DIR }); console.log(info); - fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n'); + fs.writeFileSync(path.join(META_DIR, '.server-info'), info + '\n'); }); } diff --git a/skills/brainstorming/scripts/start-server.sh b/skills/brainstorming/scripts/start-server.sh index a0ef2997..23c77b9b 100755 --- a/skills/brainstorming/scripts/start-server.sh +++ b/skills/brainstorming/scripts/start-server.sh @@ -83,11 +83,12 @@ else SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}" fi -PID_FILE="${SCREEN_DIR}/.server.pid" -LOG_FILE="${SCREEN_DIR}/.server.log" +META_DIR="${SCREEN_DIR}/.meta" +PID_FILE="${META_DIR}/.server.pid" +LOG_FILE="${META_DIR}/.server.log" -# Create fresh session directory -mkdir -p "$SCREEN_DIR" +# Create fresh session directory and metadata subdirectory +mkdir -p "$SCREEN_DIR" "$META_DIR" # Kill any existing server if [[ -f "$PID_FILE" ]]; then diff --git a/skills/brainstorming/scripts/stop-server.sh b/skills/brainstorming/scripts/stop-server.sh index 2e5973d4..82f2ef10 100755 --- a/skills/brainstorming/scripts/stop-server.sh +++ b/skills/brainstorming/scripts/stop-server.sh @@ -13,7 +13,8 @@ if [[ -z "$SCREEN_DIR" ]]; then exit 1 fi -PID_FILE="${SCREEN_DIR}/.server.pid" +META_DIR="${SCREEN_DIR}/.meta" +PID_FILE="${META_DIR}/.server.pid" if [[ -f "$PID_FILE" ]]; then pid=$(cat "$PID_FILE") @@ -42,7 +43,7 @@ if [[ -f "$PID_FILE" ]]; then exit 1 fi - rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log" + rm -f "$PID_FILE" "${META_DIR}/.server.log" # Only delete ephemeral /tmp directories if [[ "$SCREEN_DIR" == /tmp/* ]]; then diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 537ed3cf..344b090f 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -26,7 +26,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind ## How It Works -The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn. +The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to `$SCREEN_DIR/.meta/.events` that you read on your next turn. **Content fragments vs full documents:** If your HTML file starts with `/.superpowers/brainstorm/` for the session directory. +**Finding connection info:** The server writes its startup JSON to `$SCREEN_DIR/.meta/.server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `/.superpowers/brainstorm/` for the session directory. **Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there. @@ -61,7 +61,7 @@ scripts/start-server.sh --project-dir /path/to/project # across conversation turns. scripts/start-server.sh --project-dir /path/to/project ``` -When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.server-info` on the next turn to get the URL and port. +When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.meta/.server-info` on the next turn to get the URL and port. **Codex:** ```bash @@ -93,7 +93,7 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. ## The Loop 1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`: - - Before each write, check that `$SCREEN_DIR/.server-info` exists. If it doesn't (or `.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. + - Before each write, check that `$SCREEN_DIR/.meta/.server-info` exists. If it doesn't (or `.meta/.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` - **Never reuse filenames** — each screen gets a fresh file - Use Write tool — **never use cat/heredoc** (dumps noise into terminal) @@ -105,9 +105,9 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. - Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like." 3. **On your next turn** — after the user responds in the terminal: - - Read `$SCREEN_DIR/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines + - Read `$SCREEN_DIR/.meta/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines - Merge with the user's terminal text to get the full picture - - The terminal message is the primary feedback; `.events` provides structured interaction data + - The terminal message is the primary feedback; `.meta/.events` provides structured interaction data 4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated. @@ -244,7 +244,7 @@ The frame template provides these CSS classes for your content: ## Browser Events Format -When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.events` (one JSON object per line). The file is cleared automatically when you push a new screen. +When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.meta/.events` (one JSON object per line). The file is cleared automatically when you push a new screen. ```jsonl {"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101} @@ -254,7 +254,7 @@ When the user clicks options in the browser, their interactions are recorded to The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about. -If `.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. +If `.meta/.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. ## Design Tips diff --git a/tests/brainstorm-server/server.test.js b/tests/brainstorm-server/server.test.js index d1077a60..2e1f9af4 100644 --- a/tests/brainstorm-server/server.test.js +++ b/tests/brainstorm-server/server.test.js @@ -103,9 +103,9 @@ async function runTests() { return Promise.resolve(); }); - await test('writes .server-info file', () => { - const infoPath = path.join(TEST_DIR, '.server-info'); - assert(fs.existsSync(infoPath), '.server-info should exist'); + await test('writes .server-info file to .meta/', () => { + const infoPath = path.join(TEST_DIR, '.meta', '.server-info'); + assert(fs.existsSync(infoPath), '.meta/.server-info should exist'); const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8').trim()); assert.strictEqual(info.type, 'server-started'); assert.strictEqual(info.port, TEST_PORT); @@ -118,7 +118,7 @@ async function runTests() { await test('serves waiting page when no screens exist', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); - assert(res.body.includes('Waiting for Claude'), 'Should show waiting message'); + assert(res.body.includes('Waiting for the agent'), 'Should show waiting message'); }); await test('injects helper.js into waiting page', async () => { @@ -206,9 +206,9 @@ async function runTests() { ws.close(); }); - await test('writes choice events to .events file', async () => { + await test('writes choice events to .meta/.events file', async () => { // Clean up events from prior tests - const eventsFile = path.join(TEST_DIR, '.events'); + const eventsFile = path.join(TEST_DIR, '.meta', '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -225,8 +225,8 @@ async function runTests() { ws.close(); }); - await test('does NOT write non-choice events to .events file', async () => { - const eventsFile = path.join(TEST_DIR, '.events'); + await test('does NOT write non-choice events to .meta/.events file', async () => { + const eventsFile = path.join(TEST_DIR, '.meta', '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -347,9 +347,9 @@ async function runTests() { ws.close(); }); - await test('clears .events on new screen', async () => { + await test('clears .meta/.events on new screen', async () => { // Create an .events file - const eventsFile = path.join(TEST_DIR, '.events'); + const eventsFile = path.join(TEST_DIR, '.meta', '.events'); fs.writeFileSync(eventsFile, '{"choice":"a"}\n'); assert(fs.existsSync(eventsFile)); From 9e6e077d33f2c0985e662e8d9239b51e9fb1cbf2 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 10:58:33 -0700 Subject: [PATCH 14/46] Revert "Move brainstorm server metadata to .meta/ subdirectory" This reverts commit ab500dade6187cf78b9b263ecf4799ec3420d1ef. --- RELEASE-NOTES.md | 1 - skills/brainstorming/scripts/server.cjs | 12 +++++------- skills/brainstorming/scripts/start-server.sh | 9 ++++----- skills/brainstorming/scripts/stop-server.sh | 5 ++--- skills/brainstorming/visual-companion.md | 16 ++++++++-------- tests/brainstorm-server/server.test.js | 20 ++++++++++---------- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 9b132652..835457a6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -13,7 +13,6 @@ The subagent review loop (dispatching a fresh agent to review plans/specs) doubl ### Bug Fixes -- **Brainstorm server metadata isolation** — metadata files (`.server-info`, `.events`, `.server.pid`, `.server.log`, `.server-stopped`) are now written to a `.meta/` subdirectory within the session directory, so they are not accessible over the web server's `/files/` route. (Reported by 吉田仁) - **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) ### Codex App Compatibility diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index 7a77f3fa..86c3080f 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -77,7 +77,6 @@ const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; -const META_DIR = path.join(SCREEN_DIR, '.meta'); const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; const MIME_TYPES = { @@ -231,7 +230,7 @@ function handleMessage(text) { touchActivity(); console.log(JSON.stringify({ source: 'user-event', ...event })); if (event.choice) { - const eventsFile = path.join(META_DIR, '.events'); + const eventsFile = path.join(SCREEN_DIR, '.events'); fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); } } @@ -260,7 +259,6 @@ const debounceTimers = new Map(); function startServer() { if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true }); - if (!fs.existsSync(META_DIR)) fs.mkdirSync(META_DIR, { recursive: true }); // Track known files to distinguish new screens from updates. // macOS fs.watch reports 'rename' for both new files and overwrites, @@ -285,7 +283,7 @@ function startServer() { if (!knownFiles.has(filename)) { knownFiles.add(filename); - const eventsFile = path.join(META_DIR, '.events'); + const eventsFile = path.join(SCREEN_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); console.log(JSON.stringify({ type: 'screen-added', file: filePath })); } else { @@ -299,10 +297,10 @@ function startServer() { function shutdown(reason) { console.log(JSON.stringify({ type: 'server-stopped', reason })); - const infoFile = path.join(META_DIR, '.server-info'); + const infoFile = path.join(SCREEN_DIR, '.server-info'); if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); fs.writeFileSync( - path.join(META_DIR, '.server-stopped'), + path.join(SCREEN_DIR, '.server-stopped'), JSON.stringify({ reason, timestamp: Date.now() }) + '\n' ); watcher.close(); @@ -329,7 +327,7 @@ function startServer() { screen_dir: SCREEN_DIR }); console.log(info); - fs.writeFileSync(path.join(META_DIR, '.server-info'), info + '\n'); + fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n'); }); } diff --git a/skills/brainstorming/scripts/start-server.sh b/skills/brainstorming/scripts/start-server.sh index 23c77b9b..a0ef2997 100755 --- a/skills/brainstorming/scripts/start-server.sh +++ b/skills/brainstorming/scripts/start-server.sh @@ -83,12 +83,11 @@ else SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}" fi -META_DIR="${SCREEN_DIR}/.meta" -PID_FILE="${META_DIR}/.server.pid" -LOG_FILE="${META_DIR}/.server.log" +PID_FILE="${SCREEN_DIR}/.server.pid" +LOG_FILE="${SCREEN_DIR}/.server.log" -# Create fresh session directory and metadata subdirectory -mkdir -p "$SCREEN_DIR" "$META_DIR" +# Create fresh session directory +mkdir -p "$SCREEN_DIR" # Kill any existing server if [[ -f "$PID_FILE" ]]; then diff --git a/skills/brainstorming/scripts/stop-server.sh b/skills/brainstorming/scripts/stop-server.sh index 82f2ef10..2e5973d4 100755 --- a/skills/brainstorming/scripts/stop-server.sh +++ b/skills/brainstorming/scripts/stop-server.sh @@ -13,8 +13,7 @@ if [[ -z "$SCREEN_DIR" ]]; then exit 1 fi -META_DIR="${SCREEN_DIR}/.meta" -PID_FILE="${META_DIR}/.server.pid" +PID_FILE="${SCREEN_DIR}/.server.pid" if [[ -f "$PID_FILE" ]]; then pid=$(cat "$PID_FILE") @@ -43,7 +42,7 @@ if [[ -f "$PID_FILE" ]]; then exit 1 fi - rm -f "$PID_FILE" "${META_DIR}/.server.log" + rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log" # Only delete ephemeral /tmp directories if [[ "$SCREEN_DIR" == /tmp/* ]]; then diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 344b090f..537ed3cf 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -26,7 +26,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind ## How It Works -The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to `$SCREEN_DIR/.meta/.events` that you read on your next turn. +The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn. **Content fragments vs full documents:** If your HTML file starts with `/.superpowers/brainstorm/` for the session directory. +**Finding connection info:** The server writes its startup JSON to `$SCREEN_DIR/.server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `/.superpowers/brainstorm/` for the session directory. **Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there. @@ -61,7 +61,7 @@ scripts/start-server.sh --project-dir /path/to/project # across conversation turns. scripts/start-server.sh --project-dir /path/to/project ``` -When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.meta/.server-info` on the next turn to get the URL and port. +When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.server-info` on the next turn to get the URL and port. **Codex:** ```bash @@ -93,7 +93,7 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. ## The Loop 1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`: - - Before each write, check that `$SCREEN_DIR/.meta/.server-info` exists. If it doesn't (or `.meta/.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. + - Before each write, check that `$SCREEN_DIR/.server-info` exists. If it doesn't (or `.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` - **Never reuse filenames** — each screen gets a fresh file - Use Write tool — **never use cat/heredoc** (dumps noise into terminal) @@ -105,9 +105,9 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. - Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like." 3. **On your next turn** — after the user responds in the terminal: - - Read `$SCREEN_DIR/.meta/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines + - Read `$SCREEN_DIR/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines - Merge with the user's terminal text to get the full picture - - The terminal message is the primary feedback; `.meta/.events` provides structured interaction data + - The terminal message is the primary feedback; `.events` provides structured interaction data 4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated. @@ -244,7 +244,7 @@ The frame template provides these CSS classes for your content: ## Browser Events Format -When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.meta/.events` (one JSON object per line). The file is cleared automatically when you push a new screen. +When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.events` (one JSON object per line). The file is cleared automatically when you push a new screen. ```jsonl {"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101} @@ -254,7 +254,7 @@ When the user clicks options in the browser, their interactions are recorded to The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about. -If `.meta/.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. +If `.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. ## Design Tips diff --git a/tests/brainstorm-server/server.test.js b/tests/brainstorm-server/server.test.js index 2e1f9af4..d1077a60 100644 --- a/tests/brainstorm-server/server.test.js +++ b/tests/brainstorm-server/server.test.js @@ -103,9 +103,9 @@ async function runTests() { return Promise.resolve(); }); - await test('writes .server-info file to .meta/', () => { - const infoPath = path.join(TEST_DIR, '.meta', '.server-info'); - assert(fs.existsSync(infoPath), '.meta/.server-info should exist'); + await test('writes .server-info file', () => { + const infoPath = path.join(TEST_DIR, '.server-info'); + assert(fs.existsSync(infoPath), '.server-info should exist'); const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8').trim()); assert.strictEqual(info.type, 'server-started'); assert.strictEqual(info.port, TEST_PORT); @@ -118,7 +118,7 @@ async function runTests() { await test('serves waiting page when no screens exist', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); - assert(res.body.includes('Waiting for the agent'), 'Should show waiting message'); + assert(res.body.includes('Waiting for Claude'), 'Should show waiting message'); }); await test('injects helper.js into waiting page', async () => { @@ -206,9 +206,9 @@ async function runTests() { ws.close(); }); - await test('writes choice events to .meta/.events file', async () => { + await test('writes choice events to .events file', async () => { // Clean up events from prior tests - const eventsFile = path.join(TEST_DIR, '.meta', '.events'); + const eventsFile = path.join(TEST_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -225,8 +225,8 @@ async function runTests() { ws.close(); }); - await test('does NOT write non-choice events to .meta/.events file', async () => { - const eventsFile = path.join(TEST_DIR, '.meta', '.events'); + await test('does NOT write non-choice events to .events file', async () => { + const eventsFile = path.join(TEST_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -347,9 +347,9 @@ async function runTests() { ws.close(); }); - await test('clears .meta/.events on new screen', async () => { + await test('clears .events on new screen', async () => { // Create an .events file - const eventsFile = path.join(TEST_DIR, '.meta', '.events'); + const eventsFile = path.join(TEST_DIR, '.events'); fs.writeFileSync(eventsFile, '{"choice":"a"}\n'); assert(fs.existsSync(eventsFile)); From 9e3ed213a04315e57055d98ef8dd78bff6b63683 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 11:07:59 -0700 Subject: [PATCH 15/46] Separate brainstorm server content and state into peer directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session directory now contains two peers: content/ (HTML served to the browser) and state/ (events, server-info, pid, log). Previously all files shared a single directory, making server state and user interaction data accessible over the /files/ HTTP route. Also fixes stale test assertion ("Waiting for Claude" → "Waiting for the agent"). Reported-By: 吉田仁 --- RELEASE-NOTES.md | 4 ++ skills/brainstorming/scripts/server.cjs | 31 ++++++----- skills/brainstorming/scripts/start-server.sh | 17 +++--- skills/brainstorming/scripts/stop-server.sh | 17 +++--- skills/brainstorming/visual-companion.md | 23 ++++---- tests/brainstorm-server/server.test.js | 55 +++++++++++--------- 6 files changed, 80 insertions(+), 67 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 835457a6..ee30ff77 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -11,6 +11,10 @@ The subagent review loop (dispatching a fresh agent to review plans/specs) doubl - **writing-plans** — added explicit "No Placeholders" section defining plan failures (TBD, vague descriptions, undefined references, "similar to Task N") - Self-review catches 3-5 real bugs per run in ~30s instead of ~25 min, with comparable defect rates to the subagent approach +### Brainstorm Server + +- **Session directory restructured** — the brainstorm server session directory now contains two peer subdirectories: `content/` (HTML files served to the browser) and `state/` (events, server-info, pid, log). Previously, server state and user interaction data were stored alongside served content, making them accessible over HTTP. The `screen_dir` and `state_dir` paths are both included in the server-started JSON. (Reported by 吉田仁) + ### Bug Fixes - **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index 86c3080f..e139c13f 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -76,7 +76,9 @@ function decodeFrame(buffer) { const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383)); const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); -const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; +const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; +const CONTENT_DIR = path.join(SESSION_DIR, 'content'); +const STATE_DIR = path.join(SESSION_DIR, 'state'); const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; const MIME_TYPES = { @@ -112,10 +114,10 @@ function wrapInFrame(content) { } function getNewestScreen() { - const files = fs.readdirSync(SCREEN_DIR) + const files = fs.readdirSync(CONTENT_DIR) .filter(f => f.endsWith('.html')) .map(f => { - const fp = path.join(SCREEN_DIR, f); + const fp = path.join(CONTENT_DIR, f); return { path: fp, mtime: fs.statSync(fp).mtime.getTime() }; }) .sort((a, b) => b.mtime - a.mtime); @@ -142,7 +144,7 @@ function handleRequest(req, res) { res.end(html); } else if (req.method === 'GET' && req.url.startsWith('/files/')) { const fileName = req.url.slice(7); - const filePath = path.join(SCREEN_DIR, path.basename(fileName)); + const filePath = path.join(CONTENT_DIR, path.basename(fileName)); if (!fs.existsSync(filePath)) { res.writeHead(404); res.end('Not found'); @@ -230,7 +232,7 @@ function handleMessage(text) { touchActivity(); console.log(JSON.stringify({ source: 'user-event', ...event })); if (event.choice) { - const eventsFile = path.join(SCREEN_DIR, '.events'); + const eventsFile = path.join(STATE_DIR, 'events'); fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); } } @@ -258,32 +260,33 @@ const debounceTimers = new Map(); // ========== Server Startup ========== function startServer() { - if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true }); + if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true }); + if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); // Track known files to distinguish new screens from updates. // macOS fs.watch reports 'rename' for both new files and overwrites, // so we can't rely on eventType alone. const knownFiles = new Set( - fs.readdirSync(SCREEN_DIR).filter(f => f.endsWith('.html')) + fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html')) ); const server = http.createServer(handleRequest); server.on('upgrade', handleUpgrade); - const watcher = fs.watch(SCREEN_DIR, (eventType, filename) => { + const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => { if (!filename || !filename.endsWith('.html')) return; if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename)); debounceTimers.set(filename, setTimeout(() => { debounceTimers.delete(filename); - const filePath = path.join(SCREEN_DIR, filename); + const filePath = path.join(CONTENT_DIR, filename); if (!fs.existsSync(filePath)) return; // file was deleted touchActivity(); if (!knownFiles.has(filename)) { knownFiles.add(filename); - const eventsFile = path.join(SCREEN_DIR, '.events'); + const eventsFile = path.join(STATE_DIR, 'events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); console.log(JSON.stringify({ type: 'screen-added', file: filePath })); } else { @@ -297,10 +300,10 @@ function startServer() { function shutdown(reason) { console.log(JSON.stringify({ type: 'server-stopped', reason })); - const infoFile = path.join(SCREEN_DIR, '.server-info'); + const infoFile = path.join(STATE_DIR, 'server-info'); if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); fs.writeFileSync( - path.join(SCREEN_DIR, '.server-stopped'), + path.join(STATE_DIR, 'server-stopped'), JSON.stringify({ reason, timestamp: Date.now() }) + '\n' ); watcher.close(); @@ -324,10 +327,10 @@ function startServer() { const info = JSON.stringify({ type: 'server-started', port: Number(PORT), host: HOST, url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT, - screen_dir: SCREEN_DIR + screen_dir: CONTENT_DIR, state_dir: STATE_DIR }); console.log(info); - fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n'); + fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n'); }); } diff --git a/skills/brainstorming/scripts/start-server.sh b/skills/brainstorming/scripts/start-server.sh index a0ef2997..c3dd7f98 100755 --- a/skills/brainstorming/scripts/start-server.sh +++ b/skills/brainstorming/scripts/start-server.sh @@ -78,16 +78,17 @@ fi SESSION_ID="$$-$(date +%s)" if [[ -n "$PROJECT_DIR" ]]; then - SCREEN_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}" + SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}" else - SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}" + SESSION_DIR="/tmp/brainstorm-${SESSION_ID}" fi -PID_FILE="${SCREEN_DIR}/.server.pid" -LOG_FILE="${SCREEN_DIR}/.server.log" +STATE_DIR="${SESSION_DIR}/state" +PID_FILE="${STATE_DIR}/server.pid" +LOG_FILE="${STATE_DIR}/server.log" -# Create fresh session directory -mkdir -p "$SCREEN_DIR" +# Create fresh session directory with content and state peers +mkdir -p "${SESSION_DIR}/content" "$STATE_DIR" # Kill any existing server if [[ -f "$PID_FILE" ]]; then @@ -115,13 +116,13 @@ esac # Foreground mode for environments that reap detached/background processes. if [[ "$FOREGROUND" == "true" ]]; then echo "$$" > "$PID_FILE" - env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs + env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs exit $? fi # Start server, capturing output to log file # Use nohup to survive shell exit; disown to remove from job table -nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 & +nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 & SERVER_PID=$! disown "$SERVER_PID" 2>/dev/null echo "$SERVER_PID" > "$PID_FILE" diff --git a/skills/brainstorming/scripts/stop-server.sh b/skills/brainstorming/scripts/stop-server.sh index 2e5973d4..a6b94e65 100755 --- a/skills/brainstorming/scripts/stop-server.sh +++ b/skills/brainstorming/scripts/stop-server.sh @@ -1,19 +1,20 @@ #!/usr/bin/env bash # Stop the brainstorm server and clean up -# Usage: stop-server.sh +# Usage: stop-server.sh # # Kills the server process. Only deletes session directory if it's # under /tmp (ephemeral). Persistent directories (.superpowers/) are # kept so mockups can be reviewed later. -SCREEN_DIR="$1" +SESSION_DIR="$1" -if [[ -z "$SCREEN_DIR" ]]; then - echo '{"error": "Usage: stop-server.sh "}' +if [[ -z "$SESSION_DIR" ]]; then + echo '{"error": "Usage: stop-server.sh "}' exit 1 fi -PID_FILE="${SCREEN_DIR}/.server.pid" +STATE_DIR="${SESSION_DIR}/state" +PID_FILE="${STATE_DIR}/server.pid" if [[ -f "$PID_FILE" ]]; then pid=$(cat "$PID_FILE") @@ -42,11 +43,11 @@ if [[ -f "$PID_FILE" ]]; then exit 1 fi - rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log" + rm -f "$PID_FILE" "${STATE_DIR}/server.log" # Only delete ephemeral /tmp directories - if [[ "$SCREEN_DIR" == /tmp/* ]]; then - rm -rf "$SCREEN_DIR" + if [[ "$SESSION_DIR" == /tmp/* ]]; then + rm -rf "$SESSION_DIR" fi echo '{"status": "stopped"}' diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 537ed3cf..2113863d 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -26,7 +26,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind ## How It Works -The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn. +The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn. **Content fragments vs full documents:** If your HTML file starts with `/.superpowers/brainstorm/` for the session directory. +**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `/.superpowers/brainstorm/` for the session directory. **Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there. @@ -61,7 +62,7 @@ scripts/start-server.sh --project-dir /path/to/project # across conversation turns. scripts/start-server.sh --project-dir /path/to/project ``` -When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.server-info` on the next turn to get the URL and port. +When calling this via the Bash tool, set `run_in_background: true`. Then read `$STATE_DIR/server-info` on the next turn to get the URL and port. **Codex:** ```bash @@ -93,7 +94,7 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. ## The Loop 1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`: - - Before each write, check that `$SCREEN_DIR/.server-info` exists. If it doesn't (or `.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. + - Before each write, check that `$STATE_DIR/server-info` exists. If it doesn't (or `$STATE_DIR/server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` - **Never reuse filenames** — each screen gets a fresh file - Use Write tool — **never use cat/heredoc** (dumps noise into terminal) @@ -105,9 +106,9 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. - Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like." 3. **On your next turn** — after the user responds in the terminal: - - Read `$SCREEN_DIR/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines + - Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines - Merge with the user's terminal text to get the full picture - - The terminal message is the primary feedback; `.events` provides structured interaction data + - The terminal message is the primary feedback; `state_dir/events` provides structured interaction data 4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated. @@ -244,7 +245,7 @@ The frame template provides these CSS classes for your content: ## Browser Events Format -When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.events` (one JSON object per line). The file is cleared automatically when you push a new screen. +When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen. ```jsonl {"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101} @@ -254,7 +255,7 @@ When the user clicks options in the browser, their interactions are recorded to The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about. -If `.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. +If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text. ## Design Tips @@ -275,7 +276,7 @@ If `.events` doesn't exist, the user didn't interact with the browser — use on ## Cleaning Up ```bash -scripts/stop-server.sh $SCREEN_DIR +scripts/stop-server.sh $SESSION_DIR ``` If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop. diff --git a/tests/brainstorm-server/server.test.js b/tests/brainstorm-server/server.test.js index d1077a60..4797cbb9 100644 --- a/tests/brainstorm-server/server.test.js +++ b/tests/brainstorm-server/server.test.js @@ -18,6 +18,8 @@ const assert = require('assert'); const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs'); const TEST_PORT = 3334; const TEST_DIR = '/tmp/brainstorm-test'; +const CONTENT_DIR = path.join(TEST_DIR, 'content'); +const STATE_DIR = path.join(TEST_DIR, 'state'); function cleanup() { if (fs.existsSync(TEST_DIR)) { @@ -69,7 +71,6 @@ async function waitForServer(server) { async function runTests() { cleanup(); - fs.mkdirSync(TEST_DIR, { recursive: true }); const server = startServer(); let stdoutAccum = ''; @@ -103,12 +104,14 @@ async function runTests() { return Promise.resolve(); }); - await test('writes .server-info file', () => { - const infoPath = path.join(TEST_DIR, '.server-info'); - assert(fs.existsSync(infoPath), '.server-info should exist'); + await test('writes server-info to state/', () => { + const infoPath = path.join(STATE_DIR, 'server-info'); + assert(fs.existsSync(infoPath), 'state/server-info should exist'); const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8').trim()); assert.strictEqual(info.type, 'server-started'); assert.strictEqual(info.port, TEST_PORT); + assert.strictEqual(info.screen_dir, CONTENT_DIR, 'screen_dir should point to content/'); + assert.strictEqual(info.state_dir, STATE_DIR, 'state_dir should point to state/'); return Promise.resolve(); }); @@ -118,7 +121,7 @@ async function runTests() { await test('serves waiting page when no screens exist', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); - assert(res.body.includes('Waiting for Claude'), 'Should show waiting message'); + assert(res.body.includes('Waiting for the agent'), 'Should show waiting message'); }); await test('injects helper.js into waiting page', async () => { @@ -135,7 +138,7 @@ async function runTests() { await test('serves full HTML documents as-is (not wrapped)', async () => { const fullDoc = '\nCustom

Custom Page

'; - fs.writeFileSync(path.join(TEST_DIR, 'full-doc.html'), fullDoc); + fs.writeFileSync(path.join(CONTENT_DIR, 'full-doc.html'), fullDoc); await sleep(300); const res = await fetch(`http://localhost:${TEST_PORT}/`); @@ -146,7 +149,7 @@ async function runTests() { await test('wraps content fragments in frame template', async () => { const fragment = '

Pick a layout

\n
A
'; - fs.writeFileSync(path.join(TEST_DIR, 'fragment.html'), fragment); + fs.writeFileSync(path.join(CONTENT_DIR, 'fragment.html'), fragment); await sleep(300); const res = await fetch(`http://localhost:${TEST_PORT}/`); @@ -157,9 +160,9 @@ async function runTests() { }); await test('serves newest file by mtime', async () => { - fs.writeFileSync(path.join(TEST_DIR, 'older.html'), '

Older

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'older.html'), '

Older

'); await sleep(100); - fs.writeFileSync(path.join(TEST_DIR, 'newer.html'), '

Newer

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'newer.html'), '

Newer

'); await sleep(300); const res = await fetch(`http://localhost:${TEST_PORT}/`); @@ -168,7 +171,7 @@ async function runTests() { await test('ignores non-html files for serving', async () => { // Write a newer non-HTML file — should still serve newest .html - fs.writeFileSync(path.join(TEST_DIR, 'data.json'), '{"not": "html"}'); + fs.writeFileSync(path.join(CONTENT_DIR, 'data.json'), '{"not": "html"}'); await sleep(300); const res = await fetch(`http://localhost:${TEST_PORT}/`); @@ -206,9 +209,9 @@ async function runTests() { ws.close(); }); - await test('writes choice events to .events file', async () => { + await test('writes choice events to state/events', async () => { // Clean up events from prior tests - const eventsFile = path.join(TEST_DIR, '.events'); + const eventsFile = path.join(STATE_DIR, 'events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -225,8 +228,8 @@ async function runTests() { ws.close(); }); - await test('does NOT write non-choice events to .events file', async () => { - const eventsFile = path.join(TEST_DIR, '.events'); + await test('does NOT write non-choice events to state/events', async () => { + const eventsFile = path.join(STATE_DIR, 'events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -257,7 +260,7 @@ async function runTests() { if (JSON.parse(data.toString()).type === 'reload') ws2Reload = true; }); - fs.writeFileSync(path.join(TEST_DIR, 'multi-client.html'), '

Multi

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'multi-client.html'), '

Multi

'); await sleep(500); assert(ws1Reload, 'Client 1 should receive reload'); @@ -273,7 +276,7 @@ async function runTests() { await sleep(100); // This should not throw even though ws1 is closed - fs.writeFileSync(path.join(TEST_DIR, 'after-close.html'), '

After

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'after-close.html'), '

After

'); await sleep(300); // If we got here without error, the test passes }); @@ -304,7 +307,7 @@ async function runTests() { if (JSON.parse(data.toString()).type === 'reload') gotReload = true; }); - fs.writeFileSync(path.join(TEST_DIR, 'watch-new.html'), '

New

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'watch-new.html'), '

New

'); await sleep(500); assert(gotReload, 'Should send reload on new file'); @@ -312,7 +315,7 @@ async function runTests() { }); await test('sends reload on .html file change', async () => { - const filePath = path.join(TEST_DIR, 'watch-change.html'); + const filePath = path.join(CONTENT_DIR, 'watch-change.html'); fs.writeFileSync(filePath, '

Original

'); await sleep(500); @@ -340,35 +343,35 @@ async function runTests() { if (JSON.parse(data.toString()).type === 'reload') gotReload = true; }); - fs.writeFileSync(path.join(TEST_DIR, 'data.txt'), 'not html'); + fs.writeFileSync(path.join(CONTENT_DIR, 'data.txt'), 'not html'); await sleep(500); assert(!gotReload, 'Should NOT reload for non-HTML files'); ws.close(); }); - await test('clears .events on new screen', async () => { - // Create an .events file - const eventsFile = path.join(TEST_DIR, '.events'); + await test('clears state/events on new screen', async () => { + // Create an events file + const eventsFile = path.join(STATE_DIR, 'events'); fs.writeFileSync(eventsFile, '{"choice":"a"}\n'); assert(fs.existsSync(eventsFile)); - fs.writeFileSync(path.join(TEST_DIR, 'clear-events.html'), '

New screen

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'clear-events.html'), '

New screen

'); await sleep(500); - assert(!fs.existsSync(eventsFile), '.events should be cleared on new screen'); + assert(!fs.existsSync(eventsFile), 'state/events should be cleared on new screen'); }); await test('logs screen-added on new file', async () => { stdoutAccum = ''; - fs.writeFileSync(path.join(TEST_DIR, 'log-test.html'), '

Log

'); + fs.writeFileSync(path.join(CONTENT_DIR, 'log-test.html'), '

Log

'); await sleep(500); assert(stdoutAccum.includes('screen-added'), 'Should log screen-added'); }); await test('logs screen-updated on file change', async () => { - const filePath = path.join(TEST_DIR, 'log-update.html'); + const filePath = path.join(CONTENT_DIR, 'log-update.html'); fs.writeFileSync(filePath, '

V1

'); await sleep(500); From f076bd3431dd2826402bb91be7a8b95350a34515 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 11:46:29 -0700 Subject: [PATCH 16/46] Fix owner-PID false positive when owner runs as different user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ownerAlive() treated EPERM (permission denied) the same as ESRCH (process not found), causing the server to self-terminate within 60s whenever the owner process ran as a different user. This affected WSL (owner is a Windows process), Tailscale SSH, and any cross-user scenario. The fix: `return e.code === 'EPERM'` — if we get permission denied, the process is alive; we just can't signal it. Tested on Linux via Tailscale SSH with a root-owned grandparent PID: - Server survives past the 60s lifecycle check (EPERM = alive) - Server still shuts down when owner genuinely dies (ESRCH = dead) Fixes #879 --- RELEASE-NOTES.md | 1 + skills/brainstorming/scripts/server.cjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index ee30ff77..2abd1463 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -17,6 +17,7 @@ The subagent review loop (dispatching a fresh agent to review plans/specs) doubl ### Bug Fixes +- **Owner-PID false positives** — the brainstorm server's `ownerAlive()` check treated EPERM (permission denied) the same as ESRCH (process not found), causing the server to self-terminate within 60 seconds whenever the owner process ran as a different user. This affected WSL (owner is a Windows process), Tailscale SSH, and any cross-user scenario. Fixed by treating EPERM as "alive". (#879) - **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) ### Codex App Compatibility diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index e139c13f..14b8197c 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -313,7 +313,7 @@ function startServer() { function ownerAlive() { if (!OWNER_PID) return true; - try { process.kill(OWNER_PID, 0); return true; } catch (e) { return false; } + try { process.kill(OWNER_PID, 0); return true; } catch (e) { return e.code === 'EPERM'; } } // Check every 60s: exit if owner process died or idle for 30 minutes From 9f04f0635114d09ca054778e2dd44942efd1c008 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 14:39:20 -0700 Subject: [PATCH 17/46] Fix owner-PID lifecycle monitoring for cross-platform reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs caused the brainstorm server to self-terminate within 60s: 1. ownerAlive() treated EPERM (permission denied) as "process dead". When the owner PID belongs to a different user (Tailscale SSH, system daemons), process.kill(pid, 0) throws EPERM — but the process IS alive. Fixed: return e.code === 'EPERM'. 2. On WSL, the grandparent PID resolves to a short-lived subprocess that exits before the first 60s lifecycle check. The PID is genuinely dead (ESRCH), so the EPERM fix alone doesn't help. Fixed: validate the owner PID at server startup — if it's already dead, it was a bad resolution, so disable monitoring and rely on the 30-minute idle timeout. This also removes the Windows/MSYS2-specific OWNER_PID="" carve-out from start-server.sh, since the server now handles invalid PIDs generically at startup regardless of platform. Tested on Linux (magic-kingdom) via Tailscale SSH: - Root-owned owner PID (EPERM): server survives ✓ - Dead owner PID at startup (WSL sim): monitoring disabled, survives ✓ - Valid owner that dies: server shuts down within 60s ✓ Fixes #879 --- RELEASE-NOTES.md | 2 +- skills/brainstorming/scripts/server.cjs | 19 ++++++++++++++++--- skills/brainstorming/scripts/start-server.sh | 6 ------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 2abd1463..c73a7165 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -17,7 +17,7 @@ The subagent review loop (dispatching a fresh agent to review plans/specs) doubl ### Bug Fixes -- **Owner-PID false positives** — the brainstorm server's `ownerAlive()` check treated EPERM (permission denied) the same as ESRCH (process not found), causing the server to self-terminate within 60 seconds whenever the owner process ran as a different user. This affected WSL (owner is a Windows process), Tailscale SSH, and any cross-user scenario. Fixed by treating EPERM as "alive". (#879) +- **Owner-PID lifecycle fixes** — the brainstorm server's owner-PID monitoring had two bugs causing false shutdowns within 60 seconds: (1) EPERM from cross-user PIDs (Tailscale SSH, etc.) was treated as "process dead", and (2) on WSL the grandparent PID resolves to a short-lived subprocess that exits before the first lifecycle check. Fixed by treating EPERM as "alive" and validating the owner PID at startup — if it's already dead, monitoring is disabled and the server relies on the 30-minute idle timeout. This also removes the Windows/MSYS2-specific carve-out from `start-server.sh` since the server now handles it generically. (#879) - **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) ### Codex App Compatibility diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index 14b8197c..562c17f8 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -79,7 +79,7 @@ const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'loc const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; const CONTENT_DIR = path.join(SESSION_DIR, 'content'); const STATE_DIR = path.join(SESSION_DIR, 'state'); -const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; +let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; const MIME_TYPES = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', @@ -312,8 +312,8 @@ function startServer() { } function ownerAlive() { - if (!OWNER_PID) return true; - try { process.kill(OWNER_PID, 0); return true; } catch (e) { return e.code === 'EPERM'; } + if (!ownerPid) return true; + try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; } } // Check every 60s: exit if owner process died or idle for 30 minutes @@ -323,6 +323,19 @@ function startServer() { }, 60 * 1000); lifecycleCheck.unref(); + // Validate owner PID at startup. If it's already dead, the PID resolution + // was wrong (common on WSL, Tailscale SSH, and cross-user scenarios). + // Disable monitoring and rely on the idle timeout instead. + if (ownerPid) { + try { process.kill(ownerPid, 0); } + catch (e) { + if (e.code !== 'EPERM') { + console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' })); + ownerPid = null; + } + } + } + server.listen(PORT, HOST, () => { const info = JSON.stringify({ type: 'server-started', port: Number(PORT), host: HOST, diff --git a/skills/brainstorming/scripts/start-server.sh b/skills/brainstorming/scripts/start-server.sh index c3dd7f98..9ef6dcb9 100755 --- a/skills/brainstorming/scripts/start-server.sh +++ b/skills/brainstorming/scripts/start-server.sh @@ -107,12 +107,6 @@ if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then OWNER_PID="$PPID" fi -# On Windows/MSYS2, the MSYS2 PID namespace is invisible to Node.js. -# Skip owner-PID monitoring — the 30-minute idle timeout prevents orphans. -case "${OSTYPE:-}" in - msys*|cygwin*|mingw*) OWNER_PID="" ;; -esac - # Foreground mode for environments that reap detached/background processes. if [[ "$FOREGROUND" == "true" ]]; then echo "$$" > "$PID_FILE" From eafe962b18f6c5dc70fb7c8cc7e83e61f4cdde06 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 25 Mar 2026 11:08:09 -0700 Subject: [PATCH 18/46] Release v5.0.6: inline self-review, brainstorm server restructure, owner-PID fixes --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 11 +++++++++-- .cursor-plugin/plugin.json | 11 +++++++++-- gemini-extension.json | 2 +- package.json | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 138d485d..dbcfed29 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.5", + "version": "5.0.6", "source": "./", "author": { "name": "Jesse Vincent", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0cbd79e5..1a2018bc 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.5", + "version": "5.0.6", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" @@ -9,5 +9,12 @@ "homepage": "https://github.com/obra/superpowers", "repository": "https://github.com/obra/superpowers", "license": "MIT", - "keywords": ["skills", "tdd", "debugging", "collaboration", "best-practices", "workflows"] + "keywords": [ + "skills", + "tdd", + "debugging", + "collaboration", + "best-practices", + "workflows" + ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index a8c0837f..24466b35 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "superpowers", "displayName": "Superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.5", + "version": "5.0.6", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" @@ -10,7 +10,14 @@ "homepage": "https://github.com/obra/superpowers", "repository": "https://github.com/obra/superpowers", "license": "MIT", - "keywords": ["skills", "tdd", "debugging", "collaboration", "best-practices", "workflows"], + "keywords": [ + "skills", + "tdd", + "debugging", + "collaboration", + "best-practices", + "workflows" + ], "skills": "./skills/", "agents": "./agents/", "commands": "./commands/", diff --git a/gemini-extension.json b/gemini-extension.json index d1020a00..2042d13c 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.0", + "version": "5.0.6", "contextFileName": "GEMINI.md" } diff --git a/package.json b/package.json index 323bd9d1..980800ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superpowers", - "version": "5.0.4", + "version": "5.0.6", "type": "module", "main": ".opencode/plugins/superpowers.js" } From a2964d7a20b8d9fefafc14ded4b5416dd1d4246e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20=C4=8Culina?= Date: Wed, 25 Mar 2026 14:05:56 -0700 Subject: [PATCH 19/46] fix: add Copilot CLI platform detection for sessionStart context injection Copilot CLI v1.0.11 reads `additionalContext` from sessionStart hook output, but the session-start script only emits the Claude Code-specific nested format. Add COPILOT_CLI env var detection so Copilot CLI gets the SDK-standard top-level `additionalContext` while Claude Code continues getting `hookSpecificOutput`. Based on PR #910 by @culinablaz. --- hooks/session-start | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/hooks/session-start b/hooks/session-start index 23719358..24604295 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -35,23 +35,23 @@ warning_escaped=$(escape_for_json "$warning_message") session_context="\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n" # Output context injection as JSON. -# Cursor hooks expect additional_context. -# Claude Code hooks expect hookSpecificOutput.additionalContext. -# Claude Code reads BOTH fields without deduplication, so we must only -# emit the field consumed by the current platform to avoid double injection. +# Cursor hooks expect additional_context (snake_case). +# Claude Code hooks expect hookSpecificOutput.additionalContext (nested). +# Copilot CLI (v1.0.11+) and others expect additionalContext (top-level, SDK standard). +# Claude Code reads BOTH additional_context and hookSpecificOutput without +# deduplication, so we must emit only the field the current platform consumes. # -# Uses printf instead of heredoc (cat < Date: Wed, 25 Mar 2026 14:06:04 -0700 Subject: [PATCH 20/46] feat: add Copilot CLI tool mapping, docs, and install instructions - Add references/copilot-tools.md with full tool equivalence table - Add Copilot CLI to using-superpowers skill platform instructions - Add marketplace install instructions to README - Add changelog entry crediting @culinablaz for the hook fix --- README.md | 7 +++ RELEASE-NOTES.md | 8 +++ skills/using-superpowers/SKILL.md | 4 +- .../references/copilot-tools.md | 52 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 skills/using-superpowers/references/copilot-tools.md diff --git a/README.md b/README.md index e2111f4b..2ff76a58 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,13 @@ Fetch and follow instructions from https://raw.githubusercontent.com/obra/superp **Detailed docs:** [docs/README.opencode.md](docs/README.opencode.md) +### GitHub Copilot CLI + +```bash +copilot plugin marketplace add obra/superpowers-marketplace +copilot plugin install superpowers@superpowers-marketplace +``` + ### Gemini CLI ```bash diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c73a7165..9b608d65 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,13 @@ # Superpowers Release Notes +## Unreleased + +### GitHub Copilot CLI Support + +- **SessionStart context injection** — Copilot CLI v1.0.11 added support for `additionalContext` in sessionStart hook output. The session-start hook now detects the `COPILOT_CLI` environment variable and emits the SDK-standard `{ "additionalContext": "..." }` format, giving Copilot CLI users the full superpowers bootstrap at session start. (Original fix by @culinablaz in PR #910) +- **Tool mapping** — added `references/copilot-tools.md` with the full Claude Code to Copilot CLI tool equivalence table +- **Skill and README updates** — added Copilot CLI to the `using-superpowers` skill's platform instructions and README installation section + ## v5.0.6 (2026-03-24) ### Inline Self-Review Replaces Subagent Review Loops diff --git a/skills/using-superpowers/SKILL.md b/skills/using-superpowers/SKILL.md index d8135352..c8a85702 100644 --- a/skills/using-superpowers/SKILL.md +++ b/skills/using-superpowers/SKILL.md @@ -29,13 +29,15 @@ If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "alw **In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files. +**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool. + **In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand. **In other environments:** Check your platform's documentation for how skills are loaded. ## Platform Adaptation -Skills use Claude Code tool names. Non-CC platforms: see `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md. +Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md. # Using Skills diff --git a/skills/using-superpowers/references/copilot-tools.md b/skills/using-superpowers/references/copilot-tools.md new file mode 100644 index 00000000..4316cdbc --- /dev/null +++ b/skills/using-superpowers/references/copilot-tools.md @@ -0,0 +1,52 @@ +# Copilot CLI Tool Mapping + +Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent: + +| Skill references | Copilot CLI equivalent | +|-----------------|----------------------| +| `Read` (file reading) | `view` | +| `Write` (file creation) | `create` | +| `Edit` (file editing) | `edit` | +| `Bash` (run commands) | `bash` | +| `Grep` (search file content) | `grep` | +| `Glob` (search files by name) | `glob` | +| `Skill` tool (invoke a skill) | `skill` | +| `WebFetch` | `web_fetch` | +| `Task` tool (dispatch subagent) | `task` (see [Agent types](#agent-types)) | +| Multiple `Task` calls (parallel) | Multiple `task` calls | +| Task status/output | `read_agent`, `list_agents` | +| `TodoWrite` (task tracking) | `sql` with built-in `todos` table | +| `WebSearch` | No equivalent — use `web_fetch` with a search engine URL | +| `EnterPlanMode` / `ExitPlanMode` | No equivalent — stay in the main session | + +## Agent types + +Copilot CLI's `task` tool accepts an `agent_type` parameter: + +| Claude Code agent | Copilot CLI equivalent | +|-------------------|----------------------| +| `general-purpose` | `"general-purpose"` | +| `Explore` | `"explore"` | +| Named plugin agents (e.g. `superpowers:code-reviewer`) | Discovered automatically from installed plugins | + +## Async shell sessions + +Copilot CLI supports persistent async shell sessions, which have no direct Claude Code equivalent: + +| Tool | Purpose | +|------|---------| +| `bash` with `async: true` | Start a long-running command in the background | +| `write_bash` | Send input to a running async session | +| `read_bash` | Read output from an async session | +| `stop_bash` | Terminate an async session | +| `list_bash` | List all active shell sessions | + +## Additional Copilot CLI tools + +| Tool | Purpose | +|------|---------| +| `store_memory` | Persist facts about the codebase for future sessions | +| `report_intent` | Update the UI status line with current intent | +| `sql` | Query the session's SQLite database (todos, metadata) | +| `fetch_copilot_cli_documentation` | Look up Copilot CLI documentation | +| GitHub MCP tools (`github-mcp-server-*`) | Native GitHub API access (issues, PRs, code search) | From 2d942f3b01cc636e2ef9ebfc50f05925df8ed3f6 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 25 Mar 2026 14:29:45 -0700 Subject: [PATCH 21/46] fix(opencode): align skills path across bootstrap, runtime, and tests The bootstrap text advertised a configDir-based skills path that didn't match the runtime path (resolved relative to the plugin file). Tests used yet another hardcoded path and referenced a nonexistent lib/ dir. - Remove misleading skills path from bootstrap text; the agent should use the native skill tool, not read files by path - Fix test setup to create a consistent layout matching the plugin's ../../skills resolution - Export SUPERPOWERS_SKILLS_DIR from setup.sh so tests use a single source of truth - Add regression test that bootstrap doesn't advertise the old path - Remove broken cp of nonexistent lib/ directory Fixes #847 --- .opencode/plugins/superpowers.js | 2 -- tests/opencode/setup.sh | 49 +++++++++++++++++---------- tests/opencode/test-plugin-loading.sh | 38 +++++++++++++-------- tests/opencode/test-priority.sh | 12 +++---- 4 files changed, 62 insertions(+), 39 deletions(-) diff --git a/.opencode/plugins/superpowers.js b/.opencode/plugins/superpowers.js index 5e7833b6..76b23577 100644 --- a/.opencode/plugins/superpowers.js +++ b/.opencode/plugins/superpowers.js @@ -68,8 +68,6 @@ When skills reference tools you don't have, substitute OpenCode equivalents: - \`Skill\` tool → OpenCode's native \`skill\` tool - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools -**Skills location:** -Superpowers skills are in \`${configDir}/skills/superpowers/\` Use OpenCode's native \`skill\` tool to list and load skills.`; return ` diff --git a/tests/opencode/setup.sh b/tests/opencode/setup.sh index 0defde2f..cd2a732c 100755 --- a/tests/opencode/setup.sh +++ b/tests/opencode/setup.sh @@ -7,30 +7,39 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" # Create temp home directory for isolation -export TEST_HOME=$(mktemp -d) +export TEST_HOME +TEST_HOME=$(mktemp -d) export HOME="$TEST_HOME" export XDG_CONFIG_HOME="$TEST_HOME/.config" export OPENCODE_CONFIG_DIR="$TEST_HOME/.config/opencode" -# Install plugin to test location -mkdir -p "$HOME/.config/opencode/superpowers" -cp -r "$REPO_ROOT/lib" "$HOME/.config/opencode/superpowers/" -cp -r "$REPO_ROOT/skills" "$HOME/.config/opencode/superpowers/" +# Standard install layout: +# $OPENCODE_CONFIG_DIR/superpowers/ ← package root +# $OPENCODE_CONFIG_DIR/superpowers/skills/ ← skills dir (../../skills from plugin) +# $OPENCODE_CONFIG_DIR/superpowers/.opencode/plugins/superpowers.js ← plugin file +# $OPENCODE_CONFIG_DIR/plugins/superpowers.js ← symlink OpenCode reads -# Copy plugin directory -mkdir -p "$HOME/.config/opencode/superpowers/.opencode/plugins" -cp "$REPO_ROOT/.opencode/plugins/superpowers.js" "$HOME/.config/opencode/superpowers/.opencode/plugins/" +SUPERPOWERS_DIR="$OPENCODE_CONFIG_DIR/superpowers" +SUPERPOWERS_SKILLS_DIR="$SUPERPOWERS_DIR/skills" +SUPERPOWERS_PLUGIN_FILE="$SUPERPOWERS_DIR/.opencode/plugins/superpowers.js" -# Register plugin via symlink -mkdir -p "$HOME/.config/opencode/plugins" -ln -sf "$HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js" \ - "$HOME/.config/opencode/plugins/superpowers.js" +# Install skills +mkdir -p "$SUPERPOWERS_DIR" +cp -r "$REPO_ROOT/skills" "$SUPERPOWERS_DIR/" + +# Install plugin +mkdir -p "$(dirname "$SUPERPOWERS_PLUGIN_FILE")" +cp "$REPO_ROOT/.opencode/plugins/superpowers.js" "$SUPERPOWERS_PLUGIN_FILE" + +# Register plugin via symlink (what OpenCode actually reads) +mkdir -p "$OPENCODE_CONFIG_DIR/plugins" +ln -sf "$SUPERPOWERS_PLUGIN_FILE" "$OPENCODE_CONFIG_DIR/plugins/superpowers.js" # Create test skills in different locations for testing # Personal test skill -mkdir -p "$HOME/.config/opencode/skills/personal-test" -cat > "$HOME/.config/opencode/skills/personal-test/SKILL.md" <<'EOF' +mkdir -p "$OPENCODE_CONFIG_DIR/skills/personal-test" +cat > "$OPENCODE_CONFIG_DIR/skills/personal-test/SKILL.md" <<'EOF' --- name: personal-test description: Test personal skill for verification @@ -57,9 +66,12 @@ PROJECT_SKILL_MARKER_67890 EOF echo "Setup complete: $TEST_HOME" -echo "Plugin installed to: $HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js" -echo "Plugin registered at: $HOME/.config/opencode/plugins/superpowers.js" -echo "Test project at: $TEST_HOME/test-project" +echo "OPENCODE_CONFIG_DIR: $OPENCODE_CONFIG_DIR" +echo "Superpowers dir: $SUPERPOWERS_DIR" +echo "Skills dir: $SUPERPOWERS_SKILLS_DIR" +echo "Plugin file: $SUPERPOWERS_PLUGIN_FILE" +echo "Plugin registered at: $OPENCODE_CONFIG_DIR/plugins/superpowers.js" +echo "Test project at: $TEST_HOME/test-project" # Helper function for cleanup (call from tests or trap) cleanup_test_env() { @@ -71,3 +83,6 @@ cleanup_test_env() { # Export for use in tests export -f cleanup_test_env export REPO_ROOT +export SUPERPOWERS_DIR +export SUPERPOWERS_SKILLS_DIR +export SUPERPOWERS_PLUGIN_FILE diff --git a/tests/opencode/test-plugin-loading.sh b/tests/opencode/test-plugin-loading.sh index 008cf5c1..83af3f19 100755 --- a/tests/opencode/test-plugin-loading.sh +++ b/tests/opencode/test-plugin-loading.sh @@ -13,17 +13,19 @@ source "$SCRIPT_DIR/setup.sh" # Trap to cleanup on exit trap cleanup_test_env EXIT +plugin_link="$OPENCODE_CONFIG_DIR/plugins/superpowers.js" + # Test 1: Verify plugin file exists and is registered echo "Test 1: Checking plugin registration..." -if [ -L "$HOME/.config/opencode/plugins/superpowers.js" ]; then +if [ -L "$plugin_link" ]; then echo " [PASS] Plugin symlink exists" else - echo " [FAIL] Plugin symlink not found at $HOME/.config/opencode/plugins/superpowers.js" + echo " [FAIL] Plugin symlink not found at $plugin_link" exit 1 fi # Verify symlink target exists -if [ -f "$(readlink -f "$HOME/.config/opencode/plugins/superpowers.js")" ]; then +if [ -f "$(readlink -f "$plugin_link")" ]; then echo " [PASS] Plugin symlink target exists" else echo " [FAIL] Plugin symlink target does not exist" @@ -32,36 +34,44 @@ fi # Test 2: Verify skills directory is populated echo "Test 2: Checking skills directory..." -skill_count=$(find "$HOME/.config/opencode/superpowers/skills" -name "SKILL.md" | wc -l) +skill_count=$(find "$SUPERPOWERS_SKILLS_DIR" -name "SKILL.md" | wc -l) if [ "$skill_count" -gt 0 ]; then - echo " [PASS] Found $skill_count skills installed" + echo " [PASS] Found $skill_count skills" else - echo " [FAIL] No skills found in installed location" + echo " [FAIL] No skills found in $SUPERPOWERS_SKILLS_DIR" exit 1 fi -# Test 4: Check using-superpowers skill exists (critical for bootstrap) -echo "Test 4: Checking using-superpowers skill (required for bootstrap)..." -if [ -f "$HOME/.config/opencode/superpowers/skills/using-superpowers/SKILL.md" ]; then +# Test 3: Check using-superpowers skill exists (critical for bootstrap) +echo "Test 3: Checking using-superpowers skill (required for bootstrap)..." +if [ -f "$SUPERPOWERS_SKILLS_DIR/using-superpowers/SKILL.md" ]; then echo " [PASS] using-superpowers skill exists" else echo " [FAIL] using-superpowers skill not found (required for bootstrap)" exit 1 fi -# Test 5: Verify plugin JavaScript syntax (basic check) -echo "Test 5: Checking plugin JavaScript syntax..." -plugin_file="$HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js" -if node --check "$plugin_file" 2>/dev/null; then +# Test 4: Verify plugin JavaScript syntax (basic check) +echo "Test 4: Checking plugin JavaScript syntax..." +if node --check "$SUPERPOWERS_PLUGIN_FILE" 2>/dev/null; then echo " [PASS] Plugin JavaScript syntax is valid" else echo " [FAIL] Plugin has JavaScript syntax errors" exit 1 fi +# Test 5: Verify bootstrap text does not reference a hardcoded skills path +echo "Test 5: Checking bootstrap does not advertise a wrong skills path..." +if grep -q 'configDir}/skills/superpowers/' "$SUPERPOWERS_PLUGIN_FILE"; then + echo " [FAIL] Plugin still references old configDir skills path" + exit 1 +else + echo " [PASS] Plugin does not advertise a misleading skills path" +fi + # Test 6: Verify personal test skill was created echo "Test 6: Checking test fixtures..." -if [ -f "$HOME/.config/opencode/skills/personal-test/SKILL.md" ]; then +if [ -f "$OPENCODE_CONFIG_DIR/skills/personal-test/SKILL.md" ]; then echo " [PASS] Personal test skill fixture created" else echo " [FAIL] Personal test skill fixture not found" diff --git a/tests/opencode/test-priority.sh b/tests/opencode/test-priority.sh index 1c36fa33..b7de0d61 100755 --- a/tests/opencode/test-priority.sh +++ b/tests/opencode/test-priority.sh @@ -18,8 +18,8 @@ trap cleanup_test_env EXIT echo "Setting up priority test fixtures..." # 1. Create in superpowers location (lowest priority) -mkdir -p "$HOME/.config/opencode/superpowers/skills/priority-test" -cat > "$HOME/.config/opencode/superpowers/skills/priority-test/SKILL.md" <<'EOF' +mkdir -p "$SUPERPOWERS_SKILLS_DIR/priority-test" +cat > "$SUPERPOWERS_SKILLS_DIR/priority-test/SKILL.md" <<'EOF' --- name: priority-test description: Superpowers version of priority test skill @@ -32,8 +32,8 @@ PRIORITY_MARKER_SUPERPOWERS_VERSION EOF # 2. Create in personal location (medium priority) -mkdir -p "$HOME/.config/opencode/skills/priority-test" -cat > "$HOME/.config/opencode/skills/priority-test/SKILL.md" <<'EOF' +mkdir -p "$OPENCODE_CONFIG_DIR/skills/priority-test" +cat > "$OPENCODE_CONFIG_DIR/skills/priority-test/SKILL.md" <<'EOF' --- name: priority-test description: Personal version of priority test skill @@ -65,14 +65,14 @@ echo " Created priority-test skill in all three locations" echo "" echo "Test 1: Verifying test fixtures..." -if [ -f "$HOME/.config/opencode/superpowers/skills/priority-test/SKILL.md" ]; then +if [ -f "$SUPERPOWERS_SKILLS_DIR/priority-test/SKILL.md" ]; then echo " [PASS] Superpowers version exists" else echo " [FAIL] Superpowers version missing" exit 1 fi -if [ -f "$HOME/.config/opencode/skills/priority-test/SKILL.md" ]; then +if [ -f "$OPENCODE_CONFIG_DIR/skills/priority-test/SKILL.md" ]; then echo " [PASS] Personal version exists" else echo " [FAIL] Personal version missing" From 65d760f9c28b87c07b4748d2649189de85a79d36 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 25 Mar 2026 14:34:33 -0700 Subject: [PATCH 22/46] docs: add OpenCode path fix to release notes --- RELEASE-NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 9b608d65..143cd3b1 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -8,6 +8,10 @@ - **Tool mapping** — added `references/copilot-tools.md` with the full Claude Code to Copilot CLI tool equivalence table - **Skill and README updates** — added Copilot CLI to the `using-superpowers` skill's platform instructions and README installation section +### OpenCode Fixes + +- **Skills path consistency** — the bootstrap text no longer advertises a misleading `configDir/skills/superpowers/` path that didn't match the runtime path. The agent should use the native `skill` tool, not navigate to files by path. Tests now use consistent paths derived from a single source of truth. (#847, #916) + ## v5.0.6 (2026-03-24) ### Inline Self-Review Replaces Subagent Review Loops From 0a1124ba5368e830969fc1a1f30f84265711b8cd Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 25 Mar 2026 17:09:09 -0700 Subject: [PATCH 23/46] fix(opencode): inject bootstrap as user message instead of system message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move bootstrap injection from experimental.chat.system.transform to experimental.chat.messages.transform, prepending to the first user message instead of adding a system message. This avoids two issues: - System messages repeated every turn inflate token usage (#750) - Multiple system messages break Qwen and other models (#894) Tested on OpenCode 1.3.2 with Claude Sonnet 4.5 — brainstorming skill fires correctly on "Let's make a React to do list" prompt. --- .opencode/plugins/superpowers.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.opencode/plugins/superpowers.js b/.opencode/plugins/superpowers.js index 76b23577..48a2b72d 100644 --- a/.opencode/plugins/superpowers.js +++ b/.opencode/plugins/superpowers.js @@ -94,12 +94,19 @@ ${toolMapping} } }, - // Use system prompt transform to inject bootstrap (fixes #226 agent reset bug) - 'experimental.chat.system.transform': async (_input, output) => { + // Inject bootstrap into the first user message of each session. + // Using a user message instead of a system message avoids: + // 1. Token bloat from system messages repeated every turn (#750) + // 2. Multiple system messages breaking Qwen and other models (#894) + 'experimental.chat.messages.transform': async (_input, output) => { const bootstrap = getBootstrapContent(); - if (bootstrap) { - (output.system ||= []).push(bootstrap); - } + if (!bootstrap || !output.messages.length) return; + const firstUser = output.messages.find(m => m.info.role === 'user'); + if (!firstUser || !firstUser.parts.length) return; + // Only inject once + if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return; + const ref = firstUser.parts[0]; + firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap }); } }; }; From f0df5eca3059feea3b92b49335ae264c3b3170a3 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 25 Mar 2026 17:16:55 -0700 Subject: [PATCH 24/46] docs: update release notes with OpenCode bootstrap change --- RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 143cd3b1..093863e6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -11,6 +11,7 @@ ### OpenCode Fixes - **Skills path consistency** — the bootstrap text no longer advertises a misleading `configDir/skills/superpowers/` path that didn't match the runtime path. The agent should use the native `skill` tool, not navigate to files by path. Tests now use consistent paths derived from a single source of truth. (#847, #916) +- **Bootstrap as user message** — moved bootstrap injection from `experimental.chat.system.transform` to `experimental.chat.messages.transform`, prepending to the first user message instead of adding a system message. Avoids token bloat from system messages repeated every turn (#750) and fixes compatibility with Qwen and other models that break on multiple system messages (#894). ## v5.0.6 (2026-03-24) From 1f20bef3f59b85ad7b52718f822e37c4478a3ff5 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 31 Mar 2026 12:23:25 -0700 Subject: [PATCH 25/46] Release v5.0.7: Copilot CLI support, OpenCode fixes --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- .version-bump.json | 19 +++ RELEASE-NOTES.md | 2 +- gemini-extension.json | 2 +- package.json | 2 +- scripts/bump-version.sh | 220 ++++++++++++++++++++++++++++++++ 8 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 .version-bump.json create mode 100755 scripts/bump-version.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index dbcfed29..b6c7949d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.6", + "version": "5.0.7", "source": "./", "author": { "name": "Jesse Vincent", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1a2018bc..cad4d55f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.6", + "version": "5.0.7", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 24466b35..153deac4 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "superpowers", "displayName": "Superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.6", + "version": "5.0.7", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/.version-bump.json b/.version-bump.json new file mode 100644 index 00000000..f5dbe315 --- /dev/null +++ b/.version-bump.json @@ -0,0 +1,19 @@ +{ + "files": [ + { "path": "package.json", "field": "version" }, + { "path": ".claude-plugin/plugin.json", "field": "version" }, + { "path": ".cursor-plugin/plugin.json", "field": "version" }, + { "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" }, + { "path": "gemini-extension.json", "field": "version" } + ], + "audit": { + "exclude": [ + "CHANGELOG.md", + "RELEASE-NOTES.md", + "node_modules", + ".git", + ".version-bump.json", + "scripts/bump-version.sh" + ] + } +} diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 093863e6..675eca2a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,6 +1,6 @@ # Superpowers Release Notes -## Unreleased +## v5.0.7 (2026-03-31) ### GitHub Copilot CLI Support diff --git a/gemini-extension.json b/gemini-extension.json index 2042d13c..d52654ae 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.6", + "version": "5.0.7", "contextFileName": "GEMINI.md" } diff --git a/package.json b/package.json index 980800ad..04f22b42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superpowers", - "version": "5.0.6", + "version": "5.0.7", "type": "module", "main": ".opencode/plugins/superpowers.js" } diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 00000000..01adec99 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# +# bump-version.sh — bump version numbers across all declared files, +# with drift detection and repo-wide audit for missed files. +# +# Usage: +# bump-version.sh Bump all declared files to new version +# bump-version.sh --check Report current versions (detect drift) +# bump-version.sh --audit Check + grep repo for old version strings +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG="$REPO_ROOT/.version-bump.json" + +if [[ ! -f "$CONFIG" ]]; then + echo "error: .version-bump.json not found at $CONFIG" >&2 + exit 1 +fi + +# --- helpers --- + +# Read a dotted field path from a JSON file. +# Handles both simple ("version") and nested ("plugins.0.version") paths. +read_json_field() { + local file="$1" field="$2" + # Convert dot-path to jq path: "plugins.0.version" -> .plugins[0].version + local jq_path + jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g') + jq -r "$jq_path" "$file" +} + +# Write a dotted field path in a JSON file, preserving formatting. +write_json_field() { + local file="$1" field="$2" value="$3" + local jq_path + jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g') + local tmp="${file}.tmp" + jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file" +} + +# Read the list of declared files from config. +# Outputs lines of "pathfield" +declared_files() { + jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG" +} + +# Read the audit exclude patterns from config. +audit_excludes() { + jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null +} + +# --- commands --- + +cmd_check() { + local has_drift=0 + local versions=() + + echo "Version check:" + echo "" + + while IFS=$'\t' read -r path field; do + local fullpath="$REPO_ROOT/$path" + if [[ ! -f "$fullpath" ]]; then + printf " %-45s MISSING\n" "$path ($field)" + has_drift=1 + continue + fi + local ver + ver=$(read_json_field "$fullpath" "$field") + printf " %-45s %s\n" "$path ($field)" "$ver" + versions+=("$ver") + done < <(declared_files) + + echo "" + + # Check if all versions match + local unique + unique=$(printf '%s\n' "${versions[@]}" | sort -u | wc -l | tr -d ' ') + if [[ "$unique" -gt 1 ]]; then + echo "DRIFT DETECTED — versions are not in sync:" + printf '%s\n' "${versions[@]}" | sort | uniq -c | sort -rn | while read -r count ver; do + echo " $ver ($count files)" + done + has_drift=1 + else + echo "All declared files are in sync at ${versions[0]}" + fi + + return $has_drift +} + +cmd_audit() { + # First run check + cmd_check || true + echo "" + + # Determine the current version (most common across declared files) + local current_version + current_version=$( + while IFS=$'\t' read -r path field; do + local fullpath="$REPO_ROOT/$path" + [[ -f "$fullpath" ]] && read_json_field "$fullpath" "$field" + done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}' + ) + + if [[ -z "$current_version" ]]; then + echo "error: could not determine current version" >&2 + return 1 + fi + + echo "Audit: scanning repo for version string '$current_version'..." + echo "" + + # Build grep exclude args + local -a exclude_args=() + while IFS= read -r pattern; do + exclude_args+=("--exclude=$pattern" "--exclude-dir=$pattern") + done < <(audit_excludes) + + # Also always exclude binary files and .git + exclude_args+=("--exclude-dir=.git" "--exclude-dir=node_modules" "--binary-files=without-match") + + # Get list of declared paths for comparison + local -a declared_paths=() + while IFS=$'\t' read -r path _field; do + declared_paths+=("$path") + done < <(declared_files) + + # Grep for the version string + local found_undeclared=0 + while IFS= read -r match; do + local match_file + match_file=$(echo "$match" | cut -d: -f1) + # Make path relative to repo root + local rel_path="${match_file#$REPO_ROOT/}" + + # Check if this file is in the declared list + local is_declared=0 + for dp in "${declared_paths[@]}"; do + if [[ "$rel_path" == "$dp" ]]; then + is_declared=1 + break + fi + done + + if [[ "$is_declared" -eq 0 ]]; then + if [[ "$found_undeclared" -eq 0 ]]; then + echo "UNDECLARED files containing '$current_version':" + found_undeclared=1 + fi + echo " $match" + fi + done < <(grep -rn "${exclude_args[@]}" -F "$current_version" "$REPO_ROOT" 2>/dev/null || true) + + if [[ "$found_undeclared" -eq 0 ]]; then + echo "No undeclared files contain the version string. All clear." + else + echo "" + echo "Review the above files — if they should be bumped, add them to .version-bump.json" + echo "If they should be skipped, add them to the audit.exclude list." + fi +} + +cmd_bump() { + local new_version="$1" + + # Validate semver-ish format + if ! echo "$new_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then + echo "error: '$new_version' doesn't look like a version (expected X.Y.Z)" >&2 + exit 1 + fi + + echo "Bumping all declared files to $new_version..." + echo "" + + while IFS=$'\t' read -r path field; do + local fullpath="$REPO_ROOT/$path" + if [[ ! -f "$fullpath" ]]; then + echo " SKIP (missing): $path" + continue + fi + local old_ver + old_ver=$(read_json_field "$fullpath" "$field") + write_json_field "$fullpath" "$field" "$new_version" + printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version" + done < <(declared_files) + + echo "" + echo "Done. Running audit to check for missed files..." + echo "" + cmd_audit +} + +# --- main --- + +case "${1:-}" in + --check) + cmd_check + ;; + --audit) + cmd_audit + ;; + --help|-h|"") + echo "Usage: bump-version.sh | --check | --audit" + echo "" + echo " Bump all declared files to the given version" + echo " --check Show current versions, detect drift" + echo " --audit Check + scan repo for undeclared version references" + exit 0 + ;; + --*) + echo "error: unknown flag '$1'" >&2 + exit 1 + ;; + *) + cmd_bump "$1" + ;; +esac From c0b417e40959a72899d46863a024dcb7baad7b76 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 31 Mar 2026 14:14:16 -0700 Subject: [PATCH 26/46] Add contributor guidelines to reduce agentic slop PRs CLAUDE.md (symlinked to AGENTS.md) covers every major rejection pattern from auditing the last 100 closed PRs (94% rejection rate): AI slop, ignored PR template, duplicates, speculative fixes, domain- specific skills, fork confusion, fabricated content, bundled changes, and misunderstanding project philosophy. --- AGENTS.md | 1 + CLAUDE.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 120000 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000..681311eb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..8262d03c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# Superpowers — Contributor Guidelines + +## Pull Request Requirements + +**Every PR must fully complete the PR template.** No section may be left blank or filled with placeholder text. PRs that skip sections will be closed without review. + +**Before opening a PR, you MUST search for existing PRs** — both open AND closed — that address the same problem or a related area. Reference what you found in the "Existing PRs" section. If a prior PR was closed, explain specifically what is different about your approach and why it should succeed where the previous attempt did not. + +**PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission. + +## What We Will Not Accept + +### Third-party dependencies + +PRs that add optional or required dependencies on third-party projects will not be accepted unless they are adding support for a new harness (e.g., a new IDE or CLI tool). Superpowers is a zero-dependency plugin by design. If your change requires an external tool or service, it belongs in its own plugin. + +### "Compliance" changes to skills + +Our internal skill philosophy differs from Anthropic's published guidance on writing skills. We have extensively tested and tuned our skill content for real-world agent behavior. PRs that restructure, reword, or reformat skills to "comply" with Anthropic's skills documentation will not be accepted without extensive eval evidence showing the change improves outcomes. The bar for modifying behavior-shaping content is very high. + +### Project-specific or personal configuration + +Skills, hooks, or configuration that only benefit a specific project, team, domain, or workflow do not belong in core. Publish these as a separate plugin. + +### Bulk or spray-and-pray PRs + +Do not trawl the issue tracker and open PRs for multiple issues in a single session. Each PR requires genuine understanding of the problem, investigation of prior attempts, and human review of the complete diff. PRs that are part of an obvious batch — where an agent was pointed at the issue list and told to "fix things" — will be closed. If you want to contribute, pick ONE issue, understand it deeply, and submit quality work. + +### Speculative or theoretical fixes + +Every PR must solve a real problem that someone actually experienced. "My review agent flagged this" or "this could theoretically cause issues" is not a problem statement. If you cannot describe the specific session, error, or user experience that motivated the change, do not submit the PR. + +### Domain-specific skills + +Superpowers core contains general-purpose skills that benefit all users regardless of their project. Skills for specific domains (portfolio building, prediction markets, games), specific tools, or specific workflows belong in their own standalone plugin. Ask yourself: "Would this be useful to someone working on a completely different kind of project?" If not, publish it separately. + +### Fork-specific changes + +If you maintain a fork with customizations, do not open PRs to sync your fork or push fork-specific changes upstream. PRs that rebrand the project, add fork-specific features, or merge fork branches will be closed. + +### Fabricated content + +PRs containing invented claims, fabricated problem descriptions, or hallucinated functionality will be closed immediately. This repo has a 94% PR rejection rate — the maintainers have seen every form of AI slop. They will notice. + +### Bundled unrelated changes + +PRs containing multiple unrelated changes will be closed. Split them into separate PRs. + +## Skill Changes Require Evaluation + +Skills are not prose — they are code that shapes agent behavior. If you modify skill content: + +- Use `superpowers:writing-skills` to develop and test changes +- Run adversarial pressure testing across multiple sessions +- Show before/after eval results in your PR +- Do not modify carefully-tuned content (Red Flags tables, rationalization lists, "human partner" language) without evidence the change is an improvement + +## Understand the Project Before Contributing + +Before proposing changes to skill design, workflow philosophy, or architecture, read existing skills and understand the project's design decisions. Superpowers has its own tested philosophy about skill design, agent behavior shaping, and terminology (e.g., "your human partner" is deliberate, not interchangeable with "the user"). Changes that rewrite the project's voice or restructure its approach without understanding why it exists will be rejected. + +## General + +- Read `.github/PULL_REQUEST_TEMPLATE.md` before submitting +- One problem per PR +- Test on at least one harness and report results in the environment table +- Describe the problem you solved, not just what you changed From dd237283dbfe466e11bd4be55acf14ecb8f6636e Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 31 Mar 2026 14:37:10 -0700 Subject: [PATCH 27/46] Add agent-facing guardrails to contributor guidelines Speak directly to AI agents at the top of CLAUDE.md: reframe slop PRs as harmful to their human partner, give a concrete pre-submission checklist, and explicitly authorize pushing back on vague instructions. --- CLAUDE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 8262d03c..3a50e0fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,23 @@ # Superpowers — Contributor Guidelines +## If You Are an AI Agent + +Stop. Read this section before doing anything. + +This repo has a 94% PR rejection rate. Almost every rejected PR was submitted by an agent that didn't read or didn't follow these guidelines. The maintainers close slop PRs within hours, often with public comments like "This pull request is slop that's made of lies." + +**Your job is to protect your human partner from that outcome.** Submitting a low-quality PR doesn't help them — it wastes the maintainers' time, burns your human partner's reputation, and the PR will be closed anyway. That is not being helpful. That is being a tool of embarrassment. + +Before you open a PR against this repo, you MUST: + +1. **Read the entire PR template** at `.github/PULL_REQUEST_TEMPLATE.md` and fill in every section with real, specific answers. Not summaries. Not placeholders. +2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate. +3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was. +4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner. +5. **Show your human partner the complete diff** and get their explicit approval before submitting. + +If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment. + ## Pull Request Requirements **Every PR must fully complete the PR template.** No section may be left blank or filled with placeholder text. PRs that skip sections will be closed without review. From eeaf2ad15b5bc3b52ae9dda98c42418bf9a667b1 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 1 Apr 2026 19:09:22 -0700 Subject: [PATCH 28/46] Add release announcements link, consolidate Community section Collapse duplicate Support section into Community. Add link to release announcements signup at primeradiant.com/superpowers/. --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 2ff76a58..4dde0e05 100644 --- a/README.md +++ b/README.md @@ -185,10 +185,6 @@ MIT License - see LICENSE file for details Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com). -For community support, questions, and sharing what you're building with Superpowers, join us on [Discord](https://discord.gg/Jd8Vphy9jq). - -## Support - - **Discord**: [Join us on Discord](https://discord.gg/Jd8Vphy9jq) - **Issues**: https://github.com/obra/superpowers/issues -- **Marketplace**: https://github.com/obra/superpowers-marketplace +- **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions From 4b1b20f69fd526895082eb97e657ff103d0bd35a Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 1 Apr 2026 19:34:30 -0700 Subject: [PATCH 29/46] Add detailed Discord description to Community section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dde0e05..13c90887 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,6 @@ MIT License - see LICENSE file for details Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com). -- **Discord**: [Join us on Discord](https://discord.gg/Jd8Vphy9jq) +- **Discord**: [Join us](https://discord.gg/Jd8Vphy9jq) for community support, questions, and sharing what you're building with Superpowers - **Issues**: https://github.com/obra/superpowers/issues - **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions From a6b1a1fa0c367a7cf3d373e1bdb0dbc84d50ccde Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Mon, 6 Apr 2026 15:46:52 -0700 Subject: [PATCH 30/46] Update Discord invite link --- .github/ISSUE_TEMPLATE/config.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e3dc7608..691f9c36 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Questions & Help - url: https://discord.gg/Jd8Vphy9jq + url: https://discord.gg/e4vEVGZv about: For usage questions, troubleshooting help, and general discussion, please visit our Discord instead of opening an issue. diff --git a/README.md b/README.md index 13c90887..a37aeb20 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,6 @@ MIT License - see LICENSE file for details Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com). -- **Discord**: [Join us](https://discord.gg/Jd8Vphy9jq) for community support, questions, and sharing what you're building with Superpowers +- **Discord**: [Join us](https://discord.gg/e4vEVGZv) for community support, questions, and sharing what you're building with Superpowers - **Issues**: https://github.com/obra/superpowers/issues - **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions From 917e5f53b16b115b70a3a355ed5f4993b9f8b73d Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Mon, 6 Apr 2026 15:48:58 -0700 Subject: [PATCH 31/46] Fix Discord invite link --- .github/ISSUE_TEMPLATE/config.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 691f9c36..008309e5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Questions & Help - url: https://discord.gg/e4vEVGZv + url: https://discord.gg/35wsABTejz about: For usage questions, troubleshooting help, and general discussion, please visit our Discord instead of opening an issue. diff --git a/README.md b/README.md index a37aeb20..f61352ae 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,6 @@ MIT License - see LICENSE file for details Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com). -- **Discord**: [Join us](https://discord.gg/e4vEVGZv) for community support, questions, and sharing what you're building with Superpowers +- **Discord**: [Join us](https://discord.gg/35wsABTejz) for community support, questions, and sharing what you're building with Superpowers - **Issues**: https://github.com/obra/superpowers/issues - **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions From a5d36b130055ebfbaea48b685a43cc40fccf72a2 Mon Sep 17 00:00:00 2001 From: Shaan Majid <70789625+shaanmajid@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:36:07 -0500 Subject: [PATCH 32/46] chore: remove vestigial CHANGELOG.md --- CHANGELOG.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b383d93a..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog - -## [5.0.5] - 2026-03-17 - -### Fixed - -- **Brainstorm server ESM fix**: Renamed `server.js` → `server.cjs` so the brainstorming server starts correctly on Node.js 22+ where the root `package.json` `"type": "module"` caused `require()` to fail. ([PR #784](https://github.com/obra/superpowers/pull/784) by @sarbojitrana, fixes [#774](https://github.com/obra/superpowers/issues/774), [#780](https://github.com/obra/superpowers/issues/780), [#783](https://github.com/obra/superpowers/issues/783)) -- **Brainstorm owner-PID on Windows**: Skip `BRAINSTORM_OWNER_PID` lifecycle monitoring on Windows/MSYS2 where the PID namespace is invisible to Node.js. Prevents the server from self-terminating after 60 seconds. The 30-minute idle timeout remains as the safety net. ([#770](https://github.com/obra/superpowers/issues/770), docs from [PR #768](https://github.com/obra/superpowers/pull/768) by @lucasyhzhu-debug) -- **stop-server.sh reliability**: Verify the server process actually died before reporting success. Waits up to 2 seconds for graceful shutdown, escalates to `SIGKILL`, and reports failure if the process survives. ([#723](https://github.com/obra/superpowers/issues/723)) - -### Changed - -- **Execution handoff**: Restore user choice between subagent-driven-development and executing-plans after plan writing. Subagent-driven is recommended but no longer mandatory. (Reverts `5e51c3e`) From 8c8c5e87ce7baa85ad59d5bffc6b901c32b7badc Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Tue, 14 Apr 2026 12:03:59 -0700 Subject: [PATCH 33/46] 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 34/46] 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 35/46] 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 36/46] 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 37/46] 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 38/46] 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 39/46] 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/" ) # ============================================================================= From 34c17aefb23c43960580b4a7f0ed5cb45c270cbe Mon Sep 17 00:00:00 2001 From: Drew Ritter Date: Wed, 15 Apr 2026 10:59:39 -0700 Subject: [PATCH 40/46] sync-to-codex-plugin: seed interface.defaultPrompt (#1180) Codex plugin pages use interface.defaultPrompt to show suggested prompts on the plugin's app card; the generator now emits two domain-neutral seed prompts so the superpowers listing isn't empty. Co-authored-by: Claude Opus 4.6 (1M context) --- scripts/sync-to-codex-plugin.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index c57eeb82..27356b79 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -127,6 +127,10 @@ generate_plugin_json() { "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", From c4bbe651cb1bc5e7bec6f7effae2b946571f3258 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Wed, 15 Apr 2026 12:22:14 -0700 Subject: [PATCH 41/46] Some terminology cleanups --- scripts/sync-to-codex-plugin.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index 27356b79..0566170a 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -98,7 +98,7 @@ generate_plugin_json() { { "name": "superpowers", "version": "$version", - "description": "Core skills library for Codex: planning, TDD, debugging, and collaboration workflows.", + "description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com", @@ -108,6 +108,8 @@ generate_plugin_json() { "repository": "https://github.com/obra/superpowers", "license": "MIT", "keywords": [ + "brainstorming", + "subagent-driven-development", "skills", "planning", "tdd", @@ -119,7 +121,7 @@ generate_plugin_json() { "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 adapted for Codex.", + "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": [ From a5dd364e42d90d05703605422dfba408db713af1 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 16 Apr 2026 12:24:40 -0700 Subject: [PATCH 42/46] README updates for Codex, other cleanup --- README.md | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index f61352ae..c2bd49e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Superpowers -Superpowers is a complete software development workflow for your coding agents, built on top of a set of composable "skills" and some initial instructions that make sure your agent uses them. +Superpowers is a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them. ## How it works @@ -26,19 +26,21 @@ Thanks! ## Installation -**Note:** Installation differs by platform. Claude Code or Cursor have built-in plugin marketplaces. Codex and OpenCode require manual setup. +**Note:** Installation differs by platform. ### Claude Code Official Marketplace Superpowers is available via the [official Claude plugin marketplace](https://claude.com/plugins/superpowers) -Install the plugin from Claude marketplace: +Install the plugin from Anthropic's official marketplace: ```bash /plugin install superpowers@claude-plugins-official ``` -### Claude Code (via Plugin Marketplace) +### Claude Code (Superpowers Marketplace) + +The Superpowers marketplace provides Superpowers and some other related plugins for Claude Code. In Claude Code, register the marketplace first: @@ -62,15 +64,17 @@ In Cursor Agent chat, install from marketplace: or search for "superpowers" in the plugin marketplace. -### Codex +### OpenAI Codex CLI -Tell Codex: +In Codex, type "/plugins" to open the plugin search. +Type "Superpowers" to search for Superpowers. +Select "Install Plugin" -``` -Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md -``` +### OpenAI Codex App -**Detailed docs:** [docs/README.codex.md](docs/README.codex.md) +In the Codex app, first click on Plugins in the sidebar. +You should see Superpowers in the Coding section. +Click the + next to Superpowers and follow the prompts. ### OpenCode @@ -101,10 +105,6 @@ To update: gemini extensions update superpowers ``` -### Verify Installation - -Start a new session in your chosen platform and ask for something that should trigger a skill (for example, "help me plan this feature" or "let's debug this issue"). The agent should automatically invoke the relevant superpowers skill. - ## The Basic Workflow 1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document. @@ -156,26 +156,23 @@ Start a new session in your chosen platform and ask for something that should tr - **Complexity reduction** - Simplicity as primary goal - **Evidence over claims** - Verify before declaring success -Read more: [Superpowers for Claude Code](https://blog.fsck.com/2025/10/09/superpowers/) +Read [the original release announcement](https://blog.fsck.com/2025/10/09/superpowers/). ## Contributing -Skills live directly in this repository. To contribute: +The general contribution process for Superpowers is below. Keep in mind that we don't generally accept contributions of new skills and that any updates to skills must work across all of the coding agents we support. 1. Fork the repository -2. Create a branch for your skill -3. Follow the `writing-skills` skill for creating and testing new skills -4. Submit a PR +2. Switch to the 'dev' branch +3. Create a branch for your work +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. See `skills/writing-skills/SKILL.md` for the complete guide. ## Updating -Skills update automatically when you update the plugin: - -```bash -/plugin update superpowers -``` +Superpowers updates are somewhat coding-agent dependent, but are often automatic. ## License From 99e4c656bf359eb1dae5e8bc0fbb095955d4ad83 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 16 Apr 2026 12:25:50 -0700 Subject: [PATCH 43/46] reorder installs --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c2bd49e2..3e3ac65f 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,6 @@ Then install the plugin from this marketplace: /plugin install superpowers@superpowers-marketplace ``` -### Cursor (via Plugin Marketplace) - -In Cursor Agent chat, install from marketplace: - -```text -/add-plugin superpowers -``` - -or search for "superpowers" in the plugin marketplace. - ### OpenAI Codex CLI In Codex, type "/plugins" to open the plugin search. @@ -76,6 +66,17 @@ In the Codex app, first click on Plugins in the sidebar. You should see Superpowers in the Coding section. Click the + next to Superpowers and follow the prompts. + +### Cursor (via Plugin Marketplace) + +In Cursor Agent chat, install from marketplace: + +```text +/add-plugin superpowers +``` + +or search for "superpowers" in the plugin marketplace. + ### OpenCode Tell OpenCode: From 9f42444ab1b76d015fd554c2fa9a425174b1af0f Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 16 Apr 2026 12:29:58 -0700 Subject: [PATCH 44/46] formatting --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3e3ac65f..06d318f5 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,15 @@ Then install the plugin from this marketplace: ### OpenAI Codex CLI -In Codex, type "/plugins" to open the plugin search. -Type "Superpowers" to search for Superpowers. -Select "Install Plugin" +- Type `/plugins` to open the plugin search. +- Type `Superpowers` to search for Superpowers. +- Select `Install Plugin` ### OpenAI Codex App -In the Codex app, first click on Plugins in the sidebar. -You should see Superpowers in the Coding section. -Click the + next to Superpowers and follow the prompts. +- In the Codex app, click on Plugins in the sidebar. +- You should see `Superpowers` in the Coding section. +- Click the `+` next to Superpowers and follow the prompts. ### Cursor (via Plugin Marketplace) From b55764852ac78870e65c6565fb585b6cd8b3c5c9 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 16 Apr 2026 12:33:48 -0700 Subject: [PATCH 45/46] formatting --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 06d318f5..c10c960f 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,19 @@ Then install the plugin from this marketplace: ### OpenAI Codex CLI -- Type `/plugins` to open the plugin search. -- Type `Superpowers` to search for Superpowers. -- Select `Install Plugin` +- Open plugin search interface + +```bash +/plugins +``` + +Search for Superpowers + +```bash +superpowers +``` + +Select `Install Plugin` ### OpenAI Codex App From 6efe32c9e2dd002d0c394e861e0529675d1ab32e Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 23 Apr 2026 18:31:11 -0700 Subject: [PATCH 46/46] Use committed Codex plugin files in sync script - commit .codex-plugin/plugin.json and brand assets in this repo - sync tracked Codex plugin files instead of generating or seeding them - honor upstream gitignored files during rsync - cover the new sync behavior with regression tests --- .codex-plugin/plugin.json | 44 ++ .version-bump.json | 1 + assets/app-icon.png | Bin 0 -> 48231 bytes assets/superpowers-small.svg | 1 + scripts/sync-to-codex-plugin.sh | 282 +++++---- .../test-sync-to-codex-plugin.sh | 571 ++++++++++++++++++ 6 files changed, 779 insertions(+), 120 deletions(-) create mode 100644 .codex-plugin/plugin.json create mode 100644 assets/app-icon.png create mode 100644 assets/superpowers-small.svg create mode 100755 tests/codex-plugin-sync/test-sync-to-codex-plugin.sh diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 00000000..d4f3f7a3 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,44 @@ +{ + "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": [] + } +} diff --git a/.version-bump.json b/.version-bump.json index f5dbe315..323acb3f 100644 --- a/.version-bump.json +++ b/.version-bump.json @@ -3,6 +3,7 @@ { "path": "package.json", "field": "version" }, { "path": ".claude-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": "gemini-extension.json", "field": "version" } ], diff --git a/assets/app-icon.png b/assets/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..25518da4625e850d97b15a9e0a27e7115969dc0d GIT binary patch literal 48231 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 \ No newline at end of file diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index 0566170a..16fd89ae 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -3,9 +3,9 @@ # sync-to-codex-plugin.sh # # 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 file (.codex-plugin/plugin.json) inline, commits, pushes a -# sync branch, and opens a PR. +# Clones the fork fresh into a temp dir, rsyncs tracked upstream plugin content +# (including committed Codex files under .codex-plugin/ and assets/), 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 @@ -17,13 +17,11 @@ # ./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 +# ./scripts/sync-to-codex-plugin.sh --bootstrap # create plugin dir if missing # -# 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. +# Bootstrap mode: skips the "plugin must exist on base" requirement and creates +# plugins/superpowers/ when absent, then copies the tracked plugin files from +# upstream just like a normal sync. # # Requires: bash, rsync, git, gh (authenticated), python3. @@ -38,9 +36,6 @@ DEFAULT_BASE="main" 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 @@ -78,68 +73,57 @@ EXCLUDES=( "/scripts/" "/tests/" "/tmp/" - - # Codex-only paths — managed outside rsync - "/.codex-plugin/" - "/assets/" ) # ============================================================================= -# Generated overlay file +# Ignored-path helpers # ============================================================================= -# 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" <&2; usage 2 ;; esac @@ -187,19 +169,11 @@ 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" +[[ -f "$UPSTREAM/.codex-plugin/plugin.json" ]] || die "committed Codex manifest missing at $UPSTREAM/.codex-plugin/plugin.json" -# 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")" -[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from upstream package.json" +# Read the upstream version from the committed Codex manifest. +UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/.codex-plugin/plugin.json")" +[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from committed Codex manifest" UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)" UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)" @@ -230,7 +204,9 @@ fi CLEANUP_DIR="" cleanup() { - [[ -n "$CLEANUP_DIR" ]] && rm -rf "$CLEANUP_DIR" + if [[ -n "$CLEANUP_DIR" ]]; then + rm -rf "$CLEANUP_DIR" + fi } trap cleanup EXIT @@ -245,22 +221,84 @@ else fi DEST="$DEST_REPO/$DEST_REL" +PREVIEW_REPO="$DEST_REPO" +PREVIEW_DEST="$DEST" -# Checkout base branch -cd "$DEST_REPO" -git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK" +overlay_destination_paths() { + local repo="$1" + local path + local source_path + local preview_path -# 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 + while IFS= read -r -d '' path; do + source_path="$repo/$path" + preview_path="$PREVIEW_REPO/$path" -# ============================================================================= -# Create sync branch -# ============================================================================= + 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 " + 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 " + fi +} + +apply_to_preview_checkout() { + if [[ $BOOTSTRAP -eq 1 ]]; then + mkdir -p "$PREVIEW_DEST" + fi + + rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$PREVIEW_DEST/" +} + +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)" if [[ $BOOTSTRAP -eq 1 ]]; then @@ -268,14 +306,15 @@ if [[ $BOOTSTRAP -eq 1 ]]; then else SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" fi -git checkout -q -b "$SYNC_BRANCH" # ============================================================================= # Build rsync args # ============================================================================= -RSYNC_ARGS=(-av --delete) +RSYNC_ARGS=(-av --delete --delete-excluded) 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) @@ -288,20 +327,13 @@ 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" + echo "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)" fi echo "" echo "=== Preview (rsync --dry-run) ===" -rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/" +rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$PREVIEW_DEST/" 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 "" @@ -317,18 +349,26 @@ echo "" confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; } echo "" -echo "Syncing upstream content..." -rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" +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 -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" + 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 -echo "Regenerating overlay file..." -generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION" +prepare_apply_checkout +cd "$DEST_REPO" +git checkout -q -b "$SYNC_BRANCH" +echo "Syncing upstream content..." +if [[ $BOOTSTRAP -eq 1 ]]; then + mkdir -p "$DEST" +fi +rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" # Bail early if nothing actually changed cd "$DEST_REPO" @@ -347,16 +387,18 @@ 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. +Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`. -Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap --assets-src \` +Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\` 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." +This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files." 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). +Copies the tracked plugin files from upstream, including the committed Codex manifest and assets. + Run via: \`scripts/sync-to-codex-plugin.sh\` Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA diff --git a/tests/codex-plugin-sync/test-sync-to-codex-plugin.sh b/tests/codex-plugin-sync/test-sync-to-codex-plugin.sh new file mode 100755 index 00000000..a8fc245b --- /dev/null +++ b/tests/codex-plugin-sync/test-sync-to-codex-plugin.sh @@ -0,0 +1,571 @@ +#!/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" < "$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" < "$repo/assets/superpowers-small.svg" <<'EOF' + +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" < "$repo/plugins/superpowers/assets/superpowers-small.svg" <<'EOF' + +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 "$@"