From 2cb0aa63a0f2d57ad3ed3fc82344623b0d78f50b Mon Sep 17 00:00:00 2001 From: Jonathan Langevin Date: Wed, 29 Apr 2026 14:28:02 -0400 Subject: [PATCH 1/2] feat: compaction-recovery hooks + subagent watchdog & rotation patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three resilience layers so long autonomous superpowers runs do not silently degrade after a compaction or a stuck subagent. 1. Compaction-recovery hooks (Claude Code, Cursor) PostToolUse hook on Skill|Agent|Task records each invocation to .claude/superpowers-state/in-progress.jsonl (capped at 200 lines, wiped on startup|clear). SessionStart matcher compact|resume re-injects a block containing: - the original task (extracted from the first user message in the transcript) — re-anchors a compacted SUBAGENT to its assignment - the last 30 activity entries — re-anchors the LEAD orchestrator to its place in the pipeline Both hook scripts no-op gracefully when jq is unavailable. The hook fires inside each session against its own transcript, so subagents recover automatically. 2. Watchdog cadence (every 5–10 min) subagent-driven-development now prescribes a 5–10 min health-check cadence on background subagents (still active? output flowing? API errors? off-task?) with corrective playbooks for stuck agents. Tool-specific guidance is wrapped in blocks; and get host-conditional equivalents (scratch-context tracking, status pings via thread / @mention). 3. Quality-based rotation subagent-driven-development now prescribes replacing — not re-prompting — a subagent_type that is consistently low-quality. Rotation triggers: 2 consecutive review rejections on a task, 3 cumulative session quality issues, or 2 instances of ignored guidance. Rotation should be stated visibly in user-facing text so the user can redirect. Why these patterns: 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 on hosts that support hooks; the watchdog and rotation patterns rely on the orchestrator applying them consistently. Bumps plugin version 5.2.0 -> 5.3.0 across .claude-plugin/plugin.json, .claude-plugin/marketplace.json, and .cursor-plugin/plugin.json (the last had been at 5.1.0 — synced to match the others). RELEASE-NOTES.md gets a v5.3.0 entry. Cross-host conditionals follow the existing convention (, , ); the host-neutral body is the pattern itself, host blocks carry the specific tools. --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- RELEASE-NOTES.md | 25 ++++++ hooks/hooks.json | 12 +++ hooks/record-activity | 57 +++++++++++++ hooks/session-start | 91 ++++++++++++++++++--- skills/subagent-driven-development/SKILL.md | 87 ++++++++++++++++++++ 8 files changed, 264 insertions(+), 14 deletions(-) create mode 100755 hooks/record-activity 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..8f9fc35 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 invocation to `.claude/superpowers-state/in-progress.jsonl` (capped at 200 lines; wiped on `startup|clear`). 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..1891fd4 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..ad503fe --- /dev/null +++ b/hooks/record-activity @@ -0,0 +1,57 @@ +#!/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 the Skill or Agent tool. 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}" + ;; + *) + 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" + +ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +jq -nc \ + --arg ts "$ts" \ + --arg tool "$tool_name" \ + --arg detail "$detail" \ + '{ts: $ts, tool: $tool, detail: $detail}' \ + >> "$STATE_FILE" 2>/dev/null || true + +# Cap state file at 200 lines so it doesn't grow unbounded. +if [ -f "$STATE_FILE" ] && [ "$(wc -l < "$STATE_FILE" 2>/dev/null || echo 0)" -gt 200 ]; then + tail -n 200 "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" +fi + +exit 0 diff --git a/hooks/session-start b/hooks/session-start index 0eba96b..8c92c9c 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -1,25 +1,96 @@ #!/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 [ "$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 + resumption_section+=$'\n\n## Original task (first user message in this session)\n\n'"$original_task" + 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 +103,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`). 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 `superpowers:model-tiers` agent). +- 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. From f5bb880cabbab042df77fa83975bf3a16e262ecf Mon Sep 17 00:00:00 2001 From: Jonathan Langevin Date: Wed, 29 Apr 2026 15:47:36 -0400 Subject: [PATCH 2/2] fix(hooks): address review feedback on resilience hooks - record-activity: broaden Task* matcher to capture TaskCreate/TaskList/ TaskGet/TaskUpdate (header comment + new case arm with generic best-effort detail), fix the docstring that previously implied only Skill/Agent were recorded. - record-activity: serialize the cap/rotate step to prevent concurrent writers from corrupting the JSONL during truncation. Uses flock when available, falls back to an atomic-mkdir mutex on platforms without util-linux flock (notably macOS). PID-tagged tmp file prevents tmp collisions. - hooks.json: matcher updated to Skill|Agent|Task.* to actually capture Task* family tools. - session-start: fail closed when the session source can't be determined (e.g. jq unavailable) so stale activity from a prior session can't leak into the resumption context. - session-start: fence-wrap the original_task content so tag-like text (including a literal ) in the first user message can't terminate or nest inside the resumption block. Fence length escalates if backticks already appear in the content. - SKILL.md: link to agents/model-tiers.md (the actual file) instead of a non-existent superpowers:model-tiers agent for the tier-escalation reference. - SKILL.md + RELEASE-NOTES.md: update matcher docs to reflect Task.* and the fail-closed wipe. --- RELEASE-NOTES.md | 2 +- hooks/hooks.json | 2 +- hooks/record-activity | 65 ++++++++++++++++++--- hooks/session-start | 16 ++++- skills/subagent-driven-development/SKILL.md | 4 +- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 8f9fc35..1c95e4e 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -9,7 +9,7 @@ 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 invocation to `.claude/superpowers-state/in-progress.jsonl` (capped at 200 lines; wiped on `startup|clear`). This is the activity log that the SessionStart hook replays. +- **`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. diff --git a/hooks/hooks.json b/hooks/hooks.json index 1891fd4..49087bd 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -14,7 +14,7 @@ ], "PostToolUse": [ { - "matcher": "Skill|Agent|Task", + "matcher": "Skill|Agent|Task.*", "hooks": [ { "type": "command", diff --git a/hooks/record-activity b/hooks/record-activity index ad503fe..d5ab221 100755 --- a/hooks/record-activity +++ b/hooks/record-activity @@ -2,9 +2,10 @@ # PostToolUse hook: record superpowers-relevant activity # # Appends an entry to .claude/superpowers-state/in-progress.jsonl every time -# the lead/orchestrator agent invokes the Skill or Agent tool. 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. +# 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 @@ -32,6 +33,18 @@ case "$tool_name" in 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 ;; @@ -40,18 +53,52 @@ 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") -jq -nc \ +entry=$(jq -nc \ --arg ts "$ts" \ --arg tool "$tool_name" \ --arg detail "$detail" \ - '{ts: $ts, tool: $tool, detail: $detail}' \ - >> "$STATE_FILE" 2>/dev/null || true + '{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 -# Cap state file at 200 lines so it doesn't grow unbounded. -if [ -f "$STATE_FILE" ] && [ "$(wc -l < "$STATE_FILE" 2>/dev/null || echo 0)" -gt 200 ]; then - tail -n 200 "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" +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 8c92c9c..7c5755f 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -35,7 +35,10 @@ 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 [ "$source_kind" = "startup" ] || [ "$source_kind" = "clear" ]; then +# 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 @@ -71,7 +74,16 @@ if { [ "$source_kind" = "compact" ] || [ "$source_kind" = "resume" ]; } && comma 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 - resumption_section+=$'\n\n## Original task (first user message in this session)\n\n'"$original_task" + # 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" diff --git a/skills/subagent-driven-development/SKILL.md b/skills/subagent-driven-development/SKILL.md index 3f0b892..d830a13 100644 --- a/skills/subagent-driven-development/SKILL.md +++ b/skills/subagent-driven-development/SKILL.md @@ -345,7 +345,7 @@ Hooks automate it. The plugin's `SessionStart` hook (matcher `compact|resume`) f - 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`). The state file is project-local at `.claude/superpowers-state/in-progress.jsonl`. +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. @@ -399,7 +399,7 @@ Subagents are teammates, not infrastructure. If one keeps producing low-quality - 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 `superpowers:model-tiers` agent). +- 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.