Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.2.0",
"version": "5.3.0",
"source": "./",
"author": {
"name": "Jesse Vincent",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.2.0",
"version": "5.3.0",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "superpowers",
"displayName": "Superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.1.0",
"version": "5.3.0",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
Expand Down
25 changes: 25 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Superpowers Release Notes

## v5.3.0 (2026-04-29)

### New Features

**Compaction-recovery hooks (Claude Code, Cursor)**

Long autonomous runs are now resilient to context compaction. Two hooks ship in `hooks/hooks.json`:

- **`SessionStart` (matcher `compact|resume`)** — re-injects a `<superpowers-resumption-context>` block into the resumed session containing the original task (extracted from the first user message in the transcript) and the last 30 superpowers activity entries. This re-anchors a compacted **subagent** to its original assignment and re-anchors the **lead orchestrator** to its place in the pipeline. The hook fires inside each session against its own transcript, so subagents recover their own task context automatically.
- **`PostToolUse` (matcher `Skill|Agent|Task.*`)** — appends each `Skill`, `Agent`, and `Task*`-family (`Task`, `TaskCreate`, `TaskList`, `TaskGet`, `TaskUpdate`, etc.) invocation to `.claude/superpowers-state/in-progress.jsonl` (capped at 200 lines; wiped on `startup|clear`, or when the session source can't be determined). Append+rotate is guarded by a `flock` when available so concurrent writes from the lead and subagents don't corrupt the JSONL. This is the activity log that the SessionStart hook replays.

The state file is project-local and in JSONL format. Both hooks no-op gracefully when `jq` is unavailable. On hosts without a documented hooks system (Codex, OpenCode), the same recovery pattern is described in prose as a manual discipline.

**Subagent watchdog cadence (every 5–10 minutes)**

`subagent-driven-development` now prescribes a 5–10 minute health-check cadence on background subagents: confirm still-active, output flowing, no API/rate-limit/transport errors, not flailing off-task. Includes corrective playbooks for stuck agents (send a redirecting message, or terminate and re-dispatch with a one-line note about what went wrong). Tools mentioned (`TaskList`, `TaskOutput`, `SendMessage`, `TaskStop`, `ScheduleWakeup`) are wrapped in `<host: claude-code>` blocks; Codex and OpenCode get host-conditional equivalents (scratch-context tracking, status pings via thread / `@mention`).

**Quality-based subagent rotation**

`subagent-driven-development` now prescribes replacing — not re-prompting — a `subagent_type` that is consistently low-quality. Track per-session quality signals (review rejections, corrective messages, attributable failures); rotate triggers are 2 consecutive review rejections on the same task, 3 cumulative quality issues across tasks, or 2 instances of ignored guidance. Rotation should be stated visibly in user-facing text so the user can redirect.

### Why

A subagent that compacts mid-flight, hits a transient API error, or quietly drifts off-task can burn 30+ minutes of autonomous run time before anyone notices. The hook automation handles compaction recovery deterministically; the watchdog and rotation patterns rely on the orchestrator applying them consistently. Together they close the most common silent-degradation paths in the autonomous pipeline.

## v5.0.0 (2026-03-04)

### New Features
Expand Down
12 changes: 12 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
}
]
}
],
"PostToolUse": [
{
"matcher": "Skill|Agent|Task.*",
"hooks": [
{
"type": "command",
"command": "'${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' record-activity",
"timeout": 10
}
]
}
]
}
}
104 changes: 104 additions & 0 deletions hooks/record-activity
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# PostToolUse hook: record superpowers-relevant activity
#
# Appends an entry to .claude/superpowers-state/in-progress.jsonl every time
# the lead/orchestrator agent invokes a Skill, Agent, or Task* tool
# (Task, TaskCreate, TaskList, TaskGet, TaskUpdate, etc.). The session-start
# hook replays this file on compact|resume so the agent can resume the
# pipeline it was running and check on dispatched subagents.

set -euo pipefail

# Need stdin (the PostToolUse JSON payload) and jq to do anything useful.
[ -t 0 ] && exit 0
command -v jq >/dev/null 2>&1 || exit 0

hook_input=$(cat || true)
[ -z "$hook_input" ] && exit 0

tool_name=$(printf '%s' "$hook_input" | jq -r '.tool_name // empty' 2>/dev/null || true)
cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true)
[ -z "$cwd_dir" ] && cwd_dir="${PWD}"

case "$tool_name" in
Skill)
skill=$(printf '%s' "$hook_input" | jq -r '.tool_input.skill // ""' 2>/dev/null || true)
args=$(printf '%s' "$hook_input" | jq -r '.tool_input.args // ""' 2>/dev/null || true)
detail="skill=${skill}"
[ -n "$args" ] && detail="${detail} args=${args:0:80}"
;;
Agent|Task)
sa_type=$(printf '%s' "$hook_input" | jq -r '.tool_input.subagent_type // "general-purpose"' 2>/dev/null || true)
desc=$(printf '%s' "$hook_input" | jq -r '.tool_input.description // ""' 2>/dev/null || true)
bg=$(printf '%s' "$hook_input" | jq -r '.tool_input.run_in_background // false' 2>/dev/null || true)
detail="agent=${sa_type} desc=\"${desc:0:120}\" bg=${bg}"
;;
Task*)
# Task family tools (TaskCreate/TaskList/TaskGet/TaskUpdate/etc.)
# don't share a single payload schema; record a generic best-effort
# summary from common fields rather than failing closed.
sa_type=$(printf '%s' "$hook_input" | jq -r '.tool_input.subagent_type // ""' 2>/dev/null || true)
desc=$(printf '%s' "$hook_input" | jq -r '.tool_input.description // .tool_input.title // ""' 2>/dev/null || true)
task_id=$(printf '%s' "$hook_input" | jq -r '.tool_input.id // .tool_input.task_id // ""' 2>/dev/null || true)
detail="task_tool=${tool_name}"
[ -n "$sa_type" ] && detail="${detail} agent=${sa_type}"
[ -n "$task_id" ] && detail="${detail} id=${task_id}"
[ -n "$desc" ] && detail="${detail} desc=\"${desc:0:120}\""
;;
*)
exit 0
;;
esac
Comment thread
intel352 marked this conversation as resolved.

STATE_DIR="${cwd_dir}/.claude/superpowers-state"
mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
STATE_FILE="${STATE_DIR}/in-progress.jsonl"
LOCK_FILE="${STATE_DIR}/.in-progress.lock"

ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
entry=$(jq -nc \
--arg ts "$ts" \
--arg tool "$tool_name" \
--arg detail "$detail" \
'{ts: $ts, tool: $tool, detail: $detail}' 2>/dev/null || true)
[ -z "$entry" ] && exit 0

# Append + cap under a lock so concurrent invocations from lead + subagents
# can't corrupt the JSONL during the rotate step. POSIX `O_APPEND` makes the
# raw append safe across processes already; the rotate step is what needs
# serialization (tail-to-tmp + mv races can clobber the file).
append_line() {
printf '%s\n' "$entry" >> "$STATE_FILE" 2>/dev/null || return 0
}

# Mutex-protected rotate. Uses flock when available; falls back to
# `mkdir` (atomic on POSIX) so platforms without util-linux flock
# (notably macOS) still serialize rotation correctly.
rotate_if_needed() {
[ -f "$STATE_FILE" ] || return 0
[ "$(wc -l < "$STATE_FILE" 2>/dev/null || echo 0)" -gt 200 ] || return 0
# Use a PID-tagged tmp so concurrent rotates don't clobber each other's tmp.
local tmp="${STATE_FILE}.tmp.$$"
tail -n 200 "$STATE_FILE" > "$tmp" 2>/dev/null \
&& mv "$tmp" "$STATE_FILE" 2>/dev/null
rm -f "$tmp" 2>/dev/null || true
}

append_line

if command -v flock >/dev/null 2>&1; then
(
flock -w 5 9 || exit 0
rotate_if_needed
) 9>"$LOCK_FILE"
else
# mkdir is an atomic mutex on POSIX; only one rotater wins, others bail.
if mkdir "${LOCK_FILE}.d" 2>/dev/null; then
trap 'rmdir "${LOCK_FILE}.d" 2>/dev/null || true' EXIT
rotate_if_needed
rmdir "${LOCK_FILE}.d" 2>/dev/null || true
trap - EXIT
fi
fi
Comment thread
intel352 marked this conversation as resolved.

exit 0
103 changes: 92 additions & 11 deletions hooks/session-start
Original file line number Diff line number Diff line change
@@ -1,25 +1,108 @@
#!/usr/bin/env bash
# SessionStart hook for superpowers plugin
#
# On every session start (startup|resume|clear|compact) this loads the
# using-superpowers skill so the skills system is available before the first
# turn. On compact|resume it ALSO injects a "resumption context" block that:
# - replays recent superpowers activity from .claude/superpowers-state/in-progress.jsonl
# (so a lead orchestrator knows which skills + subagents were in flight), and
# - extracts the first user message from the session transcript
# (so a subagent that just compacted can re-read its original task).

set -euo pipefail

# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

# Check if legacy skills directory exists and build warning
# Read hook payload from stdin (JSON). Tolerate empty/no-stdin invocations.
hook_input=""
if [ ! -t 0 ]; then
hook_input=$(cat || true)
fi
[ -z "$hook_input" ] && hook_input='{}'

source_kind=""
transcript_path=""
cwd_dir="${PWD}"
if command -v jq >/dev/null 2>&1; then
source_kind=$(printf '%s' "$hook_input" | jq -r '.source // empty' 2>/dev/null || true)
transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty' 2>/dev/null || true)
cwd_from_hook=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true)
[ -n "$cwd_from_hook" ] && cwd_dir="$cwd_from_hook"
fi

STATE_DIR="${cwd_dir}/.claude/superpowers-state"
STATE_FILE="${STATE_DIR}/in-progress.jsonl"

# Wipe stale state at the start of a brand-new (or cleared) session.
# If the session source can't be determined (e.g. jq is unavailable) treat
# it as a fresh session and fail closed, so stale activity from a prior
# session can't leak into the resumption context.
if [ -z "$source_kind" ] || [ "$source_kind" = "startup" ] || [ "$source_kind" = "clear" ]; then
rm -f "$STATE_FILE"
fi

# Build the resumption context (only on compact|resume).
resumption_section=""
if { [ "$source_kind" = "compact" ] || [ "$source_kind" = "resume" ]; } && command -v jq >/dev/null 2>&1; then
activity=""
if [ -f "$STATE_FILE" ]; then
activity=$(tail -n 30 "$STATE_FILE" \
| jq -r '"- " + .ts + " " + .tool + " " + (.detail // "")' 2>/dev/null \
|| tail -n 30 "$STATE_FILE")
fi

original_task=""
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
original_task=$(jq -rs '
map(select((.type // "") == "user")) |
(first // empty) as $first |
($first.message.content // $first.content // "") |
if type == "string" then .
elif type == "array" then ([.[] | select((.type // "") == "text") | (.text // "")] | join("\n"))
else "" end
' "$transcript_path" 2>/dev/null || true)
original_task="${original_task:0:4000}"
fi

if [ -n "$activity" ] || [ -n "$original_task" ]; then
resumption_section=$'\n\n<superpowers-resumption-context source="'"$source_kind"$'">'
case "$source_kind" in
compact) source_phrase="just had a context-compaction event" ;;
resume) source_phrase="just resumed from a previous transcript" ;;
*) source_phrase="was just initialized with source=${source_kind}" ;;
esac
resumption_section+=$'\nThe session '"$source_phrase"$'. BEFORE answering the next message, reorient yourself using the context below. If you were mid-pipeline (brainstorming → writing-plans → alignment-check → subagent-driven-development → finishing-a-development-branch → pr-monitoring), pick up where you left off rather than re-deriving intent.'
if [ -n "$original_task" ]; then
# The first user message can contain arbitrary markup (tag-like
# tokens, even a literal "</superpowers-resumption-context>"),
# which would terminate or nest inside the resumption block we
# just opened. Wrap it in a fenced code block and pick a fence
# length that does not appear inside the content.
fence='```'
while printf '%s' "$original_task" | grep -qF "$fence"; do
fence="${fence}\`"
done
resumption_section+=$'\n\n## Original task (first user message in this session)\n\n'"${fence}"$'\n'"${original_task}"$'\n'"${fence}"
fi
Comment thread
intel352 marked this conversation as resolved.
if [ -n "$activity" ]; then
resumption_section+=$'\n\n## Recent superpowers activity (most recent 30 events)\n\n'"$activity"
resumption_section+=$'\n\nIf any of those entries are dispatched subagents, check their status (still active? errored? rate-limited? zombied?) using your host\'s task-listing mechanism before proceeding. See superpowers:subagent-driven-development for the watchdog cadence and host-specific tools.'
fi
resumption_section+=$'\n</superpowers-resumption-context>'
fi
fi

# Legacy skills warning (preserved from upstream).
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
warning_message=$'\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code\'s skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>'
fi

# Read using-superpowers content
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")

# Escape string for JSON embedding using bash parameter substitution.
# Each ${s//old/new} is a single C-level pass - orders of magnitude
# faster than the character-by-character loop this replaces.
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}"
Expand All @@ -32,12 +115,10 @@ escape_for_json() {

using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
warning_escaped=$(escape_for_json "$warning_message")
session_context="<EXTREMELY_IMPORTANT>\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</EXTREMELY_IMPORTANT>"
resumption_escaped=$(escape_for_json "$resumption_section")

session_context="<EXTREMELY_IMPORTANT>\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}${resumption_escaped}\n</EXTREMELY_IMPORTANT>"

# Output context injection as JSON.
# Keep both shapes for compatibility:
# - Cursor hooks expect additional_context.
# - Claude hooks expect hookSpecificOutput.additionalContext.
cat <<EOF
{
"additional_context": "${session_context}",
Expand Down
Loading
Loading