Instructions for creating and configuring a FAVA Trails data repo. For day-to-day usage (scope discovery, session protocol), see AGENTS_USAGE_INSTRUCTIONS.md.
Install Jujutsu (JJ):
fava-trails install-jjInstall FAVA Trails:
# From PyPI (recommended)
pip install fava-trails
# Or from source (for development)
git clone https://github.com/MachineWisdomAI/fava-trails.git && cd fava-trails && uv syncThe Trust Gate reviews thoughts before promotion using an LLM. By default, FAVA Trails uses OpenRouter for unified access to 300β500+ models.
OpenRouter (default, recommended):
- Create a free account at https://openrouter.ai/
- Generate an API key at https://openrouter.ai/keys
- Pass it to the MCP server via the
OPENROUTER_API_KEYenvironment variable (in your MCP client configenvblock, or in your shell profile)
The default model (google/gemini-2.5-flash) costs ~$0.001 per review.
Other providers: FAVA Trails uses any-llm-sdk for unified LLM access, enabling support for additional providers (Anthropic, OpenAI, Bedrock, etc.). Configuration for provider selection will be available in future versions via config.yaml.
The data repo is a plain git repository that the MCP server JJ-colocates on first use. It holds your organization's trail data β separate from the engine.
If someone on your team already bootstrapped a data repo and pushed it to a remote:
fava-trails clone https://github.com/YOUR-ORG/fava-trails-data.git fava-trails-dataThis clones in JJ colocated mode and tracks the remote bookmark automatically. Skip to After setup.
# 1. Create an empty repo on GitHub, then clone it
git clone https://github.com/YOUR-ORG/fava-trails-data.git
# 2. Bootstrap it (creates config, .gitignore, initializes JJ)
fava-trails bootstrap fava-trails-dataThe bootstrap command creates a new data repo from scratch β it does not connect to existing remote data. Use fava-trails clone instead if the remote already has data.
git clone https://github.com/YOUR-ORG/fava-trails-data.git
cd fava-trails-dataCreate exactly two files β nothing else:
config.yaml:
trails_dir: trails
remote_url: "https://github.com/YOUR-ORG/fava-trails-data.git"
push_strategy: immediate.gitignore:
.jj/
__pycache__/
*.pyc
.venv/
CRITICAL β do NOT add
trails/to.gitignore. Trails are plain subdirectories of the monorepo tracked by the same git/JJ repo. Gitignoringtrails/means thought files are never committed and never pushed to remote.
Do not add a README, CLAUDE.md, Makefile, or any other files. The MCP server creates trails/ on first use.
# Commit and push (git β bootstrap only, LAST time you use git push)
git add config.yaml .gitignore
git commit -m "Bootstrap fava-trails-data"
git push origin main
# Initialize JJ colocated mode
jj git init --colocate
jj bookmark track main@originjj bookmark track main@origin is required once for auto-push to work. The MCP server calls jj git init --colocate automatically if .jj/ is missing, but it does not set up bookmark tracking.
Register the MCP server (see README.md), then use MCP tools (save_thought, recall, etc.) for all trail operations. Do not use git commands to manage thought files.
# 1. Clone the existing data repo
fava-trails clone https://github.com/YOUR-ORG/fava-trails-data.git fava-trails-data
# 2. Register the MCP server (same config, with local paths)Both machines push/pull through the same git remote. Use the sync MCP tool to pull latest thoughts.
# Required
trails_dir: trails # relative to FAVA_TRAILS_DATA_REPO
remote_url: "https://github.com/..." # git remote URL (null if local-only)
push_strategy: immediate # manual | immediate
# Trust Gate
trust_gate: llm-oneshot # llm-oneshot | human (future)
trust_gate_model: google/gemini-2.5-flash # model for LLM-based review
openrouter_api_key_env: OPENROUTER_API_KEY # env var name for API key
# Lifecycle hooks (optional, loaded at startup)
hooks:
- module: fava_trails.protocols.secom # built-in or PyPI module
points: [before_propose, before_save, on_recall]
order: 20
fail_mode: open
config: { ... } # passed to module's configure()
# Per-trail overrides (optional)
trails:
mw/eng/sensitive-project:
trust_gate_policy: human # override for this trail
stale_draft_days: 30 # tombstone drafts older than 30 days| Field | Type | Default | Description |
|---|---|---|---|
trails_dir |
string | trails |
Directory for trail data (relative to repo root) |
remote_url |
string | null |
Git remote URL for sync |
push_strategy |
string | manual |
immediate auto-pushes after writes; manual requires explicit sync |
trust_gate |
string | llm-oneshot |
Global trust gate policy |
trust_gate_model |
string | google/gemini-2.5-flash |
Model for LLM-based trust review |
openrouter_api_key_env |
string | OPENROUTER_API_KEY |
Env var name holding the API key for OpenRouter (default provider) |
hooks |
list | [] |
Lifecycle hook entries (see Lifecycle Hooks) |
Override global settings for specific trails via the trails map:
| Field | Type | Default | Description |
|---|---|---|---|
trust_gate_policy |
string | (inherits global) | Override trust gate for this trail |
gc_interval_snapshots |
int | 500 |
Snapshots between GC runs |
gc_interval_seconds |
int | 3600 |
Seconds between GC runs |
stale_draft_days |
int | 0 |
Tombstone drafts older than N days (0 = disabled) |
The Trust Gate uses a trust-gate-prompt.md file to instruct the reviewing LLM on what to approve or reject. These files live inside the data repo, alongside the trails they govern.
FAVA_TRAILS_DATA_REPO/
βββ config.yaml
βββ trails/
βββ trust-gate-prompt.md # Root prompt β applies to all trails
βββ mw/
βββ trust-gate-prompt.md # Company-level override
βββ eng/
βββ fava-trails/
βββ trust-gate-prompt.md # Project-level override
When reviewing a thought at scope mw/eng/fava-trails, the server checks:
trails/mw/eng/fava-trails/trust-gate-prompt.md(most specific)trails/mw/eng/trust-gate-prompt.mdtrails/mw/trust-gate-prompt.mdtrails/trust-gate-prompt.md(root fallback)
The most specific match wins. If no prompt is found at any level, propose_truth returns an error β you must create at least one prompt.
The prompt is plain markdown. It's sent to the trust gate model along with the thought's content and metadata. See TRUST_GATE_PROMPT_EXAMPLE.md for a working example.
Prompt files are cached in memory at server startup and never re-read during a session. This prevents adversarial agents from modifying prompts mid-session.
Lifecycle hooks let operators run custom Python code at key points in the thought lifecycle: before/after save, before/after promote, after supersede, on recall, on recall mix (cross-trail), and at server startup.
Add a hooks: section to your data repo's config.yaml. Each entry declares a hook module, the lifecycle points it handles, and optional configuration:
# config.yaml (at data repo root)
hooks:
# Built-in protocol (installed as a PyPI extra)
- module: fava_trails.protocols.secom
points: [before_propose, before_save, on_recall]
order: 20
fail_mode: open
config:
compression_threshold_chars: 500
target_compress_rate: 0.6
compression_engine:
type: llmlingua
# Local hook file (path relative to data repo root)
- path: ./hooks/quality_gate.py
points: [before_save, before_propose]
order: 10 # lower = runs first (default: 50)
fail_mode: open # open (skip on error) | closed (halt on error)
config:
min_confidence: 0.3
# PyPI package
- module: my_published_package.hooks
points: [before_save]
config:
endpoint: "${METRICS_URL}/push" # env var interpolationHooks are loaded once at server startup and cached (anti-tampering pattern). Restart the MCP server after changing hook configuration.
Each hook is an async function named after its lifecycle point. It receives a typed Event and returns one or more Actions:
# quality_gate.py
from fava_trails.hook_types import Reject, Warn, Proceed
async def before_save(event):
"""Reject thoughts with very low confidence."""
if event.thought and event.thought.frontmatter.confidence < 0.1:
return Reject(reason="Confidence too low", code="LOW_CONF")
if event.thought and len(event.thought.content) < 20:
return Warn(message="Very short thought", code="SHORT")
return Proceed()| Action | Effect | Valid for |
|---|---|---|
Proceed() |
Continue pipeline | all |
Reject(reason, code) |
Block operation (terminal) | before_save, before_propose |
Mutate(patch=ThoughtPatch(...)) |
Modify thought content/tags/confidence | before_save, before_propose |
Redirect(namespace) |
Save to different namespace (terminal) | before_save, before_propose |
Warn(message, code) |
Surface concern in response | all |
Advise(message, code, target) |
Guidance for agent | all |
Annotate(values={...}) |
Attach metadata | all |
RecallSelect(ordered_ulids=[...]) |
Filter/reorder recall results | on_recall, on_recall_mix |
Hooks can return a single action, None (treated as Proceed), or a list of actions.
If your hook module defines a configure(config) function, it's called at startup with the config dict from hooks.yaml (after env var interpolation):
_min_confidence = 0.3
def configure(config):
global _min_confidence
_min_confidence = config.get("min_confidence", 0.3)
async def before_save(event):
if event.thought and event.thought.frontmatter.confidence < _min_confidence:
return Reject(reason="Below minimum confidence")When hooks produce warnings, advice, or annotations, they appear in the MCP tool response under hook_feedback:
{
"status": "ok",
"thought": { "thought_id": "..." },
"hook_feedback": {
"accepted": true,
"warnings": [{"message": "Very short thought", "code": "SHORT"}],
"annotations": {"quality_score": 0.85}
}
}Hooks that need to query trail state receive a TrailContext via event.context. It provides hook-safe methods that bypass hook firing (preventing recursion):
await event.context.stats()β thought count by namespaceawait event.context.count(namespace=None)β total or per-namespace countawait event.context.recall(query, namespace, limit)β search thoughts (max 50)
| Point | When | Pipeline type |
|---|---|---|
before_save |
Before thought is written to disk | Gating (can reject/mutate/redirect) |
after_save |
After thought is committed | Observer (fire-and-forget) |
before_propose |
Before promotion from drafts | Gating (can reject/mutate/redirect) |
after_propose |
After promotion is committed | Observer |
after_supersede |
After supersession is committed | Observer |
on_recall |
During single-trail recall search | Gating (can filter/reorder via RecallSelect) |
on_recall_mix |
After cross-trail recall_multi merge |
Gating (can filter/reorder via RecallSelect) |
on_startup |
Server startup | Startup (separate contract) |
fail_mode: open(default): Hook errors/timeouts are logged and skipped β the operation proceedsfail_mode: closed: Hook errors/timeouts halt the operation with an exception- Import errors with
fail_mode: closedcausesys.exit(1)at startup
FAVA Trails ships with protocol hook modules that can be enabled via module: entries:
| Protocol | Install | Description |
|---|---|---|
| SECOM | pip install fava-trails[secom] |
Extractive compression at promote time via LLMLingua-2 (docs) |
| ACE | included | Playbook-driven reranking and anti-pattern detection (Stanford, UC Berkeley, and SambaNova ACE) |
| RLM | included | MapReduce orchestration hooks for batch workflows (MIT RLM) |
Quickest way to add a protocol β use the CLI setup command:
# Print default config (copy-paste into config.yaml)
fava-trails secom setup
fava-trails ace setup
fava-trails rlm setup
# Or write directly to config.yaml + jj commit in one step
fava-trails secom setup --write
fava-trails ace setup --write
fava-trails rlm setup --writeSECOM β enable extractive compression:
# config.yaml
hooks:
- module: fava_trails.protocols.secom
points: [before_propose, before_save, on_recall]
order: 20
fail_mode: open
config:
compression_threshold_chars: 500
verbosity_warn_chars: 1000
target_compress_rate: 0.6
compression_engine:
type: llmlinguaAfter installing and configuring, restart the MCP server. The first propose_truth that triggers compression will download the LLMLingua-2 model (~700MB) from HuggingFace Hub. Pre-download with:
fava-trails secom warmupACE β enable playbook-driven reranking:
hooks:
- module: fava_trails.protocols.ace
points: [on_startup, on_recall, before_save, after_save, after_propose, after_supersede]
order: 10
fail_mode: open
config:
playbook_namespace: preferences
telemetry_max_per_scope: 10000RLM β enable MapReduce orchestration:
hooks:
- module: fava_trails.protocols.rlm
points: [before_save, after_save, on_recall]
order: 15
fail_mode: closed
config:
expected_mappers: 5
min_mapper_output_chars: 20NEVER use git push origin main after JJ colocates. In JJ colocated mode:
- HEAD is always detached β JJ manages commits, not git
- Thought commits live on the detached HEAD chain, not on the
maingit branch git push origin mainonly pushes the gitmainbookmark β it misses all thought commits
If push_strategy: immediate is set (recommended), the server auto-pushes the main bookmark after every write. No manual action needed.
If you need to push manually:
# From within fava-trails-data:
jj bookmark set main -r @- # advance main bookmark to latest committed change
jj git push --bookmark main # push to remoteFAVA_TRAILS_DATA_REPO/ # Monorepo root (.jj/ + .git/)
βββ config.yaml # Global config (includes hooks: section)
βββ .gitignore
βββ hooks/ # Local hook files (optional, for path: entries)
β βββ quality_gate.py # Custom hook implementation
βββ trails/
βββ trust-gate-prompt.md # Root trust gate prompt
βββ mw/ # Company scope
βββ thoughts/
β βββ drafts/
β βββ decisions/
β βββ observations/
β βββ intents/
β βββ preferences/
β βββ client/
β βββ firm/
βββ eng/ # Team scope
βββ thoughts/...
βββ fava-trails/ # Project scope
βββ thoughts/...
βββ trust-gate-prompt.md # Project-specific prompt
βββ auth-epic/ # Task/epic scope
βββ thoughts/...
Each trail is a subdirectory β NOT a separate repo. The entire monorepo shares a single JJ/git history.