diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ecb23d4..047cd23 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.2.0", + "version": "5.3.0", "source": "./", "author": { "name": "Jesse Vincent", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 4d1cb97..f489e23 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.2.0", + "version": "5.3.0", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index f2daa59..db19b5c 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.1.0", + "version": "5.3.0", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 56c4824..1c95e4e 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -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 `` 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 `` 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 diff --git a/hooks/hooks.json b/hooks/hooks.json index 2dacc8a..49087bd 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -11,6 +11,18 @@ } ] } + ], + "PostToolUse": [ + { + "matcher": "Skill|Agent|Task.*", + "hooks": [ + { + "type": "command", + "command": "'${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' record-activity", + "timeout": 10 + } + ] + } ] } } diff --git a/hooks/record-activity b/hooks/record-activity new file mode 100755 index 0000000..d5ab221 --- /dev/null +++ b/hooks/record-activity @@ -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 + +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 + +exit 0 diff --git a/hooks/session-start b/hooks/session-start index 0eba96b..7c5755f 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -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' + 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 ""), + # 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 + 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' + 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\nIN 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" + warning_message=$'\n\nIN 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' 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//\\/\\\\}" @@ -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="\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" +resumption_escaped=$(escape_for_json "$resumption_section") + +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}${resumption_escaped}\n" -# Output context injection as JSON. -# Keep both shapes for compatibility: -# - Cursor hooks expect additional_context. -# - Claude hooks expect hookSpecificOutput.additionalContext. cat < + +Hooks automate it. The plugin's `SessionStart` hook (matcher `compact|resume`) fires inside the compacted session and injects a `` block containing: + +- The first user message from the transcript (the "original task") — this is what re-anchors a compacted **subagent** to its assignment. +- Recent superpowers activity (last 30 entries from `.claude/superpowers-state/in-progress.jsonl`) — this is what re-anchors the **lead** in the pipeline. + +Activity is captured by a `PostToolUse` hook (matcher `Skill|Agent|Task.*`) that appends each invocation to the JSONL state file (capped at 200 lines; wiped on `startup|clear` or when the session source can't be determined). The state file is project-local at `.claude/superpowers-state/in-progress.jsonl`. + +You don't opt in. When you see the resumption block, treat it as authoritative and reorient before responding. + + + + + +Hooks are not documented on this host. Apply the pattern manually: at the start of every reply that follows a context compaction, scroll back to the first user message in your visible transcript, re-state the task to yourself, then proceed. If you keep a running activity log in your scratch context, re-read it before issuing new subagent instructions. + + + +### 2. Watchdog cadence (every 5–10 minutes) + +When you have one or more subagents running in the background, check on them on a 5–10 minute cadence. Don't fire-and-forget. + +**On each check, verify:** +- Still running? A hung/zombied agent might not have produced any signal — explicit status confirms it's still active. +- Producing output? An agent that has stopped emitting tool calls or text for >5 minutes is suspect. +- Errored? Look for API errors, rate limits, transport failures, "context length exceeded" — these often surface as a stalled output stream rather than a crash. +- Off-track? Spot-check that the latest output is actually progressing the assigned task, not flailing on a tangent. + +**If a subagent looks stuck:** +- Send a corrective message to redirect (e.g. "you have been silent for 7 minutes; report current state and the next concrete step you will take"). +- If unrecoverable: terminate it, then re-dispatch a fresh subagent with the same brief plus a one-line note about what the prior attempt got wrong. + + + +Use `TaskList` to confirm active subagents, `TaskOutput` to read recent stdout, `SendMessage` (with `to: `) to send corrective input, and `TaskStop` to terminate. When using `ScheduleWakeup` to pace a `/loop` self-paced run, factor watchdog checks into the cadence — don't sleep past your next check window. + + + + + +Codex doesn't expose a structured task list. Track dispatched subagents in your own scratch context (one line per subagent: id, started-at, last-checked-at, current-stage). On each watchdog interval, ask each subagent for a status report directly via its thread; "report your current step and what you plan to do next, in one paragraph" forces it to surface progress or admit it's stuck. + + + + + +Use `@mention` to peer sessions to ping each background subagent for a status report on cadence. If a peer has gone silent past the check window, treat it as suspect. + + + +### 3. Quality-based rotation (replace consistently-failing teammates) + +Subagents are teammates, not infrastructure. If one keeps producing low-quality output, replace it instead of re-prompting forever. + +**Track per subagent_type, per session:** +- Number of code-review rejections it produced (spec compliance OR code quality stage). +- Number of times you had to send corrective input to redirect it. +- Number of test/build failures attributable to its work. + +**Rotation triggers:** +- 2 consecutive code-review rejections on the same task → switch to a different `subagent_type` for the retry, or escalate to a higher model tier (see `agents/model-tiers.md` for the role-to-host model mapping). +- 3 cumulative quality issues across tasks in one session → stop dispatching that subagent_type for the remainder of the session; re-route its work to an alternative. +- A subagent that ignores explicit guidance twice in a row → stop using it; the issue is the agent profile, not the prompt. + +When you rotate, briefly state the rotation in user-facing text ("Rotating off `general-purpose` for review tasks — two consecutive rejections; using `superpowers:code-reviewer` instead") so the user has a chance to redirect. + +### Why these patterns + +The hook automation handles compaction recovery deterministically on hosts that support it. The watchdog cadence and rotation rules rely on you applying them consistently — without them, a subagent that compacts, hits a transient API error, or quietly drifts off-task can burn 30+ minutes before anyone notices.