Skip to content

fix(hooks): dispatch specialistStart/Stop lifecycle hooks - #796

Open
Ayush7614 wants to merge 4 commits into
Gitlawb:mainfrom
Ayush7614:feat/specialist-lifecycle-hooks
Open

fix(hooks): dispatch specialistStart/Stop lifecycle hooks#796
Ayush7614 wants to merge 4 commits into
Gitlawb:mainfrom
Ayush7614:feat/specialist-lifecycle-hooks

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Dispatch advisory specialistStart / specialistStop hooks from specialist accounting
  • Allow specialist-name matchers (docs already claimed this; LoadConfig previously rejected it)
  • Attach the hooks dispatcher to specialist tools after startup builds it

These events were documented and accepted by zero hooks add, but never fired at runtime.

Closes #793

Test plan

  • go test ./internal/specialist/ ./internal/hooks/ -count=1
  • go vet ./internal/specialist/ ./internal/hooks/ ./internal/cli/
  • govulncheck on touched packages — no vulnerabilities

Summary by CodeRabbit

  • New Features
    • Added support for matching hooks on specialist start and stop events, including updated matcher validation.
    • Specialist lifecycle events now dispatch configured hooks in both interactive and command-line runs, and specialist start/stop activity is recorded in the audit log.
  • Documentation
    • Clarified matcher semantics (subject is the agent type) and noted that specialist/session hook failures are audited without interrupting runs.
    • Updated help text to reflect tool/specialist matcher support for the relevant hook events.
  • Tests
    • Expanded coverage for specialist hook dispatching, payload/event recording, and stop-event deduplication behavior.

These events were documented and accepted by hooks add, but never fired.
Wire advisory dispatch from specialist accounting and allow specialist
name matchers so EXTENDING.md matches runtime behavior.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 727eb075-d8bc-4141-8c97-ad30618766cf

📥 Commits

Reviewing files that changed from the base of the PR and between 19f98c2 and 7d55f56.

📒 Files selected for processing (7)
  • docs/EXTENDING.md
  • internal/cli/specialist_lifecycle_hooks_test.go
  • internal/specialist/accounting.go
  • internal/specialist/accounting_test.go
  • internal/specialist/exec.go
  • internal/specialist/output_tool.go
  • internal/specialist/registry.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/EXTENDING.md
  • internal/specialist/exec.go
  • internal/specialist/accounting.go

Walkthrough

Specialist lifecycle hooks now accept specialist matchers, dispatch specialistStart and specialistStop events from accounting, share hook state with output tooling, and connect to CLI hook dispatchers in interactive and exec modes. Tests cover matcher selection, dispatch ordering, deduplication, and wiring.

Changes

Specialist lifecycle hooks

Layer / File(s) Summary
Specialist matcher support
internal/hooks/hooks.go, internal/cli/hooks_manage.go, internal/hooks/hooks_test.go, docs/EXTENDING.md
Matchers are accepted for specialistStart and specialistStop, with updated CLI help, documentation, validation, and configuration tests.
Specialist lifecycle dispatch
internal/specialist/exec.go, internal/specialist/accounting.go, internal/specialist/accounting_test.go
The executor exposes optional lifecycle dispatch, and accounting emits start/stop events with specialist names, payloads, and deduplicated stop behavior.
Output tool lifecycle bridge
internal/specialist/output_tool.go, internal/specialist/registry.go, internal/specialist/accounting_test.go
Output tooling shares lifecycle hooks with background accounting and preserves stop-dispatch deduplication.
CLI dispatcher wiring
internal/cli/app.go, internal/cli/exec.go, internal/cli/specialist_lifecycle_hooks_test.go
Interactive and exec startup paths attach shared specialist lifecycle hooks to the hook dispatcher, with registration coverage for TaskOutput.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SpecialistExecutor
  participant SpecialistAccounting
  participant OutputTool
  participant LifecycleHooks
  participant HooksDispatcher
  SpecialistExecutor->>SpecialistAccounting: recordSpecialistStart
  SpecialistAccounting->>LifecycleHooks: Dispatch specialistStart
  OutputTool->>SpecialistAccounting: Record background stop
  SpecialistAccounting->>LifecycleHooks: Dispatch deduplicated specialistStop
  LifecycleHooks->>HooksDispatcher: Forward event and payload
Loading

Possibly related PRs

  • Gitlawb/zero#106: Both changes update hook support and matcher validation for specialist lifecycle events.
  • Gitlawb/zero#120: Both changes modify specialist accounting around start and stop events.
  • Gitlawb/zero#181: This change extends matcher validation built around the hooks event-schema work.

Suggested reviewers: anandh8x, vasanthdev2004, gnanam1990

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: dispatching specialist lifecycle hooks.
Linked Issues check ✅ Passed The PR implements all requested scope for #793: dispatching specialist lifecycle hooks, accepting specialist matchers, and wiring dispatcher attachment.
Out of Scope Changes check ✅ Passed The docs, tests, and CLI wiring all support the lifecycle-hook and matcher changes, with no clearly unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kevincodex1

Copy link
Copy Markdown
Contributor

@coderabbitai make an intensive review on this

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

@kevincodex1 Understood — I’ll perform an intensive end-to-end review, prioritizing lifecycle correctness, hook/config behavior, failure paths, CLI UX, and coverage gaps.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/specialist/accounting.go (1)

43-64: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Release accountingMu before running external hooks.

recordSpecialistStop keeps the accounting mutex held while Dispatch runs arbitrary commands. A slow lifecycle hook can block all other specialist stop accounting, especially during concurrent swarm runs. Limit the lock to the deduplicated append, then dispatch after unlocking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/accounting.go` around lines 43 - 64, Update
recordSpecialistStop so accountingMu protects only the appendSpecialistEventOnce
deduplication/write operation, releasing it before
executor.dispatchLifecycleHook runs. Preserve the deferred unlock removal and
ensure the lifecycle hook executes after the mutex is released.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/hooks/hooks_test.go`:
- Around line 213-241: Extend TestLoadConfigAcceptsMatchersOnSpecialistHooks to
table-drive both EventSpecialistStart and EventSpecialistStop, using the
corresponding event value when selecting hooks. Preserve the existing matched
and unmatched ToolName assertions so matcher behavior is verified for both
specialist lifecycle events.

In `@internal/specialist/accounting.go`:
- Around line 60-64: Update the stop handling flow around
appendSpecialistEventOnce to retain and inspect its deduplication result.
Dispatch the specialistStop lifecycle hook only when the stop event is newly
recorded, while preserving the existing event append arguments and avoiding hook
execution for duplicate stop attempts.

---

Outside diff comments:
In `@internal/specialist/accounting.go`:
- Around line 43-64: Update recordSpecialistStop so accountingMu protects only
the appendSpecialistEventOnce deduplication/write operation, releasing it before
executor.dispatchLifecycleHook runs. Preserve the deferred unlock removal and
ensure the lifecycle hook executes after the mutex is released.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f78013fc-aca2-4520-9821-72ce5b71fcc6

📥 Commits

Reviewing files that changed from the base of the PR and between d9b882e and 97910f5.

📒 Files selected for processing (9)
  • docs/EXTENDING.md
  • internal/cli/app.go
  • internal/cli/exec.go
  • internal/cli/hooks_manage.go
  • internal/hooks/hooks.go
  • internal/hooks/hooks_test.go
  • internal/specialist/accounting.go
  • internal/specialist/accounting_test.go
  • internal/specialist/exec.go

Comment thread internal/hooks/hooks_test.go
Comment thread internal/specialist/accounting.go Outdated
Release accountingMu before running lifecycle hooks, and dispatch
specialistStop only when the stop event is newly recorded so concurrent
finishers cannot re-run arbitrary hook commands.
@Ayush7614

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit review in 19f98c2 (also synced with latest main):

  • recordSpecialistStop releases accountingMu before running lifecycle hooks
  • specialistStop hooks fire only when appendSpecialistEventOnce newly records the stop (deduped under concurrency)
  • Matcher acceptance test now covers both specialistStart and specialistStop
  • Concurrency test asserts exactly one stop hook under race

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 26, 2026
@Ayush7614

Copy link
Copy Markdown
Contributor Author

cc: @kevincodex1

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of the earlier findings are properly fixed here: the dedup result is honoured now, and accountingMu brackets only the append so a slow hook cannot stall other specialist accounting. The supporting checks all pass too, the hooks are genuinely advisory (blocksOn is beforeTool only and the outcome is discarded), the specialist-name matcher now matches what the docs table already advertised, and runWithTimeout bounds a hung hook at 30s.

The problem is that fixing (1) by gating on appended introduced the inverse defect, and it is deterministic rather than a race.

[blocker] Swarm members fire specialistStart and never specialistStop, 100% of the time.

internal/swarm/launcher_specialist.go builds TaskRunOptions with ParentModel, Cwd, PermissionMode and MemberAutonomy but never sets ParentSessionID. appendSpecialistEventOnce short-circuits on an empty parent session and returns false, so if appended suppresses the stop dispatch for every swarm member, while recordSpecialistStart dispatches unconditionally with no gate. Anyone pairing the two events (a notification, a timer, resource cleanup) leaks state on every member of every swarm run.

[major] Background specialists can drop the stop hook too.

OutputTool.readOutput reaps through a fresh Executor{SessionStore: ...} literal with no LifecycleHooks, and whichever executor wins the appendSpecialistEventOnce race owns the only dispatch. When the hook-less TaskOutput poll wins, the hook-carrying onExit path then sees appended==false and suppresses the hook. Deterministic variant: if the parent process exits before the background child, the onExit goroutine dies with it and a later TaskOutput is the only reaper, so the hook can never fire.

[minor] appended, _ := also discards the error, and AppendEventUnlessExists returns false on failure as well as on already-recorded, so a session-store error silently becomes a suppressed hook with no signal anywhere.

All three are the same root cause: dispatch is coupled to session-event bookkeeping that is skipped when there is no parent session, performed by a different executor, or failed. I would dispatch when the stop is first observed by THIS executor, decoupled from whether the audit event was appended, keeping the real duplicate suppression, and pass LifecycleHooks into OutputTool so the background reaper can dispatch.

Two smaller notes: for swarm members the matcher subject is the agent type (subagent/teammate) rather than a registered specialist name, so a matcher written against real specialist names will not match once stop hooks do fire, which deserves a docs line. And no test covers the CLI wire-up at all, so deleting either attachLifecycleHooks call would still pass the suite, which is exactly the class of bug #793 reports; the new concurrency test races two hook-carrying executors, so by construction it cannot catch either failure above.

The dedup fix itself is right, it just needs to not be the thing that decides whether the hook fires. Happy to re-review quickly.

Decouple stop-hook dispatch from parent-session event append so swarm
members (empty parent) and TaskOutput still get exactly one hook via a
shared LifecycleHooks claim, and wire that bridge into TaskOutput.
@Ayush7614

Copy link
Copy Markdown
Contributor Author

Addressed @Vasanthdev2004's review on 19f98c2:

  1. Swarm specialistStop suppressed — stop-hook dispatch is no longer gated on session-event append. LifecycleHooks.claimStopDispatch dedupes by childSessionID+runID, so swarm members with an empty parent still fire exactly once.
  2. TaskOutput raceOutputTool now carries the shared LifecycleHooks bridge from RegisterTools, so a hook-less reap path cannot permanently suppress the stop hook.
  3. Docs — noted in docs/EXTENDING.md that swarm matcher subjects are agent types (subagent/teammate), not registered specialist names.
  4. Tests — empty parent still fires stop; hook-less append winner then hook-carrying second still fires once; OutputTool shares the bridge; CLI attachLifecycleHooks wire-up covers TaskOutput.

Ready for re-review. Thanks!

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.

fix(hooks): dispatch specialistStart/Stop lifecycle hooks

3 participants