# Contributing to LibreFang

Thank you for your interest in contributing to LibreFang! "Libre" means freedom, and we mean it — this project is built by its community.

**Our promise:** if your contribution positively helps the project, we merge it as-is. If it needs improvement, we provide active, constructive review to help you get it merged. Every contributor matters.

Active contributors are invited to join the LibreFang GitHub org — core participants who consistently contribute get commit access and a voice in project direction.

This guide covers everything you need to get started, from setting up your development environment to submitting pull requests.

## Table of Contents

- [Ways to Contribute](#ways-to-contribute)
- [Contributing to the Registry](#contributing-to-the-registry)
- [Development Environment](#development-environment)
- [Building and Testing](#building-and-testing)
- [Code Style](#code-style)
- [Dependency Policy](#dependency-policy)
- [Architecture Overview](#architecture-overview)
- [How to Add a New Agent Template](#how-to-add-a-new-agent-template)
- [How to Add a New Skill](#how-to-add-a-new-skill)
- [How to Add a New Channel Adapter](#how-to-add-a-new-channel-adapter)
- [How to Add a New LLM Provider](#how-to-add-a-new-llm-provider)
- [How to Add a New Tool](#how-to-add-a-new-tool)
- [How to Write Integration Tests](#how-to-write-integration-tests)
- [Release Articles](#release-articles)
- [Pull Request Process](#pull-request-process)
- [Code of Conduct](#code-of-conduct)

---

## Ways to Contribute

**You don't need to know Rust to contribute to LibreFang.** We have contribution paths for every skill level:

### No Rust Required

| What | Skills Needed | Time | Where |
|------|--------------|------|-------|
| Write an agent template | TOML + prompt engineering | 1-2 hours | `agents/` |
| Write a skill (Python) | Python | 2-4 hours | `~/.librefang/skills/` |
| Write a skill (JavaScript) | Node.js | 2-4 hours | `~/.librefang/skills/` |
| Fix typos / improve docs | Markdown | 30 min | `docs/` |
| Translate docs | Markdown + language | 1-2 hours | `docs/i18n/` |
| Report bugs with reproduction steps | Testing | 30 min | GitHub Issues |
| Test on uncommon platforms | Testing | 1 hour | GitHub Issues |

### Basic Rust

| What | Skills Needed | Time | Where |
|------|--------------|------|-------|
| Add a channel adapter | Rust + platform API | half day | `crates/librefang-channels/` |
| Add an LLM provider driver | Rust + provider API | half day | `crates/librefang-runtime/` |
| Add a built-in tool | Rust | 2-4 hours | `crates/librefang-runtime/` |
| Write/improve tests | Rust | 1-2 hours | any crate |

### Advanced Rust

| What | Skills Needed | Time | Where |
|------|--------------|------|-------|
| Kernel features | Deep Rust + architecture | 1+ days | `crates/librefang-kernel/` |
| Security hardening | Rust + security | 1+ days | multiple crates |
| Performance optimization | Rust + profiling | varies | any crate |
| WASM sandbox improvements | Rust + Wasmtime | 1+ days | `crates/librefang-runtime/` |

### Other

| What | Skills Needed | Time | Where |
|------|--------------|------|-------|
| Desktop app features | Rust + Tauri + TypeScript | varies | `crates/librefang-desktop/` |
| JavaScript SDK | TypeScript | varies | `sdk/javascript/` |
| Python SDK | Python | varies | `sdk/python/` |
| WhatsApp gateway | Node.js | varies | `packages/whatsapp-gateway/` |

> **Tip:** Look for issues labeled [`good first issue`](https://github.com/librefang/librefang/labels/good%20first%20issue) — they include the files to modify, how to test, and estimated difficulty.

### Quick Start by Contribution Type

**I want to add an agent template** (no Rust):
```bash
cp -r agents/hello-world agents/my-agent
# Edit agents/my-agent/agent.toml
# Submit a PR
```

**I want to write a Python skill** (no Rust):
```bash
mkdir -p ~/.librefang/skills/my-skill
# See https://docs.librefang.ai/agent/skills for the skill format
```

**I want to fix a bug or add a Rust feature**:
```bash
git clone https://github.com/librefang/librefang.git && cd librefang
cargo build --workspace        # Build
cargo test --workspace         # Test
cargo clippy --workspace --all-targets -- -D warnings  # Lint
```

---

## Contributing to the Registry

The [`librefang-registry`](https://github.com/librefang/librefang-registry) repo is the shared catalog the website browses (at [librefang.ai/skills](https://librefang.ai/skills), `/hands`, etc.) and the CLI pulls from. Contributions are welcome without touching the main Rust codebase.

### What lives in the registry

| Path | Format | What it is |
|------|--------|------------|
| `skills/<id>/SKILL.md` | directory | A prompt-only or WASM skill bundle (markdown + YAML frontmatter) |
| `hands/<id>/HAND.toml` | directory | An autonomous capability unit |
| `agents/<id>/agent.toml` | directory | A pre-built agent template |
| `channels/<id>.toml` | file | A messaging adapter manifest |
| `providers/<id>.toml` | file | An LLM provider adapter manifest |
| `workflows/<id>.toml` | file | A multi-step agent workflow |
| `plugins/<id>/plugin.toml` | directory | A runtime plugin manifest |
| `mcp/<id>.toml` | file | An MCP server manifest |

### Submitting a new entry

1. Fork [`librefang-registry`](https://github.com/librefang/librefang-registry).
2. Add your manifest to the right category directory. Follow the schema of an existing neighbour.
3. Required TOML fields for every entry: `id`, `name`, `description`, `category`, `icon` (one emoji).
4. Add i18n descriptions in `[i18n.zh]`, `[i18n.ja]`, `[i18n.ko]` if you can — the website renders localized descriptions when available.
5. Tag with `tags = ["popular"]` only if you've validated real usage; the site visually promotes popular entries.
6. Open a PR against the registry repo. On merge, the entry is live on librefang.ai within an hour (the Cloudflare Worker at `stats.librefang.ai` runs stale-while-revalidate with a 1-hour fresh window).

### Testing your manifest locally

```bash
# Run the official site against your local registry checkout
cd librefang/web
pnpm dev

# Or install a single skill directly into a running daemon
librefang skill install /path/to/librefang-registry/skills/your-skill
```

### What the website expects

The website's detail pages expect the TOML to be parseable and render the raw contents. No HTML or Markdown is interpreted — readers see the TOML as-is with syntax highlighting. Keep descriptions concise (≤ 280 chars) so they fit in meta tags and social-share cards.

---

## Development Environment

### Option A: GitHub Codespace (Recommended for first-time contributors)

Click the green **"Code"** button on GitHub → **"Codespaces"** → **"Create codespace on main"**. The DevContainer will automatically install Rust, Python, Node.js, and build the project. You'll have a fully working environment in your browser within a few minutes.

### Option B: Local Setup

#### Prerequisites

- **Rust 1.94.1+** (install via [rustup](https://rustup.rs/))
- **Git**
- **Python 3.8+** (optional, for Python runtime and skills)
- A supported LLM API key (Anthropic, OpenAI, Groq, etc.) for end-to-end testing

#### Clone and Build

```bash
git clone https://github.com/librefang/librefang.git
cd librefang
just setup        # one-time per clone — activates git hooks + fetches deps
cargo build
```

`just setup` (which calls `cargo xtask setup`) does three things on a fresh clone:
- Sets `git config core.hooksPath scripts/hooks` so the in-repo `pre-commit`, `pre-push`, and `commit-msg` hooks become active.
- Runs `cargo fetch` to warm up the dependency cache.
- Runs `pnpm install` in the dashboard / web / docs sub-projects.

Hooks are scoped to fast, staged-only checks. CI is the authoritative
gate (clippy, openapi/SDK drift, security audit, full test matrix).

| Hook        | Runs                                                                  | Target time |
|-------------|-----------------------------------------------------------------------|-------------|
| `pre-commit`| `cargo fmt --check` on staged `*.rs` only, CHANGELOG guard + `(@user)` attribution check, `detect-secrets` (if installed) | < 2s |
| `pre-push`  | Refuses direct push to `main` / `master`. Nothing else.                | < 100ms |
| `commit-msg`| Reject Claude / Anthropic attribution                                  | < 50ms |

Want to lint locally before pushing? Run `just lint` (or
`cargo clippy --workspace --all-targets -- -D warnings`) on demand.
That belongs in your loop when you want it, not gating every push.

Skip the pre-push branch guard with
`LIBREFANG_PREPUSH_SKIP=1 git push` (or `--no-verify`) when the
maintainers have agreed to a release / hotfix push to `main`.

For secret scanning, install `detect-secrets` once (`pipx install detect-secrets`). False positives are managed via `.secrets.baseline`:

```bash
detect-secrets scan --baseline .secrets.baseline   # update findings
detect-secrets audit .secrets.baseline             # mark each as real / false-positive
```

If you also use the `pre-commit` framework (`pipx install pre-commit`), the equivalent staged-only fmt + secret scan is wired in `.pre-commit-config.yaml`. The framework's `pre-push` stage is intentionally not wired — CI is the gate, no need to duplicate locally:

```bash
pre-commit install --install-hooks                 # commit stage
```

You only need to run it once per clone — `git pull` keeps the hooks current automatically because they live in `scripts/hooks/` rather than being copied into `.git/hooks/`.

The first build takes a few minutes because it compiles SQLite (bundled) and Wasmtime. Subsequent builds are incremental.

#### Worktrees for parallel work

For any non-trivial feature, work in a `git worktree` rather than the main clone. This avoids contending with other sessions on the shared `target/` directory and prevents accidental edits on the wrong branch:

```bash
git worktree add /tmp/librefang-<feature> -b <branch-name> origin/main
cd /tmp/librefang-<feature>
```

New worktrees inherit the main clone's `core.hooksPath` setting, so the git hooks are active there too without re-running `just setup`.

### Environment Variables

For running integration tests that hit a real LLM, set at least one provider key:

```bash
export GROQ_API_KEY=gsk_...          # Recommended for fast, free-tier testing
export ANTHROPIC_API_KEY=sk-ant-...  # For Anthropic-specific tests
```

Tests that require a real LLM key will skip gracefully if the env var is absent.

---

## Building and Testing

### Build the Entire Workspace

```bash
cargo build --workspace
```

### Run All Tests

```bash
cargo test --workspace
```

The test suite is currently 2,100+ tests. All must pass before merging.

### Run Tests for a Single Crate

```bash
cargo test -p librefang-kernel
cargo test -p librefang-runtime
cargo test -p librefang-memory
```

### Check for Clippy Warnings

```bash
cargo clippy --workspace --all-targets -- -D warnings
```

The CI pipeline enforces zero clippy warnings.

### Format Code

```bash
cargo fmt --all
```

Always run `cargo fmt` before committing. CI will reject unformatted code.

### Run the Doctor Check

After building, verify your local setup:

```bash
cargo run -- doctor
```

### Test Sharding & Build-Timings Tracking (#3311)

CI splits the full-workspace test run into **4 nextest shards** that run in
parallel. Each shard runs `cargo nextest run --workspace --partition
hash:N/4`, which deterministically buckets every test into exactly one shard
by the hash of its name. Adding or removing tests does not reshuffle existing
buckets, so cache hit rates stay stable across runs. The matrix is
`fail-fast: false` so a failure in one shard does not mask failures in the
others.

Sharding only kicks in for the **full-run** lane (push to `main`, or a PR
that touches the workspace `Cargo.toml` / `Cargo.lock`). The selective lane
(typical PR touching a few crates) keeps running on a single runner — shard
fan-out has no benefit when the affected crate set is small, and adds startup
overhead.

A weekly job (`.github/workflows/build-timings.yml`, Mondays 07:00 UTC) tracks
compile hotspots:

```bash
# Local: collect a per-crate compile-time snapshot for the current HEAD.
cargo xtask build-timings
# Writes bench-results/build-timings/<git-sha>.json by parsing
# target/cargo-timings/cargo-timing*.html (the embedded UNIT_DATA array).

# Local: compare the latest snapshot against bench-results/build-timings/baseline.json.
cargo xtask compare-build-timings
# Exits non-zero (annotated, not blocking) when any crate regressed by
# more than 10%.
```

The baseline file is seeded by committing the first weekly run's snapshot.
After that, weekly snapshots are uploaded as workflow artifacts (90-day
retention) for trend tracking — they are not auto-committed back into the
repo.

### Local Check Mode (low-spec hosts)

`cargo xtask ci`, `cargo xtask pre-commit`, and `cargo xtask coverage`
probe the host on startup and may auto-throttle cargo concurrency to avoid
OOM-ing low-spec laptops (refs #3301).

`cargo xtask bench` detects the mode and prints a loud warning when throttled
(benchmark numbers are unreliable at `CARGO_BUILD_JOBS=1`) but does **not**
apply the throttle — set `LIBREFANG_LOCAL_CHECK_MODE=full` before running
benchmarks to ensure comparable results.

`cargo xtask dev`, `cargo xtask api-docs`, and `cargo xtask codegen` do
**not** apply throttling: `dev` runs interactive hot-reload (not a
compile-heavy batch job), and `api-docs`/`codegen` generate artifacts from
already-compiled outputs rather than triggering expensive full builds.

Three modes, controlled by the `LIBREFANG_LOCAL_CHECK_MODE` environment
variable:

| Mode        | When                                                    | Effect                                                            |
|-------------|---------------------------------------------------------|-------------------------------------------------------------------|
| `full`      | `CI=true` env, or auto-detect on capable hosts          | No env tweaks (matches historical behaviour)                      |
| `throttled` | Auto-detect on `mem < 16 GB` **or** `cpus < 4`          | `CARGO_BUILD_JOBS=1`, appends `-C codegen-units=1` to `RUSTFLAGS`, sets `RUST_MIN_STACK=8388608` |
| `off`       | User explicit opt-out                                   | No env tweaks (you know your machine; we won't touch anything)    |

The mode is printed at the top of each affected subcommand:

```
xtask ci: local-check-mode = throttled (cpus=4, mem=8 GB)
```

**Auto-detect heuristic:**
- If `CI` env is set (GitHub Actions, GitLab CI, CircleCI, etc.), mode is forced to `full`.
- Else if total RAM < 16 GB or available CPU count < 4, mode is `throttled`.
- Otherwise, mode is `full`.

**Manual override:**
```bash
LIBREFANG_LOCAL_CHECK_MODE=full cargo xtask ci        # force full concurrency
LIBREFANG_LOCAL_CHECK_MODE=throttled cargo xtask ci   # force throttled
LIBREFANG_LOCAL_CHECK_MODE=off cargo xtask ci         # disable env tweaks entirely
```

Existing values for `CARGO_BUILD_JOBS` / `RUST_MIN_STACK` are preserved
(only set when unset); `RUSTFLAGS` is appended to, never replaced.

---

## Code Style

- **Formatting**: Use `rustfmt` with default settings. Run `cargo fmt --all` before every commit.
- **Linting**: `cargo clippy --workspace -- -D warnings` must pass with zero warnings.
- **Documentation**: All public types and functions must have doc comments (`///`).
- **Error Handling**: Use `thiserror` for error types. Avoid `unwrap()` in library code; prefer `?` propagation.
- **Naming**:
  - Types: `PascalCase` (e.g., `LibreFangKernel`, `AgentManifest`)
  - Functions/methods: `snake_case`
  - Constants: `SCREAMING_SNAKE_CASE`
  - Crate names: `librefang-{name}` (kebab-case)
- **Dependencies**: Workspace dependencies are declared in the root `Cargo.toml`. Prefer reusing workspace deps over adding new ones. If you need a new dependency, justify it in the PR.
- **Testing**: Every new feature must include tests. Use `tempfile::TempDir` for filesystem isolation and random port binding for network tests.
- **Serde**: All config structs use `#[serde(default)]` for forward compatibility with partial TOML.

---

## Dependency Policy

LibreFang ships as a single binary that runs other people's agents on your hardware, so every crate we pull in is part of the trust boundary. The rules below codify how we add, audit, and remove third-party code.

### Adding a new crate

1. **Justify it in the PR description.** Explain what it does, why a workspace crate or the standard library is not sufficient, and link to the upstream repo and license.
2. **License must be on the allow-list** in [`deny.toml`](deny.toml). Anything outside `Apache-2.0`, `Apache-2.0 WITH LLVM-exception`, `MIT`, `BSD-2-Clause`, `BSD-3-Clause`, `0BSD`, `ISC`, `MPL-2.0`, `Unicode-DFS-2016`, `Unicode-3.0`, `Zlib`, `CC0-1.0`, `CDLA-Permissive-2.0` requires a maintainer-level decision and an explicit `[[licenses.clarify]]` or `exceptions` entry with rationale.
3. **No git dependencies by default.** `unknown-git = "deny"` is set in `deny.toml`. If you absolutely need an unreleased upstream change, add the repo URL to `allow-git` with an inline comment linking to the upstream PR / issue and a target version where it can be removed.
4. **Reuse the workspace dependency table** in the root `Cargo.toml` (`[workspace.dependencies]`) so version bumps stay coordinated across crates.

### Patching upstream crates

Every entry under `[patch.crates-io]` (in the root `Cargo.toml`) MUST carry a comment block recording:

- **Why** the patch exists — CVE ID, bug report, or unmerged feature.
- **Upstream link** — the issue or PR we are waiting on.
- **Pinned version** — the exact upstream tag / SHA we are tracking, so reviewers can diff against it.
- **Removal trigger** — the upstream version that will let us drop the patch (e.g. "remove once foo ≥ 1.4 is published").

A patch without an audit comment is rejected at review.

### Advisories and CI enforcement

The [`cargo-deny`](.github/workflows/cargo-deny.yml) workflow runs on every PR and main push that touches `Cargo.toml`, `Cargo.lock`, `crates/**/Cargo.toml`, `xtask/Cargo.toml`, or `deny.toml`. It executes two independent checks:

- `cargo deny check advisories` — RustSec database, yanked crates, vulnerability hits.
- `cargo deny check bans licenses sources` — duplicate / wildcard / banned crates, license allow-list, registry / git source allow-list.

A `RUSTSEC-*` advisory that we cannot immediately fix may be temporarily ignored by adding it to `[advisories].ignore` in `deny.toml`, but **only with**:

- A comment naming the upstream issue / PR being tracked.
- An explicit reviewer sign-off in the PR that adds the ignore.
- A scheduled re-evaluation (typically removed within one release cycle).

### Daily catch-all

`cargo-deny.yml` also runs on a `schedule:` cron so freshly published RustSec advisories against pinned dependencies are surfaced even when no `Cargo.toml` has changed. Treat a red scheduled run as a security ticket — open an issue and either bump the dep or document the ignore as above.

---

## Architecture Overview

LibreFang is organized as a Cargo workspace with 14 crates:

| Crate | Role |
|-------|------|
| `librefang-types` | Shared type definitions, taint tracking, manifest signing (Ed25519), model catalog, MCP/A2A config types |
| `librefang-memory` | SQLite-backed memory substrate with vector embeddings, usage tracking, canonical sessions, JSONL mirroring |
| `librefang-runtime` | Agent loop, 3 LLM drivers (Anthropic/Gemini/OpenAI-compat), 53 built-in tools, WASM sandbox, MCP client/server, A2A protocol |
| `librefang-hands` | Hands system (curated autonomous capability packages), 7 bundled hands |
| `librefang-extensions` | Integration registry (25 bundled MCP templates), AES-256-GCM credential vault, OAuth2 PKCE |
| `librefang-kernel` | Assembles all subsystems: workflow engine, RBAC auth, heartbeat monitor, cron scheduler, config hot-reload |
| `librefang-api` | REST/WS/SSE API (Axum 0.8), 76 endpoints, 14-page SPA dashboard, OpenAI-compatible `/v1/chat/completions` |
| `librefang-channels` | 40 channel adapters (Telegram, Discord, Slack, WhatsApp, and 36 more), formatter, rate limiter |
| `librefang-wire` | OFP (LibreFang Protocol): TCP P2P networking with HMAC-SHA256 mutual authentication |
| `librefang-cli` | Clap CLI with daemon auto-detect (HTTP mode vs. in-process fallback), MCP server |
| `librefang-migrate` | Migration engine for importing from OpenClaw (and future frameworks) |
| `librefang-skills` | Skill system: 60 bundled skills, FangHub marketplace, OpenClaw compatibility, prompt injection scanning |
| `librefang-desktop` | Tauri 2.0 native desktop app (WebView + system tray + single-instance + notifications) |
| `xtask` | Build automation tasks |

### Key Architectural Patterns

- **`KernelHandle` trait**: Defined in `librefang-runtime`, implemented on `LibreFangKernel` in `librefang-kernel`. This avoids circular crate dependencies while enabling inter-agent tools.
- **Shared memory**: A fixed UUID (`AgentId(Uuid::from_bytes([0..0, 0x01]))`) provides a cross-agent KV namespace.
- **Daemon detection**: The CLI checks `~/.librefang/daemon.json` and pings the health endpoint. If a daemon is running, commands use HTTP; otherwise, they boot an in-process kernel.
- **Capability-based security**: Every agent operation is checked against the agent's granted capabilities before execution.

---

## How to Add a New Agent Template

Agent templates live in the `agents/` directory. Each template is a folder containing an `agent.toml` manifest.

### Steps

1. Create a new directory under `agents/`:

```
agents/my-agent/agent.toml
```

2. Write the manifest:

```toml
name = "my-agent"
version = "0.1.0"
description = "A brief description of what this agent does."
author = "librefang"
module = "builtin:chat"
tags = ["category"]

[model]
provider = "groq"
model = "llama-3.3-70b-versatile"

[resources]
max_llm_tokens_per_hour = 100000

[capabilities]
tools = ["file_read", "file_list", "web_fetch"]
memory_read = ["*"]
memory_write = ["self.*"]
agent_spawn = false
```

3. Include a system prompt if needed by adding it to the `[model]` section:

```toml
[model]
provider = "anthropic"
model = "claude-sonnet-4-20250514"
system_prompt = """
You are a specialized agent that...
"""
```

4. Test by spawning:

```bash
librefang agent spawn agents/my-agent/agent.toml
```

5. Submit a PR with the new template.

---

## How to Add a New Skill

Skills are reusable capabilities that agents can invoke. They can be written in **Python, JavaScript, or as pure prompt templates** — no Rust required.

### Skill Types

| Type | Language | Description |
|------|----------|-------------|
| `prompt` | None (TOML only) | A prompt template with variables |
| `python` | Python 3.8+ | A Python script with `run()` entry point |
| `javascript` | Node.js 18+ | A JS module with `run()` export |

### Steps (Python example)

1. Create a skill directory:

```
my-skill/
  skill.toml
  main.py
```

2. Write the manifest (`skill.toml`):

```toml
name = "my-skill"
version = "0.1.0"
description = "What this skill does."
author = "your-name"
runtime = "python"
entry = "main.py"
tags = ["utility"]

[input]
url = { type = "string", description = "URL to process", required = true }
```

3. Write the implementation (`main.py`):

```python
def run(input: dict) -> str:
    url = input["url"]
    # Your logic here
    return f"Processed: {url}"
```

4. Test locally:

```bash
librefang skill test ./my-skill --input '{"url": "https://example.com"}'
```

5. Submit as a PR to `skills/community/` or publish to FangHub.

### Steps (Prompt template)

For skills that are just prompt engineering, no code is needed:

```toml
name = "summarize-email"
version = "0.1.0"
description = "Summarize an email thread."
runtime = "promptonly"
tags = ["email", "productivity"]

[input]
thread = { type = "string", description = "The email thread text", required = true }

[prompt]
template = """
Summarize the following email thread in 3 bullet points:

{{thread}}
"""
```

---

## How to Add a New Channel Adapter

**Channels are sidecar-first.** A new channel adapter is an
out-of-process subprocess (Python or any language) that speaks
newline-delimited JSON-RPC over stdin/stdout. New *in-process* Rust
adapters are rejected by a policy gate (see the maintainers-only note
below). See `docs/architecture/sidecar-channels.md` for the full
model.

### Add a sidecar channel adapter

1. Install the SDK: `pip install librefang-sdk` (source:
   `sdk/python/`).

2. Subclass `SidecarAdapter`. Implement `on_send` (deliver to the
   platform) and, for platforms you poll, `produce` (push inbound
   messages via `emit`). Declare the rich features you support in
   `capabilities` — anything you don't declare degrades to plain text
   automatically.

   ```python
   from librefang.sidecar import Content, SidecarAdapter, protocol, run_stdio

   class MyAdapter(SidecarAdapter):
       capabilities = ["typing"]

       async def on_send(self, cmd):
           ...  # deliver cmd.text / cmd.content to the platform

       async def produce(self, emit):
           async for m in my_platform_stream():
               emit(protocol.message(m.user_id, m.user_name,
                                     content=Content.text(m.text)))

   if __name__ == "__main__":
       run_stdio(MyAdapter())
   ```

   Start from `sdk/python/librefang/sidecar/template/` and read
   `sdk/python/librefang/sidecar/adapters/ntfy.py` — the canonical
   migration (a real SSE-in / HTTP-out adapter, stdlib-only).

3. Register it in `~/.librefang/config.toml`:

   ```toml
   [[sidecar_channels]]
   name = "myplatform"
   command = "python3"
   args = ["adapters/my_adapter.py"]
   # restart / backoff / ready_timeout / message_buffer / overflow …
   # are all optional — see librefang.toml.example for defaults.
   ```

4. **stdout is the protocol channel** — never `print()` to it. Log via
   `from librefang.sidecar import logging`. The daemon supervises the
   process (crash → backoff restart → circuit-break); your job is
   platform reconnection (`with_backoff`) and being crash-safe.

5. Add tests (the `librefang.sidecar` SDK is unit-test-friendly with
   injectable I/O — see `sdk/python/tests/`) and submit a PR.

### In-process Rust adapter — maintainers only

The ~46 pre-existing in-process adapters under
`crates/librefang-channels/src/` are grandfathered in
`channels-allowlist.txt`. `scripts/hooks/pre-commit` and
`cargo xtask channel-policy` (run in CI) **reject any new** file that
`impl`s `ChannelAdapter` and is not on that allowlist. Adding a new
in-process adapter requires an explicit maintainer decision and an
allowlist entry in a separate reviewed commit — it is not the normal
path. Such adapters still owe a `tests/<channel>_wiremock.rs`
send-path test (see `crates/librefang-channels/CLAUDE.md`).

---

## How to Add a New LLM Provider

LLM provider drivers live in `crates/librefang-runtime/src/`. LibreFang uses three driver families that cover most providers:

| Driver | Covers |
|--------|--------|
| `openai_compat` | Any OpenAI-compatible API (Groq, Together, Mistral, local Ollama, etc.) |
| `anthropic` | Anthropic Claude models |
| `gemini` | Google Gemini models |

### If your provider is OpenAI-compatible

Most new providers don't need a new driver — just add an entry to the model catalog in `crates/librefang-types/src/models.rs`:

1. Add the provider constant and its base URL.
2. Add model entries with context window sizes and pricing.
3. Add aliases if desired (e.g., `"fast" -> "groq/llama-3.3-70b"`).
4. Write a test verifying the model resolves correctly.

### If your provider needs a custom driver

1. Create `crates/librefang-runtime/src/my_provider.rs`.
2. Implement the `LlmDriver` trait (see `anthropic.rs` for reference).
3. Register it in the driver factory in `crates/librefang-runtime/src/llm_driver.rs`.
4. Add config types in `crates/librefang-types/src/config.rs`.
5. Write integration tests (they should skip gracefully if the API key env var is absent).

---

## How to Add a New Tool

Built-in tools are defined in `crates/librefang-runtime/src/tool_runner.rs`.

### Steps

1. Add the tool implementation function:

```rust
async fn tool_my_tool(input: &serde_json::Value) -> Result<String, String> {
    let param = input["param"]
        .as_str()
        .ok_or("Missing 'param' field")?;

    // Tool logic here
    Ok(format!("Result: {param}"))
}
```

2. Register it in the `execute_tool` match block:

```rust
"my_tool" => tool_my_tool(input).await,
```

3. Add the tool definition to `builtin_tool_definitions()`:

```rust
ToolDefinition {
    name: "my_tool".to_string(),
    description: "Description shown to the LLM.".to_string(),
    input_schema: serde_json::json!({
        "type": "object",
        "properties": {
            "param": {
                "type": "string",
                "description": "The parameter description"
            }
        },
        "required": ["param"]
    }),
},
```

4. Agents that need the tool must list it in their manifest:

```toml
[capabilities]
tools = ["my_tool"]
```

5. Write tests for the tool function.

6. If the tool requires kernel access (e.g., inter-agent communication), accept `Option<&Arc<dyn KernelHandle>>` and handle the `None` case gracefully.

---

## How to Write Integration Tests

LibreFang has 2,100+ tests covering all crates. Every new feature must include tests. This section explains where tests live, how to structure them, and how to run them.

### Where Tests Live

Tests in LibreFang are **inline** — they live alongside the source code in `#[cfg(test)]` modules at the bottom of each `.rs` file:

```
crates/librefang-kernel/src/metering.rs     # contains #[cfg(test)] mod tests { ... }
crates/librefang-memory/src/substrate.rs    # contains #[cfg(test)] mod tests { ... }
crates/librefang-runtime/src/routing.rs     # contains #[cfg(test)] mod tests { ... }
```

This is the standard Rust convention and keeps tests close to the code they verify.

### Naming Conventions

- Test module: `#[cfg(test)] mod tests { ... }` at the bottom of the file.
- Test functions: `test_<what_is_being_tested>` in `snake_case`.
  - Good: `test_record_and_check_quota_under`, `test_substrate_kv`, `test_routing_table_lookup`
  - Avoid: `test1`, `it_works`, `my_test`

### How to Structure a Test

Follow the **setup / action / assertion** pattern:

1. **Setup** — create the dependencies your code needs (in-memory databases, config structs, etc.).
2. **Action** — call the function or method under test.
3. **Assertion** — verify the result with `assert!`, `assert_eq!`, or pattern matching.

Many crates provide helpers for setup. For example, `MemorySubstrate::open_in_memory(0.1)` creates an in-memory SQLite database, and `MeteringEngine` tests use a shared `setup()` function.

### How to Run Tests

**All tests in the workspace:**

```bash
cargo test --workspace
```

**Tests for a specific crate:**

```bash
cargo test -p librefang-kernel
cargo test -p librefang-memory
cargo test -p librefang-runtime
```

**A single test by name:**

```bash
cargo test -p librefang-kernel test_record_and_check_quota_under
```

**Show output from passing tests (useful for debugging):**

```bash
cargo test -p librefang-memory -- --nocapture
```

### Test Skeleton

#### Synchronous test

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_my_feature() {
        // Setup
        let config = MyConfig::default();

        // Action
        let result = config.validate();

        // Assertion
        assert!(result.is_ok());
    }
}
```

#### Async test (requires `tokio`)

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_my_async_feature() {
        // Setup
        let substrate = MemorySubstrate::open_in_memory(0.1).unwrap();
        let agent_id = AgentId::new();

        // Action
        substrate
            .set(agent_id, "key", serde_json::json!("value"))
            .await
            .unwrap();
        let val = substrate.get(agent_id, "key").await.unwrap();

        // Assertion
        assert_eq!(val, Some(serde_json::json!("value")));
    }
}
```

### Tips

- **Use `#[tokio::test]`** for any test that calls `.await`. Most crates in LibreFang already depend on `tokio` with the `test-util` feature.
- **Use in-memory databases** for isolation. `MemorySubstrate::open_in_memory(0.1)` avoids touching the real filesystem.
- **Use `tempfile::TempDir`** when you need a real directory (e.g., skill loading, file I/O tests). The directory is automatically cleaned up when the `TempDir` value is dropped.
- **Use `Default::default()`** to construct config structs with sensible defaults, then override only the fields relevant to your test.
- **Skip tests that need external services** by checking for environment variables:
  ```rust
  #[tokio::test]
  async fn test_llm_integration() {
      let api_key = match std::env::var("GROQ_API_KEY") {
          Ok(k) => k,
          Err(_) => {
              eprintln!("Skipping: GROQ_API_KEY not set");
              return;
          }
      };
      // ... test with real API
  }
  ```
- **Extract a `setup()` helper** when multiple tests in the same module need the same boilerplate (see `crates/librefang-kernel/src/metering.rs` for an example).
- **Test error cases too** — verify that invalid input returns the expected error, not just that the happy path works.

---

## Release Articles

Each dated release in `CHANGELOG.md` (e.g. `## [2026.4.27]`) should land with a
companion file in `articles/release-<YYYY.M.D>.md`. Two GitHub workflows
consume these files on push to `main`:

- `.github/workflows/devto-publish.yml` — creates / updates the matching
  dev.to post (title-keyed, idempotent).
- `.github/workflows/release-notify.yml` — posts a GitHub Discussion under the
  release tag using the article body.

If the article is missing on a release tag, the dev.to post and the GitHub
Discussion are silently skipped — public release comms quietly stop. The
`articles/` directory drifted out of sync with `CHANGELOG.md` after
2026-03-22 (#3397) for exactly this reason.

To scaffold an article from a CHANGELOG entry:

```bash
bash scripts/changelog-to-article.sh <YYYY.M.D> [<git-tag>]
```

The script slices the matching `## [YYYY.M.D]` section out of `CHANGELOG.md`
and writes `articles/release-<YYYY.M.D>.md` with the front matter shape
expected by `devto-publish.yml`. The optional second argument overrides the
default `v<YYYY.M.D>` placeholder for `canonical_url` — pass the real CalVer
tag (e.g. `v2026.4.27-beta6`) when you have it. Review the file, hand-edit
the lead paragraph if the release deserves a narrative beyond the bullet
list, then commit alongside the CHANGELOG bump.

---

## Pull Request Process

1. **Fork and branch**: Create a feature branch from `main`. Use descriptive names like `feat/add-matrix-adapter` or `fix/session-restore-crash`.

2. **Make your changes**: Follow the code style guidelines above.

3. Test thoroughly:
   - `cargo test --workspace` must pass (all 2,100+ tests).
   - `cargo clippy --workspace --all-targets -- -D warnings` must produce zero warnings.
   - `cargo fmt --all --check` must produce no diff.

4. **Write a clear PR description**: Explain what changed and why. Include before/after examples if applicable.

5. **One concern per PR**: Keep PRs focused. A single PR should address one feature, one bug fix, or one refactor -- not all three.

6. **Review process**: At least one maintainer must approve before merge. Maintainers give an initial response within 7 days. If your PR needs changes, we provide specific, actionable suggestions — we don't leave you guessing. Contributor attribution is always preserved. See `GOVERNANCE.md` for full project policy.

7. **CI must pass**: All automated checks must be green before merge.

### Commit Messages

Use clear, imperative-mood messages:

```
Add Matrix channel adapter with E2EE support
Fix session restore crash on kernel reboot
Refactor capability manager to use DashMap
```

### CHANGELOG Attribution

When you add a bullet to the `## [Unreleased]` section of `CHANGELOG.md`,
end the line with your GitHub login in parentheses, e.g.

```
- Add Matrix channel adapter with E2EE support (#1234) (@your-login)
```

This is enforced by `scripts/check-changelog-attribution.py` (wired into the
`pre-commit` hook and the `CHANGELOG Attribution` CI job). The check runs
**only against the lines your PR adds** — historical entries that predate
this convention are not retroactively flagged, and you should not backfill
them. To audit the current `[Unreleased]` block before cutting a release:

```
python3 scripts/check-changelog-attribution.py --all-unreleased
```

The accepted format is `(@username)` matching `\(@[A-Za-z0-9_][A-Za-z0-9_-]*\)`.
See issue #3400 for the rationale.

---

## Code of Conduct

This project follows the local [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). By participating, you agree to uphold a welcoming, inclusive, and harassment-free environment for everyone.

Please report unacceptable behavior to the maintainers.

---

## Questions?

- Ask in [GitHub Discussions](https://github.com/librefang/librefang/discussions) for questions or ideas.
- Open a [GitHub Issue](https://github.com/librefang/librefang/issues) for bugs or feature requests.
- Check the [docs/](docs/) directory for detailed guides on specific topics.
- Read [GOVERNANCE.md](GOVERNANCE.md) for decision-making, maintainer expectations, and attribution rules.
