feat: lifecycle event hooks for external plugin authors#1461
Open
cdub615 wants to merge 13 commits into
Open
Conversation
Defines the minimum-surface lifecycle event API: 4 events (PlanWritten, TaskClaimed, TaskCompleted, BlockedOnHuman), one shell-based event bus (scripts/emit-hook.sh), one env-var registry (SUPERPOWERS_HOOK_DIRS). Plugin authors drop hook scripts into a registered directory; core fires events at lifecycle moments in writing-plans, executing-plans, and subagent-driven-development. Core knows nothing about specific plugins. Designed to enable rebuilding the beads-integration fork as a standalone plugin without forking core skills.
Resolved the four open questions: - env var: SUPERPOWERS_HOOK_DIRS - timeout: 10s default, SUPERPOWERS_HOOK_TIMEOUT override - execution: sequential per/across dirs - markers: <!-- BEGIN lifecycle:Event --> matching existing beads convention
10 tasks across 3 chunks: - Chunk 1 (TDD): build emit-hook.sh + 11-test bash suite - Chunk 2: additive emit blocks in 3 core skills - Chunk 3: plugin-author reference doc + end-to-end stub verification Plan executes the spec at docs/superpowers/specs/2026-05-02-lifecycle-events-design.md
scripts/emit-hook.sh exits silently when SUPERPOWERS_HOOK_DIRS is unset. Test harness lives in tests/lifecycle-events/. Refs: superpowers-rt9.1
emit-hook.sh now scans SUPERPOWERS_HOOK_DIRS, locates matching <EventName>.sh files, and runs them with key=value args translated to SP_<KEY> env vars. Refs: superpowers-rt9.2
Nonzero hook exit codes log a warning but emit-hook still exits 0. Non-executable hook scripts produce a warning and are skipped. Refs: superpowers-rt9.3
Hooks that exceed SUPERPOWERS_HOOK_TIMEOUT (default 10s) are killed via timeout(1)/gtimeout(1) with a warning. Falls back to unbounded execution + one-time warning if neither tool is available. Refs: superpowers-rt9.4
Add 5 tests verifying behaviors already supported by emit-hook.sh: - hook stdin is /dev/null - hook stdout is discarded - multiple registered dirs run sequentially in order - key=value preserves literal '=' inside the value - missing event name logs warning and exits 0 No implementation changes required; all 11 tests pass. Refs: superpowers-rt9.5
Adds a small additive block that fires the PlanWritten lifecycle event after self-review passes. No-op when SUPERPOWERS_HOOK_DIRS is unset; the legacy markdown-only flow is unchanged. Refs: superpowers-rt9.6
Adds three additive blocks for TaskClaimed, TaskCompleted, and BlockedOnHuman at the corresponding state-transition points in Step 2 and the When-to-Stop section. No behavior change when SUPERPOWERS_HOOK_DIRS is unset. Refs: superpowers-rt9.7
Adds a Lifecycle Events section between Model Selection and Handling Implementer Status with three additive blocks (TaskClaimed, TaskCompleted, BlockedOnHuman). No behavior change when SUPERPOWERS_HOOK_DIRS is unset. Refs: superpowers-rt9.8
Documents the event catalog (PlanWritten, TaskClaimed, TaskCompleted, BlockedOnHuman), the emit-hook.sh dispatch contract, configuration env vars, failure modes, and a minimal example plugin layout. Refs: superpowers-rt9.9
Two doc fixes from final review: - Spec referenced scripts/tests/emit-hook.test.sh; actual is tests/lifecycle-events/emit-hook.test.sh (matches existing tests/<feature>/ convention). - User-facing reference doc only mentioned $SUPERPOWERS_ROOT; it now documents the CLAUDE_PLUGIN_ROOT/CURSOR_PLUGIN_ROOT fallback chain so plugin authors understand how core finds the emit script across harnesses. Refs: superpowers-rt9
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What problem are you trying to solve?
I built a Beads integration as a fork of Superpowers (visible on my fork at cdub615/superpowers:beads-integration). To make it shippable as a standalone plugin — which is what this project's contributor guidelines say domain-specific tools should be — the integration needs to react to plan/task lifecycle moments without modifying core skills. Today there's no supported mechanism for that, so the integration forks three core skills (
writing-plans,executing-plans,subagent-driven-development) to add inlinebdcalls. That's the maintenance burden every workflow plugin author hits.The specific failure mode: when I tried to extract the integration into a standalone plugin, I had nowhere to attach. The plan-mirror logic needs to fire after
writing-plans' self-review passes; thebd update --claimlogic needs to fire when a task transitions to in_progress; the close logic needs to fire on completion; thebd update --status blockedneeds to fire on BLOCKED. With no hook mechanism, the only way to wire any of those is to edit the skill files. The fork branch is the proof.I filed #1442 to propose this lifecycle-events surface; this PR is the implementation of the v1 subset of that proposal.
What does this PR change?
Adds a minimum-surface lifecycle event API:
scripts/emit-hook.sh(~90 LOC bash) — dispatches events to plugin shell scripts found in$SUPERPOWERS_HOOK_DIRS(colon-separated, like$PATH).PlanWritten,TaskClaimed,TaskCompleted,BlockedOnHuman. Each block is wrapped in<!-- BEGIN lifecycle:Event -->/<!-- END lifecycle:Event -->markers and is a no-op whenSUPERPOWERS_HOOK_DIRSis unset.SP_<KEY>env vars (e.g.,SP_PLAN_PATH,SP_TASK_NUMBER,SP_REASON).SUPERPOWERS_HOOK_TIMEOUToverride). Plugin failures (timeout, nonzero exit, missing exec bit) log a warning but never propagate —emit-hook.shalways exits 0.docs/superpowers/lifecycle-events.mdplugin author reference.tests/lifecycle-events/emit-hook.test.sh— 11 tests covering every documented behavior and failure mode.Total diff: ~600 LOC, all additive, zero rewrites of existing skill prose.
This is a deliberate v1 subset of the 10 events proposed in #1442 — only the 4 events with a concrete consumer (the Beads integration) are included. The other 6 (
BrainstormCompleted,DesignSaved,WorktreeCreated,ReviewFindingCreated,EpicCompleted,BranchFinished) are documented in the spec as roadmap items, each waiting for a real plugin that needs them. This avoids speculative additions per the project's "no theoretical fixes" rule.Is this change appropriate for the core library?
Yes — this is general-purpose plugin infrastructure, not a domain-specific skill. It benefits all users by giving them a sanctioned way to extend Superpowers' lifecycle without modifying core, which directly aligns with the contributor guidelines ("domain-specific tools belong in standalone plugins").
No third-party dependencies. No domain-specific content. No new domain skills. The Beads use case is mentioned only as evidence that the problem is real; the events themselves are agnostic to any specific consumer. Other consumers #1442 enumerates: Linear/Jira/GitHub Issues sync, Slack/email notifications, per-epic cost controls, CI pre-validation, team-level workflow telemetry — none of which can be built today without forking.
What alternatives did you consider?
$SP_PLAN_PATHif they need richer structure. Roadmap item if/when an event genuinely needs nested data.$PATH) is the smallest possible discovery surface.PlanWritten, and the implementer prompt naturally picks up the mutation since it sends full task body text.Does this PR contain multiple unrelated changes?
No. Single feature: shell-based lifecycle event hooks for external plugin observers. The 13 commits are scoped TDD increments of one design (skeleton → dispatch → failure handling → timeout → edge tests → 3 skill emit points → reference doc → spec/doc fixes from review).
Existing PRs
Most relevant prior art:
feat: add lifecycle extension system for user-defined workflow hooks #1224 (open, no maintainer response yet) —
feat: add lifecycle extension system for user-defined workflow hooksby @jhochenbaum. Same problem space (lifecycle hooks at workflow events), genuinely different mechanism. I want to surface the comparison up front so the maintainer can choose how the project wants to handle this:Skilltool inside the agent's call graph~/.superpowers/extensions.yamland.superpowers/extensions.yaml$SUPERPOWERS_HOOK_DIRS(colon-separated dirs)post-brainstorm,post-plan,pre-task,post-task,post-execution,post-review,pre-finish)PlanWritten,TaskClaimed,TaskCompleted,BlockedOnHuman)The two approaches solve adjacent problems. feat: add lifecycle extension system for user-defined workflow hooks #1224's design is an excellent fit for the use cases listed in its PR body (extensions that read agent context and contribute to its reasoning). This PR's design is the right fit for use cases like Beads (mirror plan/task state to an external database) where the plugin work happens after the agent has moved on and shouldn't pollute the agent's call graph.
Honest position: if the maintainer wants only one mechanism, feat: add lifecycle extension system for user-defined workflow hooks #1224 is the more general design. I'd rather see it land than nothing. But if external-observer hooks are seen as complementary infrastructure, this PR's design is genuinely simpler for that use case (no manifest parser, no Skill tool integration, no agent-graph participation). I'm happy to coordinate with @jhochenbaum if there's a unified approach worth pursuing.
feat(iterating-on-plans): add post-execution iteration skill #943 (open) — post-execution iteration skill. Referenced in feat: add lifecycle extension system for user-defined workflow hooks #1224 as motivation. Not a direct conflict; it's a candidate consumer that either feat: add lifecycle extension system for user-defined workflow hooks #1224 or this PR could enable.
feat: establish plugin architecture and add tinystruct developer skill #1107 — plugin architecture (skill directory organization). Different scope.
RFC: Context provider convention for domain-specific sub-agent protocols #1128 / feat: context provider convention for sub-agent protocols #1129 — context provider via MCP. Different scope.
Related issue: #1442 (this PR's motivating issue, opened by me).
Environment tested
Cross-harness validation (Cursor, Codex, Gemini, OpenCode) was not run — the script's harness fallback chain (
CLAUDE_PLUGIN_ROOT→CURSOR_PLUGIN_ROOT→SUPERPOWERS_ROOT) follows the existing precedent fromhooks/session-startand other scripts but should be validated on at least one non-Claude-Code harness before merge. Happy to run that if requested.New harness support (required if this PR adds a new harness)
N/A — does not add a new harness.
Evaluation
SUPERPOWERS_HOOK_DIRSunset → rc=0, empty output).beads-integrationrequires direct edits towriting-plans/SKILL.md,executing-plans/SKILL.md,subagent-driven-development/SKILL.md, plus injecting prompt-template content intosubagent-driven-development/implementer-prompt.md— that's 79+ lines of skill modifications a fork has to maintain. After this change, that same plugin can ship as a standalone directory of shell scripts the user adds to$SUPERPOWERS_HOOK_DIRS, with zero edits to core. Default user behavior is identical in both cases — when no plugins are installed, the lifecycle blocks are conditional no-ops.Rigor
<!-- BEGIN lifecycle:Event -->/<!-- END lifecycle:Event -->markers;git diffagainst base shows zero-content lines inskills/). No rewording of existing prose. No modifications to Red Flags / rationalization tables / "human partner" language / behavior-shaping content. The added blocks all open with the no-op-when-unset note so an agent reading them on a default install knows to skip.HOOK_DIRS, missing hook script, non-executable hook, hook exits nonzero, hook exceeds timeout, missing event name, multi-dir ordering, stdin/stdout isolation, key=value with literal=in value, malformed args. Pre-impl test runs confirmed each TDD test failed for the expected reason before the implementation landed.git diff— the skill changes touch only inserted blocks; the existing Red Flags tables, rationalization checklists, "human partner" framings, and any agent-behavior-shaping prose are byte-for-byte unchanged.Human review