Thank you for your interest in contributing to Giulia. This document explains how to contribute and what to expect from the process.
Giulia is a local-first AI development agent built on Elixir/OTP. Contributions are welcome, but please understand the project's philosophy before proposing changes:
- OTP first — state lives in GenServers and ETS, not in chat history
- Daemon-client — persistent background service, not a CLI that restarts
- Native AST — Sourceror (pure Elixir) for code analysis, not tree-sitter NIFs
- Provider agnostic — supports Anthropic, Ollama, LM Studio, Gemini, Groq
- Observable — telemetry events across the OODA pipeline, SSE dashboard
- MCP native — Model Context Protocol server alongside REST, auto-generated from skill annotations
- Sandboxed — Giulia can only access files under the project root
If your contribution aligns with these principles, it's likely a good fit.
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 tools, architectural changes, new providers), you must explicitly sign the CLA by including this statement in your PR:
I have read the Giulia 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.
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.
- New tools — code analysis, refactoring aids, project health checks
- New LLM providers — additional provider integrations for the router
- Bug fixes — especially around CubDB persistence, Property Graph edge cases
- Documentation — architecture explanations, API usage examples
- Observability — new telemetry events, monitor dashboard improvements
- Docker improvements — build performance, multi-platform support
- Test coverage — especially for tool modules and integration tests
- External service dependencies that break the local-first model
- Tree-sitter NIFs or C dependencies (Sourceror handles Elixir AST natively)
- Authentication/authorization layers (Giulia is a local development tool)
- Complexity for its own sake
- Fork the repository
- Create a branch —
git checkout -b feature/my-toolorfix/cubdb-recovery - Write your code — follow the existing patterns in
lib/giulia/ - Add tests — see TESTING.md for the test infrastructure
- Run tests —
docker compose -f docker-compose.test.yml run --rm giulia-test - Open a pull request — describe what you built and why
New tools must implement the tool behaviour:
defmodule Giulia.Tools.MyTool do
@moduledoc "One-line description of what this tool does."
@doc "Tool name as used in LLM tool calls."
def name, do: "my_tool"
@doc "Human-readable description for the tool registry."
def description, do: "Short description for tool discovery"
@doc "JSON Schema for tool parameters."
def parameters do
%{
"type" => "object",
"properties" => %{
"input" => %{"type" => "string", "description" => "What the tool needs"}
},
"required" => ["input"]
}
end
@doc "Execute the tool. Returns {:ok, result} or {:error, reason}."
def execute(params, opts) do
project_path = Keyword.fetch!(opts, :project_path)
# Tool logic here
{:ok, "result"}
end
endTools are auto-discovered by Giulia.Tools.Registry on boot.
New API endpoints must include a @skill annotation:
@skill %{
intent: "What this endpoint does in plain English",
endpoint: "GET /api/category/my_endpoint",
params: %{path: :required, module: :optional},
returns: "JSON description of the response shape",
category: "category_name"
}
get "/my_endpoint" do
# ...
endThis makes the endpoint self-describing via the Discovery API. It also automatically exposes the endpoint as an MCP tool — Giulia.MCP.ToolSchema reads all @skill annotations at boot and generates MCP tool definitions from them. No separate MCP registration is needed.
Follow CODING_CONVENTIONS.md for idiomatic Elixir patterns.
Key points:
- Pattern match in function heads, not if/else chains
- Return tagged tuples
{:ok, _} | {:error, _}— never raise for expected failures - Never create atoms from runtime strings (
String.to_existing_atomor tuple keys) - Use
Integer.parsenotString.to_integer - Keep GenServer callbacks thin — delegate to pure functions
- Every public function gets
@spec, every module gets@moduledoc - Run
mix formaton the host before committing (never inside Docker — see TESTING.md)
Every code modification MUST increment @build in mix.exs before building.
This is how we track which version is running on client vs server.
Tests run inside Docker. See TESTING.md for full details.
# Full suite (isolated environment, recommended)
docker compose -f docker-compose.test.yml run --rm giulia-test
# Single file
docker compose -f docker-compose.test.yml run --rm giulia-test test/giulia/foo_test.exsThe full test suite must pass with zero regressions from your changes.
Open an issue or start a discussion on GitHub. The project owner (Alessio Battistutta) reviews contributions personally.
Giulia — The BEAM-native AI development agent. Copyright 2026 Alessio Battistutta — Apache License 2.0