# Contributing to AlexClaw Thank you for your interest in contributing to AlexClaw. This document explains how to contribute and what to expect from the process. --- ## Before You Start AlexClaw is a personal AI agent built on Elixir/OTP. Contributions are welcome, but please understand the project's philosophy before proposing changes: - **Lean over feature-rich** — we deliberately avoid complexity - **Code over config** — skills are Elixir modules, not markdown files - **BEAM-native** — OTP supervision, not workarounds - **Auditable** — no unvetted external skill registries - **Observable** — everything is measurable via telemetry If your contribution aligns with these principles, it's likely a good fit. --- ## Contributor License Agreement (CLA) **All contributors must agree to the CLA before their code can be merged.** By submitting a pull request, you automatically agree to the CLA for minor contributions (documentation, typos, small fixes). For significant contributions (new skills, architectural changes, new providers), you must explicitly sign the CLA by including this statement in your PR: > I have read the AlexClaw CLA and agree to its terms. > My GitHub username is [username] and my legal name is [full name]. Read the full CLA in [CLA.md](CLA.md). **Why the CLA includes a relicensing clause:** The CLA allows the project to be relicensed in the future without requiring permission from every contributor. This is standard practice for projects that may evolve commercially, and does not affect your right to use your own contributions however you wish. --- ## What We Welcome - **New skills** — RSS sources, web scrapers, research tools, notification filters - **New LLM providers** — additional free-tier integrations for the router - **Bug fixes** — especially around rate limit handling and retry logic - **Documentation** — architecture explanations, usage examples - **Observability** — new telemetry events, Grafana dashboard definitions - **Docker improvements** — deployment ergonomics ## What We Don't Want - External skill registries or marketplaces - Dependencies that require running services outside the compose stack - Skills that send internal/sensitive data to external providers without explicit opt-in - Complexity for its own sake --- ## How to Contribute 1. **Fork** the repository 2. **Create a branch** — `git checkout -b feature/my-skill` or `fix/router-fallback` 3. **Write your code** — follow the existing patterns in `lib/alex_claw/` 4. **Add tests** — skills should have unit tests; use `ExUnit` 5. **Run the tests** — the new or changed test files with `make test-elixir FILES="…"`; the whole suite with `make gate-elixir` before opening the pull request 6. **Open a pull request** — describe what you built and why Tests run in the container defined by `docker-compose.test.yml`, against an isolated test database. Running `mix test` directly on the host is not supported — it connects to the wrong database or none at all. The test stack is its own compose project and publishes no ports, so it can run while the production containers are up. The Elixir targets run `scripts/test-elixir.sh` and the Python targets `scripts/test-python.sh`. Both run under a hard time limit for the whole run, build included (`TEST_TIME_LIMIT`, default 2400 seconds), write the full output to a log that is kept (`TEST_LOG_DIR`; by default an untracked directory in the checkout), print its path at the start and the end, and print a progress line every 30 seconds. A run past the time limit is stopped, and the script exits with status 124. The Elixir script also watches for a run that hangs before any test starts: if none has started within 120 seconds (`TEST_WATCHDOG_SECONDS`), the run is stopped the same way. In both cases it first has the BEAM write a crash dump (`erl_crash-.dump`) to an untracked directory in the checkout. The whole suites run only through the gate targets; targeted runs go through the same script and the same limits: | Command | Runs | |---|---| | `make gate-elixir` | the whole Elixir suite, on a freshly built test image | | `make gate-python` | the whole Python suite | | `make test-elixir FILES="test/a_test.exs test/b_test.exs"` | only those files | | `make test-failed` | only the tests that failed in the previous run (`mix test --failed`) | | `make test-stale` | only the tests affected by modules changed since the last passing whole or stale run (`mix test --stale`). Mix follows references transitively, so a change to a module many others reach (a gateway, a context) selects most of the suite | `make test-elixir` with no `FILES`, and `make test-python`, refuse to run the whole suite: that is what the gate targets are for. `--failed` and `--stale` read records of the previous run, so the build directory is kept between runs in `.test-cache/elixir` (untracked). A whole-suite run replaces it with the image's build and its records. Targeted runs reuse it, so only changed modules compile again. `rm -rf .test-cache` resets it. ### Skill Contributions New skills must implement the `AlexClaw.Skill` behaviour: ```elixir defmodule AlexClaw.Skills.MySkill do @moduledoc "One-line description of what this skill does." @behaviour AlexClaw.Skill @impl true def description, do: "Short description for the skill registry" # Optional: declare branches for conditional workflow routing. # Default is [:on_success, :on_error] if not implemented. @impl true def routes, do: [:on_results, :on_empty, :on_error] @impl true def run(args) do # args[:input] — output from previous workflow step # args[:config] — step configuration from the workflow editor # args[:resources] — attached resources # Return triple tuple with branch name for conditional routing: {:ok, result, :on_results} # Or on empty/error: # {:ok, "No results found.", :on_empty} # {:error, reason} — implicit :on_error end end ``` The LLM tier (`:light`, `:medium`, `:heavy`, `:local`) is set per workflow step in the admin UI, not in the skill module. If your skill calls `AlexClaw.LLM.complete/2`, prefer `:light` unless genuinely necessary — the router will thank you. **External skills:** If your skill fetches data from external sources (HTTP requests, APIs, RSS feeds), you must declare `def external, do: true` in the module. This enables automatic content sanitization in the workflow engine. For dynamic skills, the registry checks the source at load time: a skill calling `SkillAPI.http_*` without `external/0` is **rejected**, and one calling an HTTP or socket library directly (`Req`, `HTTPoison`, `Finch`, `Tesla`, `:gen_tcp`) is outside containment and rejected whatever it declares. A core skill (in `lib/alex_claw/skills/`) is release code and may use `Req` directly. --- ## Continuous Integration Every push and pull request runs the format check and the full test suite in the same container image used locally, so a green run locally means a green run on CI. An unformatted file fails the build, so `mix format` is not optional. Run it on the host rather than inside a container, which rewrites line endings. The public docs are checked in the same suite (`test/docs_contract_test.exs` and the documentation tests): every `make` target, script and repo path a doc names must exist; README's skills table must match the core skills and its Admin UI table the admin pages; numbers a doc states about the code, such as the injection-pattern count, must match the source; every internal link and anchor must resolve. A change that makes a doc false fails the build. --- ## Documentation and Releases The repository is public, so a doc that is false is a defect like any other. A change that alters behaviour updates the public docs it affects — README, SECURITY, INSTALLATION, and the pages under `docs/` — in the same branch. Before a branch is merged to `main` for a release, what the release changed is checked against every public doc: features added or changed, commands, settings, limits and security claims. Docs found wrong or missing are fixed in that branch, and the release notes (`.github/release-notes/v.md`) name what changed and any step an upgrade needs. A release is not merged without that check and a passing suite. --- ## Code Style - Standard Elixir formatting — run `mix format` before committing; CI rejects unformatted code - No unnecessary abstractions - Pattern match explicitly — avoid generic catch-alls where possible - Log with structured metadata: `Logger.info("event", skill: :my_skill, duration: ms)` --- ## Questions Open an issue or start a discussion on GitHub. The project owner (Alessio Battistutta) reviews contributions personally. --- *AlexClaw — The BEAM-native personal AI agent. 🦇* *Copyright 2026 Alessio Battistutta — Apache License 2.0*