Turn the repo into a Cargo workspace with the CLI in crates/cli

The root Cargo.toml becomes a workspace that holds the shared package fields, the dependency versions and the release profile. The sigit crate moves to crates/cli unchanged: same package name, same binary, same src/ and tests/. Binaries still land in target/<triple>/release, so the packaging scripts need no new paths. The release workflows build and publish with -p sigit, the PyPI manifest points at the new Cargo.toml, and the project instructions and skills use the new paths. This is groundwork for a second crate that embeds the agent in sigit-app. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: siGit Code v1.6.7 <noreply@sigit.si>

Seto Elkahfi committed Oct 12, 2026 at 08:44 UTC 991ac7a83b3180ce0978abf4a7bfdf81de4b472f
61 files changed +123 -75
.agents/AGENTS.md
+48 -38
@@ -15,7 +15,7 @@ is a TTY:
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` (see `src/main.rs` `main()`).
18 +`sigit logout`, `sigit whoami` (see `crates/cli/src/main.rs` `main()`).
19
20 ## Working in this repo
21
@@ -67,21 +67,31 @@ the repo is a symlink into it, never a copy:
67
68 - `.agents/AGENTS.md` — this file, the project instructions. The root
69 `AGENTS.md` and `CLAUDE.md` are symlinks to it, so agents.md-standard tools,
70 - Claude Code, and sigit itself (via `src/instructions.rs`, which prefers
70 + Claude Code, and sigit itself (via `crates/cli/src/instructions.rs`, which prefers
71 `AGENTS.md` over `CLAUDE.md` in the same directory and therefore reads it
72 once) all load the same content. Edit this file; never replace the root
73 symlinks with real files.
74 - `.agents/skills/` — all project skills. `.claude/skills` is a symlink to it,
75 - which is how both Claude Code and sigit itself (via `src/skills.rs`) discover
75 + which is how both Claude Code and sigit itself (via `crates/cli/src/skills.rs`) discover
76 them. Add new skills under `.agents/skills/<name>/SKILL.md`, never as real
77 files under `.claude/`.
78
79 +## Workspace layout
80 +
81 +The repo root is a Cargo workspace (`members = ["crates/cli"]`, with `crates/sigit-*` added as
82 +they land). The `sigit` binary crate lives in `crates/cli`: its `src/` and `tests/` are what the
83 +architecture section below calls `src/` and `tests/`, and it publishes as `sigit` from there.
84 +Shared package fields and every dependency version are declared once in the root
85 +`Cargo.toml` (`[workspace.package]`, `[workspace.dependencies]`) and inherited with
86 +`workspace = true`. Build outputs are unchanged: `target/<triple>/release/sigit`. Release
87 +workflows build and publish with `-p sigit`.
88 +
89 ## Build / test / lint
90
91 ```sh
92 cargo build # debug build
93 cargo build --release # release binary at target/release/sigit
84 -cargo run # launches interactive TUI (stdin is a TTY)
94 +cargo run -p sigit # launches interactive TUI (stdin is a TTY)
95 cargo test # CI runs: cargo test --locked --target <target>
96 cargo clippy --tests -- -D warnings # CI gate: clippy is -D warnings on all 4 targets
97 cargo fmt -- --check # CI gate (edition 2024)
@@ -110,7 +120,7 @@ The agent loop is backend-agnostic. The flow: a turn (messages + tool specs) goe
120 `InferenceBackend`, which returns assistant text and/or tool calls; the loop executes tools and
121 feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete backend.
122
113 -- **`src/main.rs`** — entry point, mode dispatch, the full ACP `Agent` impl (session lifecycle:
123 +- **`crates/cli/src/main.rs`** — entry point, mode dispatch, the full ACP `Agent` impl (session lifecycle:
124 new/load/fork/prompt/cancel, config options, slash-command advertisement), and the `SYSTEM_PROMPT`.
125 ACP session state owns its roots, conversation, and selected model even though the process has
126 one working directory and one live backend slot; activating a thread parks and restores all
@@ -138,7 +148,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
148 `session_store::delete`, and succeeds for an id that is already gone. The
149 `SYSTEM_PROMPT` bakes in smbCloud-specific context the agent should use when the repo is clearly
150 smbCloud, and stay general otherwise.
141 -- **`src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
151 +- **`crates/cli/src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
152 `ToolResult`, `TurnResult`). Two impls: `LocalBackend` (on-device via `onde::ChatEngine`) and
153 `OpenAiBackend` (any OpenAI-compatible HTTP endpoint). A `BackendError` is user-facing: the
154 ACP prompt handler passes it to the client verbatim and the editor puts it in an error banner,
@@ -163,18 +173,18 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
173 `round_cap_note` is appended to that round's last tool result, and the turn always closes
174 with `round_cap_stop_message`. Without both, the thread stops on the model announcing a
175 step that never runs (issue #120).
166 - Some models write tool calls into content as text; `src/inline_tool_calls.rs` recovers the
176 + Some models write tool calls into content as text; `crates/cli/src/inline_tool_calls.rs` recovers the
177 well-formed ones. A block that doesn't parse (or never closes) is dropped from both the reply
178 and history, and `OpenAiBackend::complete` retries once with a note telling the model the call
179 didn't run. Leaving the raw block in history makes the model invent `<function_results>` later.
170 - What that scanner lets through then goes through `src/harness_markup.rs`, which removes
180 + What that scanner lets through then goes through `crates/cli/src/harness_markup.rs`, which removes
181 markup the model makes up in the harness's shape (issue #122): a `<system_warning>`-style
182 block (any `system_*`/`system-*` tag) is dropped whole, from the reply and the history, and
183 other underscore-named tags like `<Option_Picker>` lose the tags but keep their text. Code is
184 never touched, and an opening tag only counts at the start of a line, since prose writes
185 placeholders like `<repo_url>` the same way. It has to run after the inline-call scanner,
186 because `<tool_call>` matches its underscore rule.
177 - `src/repetition.rs` guards the same stream against a reply that degenerates into a loop over a
187 + `crates/cli/src/repetition.rs` guards the same stream against a reply that degenerates into a loop over a
188 tiny vocabulary (issue #123): a window of 200 words with at most 40 distinct ones, fenced code
189 excepted, makes `consume_stream` stop reading, truncate the text to where the loop began (so
190 history never holds the junk), tell the user, and finish with `FinishReason::Repetition`
@@ -196,10 +206,10 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
206 takes audio, because onde-cloud drops `input_audio` parts when it parses a request, so
207 `provider::model_accepts_audio` says no for every `onde-*` id and yes for a user's own
208 endpoint. If onde-cloud ever routes audio, give it a tier table like `IMAGE_TIERS`.
199 -- **`src/provider.rs`** — decides *which* backend serves inference. Resolution order, first match
209 +- **`crates/cli/src/provider.rs`** — decides *which* backend serves inference. Resolution order, first match
210 wins: (1) override via `OPENAI_BASE_URL`+`OPENAI_API_KEY` or active profile in
211 `~/.config/sigit/providers.toml`; (2) siGit Code Cloud when logged in; (3) on-device.
202 -- **`src/tools.rs`** — agent tool schemas + execution: `read_file`, `create_directory`,
212 +- **`crates/cli/src/tools.rs`** — agent tool schemas + execution: `read_file`, `create_directory`,
213 `list_directory`, `search_files`, `glob`, `read_website`, `create_file`, `edit_file`,
214 `multi_edit`, `delete_file`, `run_command`, `write_todos`, `remember`. Add a tool in both the
215 spec list (`all_tools`) and the execute `match` (`execute_tool`). `run_command` also enforces
@@ -217,7 +227,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
227 (a subagent factory is registered — see `register_subagent_factory_for` in `main.rs`; on-device
228 registers a `None`-returning factory since onde has a single shared history). A subagent's
229 toolset is a hard-gated read-only allow-list (`SUBAGENT_TOOL_NAMES`) that is never expanded by a
220 - configurable subagent type (see `src/subagents.rs`) — only ever narrowed — so a `.sigit/agents/*.md`
230 + configurable subagent type (see `crates/cli/src/subagents.rs`) — only ever narrowed — so a `.sigit/agents/*.md`
231 file can't grant itself `edit_file`/`run_command` and bypass the permission system. Also owns
232 `web_search`: a thin native wrapper around the official MCP server's Brave-Search-backed
233 `web_search` tool (`mcp__sigit__web_search`, implemented server-side in `sigit-si`'s
@@ -229,16 +239,16 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
239 of the assembled tool list (`is_web_search_delegate`) so the model sees one clean option, not
240 two names for the same tool. Execution still forwards verbatim to `mcp::call_tool` — no second
241 HTTP/JSON-RPC implementation.
232 -- **`src/skills.rs`** — [Agent Skills](https://agentskills.io) support. Discovers skill
242 +- **`crates/cli/src/skills.rs`** — [Agent Skills](https://agentskills.io) support. Discovers skill
243 folders (each with a `SKILL.md`: YAML frontmatter `name` + `description`, then Markdown
234 - instructions) from `.sigit/skills/` and `.claude/skills/` in every project root (see `src/workspace.rs`),
244 + instructions) from `.sigit/skills/` and `.claude/skills/` in every project root (see `crates/cli/src/workspace.rs`),
245 `$SIGIT_CONFIG_DIR/skills/`,
246 and `~/.claude/skills/`. Progressive disclosure: the discovery list (name + description) is
247 baked into the dynamically-built `skill` tool's description, and activating a skill (the model
248 calls `skill` with a name) loads the full `SKILL.md` body. The `skill` tool is appended in the
249 `*_as_specs`/`build_tool_specs` layer (not in `all_tools()`) so its description can be dynamic,
250 and only when at least one skill exists.
241 -- **`src/commands.rs`** — user-defined slash commands. Discovers Markdown files (each an
251 +- **`crates/cli/src/commands.rs`** — user-defined slash commands. Discovers Markdown files (each an
252 optional YAML frontmatter block — `description`, `argument-hint` — followed by a prompt-template
253 body) from `.sigit/commands/` and `.claude/commands/` in every project root, `$SIGIT_CONFIG_DIR/commands/`,
254 and `~/.claude/commands/`. A subdirectory namespaces the command with `:`
@@ -251,7 +261,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
261 before falling back to "unknown command". A custom command sharing a name with a built-in is
262 unreachable (built-ins match first in `parse_slash`) and is skipped when advertised to ACP
263 clients, with a warning logged.
254 -- **`src/subagents.rs`** — configurable subagent types for the `task` tool. Discovers Markdown
264 +- **`crates/cli/src/subagents.rs`** — configurable subagent types for the `task` tool. Discovers Markdown
265 files (YAML frontmatter `name` + `description`, optional comma-separated `tools:` allow-list,
266 then a Markdown body that becomes the subagent's system prompt) from `.sigit/agents/` and
267 `.claude/agents/` in every project root, `$SIGIT_CONFIG_DIR/agents/`, and `~/.claude/agents/`. Passing a
@@ -261,9 +271,9 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
271 module only discovers and parses files. `SubagentFactory` (in `tools.rs`) takes the resolved
272 system prompt per call rather than baking one in at registration, so a single registered
273 factory serves both the default research subagent and every configured type.
264 -- **`src/frontmatter.rs`** — shared "YAML frontmatter + Markdown body" parsing used by both
265 - `src/skills.rs` (`SKILL.md`) and `src/commands.rs` (`.sigit/commands/*.md`).
266 -- **`src/mcp.rs`** — [Model Context Protocol](https://modelcontextprotocol.io) *client*. Two
274 +- **`crates/cli/src/frontmatter.rs`** — shared "YAML frontmatter + Markdown body" parsing used by both
275 + `crates/cli/src/skills.rs` (`SKILL.md`) and `crates/cli/src/commands.rs` (`.sigit/commands/*.md`).
276 +- **`crates/cli/src/mcp.rs`** — [Model Context Protocol](https://modelcontextprotocol.io) *client*. Two
277 transports: **Streamable HTTP** (one JSON-RPC POST endpoint, `url` in `mcp.toml`; replies are
278 `application/json` or SSE) and **stdio** (`command` + optional `args`/`[server.env]` in
279 `mcp.toml`; sigit spawns the server and speaks newline-delimited JSON-RPC over its
@@ -282,7 +292,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
292 `SIGIT_MCP_SMBCLOUD=off`). A user-defined entry named `sigit` or `smbcloud` overrides the
293 corresponding baked-in one. Extra servers live in `mcp.toml` (global
294 `$SIGIT_CONFIG_DIR/mcp.toml` and project-local `.sigit/mcp.toml`). The stdio path is covered by
285 - `tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
295 + `crates/cli/tests/mcp_stdio.rs`, driven by the test-only `crates/cli/src/bin/mcp_stdio_stub.rs` helper binary
296 (excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
297 server is *also* listed in the public MCP Registry as `si.sigit/sigit` — a **remote**
298 Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
@@ -306,7 +316,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
316 servers are never offered elicitation, and `rmcp` answers `ping` and refuses other server
317 requests itself. Forwarding needs that hook in `ed-mcp` first
318 (tracked upstream at `ondeinference/ed#33`).
309 -- **`src/permissions.rs`** — tool permission policy. Every tool call passes through
319 +- **`crates/cli/src/permissions.rs`** — tool permission policy. Every tool call passes through
320 `decision_for` before executing: read-only tools always run; mutating tools (and all
321 `mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
322 mutating tools with a present-a-plan message), session "always deny" and "always allow"
@@ -348,7 +358,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
358 that session's own model (`config_options_for_session`), never `activate_session`. Unknown session ids are `-32002` (resource not found) everywhere,
359 and `authenticate` accepts only `AUTH_METHOD_ID`. Tool
360 execution, subagents included, still runs one session at a time.
351 -- **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
361 +- **`crates/cli/src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
362 Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
363 walking from the session cwd up to the repo root (nearest ancestor with `.git`, never above it),
364 plus a global file under `$SIGIT_CONFIG_DIR`. Files are ordered outermost-first so the deepest
@@ -356,7 +366,7 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
366 (`load_workspace_instructions`). The combined block is injected via `session_context_message`
367 in `main.rs` — pushed as a system message at every ACP session entry point (new/load/fork +
368 model switch) and appended to the system prompt on the cloud and TUI-startup paths.
359 -- **`src/workspace.rs`** — the directories the session treats as project roots. An editor can
369 +- **`crates/cli/src/workspace.rs`** — the directories the session treats as project roots. An editor can
370 open several at once (Zed calls it a multi-root project) and ACP carries the extras as
371 `additional_directories` on every session request; the headless CLI takes them as repeatable
372 `--add-dir` flags. A client only sends those extras to an agent that advertises
@@ -374,8 +384,8 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
384 discovery reads it: skills, slash commands, subagent types, and instruction files all scan
385 every root. MCP is deliberately not on that list — `mcp::init` runs once at startup, before
386 any session exists, so a second root's `.sigit/mcp.toml` has nobody to tell. (The servers a
377 - client names in `mcpServers` are a different thing and are per session; see `src/mcp.rs`.)
378 -- **`src/client_fs.rs`** — file reads and writes through the ACP client. A client that
387 + client names in `mcpServers` are a different thing and are per session; see `crates/cli/src/mcp.rs`.)
388 +- **`crates/cli/src/client_fs.rs`** — file reads and writes through the ACP client. A client that
389 advertises `fs.readTextFile` / `fs.writeTextFile` in `initialize` serves `fs/read_text_file`
390 and `fs/write_text_file` from its buffers, so `read_file` sees unsaved changes and
391 `edit_file` lands in the open buffer instead of underneath it. `tools.rs` has no access to the
@@ -395,8 +405,8 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
405 seconds is retried on disk with a warning in the log. The client is only ever asked from
406 inside a spawned prompt turn, never from `handle_initialize` itself (deadlock, same as
407 permission requests). Existence checks and `create_file`'s parent directories still use the
398 - disk. `SIGIT_CLIENT_FS=off` turns the routing off. Covered by `tests/acp_client_fs.rs`.
399 -- **`src/client_terminal.rs`** — `run_command` in the ACP client's terminal. A client that
408 + disk. `SIGIT_CLIENT_FS=off` turns the routing off. Covered by `crates/cli/tests/acp_client_fs.rs`.
409 +- **`crates/cli/src/client_terminal.rs`** — `run_command` in the ACP client's terminal. A client that
410 advertises `terminal` in `initialize` runs a foreground command itself (`terminal/create`, with
411 the platform shell as `command` and the whole command line as one argument), and
412 `exec_run_command_via_client` embeds the terminal in the tool call as `terminal` content, so the
@@ -412,15 +422,15 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
422 after that is reported as the tool's error instead, because running it again could repeat its
423 effects. The finished card keeps the terminal and drops the result text, which still goes out
424 as `raw_output`. `SIGIT_CLIENT_TERMINAL=off` turns the routing off. Covered by
415 - `tests/acp_client_terminal.rs`.
416 -- **`src/chat.rs`** — the Unix-only ratatui TUI. Loading-spinner phase then chat; uses
425 + `crates/cli/tests/acp_client_terminal.rs`.
426 +- **`crates/cli/src/chat.rs`** — the Unix-only ratatui TUI. Loading-spinner phase then chat; uses
427 `tokio::select!` to multiplex terminal events with streaming tokens.
418 -- **`src/headless.rs`** — non-interactive `sigit run` execution for scripts, CI, and Factory
428 +- **`crates/cli/src/headless.rs`** — non-interactive `sigit run` execution for scripts, CI, and Factory
429 clients. The legacy `-p` form reaches the same parser. Every new run gets a UUID session id;
430 `--resume <id>` restores that session through `session_store`, and `--output jsonl` emits a
431 structured session/delta/tool/result stream while logs stay on stderr. Headless permission
432 prompts collapse to denial unless the tool was pre-approved with `--allow-tool`.
423 -- **`src/session_store.rs`** — durable conversations: one JSON-lines history file per session at
433 +- **`crates/cli/src/session_store.rs`** — durable conversations: one JSON-lines history file per session at
434 `$SIGIT_CONFIG_DIR/sessions/<id>.jsonl`, written atomically, restorable into either backend.
435 Each save also writes a `<id>.meta.json` sidecar naming the session's `cwd` and the extra roots
436 of a multi-root project. That sidecar is what makes a thread *listable*: ACP's `session/list`
@@ -432,16 +442,16 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
442 the client as a `session_info_update` carrying a fresh `updatedAt`, plus the title when it
443 differs from the last one sent (`announced_titles`), so a new thread gets its title in the
444 sidebar without waiting for the next `session/list`.
435 -- **`src/setup.rs`** — model cache location, local model discovery, selected-model persistence.
445 +- **`crates/cli/src/setup.rs`** — model cache location, local model discovery, selected-model persistence.
446 Must run (`setup_shared_model_cache`) *before* anything touches `ChatEngine`/`hf-hub`, since
447 those read env vars once at init.
438 -- **`src/account.rs`** — siGit Code Cloud auth (`/login`, `/logout`, `/whoami`); authenticates
448 +- **`crates/cli/src/account.rs`** — siGit Code Cloud auth (`/login`, `/logout`, `/whoami`); authenticates
449 against the account API and stores a session token. Performs no console I/O.
450 ACP `logout` (the editor's sign out button, shown only because `handle_initialize` advertises
451 `agentCapabilities.auth.logout`) and `/logout` both go through `SiGitAgent::sign_out` in
452 `main.rs`. The request names no session, so `leave_cloud` moves the parked threads off their
453 cloud tier along with the live one and each of them gets a fresh model picker.
444 -- **`src/browser_auth.rs`** — browser sign-in, the path the editor's "Sign in to siGit Code"
454 +- **`crates/cli/src/browser_auth.rs`** — browser sign-in, the path the editor's "Sign in to siGit Code"
455 button takes. sigit is a public OAuth client (client id `sigit-code-cli`, no secret, PKCE
456 S256) against the authorization server at `$SIGIT_API_URL/oauth`. Two ways the code gets
457 back: a loopback listener on `127.0.0.1:0` (the registration lists `http://127.0.0.1/callback`
@@ -457,15 +467,15 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
467 so that answer can be routed), a bare `/login`
468 in either chat surface, and `sigit login` (which adds the paste fallback, plus `--paste` to
469 force it and `--password` for the old email/password prompt).
460 -- **`src/credentials.rs`** — local session-token store (TOML, `0600` on Unix).
461 -- **`src/models.rs`** — model-picker types shared across platforms.
470 +- **`crates/cli/src/credentials.rs`** — local session-token store (TOML, `0600` on Unix).
471 +- **`crates/cli/src/models.rs`** — model-picker types shared across platforms.
472
473 Slash commands (`/help`, `/models`, `/skills`, `/agents`, `/commands`, `/mcp`, `/login`, `/logout`,
474 `/whoami`, `/reload`, `/plan`, `/permissions`, `/init`, `/clear`, `/status`) are advertised via
475 `advertise_commands` in `main.rs` and handled in both the TUI and ACP sessions. `/init` is
476 special: instead of replying directly it substitutes `instructions::INIT_PROMPT` for the user
477 text and runs a normal agent turn that explores the repo and writes (or improves) `AGENTS.md`
468 -through the ordinary tools and permission checks. User-defined commands (see `src/commands.rs`)
478 +through the ordinary tools and permission checks. User-defined commands (see `crates/cli/src/commands.rs`)
479 get the same treatment via the `SlashCommand::Unknown` fallback path, so anyone can add their own
480 `/name` commands without touching the built-in command list.
481
@@ -498,7 +508,7 @@ mutating tools; the escape hatch for clients without permission-request support)
508
509 ## Releasing
510
501 -Version lives in `Cargo.toml`. Update `CHANGELOG.md` for releases.
511 +Version lives in `[workspace.package]` in the root `Cargo.toml`. Update `CHANGELOG.md` for releases.
512
513 Publishing splits into two groups. **Language registries** each get their own tag-triggered
514 workflow: `release-crates`, `release-npm`, `release-nuget`, `release-pypi`. The `npm/`, `nuget/`,
.agents/skills/agent-client-protocol/SKILL.md
+4 -4
@@ -26,7 +26,7 @@ Spec: https://agentclientprotocol.com
26 > protocol (`Agent.v2()`, `V2ConnectionTo`) sits behind the `unstable_protocol_v2`
27 > feature and siGit does not enable it.
28
29 -siGit's entire ACP server lives in `src/main.rs` (`run_acp_server`, the
29 +siGit's entire ACP server lives in `crates/cli/src/main.rs` (`run_acp_server`, the
30 `SiGitAgent` struct, and its `handle_*` methods). Read it alongside this skill.
31
32 ---
@@ -594,7 +594,7 @@ Editor Agent
594
595 ## Where to look in the code
596
597 -Everything ACP lives in `src/main.rs`:
597 +Everything ACP lives in `crates/cli/src/main.rs`:
598
599 - `run_acp_server` — builder wiring + transport.
600 - `SiGitAgent` + `handle_*` — the handlers.
@@ -602,6 +602,6 @@ Everything ACP lives in `src/main.rs`:
602 - `parse_slash` / `exec_slash_acp` — slash commands.
603 - `handle_response` — the `Responder` helper.
604
605 -`src/backend.rs` holds the `InferenceBackend` trait (`LocalBackend` /
606 -`OpenAiBackend`) used by `handle_prompt`'s tool-calling loop; `src/tools.rs`
605 +`crates/cli/src/backend.rs` holds the `InferenceBackend` trait (`LocalBackend` /
606 +`OpenAiBackend`) used by `handle_prompt`'s tool-calling loop; `crates/cli/src/tools.rs`
607 defines the agent tools and `execute_tool`.
.agents/skills/run-sigit/SKILL.md
+1 -1
@@ -94,7 +94,7 @@ tmux kill-session -t sigit
94 ## Run (human path)
95
96 ```bash
97 -cargo run # stdin is your TTY → launches the TUI
97 +cargo run -p sigit # stdin is your TTY → launches the TUI
98 ```
99
100 A full-screen chat opens; type a message or `/help`, Ctrl+C to quit. Useless
.agents/skills/sigit-code-release/SKILL.md
+3 -3
@@ -17,20 +17,20 @@ Use this skill when preparing a release for this repository.
17 ## Working approach
18
19 1. Read the files that define the release version and distribution metadata before proposing any changes.
20 -2. Treat `Cargo.toml` as the primary release version source for the Rust crate and the PyPI package in this repository.
20 +2. Treat `[workspace.package].version` in the root `Cargo.toml` as the primary release version source for the Rust crate and the PyPI package in this repository.
21 3. Check how the release workflows rewrite or derive versions before editing checked-in npm or Python metadata.
22 4. Prefer targeted verification commands that match CI or packaging workflows already present in the repository.
23 5. Report blockers clearly: failed checks, version mismatches, missing changelog entries, or unpublished packaging changes.
24
25 ## Repo-specific release rules
26
27 -- Bump the crate version in `Cargo.toml`.
27 +- Bump `[workspace.package].version` in the root `Cargo.toml`; every workspace crate inherits it.
28 - Update the root `sigit` package entry in `Cargo.lock` when the crate version changes.
29 - Add or update the top changelog entry in `CHANGELOG.md` for the release being cut.
30 - Do not treat `npm/sigit/package.json` `0.0.0-dev` as a bug by default. The npm release workflow rewrites it at publish time using `npm/scripts/render-main-package.cjs` and the release tag.
31 - Every npm package publishes twice, under `@getsigit` and under `@smbcloud`. `@smbcloud` is the pre-1.5.5 name, mirrored so an install predating the rename keeps updating; `@getsigit` is canonical and is what the README, the Homebrew tap, and the ACP registry entry point at. All fourteen names (two base packages, twelve platform packages) publish over OIDC and each needs its own Trusted Publisher on npm pointing at `release-npm.yml`.
32 - The scope is a render-time argument, not a second copy of the packaging: `render-platform-package.cjs` takes it as a fifth argument, and the base package picks it up from `package-main.json.tmpl` versus `package-compat.json.tmpl`. `npm/sigit/src/index.ts` is built once and shipped under both names — it reads its own `package.json` at runtime to decide which scope to resolve the platform binary from, and prints the migration notice only when it finds itself running as `@smbcloud/sigit`.
33 -- Do not add a hardcoded version to `pypi/pyproject.toml` for normal releases. PyPI uses `maturin` with `dynamic = ["version"]` and derives the published package version from `Cargo.toml`.
33 +- Do not add a hardcoded version to `pypi/pyproject.toml` for normal releases. PyPI uses `maturin` with `dynamic = ["version"]` and derives the published package version from `crates/cli/Cargo.toml`, which inherits the workspace version.
34 - Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-nuget.yml`, and `release-mcp-registry.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
35 - The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
36 - `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
.agents/skills/tool-calling/SKILL.md
+7 -7
@@ -13,7 +13,7 @@ Tool calling spans these layers:
13
14 ```
15 siGit (agent loop + tool execution)
16 - → InferenceBackend (src/backend.rs — LocalBackend or OpenAiBackend/cloud)
16 + → InferenceBackend (crates/cli/src/backend.rs — LocalBackend or OpenAiBackend/cloud)
17 → onde ChatEngine (tool-aware API) ── for LocalBackend
18 → mistral.rs (model inference + tool call parsing)
19 └ OpenAI-compatible HTTP endpoint ── for OpenAiBackend (siGit Code Cloud)
@@ -31,7 +31,7 @@ a remote OpenAI-compatible endpoint (the siGit Code Cloud tiers). Both implement
31 Tool calling needs a model mistral.rs has a tool-call parser for. The supported
32 set is the **Qwen 3 family** (`<tool_call>...</tool_call>` XML) plus **Qwen 2.5
33 Coder 7B**. Plain Qwen 2.5 and the smaller Qwen 2.5 Coder variants do NOT support
34 -tool calling. The authoritative list is `is_tool_calling()` in `src/models.rs`.
34 +tool calling. The authoritative list is `is_tool_calling()` in `crates/cli/src/models.rs`.
35
36 | Model | Constructor | Size | Tool calling |
37 |-------|-----------|------|:---:|
@@ -54,7 +54,7 @@ fallback. Users pick a tool-calling model via the `/models` picker.
54
55 ### max_tokens
56
57 -`max_tokens_for()` in `src/models.rs` gives tool-calling models **4096** tokens
57 +`max_tokens_for()` in `crates/cli/src/models.rs` gives tool-calling models **4096** tokens
58 and non-tool models **512** (tool models need headroom because `<think>` blocks
59 eat the budget). The TUI startup load in `run_interactive` overrides this to
60 **8192**. Don't assume a single value.
@@ -139,7 +139,7 @@ In ACP mode, `handle_prompt` currently always passes the full tool set (`agent_t
139 | `send_message_with_tools(msg, &[ToolDefinition])` | Returns `ToolAwareResult` with possible tool calls |
140 | `send_tool_results(Vec<ToolResult>, Option<&[ToolDefinition]>)` | Feed results back; `None` forces text response |
141
142 -#### Layer 2.5: the `InferenceBackend` abstraction (`src/backend.rs`)
142 +#### Layer 2.5: the `InferenceBackend` abstraction (`crates/cli/src/backend.rs`)
143
144 siGit doesn't call the engine directly from the agent loop — it goes through the
145 `InferenceBackend` trait so on-device and cloud inference share one code path:
@@ -155,7 +155,7 @@ siGit doesn't call the engine directly from the agent loop — it goes through t
155 `handle_prompt` snapshots `self.backend.lock().await.clone()` once per turn so a
156 mid-turn model/tier switch can't split the conversation across backends. When
157 `backend.is_remote()` it skips the local model load + readiness wait. Cloud tiers
158 -(`fast`, `balanced`, `large`) come from `src/provider.rs` and are sign-in gated.
158 +(`fast`, `balanced`, `large`) come from `crates/cli/src/provider.rs` and are sign-in gated.
159
160 #### Internal details
161
@@ -167,7 +167,7 @@ mid-turn model/tier switch can't split the conversation across backends. When
167
168 ### Layer 3: siGit (agent-level)
169
170 -#### ACP session handling (`src/main.rs`)
170 +#### ACP session handling (`crates/cli/src/main.rs`)
171
172 All session handlers (`load_session`, `fork_session`, `new_session`) do:
173
@@ -252,7 +252,7 @@ Models are stored in the shared Onde App Group container on macOS:
252
253 ## Adding a New Tool
254
255 -1. Add an `AgentTool` entry to `all_tools()` in `src/tools.rs`
255 +1. Add an `AgentTool` entry to `all_tools()` in `crates/cli/src/tools.rs`
256 2. Add a match arm to `execute_tool()` — use `spawn_blocking` if the implementation blocks
257 3. Write `exec_your_tool(arguments: &str) -> String`
258 4. Update `test_all_tools_count` test (currently expects 9)
.github/workflows/release-crates.yml
+1 -1
@@ -95,4 +95,4 @@ jobs:
95 if: steps.crates-check.outputs.exists != 'true'
96 env:
97 CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
98 - run: cargo publish --locked
98 + run: cargo publish --locked -p sigit
.github/workflows/release-github.yml
+1 -1
@@ -94,7 +94,7 @@ jobs:
94
95 - name: Build binary
96 shell: bash
97 - run: cargo build --locked --release --target ${{ matrix.target }}
97 + run: cargo build --locked --release -p sigit --target ${{ matrix.target }}
98
99 - name: Check Windows binary has no VC++ runtime dependency
100 if: contains(matrix.target, 'windows')
.github/workflows/release-npm.yml
+1 -1
@@ -101,7 +101,7 @@ jobs:
101
102 - name: Build binary
103 shell: bash
104 - run: cargo build --locked --release --target ${{ matrix.build.TARGET }}
104 + run: cargo build --locked --release -p sigit --target ${{ matrix.build.TARGET }}
105
106 - name: Install Node
107 uses: actions/setup-node@v6
.github/workflows/release-nuget.yml
+1 -1
@@ -90,7 +90,7 @@ jobs:
90
91 - name: Build binary
92 shell: bash
93 - run: cargo build --locked --release --target ${{ matrix.build.TARGET }}
93 + run: cargo build --locked --release -p sigit --target ${{ matrix.build.TARGET }}
94
95 - name: Prepare artifact
96 shell: bash
Cargo.toml
+7 -17
@@ -1,27 +1,17 @@
1 -[package]
2 -name = "sigit"
1 +[workspace]
2 +resolver = "3"
3 +members = ["crates/cli"]
4 +
5 +[workspace.package]
6 version = "1.6.7"
7 edition = "2024"
5 -description = "siGit Code — ACP-compatible AI coding agent. Sí, git."
6 -documentation = "https://github.com/getsigit/sigit"
8 license = "Apache-2.0"
9 repository = "https://github.com/getsigit/sigit"
10 homepage = "https://sigit.si"
10 -readme = "README.md"
11 -keywords = ["sigit", "cli", "ai", "coding-agent", "llm"]
12 -categories = ["command-line-utilities"]
11 +documentation = "https://github.com/getsigit/sigit"
12 authors = ["Seto Elkahfi <seto@sigit.si>", "paydii <193906237+paydii@users.noreply.github.com>", "keypair34 <216233964+keypair34@users.noreply.github.com>"]
14 -# `src/bin/` holds test-only helper binaries (the MCP stdio stub used by the
15 -# integration tests). Excluding it keeps them out of the published crate, so
16 -# `cargo install sigit` installs exactly one binary. The integration test that
17 -# spawns the stub goes with it, or the packaged crate's tests wouldn't compile.
18 -exclude = ["src/bin/", "tests/mcp_stdio.rs"]
19 -
20 -[[bin]]
21 -name = "sigit"
22 -path = "src/main.rs"
13
24 -[dependencies]
14 +[workspace.dependencies]
15 # ACP protocol SDK
16 agent-client-protocol = { version = "2.2", features = ["unstable_session_fork"] }
17
crates/cli/Cargo.toml new
+48
@@ -0,0 +1,48 @@
1 +[package]
2 +name = "sigit"
3 +version.workspace = true
4 +edition.workspace = true
5 +description = "siGit Code — ACP-compatible AI coding agent. Sí, git."
6 +documentation.workspace = true
7 +license.workspace = true
8 +repository.workspace = true
9 +homepage.workspace = true
10 +readme = "../../README.md"
11 +keywords = ["sigit", "cli", "ai", "coding-agent", "llm"]
12 +categories = ["command-line-utilities"]
13 +authors.workspace = true
14 +# `src/bin/` holds test-only helper binaries (the MCP stdio stub used by the
15 +# integration tests). Excluding it keeps them out of the published crate, so
16 +# `cargo install sigit` installs exactly one binary. The integration test that
17 +# spawns the stub goes with it, or the packaged crate's tests wouldn't compile.
18 +exclude = ["src/bin/", "tests/mcp_stdio.rs"]
19 +
20 +[[bin]]
21 +name = "sigit"
22 +path = "src/main.rs"
23 +
24 +[dependencies]
25 +agent-client-protocol.workspace = true
26 +ed-mcp.workspace = true
27 +onde.workspace = true
28 +tokio.workspace = true
29 +tokio-util.workspace = true
30 +futures.workspace = true
31 +ratatui.workspace = true
32 +crossterm.workspace = true
33 +anyhow.workspace = true
34 +libc.workspace = true
35 +log.workspace = true
36 +tracing-subscriber.workspace = true
37 +serde.workspace = true
38 +serde_json.workspace = true
39 +toml.workspace = true
40 +async-trait.workspace = true
41 +regex.workspace = true
42 +reqwest.workspace = true
43 +uuid.workspace = true
44 +rpassword.workspace = true
45 +base64.workspace = true
46 +rand.workspace = true
47 +sha2.workspace = true
48 +url.workspace = true
crates/cli/src/account.rs renamed
crates/cli/src/backend.rs renamed
crates/cli/src/bin/mcp_stdio_stub.rs renamed
crates/cli/src/browser_auth.rs renamed
crates/cli/src/chat.rs renamed
crates/cli/src/client_fs.rs renamed
crates/cli/src/client_terminal.rs renamed
crates/cli/src/commands.rs renamed
crates/cli/src/credentials.rs renamed
crates/cli/src/frontmatter.rs renamed
crates/cli/src/harness_markup.rs renamed
crates/cli/src/headless.rs renamed
crates/cli/src/hooks.rs renamed
crates/cli/src/inline_tool_calls.rs renamed
crates/cli/src/instructions.rs renamed
crates/cli/src/main.rs renamed
crates/cli/src/mcp.rs renamed
crates/cli/src/models.rs renamed
crates/cli/src/permissions.rs renamed
crates/cli/src/provider.rs renamed
crates/cli/src/repetition.rs renamed
crates/cli/src/session_store.rs renamed
crates/cli/src/settings.rs renamed
crates/cli/src/setup.rs renamed
crates/cli/src/skills.rs renamed
crates/cli/src/subagents.rs renamed
crates/cli/src/tools.rs renamed
crates/cli/src/workspace.rs renamed
crates/cli/tests/acp_audio.rs renamed
crates/cli/tests/acp_client_fs.rs renamed
crates/cli/tests/acp_client_terminal.rs renamed
crates/cli/tests/acp_concurrent_sessions.rs renamed
crates/cli/tests/acp_elicitation.rs renamed
crates/cli/tests/acp_embedded_context.rs renamed
crates/cli/tests/acp_endpoint_errors.rs renamed
crates/cli/tests/acp_error_codes.rs renamed
crates/cli/tests/acp_harness_markup.rs renamed
crates/cli/tests/acp_images.rs renamed
crates/cli/tests/acp_logout.rs renamed
crates/cli/tests/acp_multi_root.rs renamed
crates/cli/tests/acp_permissions.rs renamed
crates/cli/tests/acp_session_list.rs renamed
crates/cli/tests/acp_session_load.rs renamed
crates/cli/tests/acp_stop_reasons.rs renamed
crates/cli/tests/acp_tool_call_name.rs renamed
crates/cli/tests/acp_tool_stdin.rs renamed
crates/cli/tests/headless_mode.rs renamed
crates/cli/tests/mcp_stdio.rs renamed
crates/cli/tests/version_flag.rs renamed
pypi/pyproject.toml
+1 -1
@@ -37,6 +37,6 @@ Repository = "https://github.com/getsigit/sigit"
37 Issues = "https://github.com/getsigit/sigit/issues"
38
39 [tool.maturin]
40 -manifest-path = "../Cargo.toml"
40 +manifest-path = "../crates/cli/Cargo.toml"
41 bindings = "bin"
42 strip = true