Skip to content

feat: lifecycle event hooks for external plugin authors#1461

Open
cdub615 wants to merge 13 commits into
obra:mainfrom
cdub615:lifecycle-events
Open

feat: lifecycle event hooks for external plugin authors#1461
cdub615 wants to merge 13 commits into
obra:mainfrom
cdub615:lifecycle-events

Conversation

@cdub615
Copy link
Copy Markdown

@cdub615 cdub615 commented May 3, 2026

Addresses #1442 (which I opened to propose this lifecycle-events surface). The issue lays out the motivating use case in more detail; this PR is the v1 implementation.

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 inline bd calls. 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; the bd update --claim logic needs to fire when a task transitions to in_progress; the close logic needs to fire on completion; the bd update --status blocked needs 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:

  • New scripts/emit-hook.sh (~90 LOC bash) — dispatches events to plugin shell scripts found in $SUPERPOWERS_HOOK_DIRS (colon-separated, like $PATH).
  • 4 events emitted from existing core skills via additive markdown blocks: PlanWritten, TaskClaimed, TaskCompleted, BlockedOnHuman. Each block is wrapped in <!-- BEGIN lifecycle:Event --> / <!-- END lifecycle:Event --> markers and is a no-op when SUPERPOWERS_HOOK_DIRS is unset.
  • Payloads pass as SP_<KEY> env vars (e.g., SP_PLAN_PATH, SP_TASK_NUMBER, SP_REASON).
  • 10s default hook timeout (SUPERPOWERS_HOOK_TIMEOUT override). Plugin failures (timeout, nonzero exit, missing exec bit) log a warning but never propagate — emit-hook.sh always exits 0.
  • New docs/superpowers/lifecycle-events.md plugin author reference.
  • New 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?

  1. Direct integration of each plugin into core — what my fork currently does. Doesn't scale; every plugin author hits the same wall and either forks or gives up.
  2. JSON-on-stdin payloads instead of env vars — overkill for v1. Current events have flat key/value payloads (paths, ids, free-text reasons). Plugins re-read the canonical artifact (the plan file) from $SP_PLAN_PATH if they need richer structure. Roadmap item if/when an event genuinely needs nested data.
  3. Manifest-based plugin discovery (YAML/TOML) — adds a parser and a discovery convention to core. Deferred until ecosystem maturity (3+ plugins). For v1, env-var path list (familiar from $PATH) is the smallest possible discovery surface.
  4. Filter events with a return channel — would let plugins inject text back into agent prompts. Rejected because it makes core aware of plugin output shape and complicates the contract. Pure observers are sufficient: plugins that need to enrich prompts mutate the plan file in PlanWritten, and the implementer prompt naturally picks up the mutation since it sends full task body text.
  5. All ten events from Expose lifecycle events beyond SessionStart for external integration #1442 in v1 — explicitly deferred to keep the PR scoped to events with proven consumers.

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

  • I have reviewed all open AND closed PRs for duplicates or prior art

Most relevant prior art:

Related issue: #1442 (this PR's motivating issue, opened by me).

Environment tested

Harness Harness version Model Model version/ID
Claude Code 2.1.126 Claude Opus 4.7 (1M) claude-opus-4-7

Cross-harness validation (Cursor, Codex, Gemini, OpenCode) was not run — the script's harness fallback chain (CLAUDE_PLUGIN_ROOTCURSOR_PLUGIN_ROOTSUPERPOWERS_ROOT) follows the existing precedent from hooks/session-start and 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

  • Initial prompt: "review the beads-integration branch and look at what events we'd need to expose in order to create a standalone plugin that would replicate the functionality in that branch."
  • Sessions run: Single end-to-end session via SDD: brainstorming → spec design (with 4 explicit decision gates) → plan writing → 10-task execution loop with per-task spec compliance + code-quality reviews → final whole-implementation review → spec/doc fixes from review feedback.
  • Outcome: 11/11 unit tests pass; 4 events fire correctly with correct payloads in end-to-end stub-plugin verification; no-plugins case is silent (SUPERPOWERS_HOOK_DIRS unset → rc=0, empty output).
  • Before/after: Before this change, my Beads integration on beads-integration requires direct edits to writing-plans/SKILL.md, executing-plans/SKILL.md, subagent-driven-development/SKILL.md, plus injecting prompt-template content into subagent-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

  • Skills change discipline: Each emit block is purely additive (paired <!-- BEGIN lifecycle:Event --> / <!-- END lifecycle:Event --> markers; git diff against base shows zero - content lines in skills/). 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.
  • Tested adversarially, not just happy path: the test suite covers every documented failure mode — unset 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.
  • Did not modify carefully-tuned content: verified by 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

  • A human has reviewed the COMPLETE proposed diff before submission. The work was developed via Subagent-Driven Development with per-task spec + code-quality reviews, a final whole-implementation review, and explicit human approval at every decision gate (design choices, scope, alternatives, mechanism, payload format, event naming, marker style, timeout policy, and pre-submission positioning against feat: add lifecycle extension system for user-defined workflow hooks #1224).

cdub615 added 13 commits May 2, 2026 22:12
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant