1 # CLAUDE.md
2
3 This file provides guidance to AI coding agents when working with code in this repository.
4
5 ## What this is
6
7 `sigit` ("siGit Code") is a single Rust binary: a local-first AI coding agent that runs LLM
8 inference on-device (via the `onde` crate / GGUF models) or against a hosted/OpenAI-compatible
9 endpoint. It exposes itself two ways from the *same* binary, chosen at startup by whether stdin
10 is a TTY:
11
12 - **ACP mode** (stdin not a TTY): speaks the Agent Client Protocol over stdio for editor
13 integration (Zed, VS Code ACP Client). Cross-platform.
14 - **Interactive terminal mode** (stdin is a TTY): a full-screen ratatui chat UI. **Unix-only**
15 it relies on fd redirection to keep logs out of the TUI, so Windows gets ACP mode only.
16
17 Before the TTY/ACP split, `main` also dispatches the account subcommands `sigit login`,
18 `sigit logout`, `sigit whoami`, and the headless one-shot mode `sigit run` (see `src/main.rs`
19 `main()`). `sigit run` (`src/headless.rs`) executes a single task non-interactively — JSONL
20 progress events on stdout, logs on stderr, exit 0/1/2 — and is what cloud runners (siGit Code
21 Cloud Agent) drive; it never falls back to on-device inference and declines permission prompts
22 unless `SIGIT_PERMISSIONS=allow` is set. Its tool loop mirrors `handle_prompt`; keep the two in
23 sync.
24
25 ## Working in this repo
26
27 **IMPORTANT — branch naming:** Prefix every working branch with `feature/` (new functionality)
28 or `fix/` (bug fixes) — never a tool- or agent-name prefix like `claude/`. Name the branch after
29 the *changes it contains*, as a short, descriptive, kebab-case slug that is self-explanatory
30 from the name alone (e.g. `feature/tool-permission-system`, `fix/glob-mtime-sort`), never after
31 a task, ticket, or session id (not `feature/task-q003hm`).
32
33 **IMPORTANT — pull request target:** Always open pull requests against the `development` branch,
34 never `main`. `main` is release-only; `development` is where day-to-day work integrates.
35
36 **IMPORTANT — branch off `development`:** Start every working branch from the latest
37 `origin/development`. Exception: when new work *depends on* a feature branch that has not merged
38 yet (e.g. it builds on tools or APIs that branch introduces), it may be stacked on top of that
39 branch instead. When stacking: merge the base PR into `development` first, then rebase the
40 stacked branch onto `development` before opening its pull request, so each PR shows only its own
41 commits.
42
43 **IMPORTANT — run CI before pushing:** Run the full CI gate locally and confirm it is green
44 *before* pushing a branch or opening a pull request — never push work that fails these:
45
46 ```sh
47 cargo fmt -- --check # formatting
48 cargo clippy --tests -- -D warnings # lint (warnings are errors)
49 cargo test --locked # tests
50 ```
51
52 ## Build / test / lint
53
54 ```sh
55 cargo build # debug build
56 cargo build --release # release binary at target/release/sigit
57 cargo run # launches interactive TUI (stdin is a TTY)
58 cargo test # CI runs: cargo test --locked --target <target>
59 cargo clippy --tests -- -D warnings # CI gate: clippy is -D warnings on all 4 targets
60 cargo fmt -- --check # CI gate (edition 2024)
61 ```
62
63 CI (`.github/workflows/ci.yml`) runs fmt + clippy + test across four targets:
64 `aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-unknown-linux-gnu`,
65 `x86_64-pc-windows-msvc`. Clippy is `-D warnings`, so warnings fail the build.
66
67 Run a single test: `cargo test <test_name>`.
68
69 ## Critical platform constraint: `#[cfg(unix)]` dead code
70
71 The interactive client is `#[cfg(unix)]`-only. The `InferenceBackend` seam (`backend.rs`) and
72 provider resolution (`provider.rs`) are consumed by both the interactive client and the ACP
73 server, but several of their items are reached only through the Unix-only interactive paths, so
74 the dead-code lint is suppressed *on non-Unix targets only*.
75
76 Consequence: code can pass clippy on macOS/Linux but fail on the Windows target (or vice versa).
77 When touching `backend.rs`, `provider.rs`, or the interactive path, keep the `cfg` gates intact —
78 don't "fix" an unused-warning by deleting code that's live on Unix.
79
80 ## Architecture
81
82 The agent loop is backend-agnostic. The flow: a turn (messages + tool specs) goes to an
83 `InferenceBackend`, which returns assistant text and/or tool calls; the loop executes tools and
84 feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete backend.
85
86 - **`src/main.rs`** — entry point, mode dispatch, the full ACP `Agent` impl (session lifecycle:
87 new/load/fork/prompt/cancel, config options, slash-command advertisement), and the `SYSTEM_PROMPT`
88 (note: it bakes in smbCloud-specific context the agent should use when the repo is clearly
89 smbCloud, and stay general otherwise).
90 - **`src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
91 `ToolResult`, `TurnResult`). Two impls: `LocalBackend` (on-device via `onde::ChatEngine`) and
92 `OpenAiBackend` (any OpenAI-compatible HTTP endpoint).
93 - **`src/provider.rs`** — decides *which* backend serves inference. Resolution order, first match
94 wins: (1) override via `OPENAI_BASE_URL`+`OPENAI_API_KEY` or active profile in
95 `~/.config/sigit/providers.toml`; (2) siGit Code Cloud when logged in; (3) on-device.
96 - **`src/tools.rs`** — agent tool schemas + execution: `read_file`, `create_directory`,
97 `list_directory`, `search_files`, `glob`, `read_website`, `create_file`, `edit_file`,
98 `multi_edit`, `delete_file`, `run_command`, `write_todos`, `remember`. Add a tool in both the
99 spec list (`all_tools`) and the execute `match` (`execute_tool`). `run_command` also enforces
100 commit attribution: when a command creates a new commit that lacks the
101 `Co-Authored-By: siGit Code` trailer (`COMMIT_CO_AUTHOR_TRAILER`), it amends the trailer in —
102 unless the commit already exists on a remote, which is never rewritten.
103 - **`src/skills.rs`**[Agent Skills](https://agentskills.io) support. Discovers skill
104 folders (each with a `SKILL.md`: YAML frontmatter `name` + `description`, then Markdown
105 instructions) from `.sigit/skills/` and `.claude/skills/` in the cwd, `$SIGIT_CONFIG_DIR/skills/`,
106 and `~/.claude/skills/`. Progressive disclosure: the discovery list (name + description) is
107 baked into the dynamically-built `skill` tool's description, and activating a skill (the model
108 calls `skill` with a name) loads the full `SKILL.md` body. The `skill` tool is appended in the
109 `*_as_specs`/`build_tool_specs` layer (not in `all_tools()`) so its description can be dynamic,
110 and only when at least one skill exists.
111 - **`src/mcp.rs`**[Model Context Protocol](https://modelcontextprotocol.io) *client*. Connects to
112 MCP servers over the **Streamable HTTP** transport (one JSON-RPC POST endpoint; replies are
113 `application/json` or SSE), runs the `initialize`/`tools/list` handshake, and forwards `tools/call`.
114 Discovery is best-effort at startup (`mcp::init`, called from both branches of `main()`) and cached
115 in a process-global so the synchronous spec builders (`mcp::tool_specs`) and the async dispatch
116 (`mcp::call_tool`) can both read it. Tools are namespaced `mcp__<server>__<tool>`, appended in the
117 `*_as_specs`/`build_tool_specs` layer and routed in `tools::execute_tool` via `mcp::is_mcp_tool`. The
118 official server (`<cloud>/mcp`, default `https://sigit.si/api/v1/mcp`) is baked in and authed with the
119 cloud session token; extra servers live in `mcp.toml` (global `$SIGIT_CONFIG_DIR/mcp.toml` and
120 project-local `.sigit/mcp.toml`). stdio transport is not supported.
121 - **`src/permissions.rs`** — tool permission policy. Every tool call passes through
122 `decision_for` before executing: read-only tools always run; mutating tools (and all
123 `mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
124 mutating tools with a present-a-plan message), session "always allow" grants, per-tool
125 overrides and the default mode from `[permissions]` in `settings.toml` (`allow`/`ask`/`deny`,
126 default `ask`; `SIGIT_PERMISSIONS` env overrides the default). On `ask`, the ACP path sends
127 `session/request_permission` (allow once / allow for session / deny) and the TUI pauses the
128 inference task on a y/a/n prompt. Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
129 serialized by `SiGitAgent::turn_lock` so the dispatch loop can route the client's permission
130 answer mid-turn — don't move them back inline, and don't await client requests from inline
131 handlers (deadlock).
132 - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
133 Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
134 walking from the session cwd up to the repo root (nearest ancestor with `.git`, never above it),
135 plus a global file under `$SIGIT_CONFIG_DIR`. Files are ordered outermost-first so the deepest
136 (most specific) wins. The combined block is injected via `session_context_message` in `main.rs`
137 — pushed as a system message at every ACP session entry point (new/load/fork + model switch)
138 and appended to the system prompt on the cloud and TUI-startup paths.
139 - **`src/chat.rs`** — the Unix-only ratatui TUI. Loading-spinner phase then chat; uses
140 `tokio::select!` to multiplex terminal events with streaming tokens.
141 - **`src/setup.rs`** — model cache location, local model discovery, selected-model persistence.
142 Must run (`setup_shared_model_cache`) *before* anything touches `ChatEngine`/`hf-hub`, since
143 those read env vars once at init.
144 - **`src/account.rs`** — siGit Code Cloud auth (`/login`, `/logout`, `/whoami`); authenticates
145 against the account API and stores a session token. Performs no console I/O.
146 - **`src/credentials.rs`** — local session-token store (TOML, `0600` on Unix).
147 - **`src/models.rs`** — model-picker types shared across platforms.
148
149 Slash commands (`/help`, `/models`, `/skills`, `/mcp`, `/login`, `/logout`, `/whoami`, `/reload`,
150 `/plan`, `/permissions`, `/clear`, `/status`) are advertised via `advertise_commands` in `main.rs`
151 and handled in both the TUI and ACP sessions.
152
153 ## Model cache (macOS)
154
155 On macOS the HF model cache lives in an App Group container shared with the siGit desktop app:
156 `~/Library/Group Containers/group.com.ondeinference.apps/models/`. Other platforms fall back to
157 `~/.cache/huggingface/`. The CLI reuses a model the desktop app already downloaded. First run
158 downloads a GGUF model (~1–2 GB) from Hugging Face.
159
160 ## Logging
161
162 In TTY (interactive) mode, *all* output — `log`, `tracing`, stray `println!` — is redirected to
163 `$TMPDIR/sigit.log` so the ratatui surface stays clean; the TUI holds a separate fd to the real
164 terminal. In ACP mode, stdout is reserved for protocol JSON and logs go to stderr. Control
165 verbosity with `RUST_LOG`.
166
167 ## Relevant env vars
168
169 `OPENAI_BASE_URL` / `OPENAI_API_KEY` (provider override), `SIGIT_API_URL` (account API base,
170 default `https://sigit.si`), `SIGIT_CLOUD_URL`, `SIGIT_CONFIG_DIR` (default `~/.config/sigit`),
171 `SIGIT_MODEL`, `SIGIT_MCP` (`off` disables MCP), `SIGIT_MCP_OFFICIAL` (`off` drops the baked-in
172 server), `SIGIT_PERMISSIONS` (`allow`/`ask`/`deny` — overrides the default permission mode for
173 mutating tools; the escape hatch for clients without permission-request support),
174 `HF_HOME` / `HF_HUB_CACHE`, `RUST_LOG`.
175
176 ## Releasing
177
178 Version lives in `Cargo.toml`. The binary is published to five registries via separate workflows
179 (`release-crates`, `release-github`, `release-homebrew`, `release-npm`, `release-pypi`); the
180 `npm/` and `pypi/` dirs hold the wrapper-package templates. Update `CHANGELOG.md` for releases.