Defaults are sensible, so a config file is optional. When you want to pin
choices, Roteiro merges two layers: a per-user ~/.roteiro/config.toml and a
per-project roteiro.toml at the repository root (project wins). roteiro config prints the effective, merged values and labels which layer set each one.
One key inverts that: [remote] enabled, which opens Roteiro's only egress
path, may be switched off by the committed project file but never on — see
below.
# roteiro.toml — every key is optional; shown with its default
[models] # unset ⇒ offline defaults; `roteiro config` shows what each surface resolved to
embedding = "bge-small-en-v1.5-gguf" # embedding model for `infer`
generative = "qwen3-0.6b" # model for `spec draft` / serving / Ask
vision = "smolvlm-500m-gguf" # image description for `media build`
audio = "voxtral-mini-3b" # speech transcription for `media build`
ocr = "ocrs-text" # literal image text for `sync`
[remote] # the optional remote model tier — OFF by default, and the one key
enabled = false # whose precedence inverts: roteiro.toml may set this false for
# everyone, and may NEVER set it true. Only your own
# ~/.roteiro/config.toml can grant it — and a run still needs
# --allow-remote. Both are required; neither alone is enough.
endpoint = "https://…/v1/chat/completions" # ordinary key: either layer may choose the destination
model = "vendor/model-name" # a vendor model string is a mutable pointer, so anything recorded
# from this tier is `vendor_asserted`, never digest-pinned
[ingest] # which blob content `sync` embeds
prose = true # Markdown / plain text bodies
pdf = true # PDF text (needs the pdf-text feature)
ocr = true # image OCR (needs the image-ocr feature)
vision = true # image description (needs image-vision)
[paths] # which repository paths the scan reads at all
exclude = ["raw/**"] # not in the graph at all — no node, bytes never read
opaque = ["manifest/**"] # a file node and nothing else — no content, no config
# keys, no markers, not an ADR however it declares itself
[infer]
min_confidence = 0.4 # cosine-similarity floor for a suggestion
top_k = 5 # max suggestions per node
[duplicates]
min_similarity = 0.9 # floor for a near-duplicate pair
limit = 50 # max pairs reported
[serve] # the network server (roteiro serve): /v1 + graph API + UI
addr = "127.0.0.1:8017" # bind address/port (CLI --addr overrides)
scope = "all" # what to serve: here (default) | all | workspace <NAME> | bundle <PATH>
models = ["qwen3-0.6b"] # which installed models to expose (default: all)
tools = true # expose graph tools to the model
tls_cert = "/etc/roteiro/tls/fullchain.pem" # in-process HTTPS (with tls_key); omit both for plain HTTP
tls_key = "/etc/roteiro/tls/privkey.pem" # PEM private key paired with tls_cert
[mcp] # the MCP graph server (roteiro mcp, serve --mcp)
tools = ["query", "quality"] # restrict the advertised surface — a class, a tool name, or "read-only"
[workspace] # host many repos from one server (ADR-0008)
roots = ["~/code"] # scanned ONE LEVEL deep — each immediate child that is a repo
repos = [] # …or list explicit repo paths (any depth)
include_worktrees = false # host linked git worktrees a root finds (default false)
[[workspaces]] # several named workspaces (a hub + its spokes)
name = "payments" # select with --workspace-name, or by cwd
repos = ["~/work/app", "~/work/deploy"]
[standalone] # repos with no cross-repo links (each its own graph)
repos = ["~/code/dotfiles"]
[[links]] # authored cross-repo link (ADR-0009)
to = "app::sym:rust:src/config.rs#ServeConfig" # a <project>::<key> in another repo
from = "file:k8s/deployment.yaml" # optional local anchor in this repo
[pins] # map a deployed artifact → a hub git ref
app = "release-{tag}" # image app:1.4 → git ref release-1.4
[[links]] and [pins] tables belong to Roteiro's cross-repo
layer — see Cross-repo: a hub and its spokes.[mcp] tools narrows and never widens. Naming a
tool declines every tool not named, so the layers intersect: --tools,
a project roteiro.toml and your ~/.roteiro/config.toml each may narrow the
surface and none may widen it — a committed project file cannot restore a tool you removed. An
unknown name, or a restriction leaving nothing to serve, refuses to start rather than quietly
advertising everything. See ADR-0007 v1.5.query,
quality, security and sandbox are accepted wherever a tool
name is, and a hand-written list of names is exactly what goes stale when a tool is added. Every
advertised tool costs tokens on every turn whether the session could reach it or not, and
security + sandbox are roughly two fifths of it. The default is still
every class. Whatever you leave out, list_tool_classes stays advertised and says so
— see MCP mode.roots is scanned one level deep. Each
immediate child of a root that holds a .git becomes a project, plus the root
itself if it is one — never recursively. A repo at ~/code/<org>/<repo> is
not found: name each <org> directory as its own root, or list the repos
explicitly. roteiro serve, roteiro mcp and the
explorer print what each root offered beside the project count,
so a near-empty workspace says why.roots scan walks past git worktrees. A
linked worktree — a second checkout made by git worktree add, whose .git
is a file pointing into the main checkout — is not an independent project, and hosting it presents
one repository as several peers at several revisions: coupling, hotspot and debt figures count its
symbols once per checkout, and a workspace-scoped question can retrieve one file on three branches
as three independent sources. roteiro serve, roteiro mcp and the explorer
say how many each root walked past, beside the project count.
Two ways to host one anyway. repos = ["…/my-worktree"] names it directly and always
works — an explicit path is never discovered, so the rule does not apply to it; that is the
one-off case. include_worktrees = true sets the rule for one workspace's roots, for a
directory that holds a pool of them on purpose. The key sits beside roots in
[workspace], in any [[workspaces]] entry and in
[standalone], and belongs to that group alone — two workspaces may name the same root
and answer differently.
Standing inside a worktree is different. Skipping applies to
discovery, not to selection: cd <worktree> && roteiro serve
serves that worktree, scoped to it alone and at its own revision, because being there is as
explicit an act as naming it. The startup line says so, and names the repository the worktree
belongs to and the branch it is checked out on — or reports at a detached HEAD when it
has no branch, which git worktree add --detach and adding at a tag both produce. A
worktree presented as though it were the repository is the same silent misrepresentation, from the
other side, and naming a branch it does not have would be that misrepresentation one level down. Sibling repositories beside
the worktree are not picked up: --scope here means this checkout.
A worktree whose main checkout is outside every root is skipped too. Nothing else in the scan holds that content, so this is the one case where the rule drops something that used to be graphed; the note names it and either escape hatch restores it. See issue #837.
[workspace] is the default workspace; add [[workspaces]] for several named hub-and-spoke groups and [standalone] for repos that stand alone. roteiro links, roteiro serve, roteiro mcp and the explorer pick one with --workspace-name (or the workspace containing your current directory).[ingest] class off changes what sync
extracts, so affected blobs are re-processed on the next run — the content-addressed
cache folds the ingestion config into its key. [paths] is folded into the same
key, for the same reason.[paths] removes the node; [debt] ignore
only mutes the report. They look alike and are not: an ignored path still has its
nodes in the store, so search, explain and roteiro export
still see it. A path under exclude has none, and one under opaque has
only its identity — path, size, blob id — with nothing derived from what the bytes say.
Three states, because two of the requests differ: a committed data manifest
usually wants opaque (still findable, so a missing source is detectable, but not
mined into hundreds of "configuration settings" and false debt markers), while a corpus of source
documents you ingest another way wants exclude. Both are consulted by every reader,
including the one that decides whether a markdown file is one of your ADRs — so a downloaded
document carrying type: adr under a declared path is not adopted as your decision.
Nothing is excluded by default, including raw/: a built-in would
drop a directory out of your graph on upgrade without saying so.