Skip to content

feat: Add lifecycle hooks system (Spec 17) - #11

Merged
timeleft-- merged 14 commits into
mainfrom
feature-17-lifecycle-hooks
Mar 8, 2026
Merged

timeleft-- merged 14 commits into
mainfrom
feature-17-lifecycle-hooks

Conversation

@timeleft--

Copy link
Copy Markdown
Member

Summary

  • Event-Action pipeline for lifecycle hooks: typed Events flow through hooks that return typed Actions (Proceed, Reject, Warn, Advise, Mutate, Redirect, Annotate, RecallSelect)
  • Manifest-based registration via hooks/hooks.yaml — Pydantic-validated, env var interpolation, module/path resolution, config injection
  • 7 lifecycle points: before_save, after_save, before_propose, after_propose, after_supersede, on_recall, on_startup
  • Gating pipelines (before_/on_recall) can reject, mutate, or redirect; observer pipelines (after_) fire-and-forget
  • HookFeedback surfaces warnings, advice, and annotations in MCP responses
  • TrailContext gives hooks safe access to trail state via _recall_internal() (prevents recursive hook firing)
  • Security: frozen dataclasses for Events/Actions, namespace redirect sanitization, mutation cascade guard (MAX_MUTATIONS=5), timeout enforcement

New files

File Purpose
src/fava_trails/hook_types.py Type system — Events, Actions, HookFeedback, ThoughtPatch, TrailContext
src/fava_trails/hook_manifest.py Manifest parser, HookRegistry, module resolution
src/fava_trails/hook_pipeline.py Pipeline execution engine (run_pipeline, dispatch_observer)
tests/test_hook_types.py 63 unit tests for type system
tests/test_hook_manifest.py 27 unit tests for manifest/registry
tests/test_hook_pipeline.py 24 unit tests for pipeline engine
tests/test_hooks.py 15 integration tests (TrailManager + hooks end-to-end)

Modified files

File Changes
src/fava_trails/trail.py Wire hooks into save_thought, propose_truth, supersede, recall
src/fava_trails/server.py Load hooks at startup, run on_startup hooks, attach HookFeedback to responses
AGENTS_SETUP_INSTRUCTIONS.md Document hooks setup, manifest format, actions, lifecycle points

Test plan

  • All 317 tests pass (uv run pytest)
  • Hook type system: construction, immutability, validity matrix, feedback accumulation
  • Manifest: YAML parsing, env var interpolation, Pydantic validation, module resolution
  • Pipeline: gating flow (reject/mutate/redirect), observer dispatch, timeout, error handling
  • Integration: save/propose/supersede/recall with hooks, _recall_internal bypass, multi-action
  • Code reviewed by GPT-5.1 Codex and GPT-5.3 Codex — all findings addressed

🤖 Generated with Claude Code

timeleft-- and others added 10 commits March 7, 2026 14:47
Operator-defined Python hooks fired at 7 TrailManager lifecycle points:
before_save, after_save, before_propose, after_propose, after_supersede,
on_recall, on_startup.

Hook files live in data repo hooks/ directory (or FAVA_TRAILS_HOOKS_DIR),
loaded once at startup (anti-tampering, mirrors TrustGatePromptCache).
HookRegistry passed as optional parameter to TrailManager — zero overhead
when no hooks exist, full backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change sys.path.insert(0, ...) to sys.path.append() in hooks.py to
  prevent hooks directory from shadowing stdlib modules
- Move `import copy` to module-level in trail.py for consistency

Review: GPT-5.1 Codex (full review, no critical/high issues)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…se 1)

New hook_types.py module with:
- 7 typed Event dataclasses (BeforeSaveEvent, AfterSaveEvent, etc.)
- 8 typed Action dataclasses (Proceed, Reject, Mutate, Redirect, Warn, Advise, Annotate, RecallSelect)
- Startup contract types (StartupOk/StartupWarn/StartupFail)
- ThoughtPatch for Mutate actions with apply() method
- HookFeedback accumulator with merge(), size limits, and MCP serialization
- TrailContext for hook-safe trail queries via _recall_internal
- ACTION_VALIDITY matrix enforcing valid actions per lifecycle point

63 tests covering all types, immutability, limits, and context behavior.

[Spec 17][Phase 1: Type System]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New hook_manifest.py module replacing convention-based discovery:
- Pydantic HookManifest/HookEntry models for hooks.yaml validation
- module: (PyPI) and path: (local file/dir) resolution
- Config injection via optional configure() export
- ${VAR} env var interpolation in config values
- Order-based sorting for multi-hook lifecycle points
- Startup validation: import checks, async signature verification
- fail_mode=closed causes sys.exit(1) on load errors

27 tests covering parsing, resolution, config injection, env vars,
ordering, and all validation error paths.

[Spec 17][Phase 2: Manifest Registration]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pipeline engine with run_pipeline() for synchronous gating (before_*/on_recall)
and dispatch_observer() for async fire-and-forget (after_*). Supports full action
composition: Reject/Redirect terminal, Mutate cascade with max=5 guard,
Warn/Advise/Annotate accumulation, RecallSelect subset validation. 24 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…se 4)

Replace v1 hook system with v2 Event-Action Pipeline:
- trail.py: typed events + run_pipeline/dispatch_observer replace fire_before/fire_after/fire_recall
- trail.py: add _recall_internal() for hook-safe TrailContext access
- server.py: manifest-based loading (hooks.yaml) replaces directory scanning
- server.py: HookFeedback attached to MCP tool responses
- Delete hooks.py (v1) — fully replaced by hook_types/hook_manifest/hook_pipeline
- Rewrite integration tests for v2 hook contract (15 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document hooks.yaml manifest, hook contract (v2 Event-Action model),
available actions, config injection, HookFeedback in MCP responses,
TrailContext, lifecycle points, and error handling. Update data repo
layout diagram to show hooks/ directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Sanitize redirect namespace from hooks (path traversal prevention)
- Handle Redirect action in before_propose (was silently ignored)
- Clear _last_feedback at start of recall() to prevent stale leakage
- Add timeout enforcement to on_startup hooks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove FAVA_TRAILS_HOOKS_DIR env var (v1 artifact, manifest path is
  always {data_repo}/hooks/hooks.yaml)
- Remove unused HookExecutionError import from server.py
- Clean up docstrings: remove "Spec 17 v2", "Phase N", "Replaces v1"
  references — v1 never shipped
- Remove "backward compat" comments from hooks tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix empty RecallSelect being ignored (falsy [] vs explicit None check)
- Store pipeline_result in _last_feedback during recall so hook
  warnings/annotations appear in MCP responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timeleft--
timeleft-- force-pushed the feature-17-lifecycle-hooks branch from 560602c to 9d731c6 Compare March 7, 2026 19:48
timeleft-- and others added 2 commits March 8, 2026 10:14
- Fix _last_feedback race condition: replace shared mutable state with
  task-scoped feedback via _set_feedback/consume_feedback (asyncio.current_task)
- Handle on_startup hook return values (StartupFail/StartupWarn)
- Use dataclasses.replace() instead of object.__setattr__ for frozen
  event mutation in pipeline (preserves immutability contract)
- Isolate observer hooks with deepcopy to prevent cross-hook interference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/fava_trails/hook_pipeline.py Fixed
Comment thread src/fava_trails/hook_pipeline.py Fixed
Comment thread tests/test_hook_manifest.py Fixed
Comment thread tests/test_hook_pipeline.py Fixed
Comment thread tests/test_hook_pipeline.py Fixed
Comment thread tests/test_hook_pipeline.py Fixed
Comment thread tests/test_hook_pipeline.py Fixed
Comment thread tests/test_hook_pipeline.py Fixed
Comment thread tests/test_hook_pipeline.py Fixed
Comment thread tests/test_hooks.py Fixed
timeleft-- and others added 2 commits March 8, 2026 10:25
- Fix ruff import sorting (isort)
- Add `from exc` to all `raise` in except clauses (B904)
- Remove unused variable assignments in tests (F841)
- Use builtin TimeoutError instead of asyncio.TimeoutError (UP041)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timeleft--
timeleft-- merged commit 8753f1d into main Mar 8, 2026
7 checks passed
@timeleft--
timeleft-- deleted the feature-17-lifecycle-hooks branch March 8, 2026 14:30
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