hooks

package
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package hooks provides a hook event dispatch system for the pasture protocol.

Hook events are fired at key lifecycle transitions (phase changes, slice starts/completions, epoch boundaries, review cycles) and error conditions (slice failures, constraint violations, connection loss). Session events track Claude Code agent session start/end for observability.

The Manager dispatches events concurrently to all registered handlers. Dispatch is non-blocking: each handler runs in its own goroutine under a context deadline. Slow or failing handlers do not block the caller.

Package hooks — singleton.go previously held the module-level hooksMgr singleton, InitHooksManager, GetManager, and DispatchHook.

These have been removed as part of the Activity struct DI refactor (Revision 3). Hook dispatch now flows through Activities.DispatchHook, which receives the hooks.Manager via constructor injection (Activities.HooksMgr field).

This file is intentionally empty except for the package declaration.

Index

Constants

View Source
const DefaultDispatchTimeout = 5 * time.Second

DefaultDispatchTimeout is the maximum time a single handler invocation may run before it is abandoned. Chosen to be long enough for non-trivial I/O (file write, HTTP webhook) but short enough to prevent indefinite blocking.

View Source
const GitCommitDataKey = "sha"

GitCommitDataKey is the conventional key in HookPayload.Data that carries the git commit SHA. The GitRecorder hook handler reads this key when a hook payload is dispatched to it; payloads missing or with a non-string value are skipped (the hook may be unrelated to git).

Variables

AllHookEvents is the ordered slice of all valid HookEvent values. Useful for registration completeness checks and tests.

Functions

This section is empty.

Types

type DispatchResult added in v0.0.4

type DispatchResult struct {
	// RecordedEventIDs are the audit_events row ids written by all handlers
	// during this dispatch. Nil/empty means nothing was recorded.
	RecordedEventIDs []int64
}

DispatchResult aggregates the outcomes of every handler invoked for a single Dispatch call.

RecordedEventIDs is the concatenation of each handler's HandleOutcome.RecordedEventIDs for THIS dispatch only. Because every id is produced by a handler from the one payload passed to Dispatch (no shared mutable slot, no cross-dispatch carry-over), the ids are correlated to this specific dispatch by construction — safe to read even when the same Manager services concurrent dispatches from many goroutines. Handlers that recorded nothing contribute no ids. Order across handlers is not significant (handlers run concurrently); order WITHIN a handler is that handler's write order.

type GitRecorder

type GitRecorder struct {
	// contains filtered or unexported fields
}

GitRecorder is a HookHandler that records git commit events to the unified pasture.db audit trail.

Construct via NewGitRecorder; the zero value is unusable (the embedded TaskTracker / *sql.DB would be nil).

Concurrency: GitRecorder is safe for concurrent calls to Handle and RecordCommit because both operations delegate to tasks.RecordGitEvent, which is itself safe for concurrent use (the underlying *sql.DB and TaskTracker are both goroutine-safe).

func NewGitRecorder

func NewGitRecorder(
	tracker protocol.TaskTracker,
	auditDB *sql.DB,
	opts ...GitRecorderOption,
) (*GitRecorder, error)

NewGitRecorder constructs a GitRecorder bound to the supplied TaskTracker and auxiliary auditDB handle (the same handle returned by tasks.OpenAuditDBForFreeFloating). Both arguments MUST be non-nil; the constructor returns an actionable *pasterrors.StructuredError otherwise so the caller catches the wiring mistake at startup, not at first dispatch.

The auditDB handle is used only for the post-write SELECT MAX(id) lookup inside tasks.RecordGitEvent — see free_floating.go for the rationale.

The bare constructor default subscription is HookSliceCompleted (backward compatibility); production callers use RegisterDefaultRecorders, which passes WithSubscribedEvents(HookGitCommit). Pass WithSubscribedEvents to override (see the package doc comment).

func RegisterDefaultRecorders

func RegisterDefaultRecorders(
	mgr *Manager,
	tracker protocol.TaskTracker,
	auditDB *sql.DB,
) (*GitRecorder, error)

RegisterDefaultRecorders registers the in-tree free-floating event recorders (currently: GitRecorder) with the supplied hooks.Manager. Returns the constructed GitRecorder so the caller can also call its RecordCommit method directly (e.g., from a Claude Code Stop-hook bridge S7+ will introduce).

This helper exists so cmd/pastured/main.go can wire the recorder with a single line of glue, keeping the daemon's main.go change minimal — the goal is to avoid stepping on S7's parallel modifications to the same file.

Both `tracker` and `auditDB` MUST be non-nil; this function returns the underlying *pasterrors.StructuredError surface from NewGitRecorder when either is nil so the caller's startup fails fast with an actionable error.

When the audit backend is the in-memory implementation (no SQLite file), callers should NOT invoke this helper — there is no auxiliary *sql.DB to pass, and free-floating event recording requires a durable store.

func (*GitRecorder) Events

func (g *GitRecorder) Events() []HookEvent

Events returns the set of HookEvent values this recorder subscribes to. Implements the HookHandler interface.

func (*GitRecorder) Handle

func (g *GitRecorder) Handle(ctx context.Context, payload HookPayload) (HandleOutcome, error)

Handle is called by hooks.Manager when one of the subscribed HookEvents fires. The recorder looks for a string SHA at payload.Data[GitCommitDataKey]; if present and non-empty, it records a free-floating git commit event via tasks.RecordGitEvent and returns a HandleOutcome carrying the new audit_events row id. If the key is absent / not a string / empty, Handle returns a zero HandleOutcome and nil error (the hook fired for a non-git reason, which is normal).

Implements the HookHandler interface.

func (*GitRecorder) RecordCommit

func (g *GitRecorder) RecordCommit(
	ctx context.Context,
	sha string,
	payload map[string]any,
) (int64, error)

RecordCommit records a free-floating git commit event directly, bypassing the hooks.Manager dispatch path. This is the entry point production wiring (cmd/pastured + the Claude Code Stop-hook bridge S7 will introduce) calls when a commit completes.

sha MUST be the non-empty git commit SHA (becomes the ContextGit edge's context_id). payload SHOULD include "sha" for symmetry; it is NOT enforced.

Returns the int64 audit_events.id so callers can attach further contexts (Scenario 7 multi-context attachment — e.g., a post-epoch commit citing epoch X also gets ContextEpoch).

Errors are *pasterrors.StructuredError; see tasks.RecordGitEvent for the full error contract.

type GitRecorderOption

type GitRecorderOption func(*GitRecorder)

GitRecorderOption is a functional option for NewGitRecorder.

func WithSubscribedEvents

func WithSubscribedEvents(events ...HookEvent) GitRecorderOption

WithSubscribedEvents replaces (does NOT append to) the default subscription set. This is how production wiring points the recorder at HookGitCommit: RegisterDefaultRecorders passes WithSubscribedEvents(HookGitCommit) so the recorder handles the event the CLI and Stop-hook bridge dispatch.

type HandleOutcome added in v0.0.4

type HandleOutcome struct {
	// RecordedEventIDs are the audit_events row ids written during this Handle
	// call, in write order. Nil/empty means the handler recorded nothing.
	RecordedEventIDs []int64
}

HandleOutcome is the per-handler result of a single Handle invocation.

A handler that wrote nothing (for example, a payload it chose to ignore) returns the zero value (RecordedEventIDs == nil). See DispatchResult for the race-safety rationale that applies equally here.

type HookEvent

type HookEvent string

HookEvent is a typed identifier for a pasture hook event. Values are lowercase snake_case strings suitable for JSON serialization and external webhook payloads.

const (

	// HookPhaseTransition fires whenever the epoch transitions between phases.
	HookPhaseTransition HookEvent = "phase_transition"
	// HookVoteRecorded fires when a reviewer submits a vote.
	HookVoteRecorded HookEvent = "vote_recorded"
	// HookSliceStarted fires when a worker slice begins execution.
	HookSliceStarted HookEvent = "slice_started"
	// HookSliceCompleted fires when a worker slice finishes successfully.
	HookSliceCompleted HookEvent = "slice_completed"
	// HookEpochStarted fires when a new epoch workflow is created.
	HookEpochStarted HookEvent = "epoch_started"
	// HookEpochCompleted fires when an epoch reaches the "complete" terminal phase.
	HookEpochCompleted HookEvent = "epoch_completed"
	// HookReviewCycle fires when a review round begins.
	HookReviewCycle HookEvent = "review_cycle"

	// HookSliceFailed fires when a worker slice activity fails or panics.
	HookSliceFailed HookEvent = "slice_failed"
	// HookConstraintViolation fires when a phase-advance constraint check fails.
	HookConstraintViolation HookEvent = "constraint_violation"
	// HookConnectionLost fires when the connection to the Temporal server is lost.
	HookConnectionLost HookEvent = "connection_lost"

	// HookSessionStarted fires when a Claude Code agent session is registered.
	HookSessionStarted HookEvent = "session_started"
	// HookSessionEnded fires when a Claude Code agent session ends.
	HookSessionEnded HookEvent = "session_ended"

	// HookGitCommit fires after a git commit completes (e.g. from a Claude
	// Code Stop hook firing after `git agent-commit`, or directly via
	// `pasture hook record --event git-commit --sha <sha>`). The payload's
	// Data map carries the commit SHA under GitCommitDataKey ("sha") plus
	// optional metadata (message, author, branch, timestamp). GitRecorder
	// subscribes to this event to write a free-floating GitCommit audit row.
	HookGitCommit HookEvent = "git_commit"
)

type HookHandler

type HookHandler interface {
	// Handle processes a hook payload. Must respect ctx cancellation. The
	// returned HandleOutcome reports any audit rows recorded for this payload.
	Handle(ctx context.Context, payload HookPayload) (HandleOutcome, error)
	// Events returns the set of HookEvent values this handler subscribes to.
	Events() []HookEvent
}

HookHandler is the interface that hook consumers must implement.

Events returns the set of HookEvent values this handler is interested in. The Manager only dispatches payloads whose Event is in the returned set.

Handle is called with the payload and a context that carries the dispatch deadline set on the Manager (see WithDispatchTimeout; default is DefaultDispatchTimeout). Implementations should respect ctx.Done() and return promptly when the context is cancelled. Handle returns a HandleOutcome describing any audit rows it recorded for this payload (zero value if none).

type HookPayload

type HookPayload struct {
	Event   HookEvent        `json:"event"`
	EpochId string           `json:"epochId"`
	Phase   protocol.PhaseId `json:"phase,omitempty"`
	Data    map[string]any   `json:"data,omitempty"`
}

HookPayload carries the data emitted with every hook event.

Phase is optional (zero-value "" for non-phase events such as HookSessionStarted/HookSessionEnded). Data holds arbitrary event-specific key/value pairs.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager routes HookPayloads to registered HookHandlers.

Handlers are registered per-event at startup via Register. At runtime, Dispatch fans out to all handlers subscribed to the payload's event. Each handler runs in its own goroutine under a fresh context bounded by the Manager's dispatchTimeout (default: DefaultDispatchTimeout). Errors from individual handlers are collected and returned as a combined error, but do not prevent other handlers from running.

Manager is safe for concurrent use from multiple goroutines after all Register calls complete (typically at startup before any Dispatch calls).

func NewManager

func NewManager(opts ...ManagerOption) *Manager

NewManager creates an empty Manager with no registered handlers. Callers may pass functional options to override defaults (e.g. WithDispatchTimeout).

func (*Manager) Dispatch

func (m *Manager) Dispatch(ctx context.Context, payload HookPayload) (DispatchResult, error)

Dispatch sends payload to all handlers registered for payload.Event.

Each handler is invoked in its own goroutine under a context derived from ctx, with a hard deadline of DefaultDispatchTimeout. Dispatch blocks until ALL handler goroutines have returned (or timed out), then returns a DispatchResult aggregating every handler's recorded-event ids plus a combined error if any handler failed.

"Non-blocking" here means: Dispatch does not block the CALLER indefinitely — handlers run with bounded timeouts. The caller must still await Dispatch's return to learn of errors. To fire-and-forget, wrap Dispatch in a goroutine.

func (*Manager) Register

func (m *Manager) Register(h HookHandler)

Register subscribes h to all events returned by h.Events(). Registration is additive — calling Register multiple times for the same handler appends it each time. Register is not safe to call concurrently with Dispatch; register all handlers before starting dispatch.

type ManagerOption

type ManagerOption func(*Manager)

ManagerOption is a functional option for configuring a Manager at construction.

func WithDispatchTimeout

func WithDispatchTimeout(d time.Duration) ManagerOption

WithDispatchTimeout sets the per-handler deadline used by Dispatch. If d is zero or negative, DefaultDispatchTimeout is used instead.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL