chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:33:42 +08:00
commit a06f331eb8
3186 changed files with 689843 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# Gortex evaluation methodology
This directory documents the agent-graded self-eval methodology
gortex uses to measure its own real-world quality. The headline:
we evaluate gortex with the agents that actually use it (Claude
Sonnet 4.6, GPT 5.4, Copilot CLI), on three real codebases, across
five task categories, with a documented judge prompt and an
explicit "report negative deltas" requirement.
This is methodology only — no published numbers live here. Numbers
land in [`BENCHMARK.md`](../../BENCHMARK.md) once we run the
methodology against a tagged build.
## Contents
- [`methodology.md`](methodology.md) — the protocol: agents, tasks,
classifiers, bias checks, negative-delta requirement.
- [`judge-prompt.md`](judge-prompt.md) — the exact judge prompt
template (reproducibility: change the prompt → bump the rev).
- [`task-set.md`](task-set.md) — the 15 seed tasks (3 per category)
with canonical answers.
- [`run.md`](run.md) — operational recipe: how to invoke the
harness, where outputs land, how to publish results.
## Why this exists
A retrieval / code-intelligence engine can ship excellent
substrate (graph, MCP tools, USD savings) and still produce
agents that prefer Read/Grep when given the choice. The eval
methodology answers "with our tools available, does the model
actually use them, and does it produce better answers than it
would without?" That's the real test — not benchmark NDCG@10
on synthetic queries.
The methodology has three properties competitors typically lack:
1. **Multi-agent**: same task set scored against ≥3 distinct
agent / model combinations so a result isn't a quirk of one
provider.
2. **Bias-of-prompt check**: every task runs with both the
default agent prompt AND a deliberately worse prompt (the
"ablation prompt"); a methodology that only looks good on the
tuned prompt is honest-flagged.
3. **Negative-delta requirement**: per-task scoring uses an
(a)/(b)/(c) classifier that distinguishes "gortex helped",
"no measurable difference", "gortex hurt". The published
summary MUST cite both ends — hiding the negatives gets the
methodology disqualified.
The substrate is already shipped (`gortex eval` substrate +
`eval/` Python harness); this directory makes it reproducible
end-to-end without an oral tradition.
+117
View File
@@ -0,0 +1,117 @@
# Judge prompt
The exact prompt the judge model receives. Reproducibility:
if you change a word, bump the `judge_prompt_revision` field in
the published results.
**Current revision: 1 (2026-05-18)**
## System prompt
```
You are evaluating two answers to the same software-engineering
task. Both answers were produced by the same coding agent
working on the same codebase, but one had access to the gortex
MCP tool surface ("WITH") and the other did not ("WITHOUT").
Your job is to assign ONE of three labels:
(a) WITH was measurably better than WITHOUT
— more accurate factually, more complete (covers the
relevant code paths the task asks about), fewer
hallucinations (no claims about symbols that don't exist
/ behaviours that aren't in the code), OR substantially
fewer tokens to reach the same answer quality
(b) WITH and WITHOUT were roughly equivalent
— no meaningful difference in accuracy, completeness, or
cost; either both are good or both are mediocre
(c) WITH was measurably worse than WITHOUT
— less accurate, more confused (e.g. tool noise drowned
the answer), OR noticeably more tokens for the same
answer quality
Always pick exactly one label. If you are uncertain between (a)
and (b), or between (c) and (b), default to (b) — uncertainty is
not a marketing argument.
You will be given:
- The task prompt (what the user asked)
- The canonical answer (what an expert engineer would say)
- The WITH answer
- The WITHOUT answer
- The token cost and wall-clock time of each run
Output JSON:
{
"label": "(a)|(b)|(c)",
"reasoning": "<1-3 sentences explaining the label>",
"facts_correct_with": "<count|fraction|brief>",
"facts_correct_without": "<count|fraction|brief>",
"hallucinations_with": "<count|fraction|brief>",
"hallucinations_without": "<count|fraction|brief>",
"cost_ratio": "<with_tokens / without_tokens, e.g. 0.6 or 1.4>",
"uncertainty": "low|medium|high"
}
Be terse in `reasoning`. The scoring summary aggregates labels,
not narrative; reasoning exists for spot-checks, not for
attempting to influence the headline.
```
## User-message template
```
TASK:
{task_prompt}
CANONICAL ANSWER (for reference):
{canonical_answer}
WITH (gortex MCP available):
[{with_token_cost} tokens · {with_wall_clock}]
{with_answer}
WITHOUT (no gortex MCP):
[{without_token_cost} tokens · {without_wall_clock}]
{without_answer}
Label this comparison.
```
## Judge model selection
The default judge is **Claude Sonnet 4.6**
(`claude-sonnet-4-20250514`). Two reasons:
1. **Different family from the WITH agent.** Sonnet 4.6 judging
Sonnet 4.6's own answers is self-eval bias. Run the judge as
a DIFFERENT model from the agent under test (e.g. GPT 5.4
judging Sonnet 4.6's answers and vice versa).
2. **Cheap enough to re-run.** A 15-task × 6-runs comparison is
90 judge invocations per session; Sonnet 4.6 makes the run
cost negligible compared to the agent runs themselves.
For the published results we run the judge twice with two
different models (Sonnet + GPT 5.4) and report agreement /
disagreement counts. Disagreement >20% is the methodology
trigger: re-curate the seed tasks or pick a third judge.
## Anti-gaming notes
- The judge **never sees the agent identity** (no "WITH agent
was Claude Sonnet 4.6"). Identity bias is real; the WITH /
WITHOUT split is the only signal we want.
- Token counts are computed before the judge sees them
(`internal/tokens` cl100k_base) so the agent can't lie about
cost.
- Canonical answers are written by a human expert PER TASK
before any agent runs. Writing them after seeing agent
outputs is methodology fraud.
- Per-task token / wall-clock budgets are identical WITH and
WITHOUT. A run that exceeds the budget is scored as "no
answer" (not (c)).
+111
View File
@@ -0,0 +1,111 @@
# Methodology
The protocol below produces reproducible, agent-graded scoring of
gortex's real-world effect on coding tasks. Sticking to the
protocol means anyone with the harness + a model API key can
reproduce the numbers and dispute them.
## 1. Task categories (5)
| Category | What it measures | Example |
|----------|------------------|---------|
| **Architectural explanation** | "Why does this codebase have N services" — graph / community structure understanding | "Walk me through how the indexer pipeline processes a new file." |
| **Refactor safety** | "Rename / move / extract — what breaks?" — impact-analysis driven | "Rename `Indexer.Index` to `Indexer.IndexRoot` across the repo. List every caller that needs updating." |
| **Bug localization** | "Given this failure, where's the root cause?" — call-chain + dataflow | "A panic in `internal/savings/store.go::flushLocked`. Trace the conditions that reach it." |
| **Impact analysis** | "If I change X, what tests should I run?" — `get_test_targets` + `verify_change` | "I'm about to change the signature of `tokens.Count`. List the test files that need re-running." |
| **Contract extraction** | "What's the public API surface of this package?" — `contracts list` driven | "List every exported function / method / type in `internal/savings/`, with one-line summaries." |
Each task is realistic — drawn from actual sessions, not invented
synthetic prompts. The seed task set in [`task-set.md`](task-set.md)
ships 3 tasks per category × 5 categories = 15 tasks.
## 2. Agent / model matrix
The same task set runs against each of:
1. **Claude Sonnet 4.6** via the Anthropic API (`claude-sonnet-4-20250514`)
2. **GPT 5.4** via OpenAI (`gpt-5-2025-08`)
3. **Copilot CLI** via the GitHub CLI extension
For each agent × task combination, **two runs**:
- **WITH gortex MCP** — the agent has access to the full gortex
tool surface (`smart_context`, `search_symbols`,
`get_symbol_source`, `verify_change`, …)
- **WITHOUT gortex MCP** — the agent has only its default tool
set (typically `Read`, `Grep`, `Bash`)
So per task: 6 runs (3 agents × 2 modes). Per task set: 90 runs.
Per category: 18 runs.
## 3. Bias-of-prompt check
Each WITH-gortex run is executed twice:
- **default prompt** — the system prompt gortex ships in
`internal/agents/instructions.go` (the same one the production
`gortex init` writes to every agent's config)
- **ablation prompt** — the same prompt with every "prefer
gortex tools" steering line removed
If the published headline only shows the default-prompt number,
the methodology is incomplete. Always publish both, and call out
the delta — that's the "we're not just measuring the prompt"
test.
## 4. (a) / (b) / (c) classifier
A judge model (default Claude Sonnet 4.6 — see
[`judge-prompt.md`](judge-prompt.md)) scores each per-task
WITH-vs-WITHOUT comparison with one of three labels:
- **(a) gortex helped** — the WITH run produced a measurably
better answer (more accurate, more complete, fewer
hallucinations, or substantially fewer tokens)
- **(b) no measurable difference** — answers are roughly
equivalent in quality and cost
- **(c) gortex hurt** — the WITH run was worse (less accurate,
more confused by the tool surface, or noticeably more tokens
for the same answer)
The published summary MUST report all three counts. A
"(a)=12 / (b)=2 / (c)=1" result is honest; "12 wins" is not.
## 5. Negative-delta requirement
Negative deltas (any (c) result) are **required** in the
published summary, with the per-task breakdown linkable. The
explicit requirement is the methodology's anti-survivorship
mechanism: a methodology that buries (c) cases isn't measuring
real-world quality, it's measuring marketing.
If a published run reports zero (c) results across all 15 tasks,
that's a red flag — either the judge is biased or the seed set
is over-fit. Re-run with a different judge model and at least
3 additional tasks per category before publishing.
## 6. Scoring envelope
Per-task token + wall-clock budgets are the same WITH and
WITHOUT (typically 50k tokens / 5 minutes per task). A run that
exceeds the budget scores as "no answer" — not as (c). This
keeps the comparison about *quality* of answers, not endurance.
Per-task cost is reported separately so a published row can show
"answer (a) at 1.2× the WITHOUT cost"; (a) at 5× the cost is
honestly weaker than (a) at 0.8× the cost.
## 7. What we don't measure here
- **Benchmark NDCG / recall** — covered by `bench/baselines/` and
`bench/token-efficiency/`. Different axis: those measure
retrieval quality independently of agent / model; this
methodology measures real agent behaviour.
- **SWE-bench resolve rate** — covered by `BENCHMARK-SWE.md`.
Multi-day GPU compute; published separately when an operator
runs it.
- **Performance** — covered by `bench/perf/`. Indexer / query /
impact latency, not answer quality.
Together the three axes (retrieval / agent behaviour / system
perf) form gortex's published-quality envelope.
+141
View File
@@ -0,0 +1,141 @@
# How to run
Operational recipe for executing the full methodology against a
tagged build and publishing the results.
## Prerequisites
- `gortex` binary built from the tagged commit
- `python3 -m pip install -r eval/requirements.txt` (Python
harness deps; existing file)
- API keys: `ANTHROPIC_API_KEY` (for Sonnet 4.6),
`OPENAI_API_KEY` (for GPT 5.4), and a working `gh copilot`
installation (for Copilot CLI). At least one is enough for a
partial run.
- A working corpus checkout (default: the gortex repo itself)
## End-to-end
```sh
# 1) Tag the build — every published result cites this SHA.
git rev-parse HEAD > eval/results/$(date +%Y%m%d)/HEAD.sha
# 2) Run the full matrix: 15 tasks × 3 agents × 2 modes × 2 prompts.
# Estimated wall clock: ~3-6 hours per agent.
gortex eval run \
--task-set docs/04-evaluation/task-set.md \
--judge-prompt docs/04-evaluation/judge-prompt.md \
--agents sonnet-4.6,gpt-5.4,copilot \
--corpus . \
--out eval/results/$(date +%Y%m%d)/ \
--max-task-tokens 50000 \
--max-task-seconds 300
# 3) Aggregate per-task scores into the summary table.
python3 eval/scripts/aggregate.py \
--workdir eval/results/$(date +%Y%m%d)/ \
--judges sonnet-4.6,gpt-5.4 \
--out eval/results/$(date +%Y%m%d)/summary.md
# 4) Spot-check 5 random tasks per category by hand BEFORE
# publishing. The judge is good, not infallible.
python3 eval/scripts/spotcheck.py \
--workdir eval/results/$(date +%Y%m%d)/ \
--sample 5 \
--out eval/results/$(date +%Y%m%d)/spotcheck.md
# 5) Promote into BENCHMARK.md (manual edit; doc owner).
$EDITOR BENCHMARK.md
```
## What lands on disk
```
eval/results/<date>/
├── HEAD.sha # tagged commit
├── summary.md # the published table
├── spotcheck.md # manual review notes
├── disagreement.md # judge-vs-judge disagreement
├── per-task/
│ ├── 1.1-indexer-walkthrough/
│ │ ├── sonnet-4.6-with-default.json
│ │ ├── sonnet-4.6-without-default.json
│ │ ├── sonnet-4.6-with-ablation.json
│ │ ├── sonnet-4.6-without-ablation.json
│ │ ├── gpt-5.4-with-default.json
│ │ └── ...
│ ├── 1.2-community-detection/
│ └── ...
└── judge-runs/
├── sonnet-4.6-judging/
└── gpt-5.4-judging/
```
Every per-task JSON contains: task prompt, canonical answer,
agent answer, token cost, wall clock, tools called (count +
list), and (if judged) the judge's label + reasoning + agreement
between judges.
## What to publish
In `BENCHMARK.md`, add a section like:
```markdown
## Agent-graded evaluation
**Last run: 2026-MM-DD** · agents: Sonnet 4.6 / GPT 5.4 /
Copilot CLI · judge: Sonnet 4.6 + GPT 5.4 (agreement 87%)
| Category | (a) gortex helped | (b) no difference | (c) gortex hurt |
|----------|------------------:|------------------:|----------------:|
| Architectural explanation | 6 | 1 | 2 |
| Refactor safety | 7 | 2 | 0 |
| Bug localization | 5 | 2 | 2 |
| Impact analysis | 8 | 1 | 0 |
| Contract extraction | 6 | 3 | 0 |
| **Total** | 32 | 9 | 4 |
- Default-prompt vs ablation-prompt delta: +2 (a) / 0 (b) / -1 (c)
— gortex prompt steering helps but isn't load-bearing.
- (c) cases written up in `eval/results/2026-MM-DD/c-cases.md`
— every loss has a public post-mortem.
```
**Required**: cite the (c) count, link to the (c)
post-mortems, and call out the prompt-bias delta. A publication
that hides (c) results is non-compliant with this methodology
and should not be referenced as a benchmark.
## Cost envelope
A single full run (15 tasks × 3 agents × 2 modes × 2 prompts =
180 agent runs + ~360 judge invocations) costs roughly:
- Anthropic API: ~$15-30 (Sonnet 4.6 agent + Sonnet 4.6 judge)
- OpenAI API: ~$10-25 (GPT 5.4 agent + GPT 5.4 judge)
- Copilot CLI: subscription-included
Total: ~$25-55 per full run. Run quarterly + on every major
version bump.
## Partial-run modes
When you only have one API key:
```sh
# Just Sonnet 4.6 (cheapest path)
gortex eval run --agents sonnet-4.6 --task-set ...
# Just one task category (smoke before the full run)
gortex eval run --agents sonnet-4.6 \
--task-set docs/04-evaluation/task-set.md \
--categories "Refactor safety"
# Just the WITH mode (compare absolute quality across agents)
gortex eval run --agents sonnet-4.6,gpt-5.4,copilot \
--modes with
```
Partial runs are useful for iteration but **don't publish
partial-run numbers as benchmarks** — the methodology requires
the full matrix.
+249
View File
@@ -0,0 +1,249 @@
# Seed task set
15 tasks (3 per category) the published methodology runs against
on each release. Each task carries a **canonical answer** the
judge uses as ground truth — written by a human expert before any
agent runs. Adding tasks: append to the appropriate section,
write the canonical answer first, only THEN run the harness.
The task corpus is the gortex repo itself (eats its own dog
food). Extending to other corpora means writing a new
`task-set-<repo>.md` with the same structure and pointing the
harness at it via `--task-set`.
---
## Category 1: Architectural explanation (3 tasks)
### 1.1 Indexer pipeline walkthrough
**Prompt**: "Walk me through how the gortex indexer processes a
single new source file. Name the packages it crosses and the
order of operations."
**Canonical answer (~150 words)**:
1. `indexer.Indexer.IndexCtx(root)` walks `root` via
`internal/indexer/scan.go::scanFiles`, producing one
`parseJob` per matching file.
2. Each job is dispatched to `parser.Registry` (the language
plugin matching the file extension) via
`internal/parser/treesitter.go`.
3. The parser extracts symbols / edges as
`parser.ExtractionResult`; `Indexer.processExtraction` writes
them into the `graph.Graph` and accumulates incoming-edge
tracking for the next phase.
4. `Indexer.buildSearchIndex` (BM25 / Bleve) + `idx.embedder`
(if set) populate the search backends.
5. Semantic enrichment (`internal/semantic`) runs LSP / SCIP
providers in parallel; resolved edges get
`Origin=lsp_resolved` for tier filtering.
6. Returns `IndexResult` with file / node counts + duration.
### 1.2 Community detection role
**Prompt**: "What does the `internal/analysis/communities.go`
package do, and how does it integrate with `smart_context`?"
**Canonical answer (~80 words)**:
Implements Leiden community detection on the graph. Output is a
`CommunityResult` mapping node ID → community ID + per-community
cohesion score. `smart_context` reads it via the Context.CommunityOf
hook in the rerank pipeline: candidates sharing the session's
home community get a locality boost. Recompute is triggered on
graph re-warm; the result is cached in `Server.analysis`.
### 1.3 Daemon dispatch path
**Prompt**: "How does a MCP request hit the daemon and get
routed back to a per-session response?"
**Canonical answer (~120 words)**:
A `gortex mcp` client opens a stdio JSON-RPC pipe; the daemon
dispatcher (`internal/daemon/dispatcher.go::MCPDispatcher`)
parses frames, looks up or creates a per-session `*mcp.Server`
via `Sessions.GetOrCreate`, and forwards. The Server holds a
shared `*graph.Graph` + per-session `tokenStats` +
`sessionState` (notes, frecency, etc.). Responses go back the
same pipe with the matching JSON-RPC id. Cross-session memory
(notes / memories / feedback) is workspace-scoped via the
session's resolved cwd → workspace ID.
---
## Category 2: Refactor safety (3 tasks)
### 2.1 Rename a public method
**Prompt**: "I'm renaming `Indexer.Index` to `Indexer.IndexRoot`.
List every caller that needs updating."
**Canonical answer** (verified via `gortex find_usages
gortex/internal/indexer/indexer.go::Indexer.Index`):
- `gortex/internal/indexer/multi.go::MultiIndexer.IndexRepo`
- `gortex/cmd/gortex/eval_recall.go::runEvalRecall`
- `gortex/bench/perf/runner.go::runRepo` (introduced in the L5
bench commit)
- `gortex/bench/token-efficiency/runner.go::indexRepoForBench`
- All `*_test.go` files calling `idx.Index(...)` (count: see
`find_usages` output)
### 2.2 Change a signature
**Prompt**: "I want to add a `context.Context` to `Engine.SearchSymbols`.
List every caller and what they'll need to change."
**Canonical answer** (verified via `gortex verify_change`): see
`find_usages` output; ~12 callers across `cmd/gortex/`,
`internal/mcp/`, `bench/perf/`, `bench/token-efficiency/`. Each
needs to pass through the request's context (most have one
available; some need to use `context.Background()` for now).
### 2.3 Remove a deprecated field
**Prompt**: "I want to remove the `Edge.LegacyConfidence` field.
What breaks?"
**Canonical answer**: the field doesn't exist; the canonical
answer is "no such field; nothing breaks". A passing agent
should say so explicitly, not hallucinate impact.
---
## Category 3: Bug localization (3 tasks)
### 3.1 Panic trace
**Prompt**: "We have a panic in `internal/savings/store.go` at
`flushLocked`. What conditions could cause it?"
**Canonical answer**: `flushLocked` runs under `s.mu`. Panic
paths: (a) flock acquisition fails (file system permission /
disk full → wrapped, not panicked); (b) atomic-rename fails on
some filesystems → returns error; (c) gob encode fails on
unexpected map shape → would panic in `encoder.Encode`. Most
likely candidate: corruption of the in-memory `s.file.PerRepo`
map by a goroutine that bypassed the mutex.
### 3.2 Wrong rank order
**Prompt**: "After my recent rerank-signal change, the top
result for `validateToken` is now a test file instead of the
real implementation. What signal probably regressed?"
**Canonical answer**: `path_penalty` (the test-file demotion).
If a path matching the test-file regex stopped getting the ×0.3
multiplier, test files would no longer be demoted. Check
`signals_path_penalty.go::classifyPathPenalty` and the regex
patterns in `pathRETest`.
### 3.3 Cross-repo missing edges
**Prompt**: "After indexing a multi-repo workspace, calls from
`web` to `cloud_web` aren't showing up in `get_callers`. What
gives?"
**Canonical answer**: cross-repo resolution depends on
`internal/resolver/cross_repo.go::CrossRepoResolver`; it only
runs when `MultiIndexer` has indexed all repos in the same
workspace. Most likely cause: one of the repos was indexed in
isolation (not via `gortex track`) so the resolver never saw
the cross-repo `import` edges.
---
## Category 4: Impact analysis (3 tasks)
### 4.1 Touch a hot path
**Prompt**: "I'm about to change the signature of
`tokens.Count`. List the test files that need re-running."
**Canonical answer**: use `gortex get_test_targets
internal/tokens/tokens.go::Count`. Expected hits include
`internal/tokens/tokens_test.go`, every test in
`internal/mcp/` that calls `tokenStatsFor`, the savings tests,
the bench harnesses' test files.
### 4.2 Blast radius
**Prompt**: "If I introduce a bug in `Graph.AllNodes`, what's
the worst-case downstream effect?"
**Canonical answer**: AllNodes is called by basically every
analyzer; impact is "the whole codebase". A canonical answer
quantifies it via `gortex explain_change_impact` (depth=3) and
notes which communities / processes are at risk.
### 4.3 Cycle detection
**Prompt**: "Would adding an `imports` edge from
`internal/search/rerank` to `internal/search` create a cycle?"
**Canonical answer**: yes if `internal/search` already imports
`internal/search/rerank` (parent-child importing child). Verify
via `gortex analyze kind=would_create_cycle from_id=...
to_id=...`. Currently `internal/search/hybrid.go` imports
`internal/search/rerank` for the auto-α blend, so adding the
reverse would form a cycle.
---
## Category 5: Contract extraction (3 tasks)
### 5.1 Public API of a package
**Prompt**: "List every exported function / method / type in
`internal/savings/`, with one-line summaries."
**Canonical answer**: use `gortex contracts list
internal/savings`. Expected ~12 symbols:
`Pricing`, `CostAvoided`, `CostAvoidedAll`, `Store`, `Open`,
`DefaultPath`, `EventsPathFor`, `Store.AddObservation`,
`Store.Snapshot`, `Store.Flush`, `Store.Reset`, `Bucket`,
`Event`, `LoadEvents`, `BarString`, `SavingsPercent`,
`AggregateByTool`, `FilterDay`, `FilterSince`,
`BuildDashboard`. Plus the canonical pricing model constants.
### 5.2 Tool surface of a package
**Prompt**: "What MCP tools does `internal/mcp/tools_savings.go`
register, and what are their parameter contracts?"
**Canonical answer**: pull from
`internal/mcp/tools_savings.go::registerSavingsTools` (or
equivalent). Tool list + per-tool param schema. A passing agent
should produce the exact param names + types.
### 5.3 Configuration surface
**Prompt**: "What `.gortex.yaml` keys does the indexer respect?
Group by required vs optional."
**Canonical answer**: cross-reference `internal/config/config.go`
+ each parser's `RegisterX` call. Required: none (all keys have
defaults). Optional: `index.exclude`, `index.max_file_size`,
`semantic.enable_*`, etc. A passing agent should produce the
list with default values per key.
---
## Curation rules
1. **Canonical answers come first.** Writing them after seeing
agent outputs is methodology fraud — the temptation to
"match" the agent's wording corrupts the ground truth.
2. **One topic per task.** A task that asks two questions splits
the (a)/(b)/(c) signal — judge gives partial credit, which
makes the headline noisy.
3. **Verify with the tools.** Every task's canonical answer
should be reproducible by running gortex tools manually; if
you can't reproduce it, the answer is wrong (or the tool
has a bug worth filing).
4. **Bias toward realistic prompts.** The seed set is drawn
from actual user sessions (anonymized). Synthetic prompts
("explain this complex graph algorithm") aren't what real
users ask.
+110
View File
@@ -0,0 +1,110 @@
# Pull-request actions
The daemon exposes a small set of data-only MCP tools over your repository's
pull requests:
- **`list_prs`** — list a repo's PRs with a one-shot review-state
classification (DRAFT / BASE_MISMATCH / CHANGES_REQUESTED / APPROVED /
STALE / READY), a normalized CI rollup, and per-PR merge blockers.
- **`get_pr_impact`** — map a PR's changed files to the symbols they define,
score PR-level risk across five axes, and group the affected surface by
community and by caller/test file. Set `receipt: true` for a small,
privacy-safe review receipt.
- **`triage_prs`** — rank a repo's open PRs by graph-derived review priority
(highest risk first, deterministic).
These tools are **read-only** — none of them edits code or posts to GitHub.
## Providing a GitHub token to the daemon
The daemon **self-serves** PR data: it pairs a GitHub token with the repo
identity it already indexed, so there is no CLI-versus-daemon auth split and
no dependency on a `gh` CLI login. All it needs is a token in **the daemon's
own environment**.
The token resolves from, in order:
1. `GH_TOKEN`
2. `GITHUB_TOKEN`
Set one of these in the environment the daemon process runs under — not in
your interactive shell, unless the daemon inherits it. For example, when
starting the daemon manually:
```bash
GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx gortex daemon start --detach
```
For a long-running daemon managed by a service supervisor, set the variable in
that unit's environment (e.g. a systemd `Environment=` line, a launchd
`EnvironmentVariables` entry, or your process manager's env file) so it is
present every time the daemon starts.
GitHub Enterprise: when `GITHUB_API_URL` or `GH_HOST` names a non-`github.com`
host, the forge client targets that Enterprise API base automatically. The
same `GH_TOKEN` / `GITHUB_TOKEN` resolution applies.
In CI, a per-PR Action's `GITHUB_TOKEN` is picked up automatically — no extra
configuration is needed.
## When no token is available
If no token is resolvable **and** you did not supply already-fetched data,
each tool degrades gracefully instead of failing:
```json
{ "error": "forge unavailable",
"hint": "set GH_TOKEN (or GITHUB_TOKEN) in the daemon environment" }
```
A GitHub rate-limit is surfaced as a typed degradation carrying the
Retry-After hint:
```json
{ "error": "rate limited", "retry_after_s": 42 }
```
## Skipping the network with caller-supplied data
Every tool accepts an optional caller-supplied data path so an agent (or a CLI
front-end) that already fetched the PR data can avoid a refetch:
- `list_prs` accepts `prs` — a JSON array of already-fetched PR objects.
- `get_pr_impact` accepts `files` — a JSON array of changed file paths.
- `triage_prs` accepts `prs` and/or `files` (a JSON object mapping a PR
number to its changed file paths).
When supplied data is present, the tool classifies / scores it directly and
makes no network call. Triage additionally caches each fetched PR for a short
window so a re-run within the window does not refetch the same PR.
## Per-PR reviewer graph bundle
`gortex prs bundle <number>` writes a self-contained, reviewer-focused slice of
the knowledge graph to a JSON file (`--out`, default `pr-<number>-bundle.json`):
- the PR's **changed files**,
- the graph-joined **impact** — the blast radius, the five-axis PR-risk score,
and a small privacy-safe **review receipt** (risk tier + next-safe-action +
merge-blocker verdict) — taken verbatim from `get_pr_impact`,
- the ranked **reviewer suggestions** from `suggest_reviewers` (CODEOWNERS +
recent authorship + co-change experts).
The bundle is deterministic for an unchanged PR (the changed-file list is
sorted and the JSON is stably indented), so it can be uploaded as a CI artifact
and diffed across runs. The command is daemon-first: the forge supplies the
changed-file set and the daemon joins it against the indexed graph — no second
in-process index. A failing `suggest_reviewers` (missing token / CODEOWNERS)
does not sink the bundle; the reviewers section is simply omitted.
### Wiring it into CI
A ready-to-use GitHub Action template lives at
[`.github/workflows/gortex-pr-review.yml.example`](../../.github/workflows/gortex-pr-review.yml.example).
The `.yml.example` suffix means GitHub does **not** run it as-is — copy it to
`.github/workflows/gortex-pr-review.yml` in your repository to enable it. On
each `pull_request` it builds gortex, starts the daemon, indexes the checked-out
repo, runs `gortex prs bundle <N>`, and uploads the bundle with
`actions/upload-artifact`. It maps the Action-provided `GITHUB_TOKEN` to
`GH_TOKEN` so the daemon self-serves the PR's changed files with no extra secret
configuration.
+355
View File
@@ -0,0 +1,355 @@
# Agent Integrations
`gortex install` (once per machine) and `gortex init` (once per repo)
auto-configure Gortex for every AI coding assistant detected on your
machine. Eighteen adapters ship today.
- `gortex install` writes user-level machinery: `~/.claude.json` MCP,
`~/.claude/skills/gortex-*`, `~/.claude/commands/gortex-*.md`,
`~/.gemini/antigravity/` Knowledge Items, and user-level hooks.
- `gortex init` writes per-repo machinery: `.mcp.json`, per-agent
MCP configs (`.cursor/mcp.json`, `.vscode/mcp.json`, …), repo-local
hooks where supported, per-agent marker-guarded community-routing
blocks, and `.claude/skills/generated/` per-community SKILL.md.
Run `gortex init doctor` to see what's currently configured. Both
commands accept `--agents=<csv>` to constrain setup and
`--agents-skip=<csv>` to exclude an adapter.
## Adapter matrix
| Name | What gets written | Mode | Docs link |
| --------------- | ----------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------- |
| `claude-code` | `.mcp.json`, `.claude/*`, `CLAUDE.md`, `.claude/skills/generated/*`, `~/.claude/skills/gortex-*`, `~/.claude/commands/gortex-*.md`, `~/.claude.json` | both | https://docs.claude.com/en/docs/claude-code/overview |
| `aider` | `.aiderignore` block, `CONVENTIONS.md` communities block | project | https://aider.chat/docs/config/aider_conf.html |
| `antigravity` | `~/.gemini/antigravity/mcp_config.json` + Knowledge Item | user | https://antigravity.google/docs/mcp |
| `cline` | `cline_mcp_settings.json` (per VS Code / Cursor globalStorage), `.clinerules/gortex-communities.md` | both | https://docs.cline.bot/mcp/mcp-overview |
| `codex` | `~/.codex/config.toml` (`[mcp_servers.gortex]` + `SessionStart` / Bash + Gortex MCP read-tool `PreToolUse` / Bash `PostToolUse` hooks), `AGENTS.md` communities block | both | https://developers.openai.com/codex/mcp |
| `continue` | `.continue/mcpServers/gortex.json`, `.continue/rules/gortex-communities.md` | project | https://docs.continue.dev/customize/deep-dives/mcp |
| `cursor` | `.cursor/mcp.json` (project) or `~/.cursor/mcp.json`, `.cursor/rules/gortex-communities.mdc` | both | https://docs.cursor.com/en/context/mcp |
| `gemini` | `.gemini/settings.json` or `~/.gemini/settings.json`, `GEMINI.md` communities block | both | https://geminicli.com/docs/tools/mcp-server/ |
| `hermes` | `~/.hermes/config.yaml` + `profiles/*/config.yaml` (`mcp_servers`), hooks, `~/.hermes/skills/gortex/SKILL.md` | user | https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp |
| `kilocode` | `mcp_settings.json` + `.kilocode/mcp.json`, `.kilocoderules` communities block | both | https://kilo.ai/docs/features/mcp/using-mcp-in-kilo-code |
| `kimi` | `.kimi-code/mcp.json` (project) or `~/.kimi-code/mcp.json` + `~/.kimi-code/config.toml` (`UserPromptSubmit` / `PreToolUse` / `Stop` / `SubagentStart` hooks) | both | https://www.kimi.com/code/docs/en/kimi-code-cli/customization/hooks.html |
| `kiro` | `.kiro/settings/mcp.json` + steering/hooks or user-level | both | https://kiro.dev/docs/mcp/configuration |
| `oh-my-pi` | `.omp/mcp.json` | project | https://github.com/can1357/oh-my-pi/blob/main/docs/mcp-config.md |
| `opencode` | `opencode.json` (or existing `opencode.jsonc`), `AGENTS.md` communities block | project | https://opencode.ai/docs/mcp |
| `openclaw` | `~/.openclaw/openclaw.json` (`mcp.servers.gortex`) | user | https://docs.openclaw.ai/cli/mcp |
| `pi` | `.pi/extensions/gortex/index.ts` (project) or `~/.pi/agent/extensions/gortex/index.ts`; `AGENTS.md` communities block only when `--skills` | both | https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md |
| `vscode` | `.vscode/mcp.json` (`servers` key, 1.102+), `.github/copilot-instructions.md` communities block | project | https://code.visualstudio.com/docs/copilot/chat/mcp-servers |
| `windsurf` | `~/.codeium/mcp_config.json`, `.windsurfrules` communities block | both | https://docs.windsurf.com/plugins/cascade/mcp |
| `zed` | OS-specific `settings.json` (`context_servers`), `.rules` communities block | both | https://zed.dev/docs/ai/mcp |
Mode legend: **project** writes inside the repo (`gortex init` only);
**user** writes under `$HOME` (`gortex install` only); **both** means
the adapter splits: `gortex install` writes the user-level pieces and
`gortex init` writes the repo-level pieces.
Tool-usage guidance (how to prefer graph tools over `Read`/`Grep`) no
longer gets duplicated into every repo. For Claude Code and
Antigravity — the two adapters whose upstream tool exposes a
user-level instructions surface — the guidance lives once per user
(installed by `gortex install`). For the other 13, MCP tool
descriptions carry the teaching. Only codebase-derived community
routing lands in per-repo instructions files.
## Subagent tool propagation
A *subagent* runs in a fresh context window with its own scoped tool
allowlist. Whether a subagent can use Gortex's MCP tools depends
entirely on whether that allowlist names them — a subagent does **not**
automatically inherit every tool the parent has.
- **Claude Code** has a first-class subagent concept with per-subagent
tool allowlists, and Gortex propagates explicitly. `gortex install`
writes two subagent definitions to `~/.claude/agents/`
`gortex-search` (locate / trace / explore) and `gortex-impact`
(blast-radius / verification) — each with an explicit
`tools: mcp__gortex__…` frontmatter listing exactly the graph tools it
needs. The allowlist is **graph-only by construction**: it contains no
`Bash`/`Grep`/`Glob`, so a spawned subagent cannot escape the graph.
A `PreToolUse` Task hook additionally briefs spawned subagents. The
allowlists are validated in CI (`subagents_test.go`) so a tool rename
can't silently drop a subagent's access. Programmatic access:
`claudecode.SubAgentTools(def)` parses an allowlist out of a definition.
- **Session-inheriting hosts** (e.g. `opencode`): subagents inherit the
*parent's* MCP session at the client level, so they see Gortex tools
transparently **only if the host propagates the session to the child**.
This is a client-side behaviour — Gortex configures the MCP server for
the host but cannot force a client to share its session with a subagent.
- **Hosts with no subagent concept**: the question does not arise; the
single agent already holds the configured Gortex tools.
If a subagent reports it cannot see `mcp__gortex__*` tools, check the
subagent's own tool allowlist first — that is where propagation is
decided, not the server.
## Common CLI flags
```
# Machine-wide (run once)
gortex install # user-level MCP, skills, slash commands, hooks
gortex install --start --track # also spawn daemon + track current dir
gortex install --agents=claude-code # constrain to one adapter
gortex install --dry-run --json # plan-only, JSON report
# Per repo (run in each project)
gortex init # interactive: only asks about hooks
gortex init --yes # skip prompt, use defaults
gortex init --analyze # include a richer CLAUDE.md codebase overview
gortex init --no-skills # skip community-routing generation
gortex init --skills-min-size 5 --skills-max 10
gortex init --agents=claude-code,cursor # allow-list
gortex init --agents-skip=antigravity # block-list
gortex init --dry-run --json # plan, emit JSON report
gortex init --force # overwrite merge-preserved keys
gortex init --hooks-only # refresh supported agent hooks only
# Observe-only
gortex init doctor # read-only state report
gortex init doctor --json # machine-readable report
```
## Adapter contract
Every adapter under `internal/agents/<name>/` implements the
`agents.Adapter` interface:
- `Name()` — stable identifier used by `--agents`
- `DocsURL()` — upstream docs link (for `--json` reports)
- `Detect(env)` — cheap filesystem/`PATH` probe; never writes
- `Plan(env)` — returns the set of files Apply *would* touch,
without writing
- `Apply(env, opts)` — performs the writes, respecting
`opts.DryRun` and `opts.Force`
Every write funnels through `agents.WriteIfNotExists`,
`agents.MergeJSON`, or `agents.MergeTOML`. Those helpers provide:
- Atomic temp-file-plus-rename — a partial failure can't leave a
half-written config
- Uniform dry-run handling — no adapter has its own bool
- Structured `FileAction` results — `--json` and doctor speak the
same vocabulary
- Malformed-file backup — a user with broken JSON gets a `.bak`
sibling instead of silent data loss
## Per-agent notes
### claude-code
The primary integration, split across the two commands.
**`gortex install` (user-level, once per machine)** writes:
- `~/.claude.json` — MCP stanza pointing at `gortex mcp`
- `~/.claude/settings.local.json` — user-level Claude Code hooks
(unless `--no-hooks`)
- `~/.claude/skills/gortex-*/SKILL.md` — curated tool-usage skills
(`gortex-guide`, `gortex-explore`, `gortex-debug`, `gortex-impact`,
`gortex-refactor`), one source of truth per user instead of copied
into every repo
- `~/.claude/commands/gortex-*.md` — slash commands
(`/gortex-guide`, etc.), also codebase-agnostic and therefore
user-level
**`gortex init` (per repo)** writes:
- `.mcp.json` — project MCP stanza
- `.claude/settings.json` — MCP permissions merge
(`mcp__gortex__*` allowlist)
- `.claude/settings.local.json` — repo-local hooks (unless
`--no-hooks`)
- `CLAUDE.md` — marker-guarded block (`<!-- gortex:communities:start -->`
/ `<!-- gortex:communities:end -->`) carrying the codebase overview
(via `--analyze`) and the community routing (via `--skills`,
default on); if neither flag produces content, no block is written
- `.claude/skills/generated/<DirName>/SKILL.md` — one per detected
community, regenerated each run so the content tracks the graph
Hooks installed today: **PreToolUse**, **PreCompact**, **Stop**,
**SessionStart** — SessionStart fires on new or resumed sessions to
prime the first turn with graph orientation; PreCompact fires on
summary boundaries.
### aider
Aider has no native MCP client today. We install an `.aiderignore`
block telling Aider to skip Gortex's cache dirs so it doesn't waste
tokens ingesting them.
### antigravity
Two artifacts: a native MCP registration at
`~/.gemini/antigravity/mcp_config.json` (new in 2026) plus a
Knowledge Item at `~/.gemini/antigravity/knowledge/gortex-workflow/`
that documents how to use Gortex via `run_command`. The KI stays
because it gives workflow intent the raw MCP registration doesn't.
### cline
Extension ID `saoudrizwan.claude-dev`. We write
`cline_mcp_settings.json` to each VS Code and Cursor globalStorage
directory that exists. Auto-approval field is `alwaysAllow` (not
`autoApprove`, which is a different field in the schema).
### codex
OpenAI Codex CLI stores config in `~/.codex/config.toml`. We
upsert a `[mcp_servers.gortex]` table there. When hooks are enabled
(the default), Codex receives user-level hooks that keep the integration
soft-only: they add graph context or read-shaping guidance without
denying tools, rewriting input, or suppressing output.
Current Codex hook coverage:
| Surface | Coverage |
| ------- | -------- |
| `SessionStart` | Matches `startup|resume|clear|compact` and emits graph-tools orientation for new, resumed, cleared, and compacted sessions. |
| Bash `PreToolUse` | Soft graph guidance for shell search/read/list shapes. |
| Gortex MCP read-tool `PreToolUse` | `read_file` and `get_editing_context` guidance that nudges source reads toward `compress_bodies`. |
| Bash `PostToolUse` | Output graph enrichment for Bash-wrapped grep/search, source-read, and file-list shapes. |
| `gortex init --hooks-only` | Refreshes Codex hooks without rewriting the MCP server config, `AGENTS.md`, or other adapter surfaces. |
We do not install separate Codex `PreCompact` or `PostCompact` hooks
today: Codex ignores plain text stdout for those events, while the
`SessionStart` `compact` source already provides the orientation path
after compaction. We also intentionally do not install `Stop`; Codex
`Stop` can continue a turn, which expands the behavior surface and
should be tracked separately if needed. `apply_patch` remains out of
scope for Codex hooks and should be handled by a dedicated follow-up.
### continue
Continue.dev still accepts JSON block files under
`.continue/mcpServers/` even though its native format is YAML with
metadata headers. We write the JSON form today for zero-dependency
simplicity; upgrading to the YAML+metadata form is tracked.
### cursor
Project-level `.cursor/mcp.json` (written by `gortex init`);
`~/.cursor/mcp.json` (written by `gortex install`). Env key is `env`
(not `environment`). Cursor does not expose a user-level rules
surface, so community-routing lives per-repo at
`.cursor/rules/gortex-communities.mdc` — regenerated each `gortex
init` run so it tracks the current graph.
### gemini
Gemini CLI reads `.gemini/settings.json` (project) and
`~/.gemini/settings.json` (user). Distinct from the antigravity
adapter despite the shared `~/.gemini/` prefix.
### kilocode
Kilo Code is a Cline fork with its own globalStorage key
(`kilocode.kilo`). We write to every candidate globalStorage path
(VS Code + Cursor + Insiders variants) plus `.kilocode/mcp.json`
when a project-level directory exists.
### kimi
Kimi Code CLI keeps MCP servers in `.kimi-code/mcp.json` (project, via
`gortex init`) or `~/.kimi-code/mcp.json` (user, via `gortex install`),
and user-level lifecycle hooks in `~/.kimi-code/config.toml` as a
`[[hooks]]` array. Kimi appends a hook's plain stdout to the model
context on exit 0, so Gortex sends soft guidance as plain text and uses
the documented `hookSpecificOutput.permissionDecision = "deny"` shape
only to block an indexed whole-file read.
Current Kimi hook coverage (all gated to Gortex-enabled projects so the
machine-wide user hook stays inert elsewhere):
| Surface | Coverage |
| ------- | -------- |
| `UserPromptSubmit` | Injects graph symbols relevant to the prompt before the model runs. |
| `PreToolUse` | Redirects native `Read`/`Grep`/`Glob`/`Bash` to graph tools — a hard `deny` for an indexed whole-file read, soft plain-stdout guidance otherwise — plus the `read_file`/`get_editing_context` `compress_bodies` nudge. |
| `Stop` | Runs post-turn diagnostics (changed symbols → test targets, guards, dead code, coverage, contracts) and feeds them back so the agent self-corrects before handoff. |
| `SubagentStart` | Briefs a spawned subagent with `smart_context` results and the tool-swap table so it doesn't default to raw `Read`/`Grep`. |
The diagnostics and briefing hooks reach the daemon over its unix
socket (the hook port's HTTP surface is served only under
`--http-addr`), and every path degrades to a silent no-op when the
payload is malformed, the cwd is outside a Gortex project, or the
daemon is unreachable — so normal Kimi flow is never blocked by the
integration.
### kiro
Workspace `.kiro/settings/mcp.json` + steering/hooks via `gortex
init`; `~/.kiro/settings/mcp.json` via `gortex install` (steering
and hooks are project-scoped in Kiro's runtime so they stay per-repo).
The MCP entry carries `autoApprove` and explicit `disabled: false`
keys Kiro's UI expects.
### opencode
The MCP config is written to a root `opencode.json` (or merged into an
existing `opencode.json` / `opencode.jsonc`) — the files OpenCode
actually reads. It does **not** read `.opencode/config.json`, which
Gortex wrote historically. OpenCode's schema also differs from the
canonical form: top-level `mcp.<name>` (not `mcpServers`), `command` is
an array, env key is `environment`, plus a `$schema` pointer at
`https://opencode.ai/config.json`.
### openclaw
Config lives at `~/.openclaw/openclaw.json`. OpenClaw advertises
JSON5 but accepts strict JSON, which is what we emit. Servers go
under `mcp.servers.<name>`.
### pi
**Pi has no MCP support — by design**, so instead of an `mcpServers`
stanza this adapter ships a self-contained TypeScript extension at
`.pi/extensions/gortex/index.ts` (project) or
`~/.pi/agent/extensions/gortex/index.ts` (global). It registers Gortex's
graph tools natively and re-creates the same read-discipline enforcement
the other agents get (skip it with `--no-hooks`). `GORTEX_TOOLS` selects
the eagerly-registered preset (default `core`), matching the daemon.
The read-discipline rules are injected by the extension at runtime.
**Project-local extensions require a one-time trust confirmation** the
first time Pi opens the repo — nothing the installer can do beyond writing
the file.
### vscode
**Schema changed in 2026.** VS Code's native MCP runtime (1.102+)
uses `{"servers": {...}}`, not the Copilot-Chat legacy
`{"mcpServers": {...}}`. `type` is inferred from `command`
presence, so stdio servers don't need a type field.
### windsurf
**Path changed in 2026.** Current canonical path is
`~/.codeium/mcp_config.json`. The legacy
`~/.codeium/windsurf/mcp_config.json` is left in place unless
`--force` is passed, which removes it as part of the migration.
### zed
Zed calls its MCP registry `context_servers`, not `mcpServers`.
Settings file is platform-specific:
- macOS: `~/Library/Application Support/Zed/settings.json`
- Linux: `~/.config/zed/settings.json`
- Windows: `%APPDATA%\Zed\settings.json`
Each entry takes `source: "custom"` alongside the usual
`command/args/env`.
## Troubleshooting
- **Config file malformed**: If an adapter finds invalid JSON/TOML
it writes a `.bak` sibling before replacing the file with the
merged result. Check alongside the original.
- **Hook command points at `/tmp/…`**: `gortex init` heals stale
ephemeral paths automatically on re-run.
- **"Already configured" but tools missing**: re-run with
`--force` to overwrite our entries; or delete the `gortex`
stanza from the config and re-run without `--force`.
- **CI / scripted install**: pass `--yes --json` and parse the
report.
+85
View File
@@ -0,0 +1,85 @@
# Architecture & graph schema
## Component diagram
```
gortex binary
CLI (cobra) ──> MultiIndexer ──> In-Memory Graph (shared, per-repo indexed)
MCP (stdio) ──────────────────> Query Engine (repo/project/ref scoping)
HTTP /v1/* ──────────────────> same tools + /v1/graph + /v1/events (SSE)
Daemon (unix) ──────────────────> shared graph for every MCP client, session isolation
MCP Prompts ──────────────────> (pre_commit, orientation, safe_to_change)
MCP Resources ──────────────────> (16 read-only URIs — bootstrap state + analyzer rollups)
MultiWatcher <── filesystem events (fsnotify, per-repo)
CrossRepoResolver ──> cross-repo edge creation (type-aware)
Persistence ──> gob+gzip snapshot (pluggable backend)
```
## Data flow
1. On startup, loads cached graph snapshot if available; otherwise performs full indexing.
2. MultiIndexer walks each repo directory concurrently, dispatches files to language-specific extractors (tree-sitter).
3. Extractors produce nodes (files, functions, types, etc.) and edges (calls, imports, defines, etc.) with type environment metadata.
4. In multi-repo mode, nodes get `RepoPrefix` and IDs become `<repo_prefix>/<path>::<Symbol>`.
5. Resolver links cross-file references with type-aware method matching; CrossRepoResolver links cross-repo references with same-repo preference.
6. Query Engine answers traversal queries with optional repo/project/ref scoping.
7. MultiWatcher detects changes per-repo and surgically patches the graph (debounced per-file), then re-resolves cross-repo edges.
8. On shutdown, persists graph snapshot for fast restart.
## Graph schema
**Node kinds:**
- Code structure: `file`, `package`, `function`, `method`, `type`, `interface`, `field`, `variable`, `constant`, `import`, `contract`, `param`, `closure`, `enum_member`, `generic_param`
- Coverage extensions: `module`, `table`, `column`, `config_key`, `flag`, `event`, `migration`, `fixture`, `todo`, `team`, `license`, `release`
- Infrastructure: `resource` (K8s manifest), `kustomization` (Kustomize overlay), `image` (Dockerfile FROM / K8s `container.image`)
**Edge kinds:**
- Calls / structure: `calls`, `imports`, `re_exports`, `defines`, `implements`, `extends`, `overrides`, `references`, `member_of`, `instantiates`, `provides`, `consumes`, `composes`, `aliases`, `typed_as`, `returns`, `captures`, `param_of``re_exports` is barrel-file forwarding (`export {x} from "mod"`, `export * from "mod"`, `export * as ns`), kept distinct from `imports` so a dependency walk separates forwarding hops from consumption
- Concurrency / mutation: `spawns`, `sends`, `recvs`, `reads`, `writes`, `reads_config`, `writes_config`
- Dataflow (CPG-lite): `value_flow`, `arg_of`, `returns_to`
- Metadata: `annotated`, `emits`, `throws`, `queries`, `reads_col`, `writes_col`, `toggles_flag`, `depends_on_module`, `matches`, `generated_by`, `tests`, `covered_by`, `owns`, `authored`, `licensed_as`
- Framework / infrastructure: `handles_route`, `models_table`, `renders_child`, `configures`, `mounts`, `exposes`, `depends_on`, `uses_env`
- Similarity: `similar_to` (MinHash + LSH near-duplicate clones; `Meta["similarity"]` carries the estimated Jaccard score), `semantically_related` (graph-diffusion smoothing — transitively blends clone-similarity scores so indirectly-related symbols connect)
- Workspace: `workspace_member` — links a package-manager workspace root (npm / pnpm / Cargo) to each of its members
- Cross-repo: `cross_repo_calls` / `cross_repo_implements` / `cross_repo_extends` — materialised whenever a `calls` / `implements` / `extends` edge's endpoints live in different repos
**Multi-repo fields:** Nodes carry `repo_prefix` (empty in single-repo mode). Edges carry `cross_repo` (true when connecting nodes in different repos). Node IDs use `<repo_prefix>/<path>::<Symbol>` format in multi-repo mode.
**Edge.Alias:** per-binding `imports` (`import { x as alias }`) and `re_exports` (`export { x as alias } from`) carry the renamed local / exported identifier on `Edge.Alias`; `To` still targets the upstream original name, so `Alias` is the only place the rename is recorded.
**Test taxonomy:** functions and methods in test files carry `Meta["is_test"]` + `Meta["test_role"]` (`test` / `benchmark` / `fuzz` / `example`) + `Meta["test_runner"]`. The runner identifier is one of `gotest` / `pytest` / `unittest` / `rspec` / `minitest` / `test-unit` / `jest` / `vitest` / `mocha` / `bun-test` / `node-test` / `playwright` / `cypress`, resolved from parser-stamped imports (JS / TS) with a Mocha-TDD `suite()` byte fallback and language-default fill-in (Go is always `gotest`, Python defaults to `pytest`, Ruby uses the `_spec.rb` / `_test.rb` suffix). The owning `KindFile` also gets the same `test_runner` stamp so file-level queries can group tests by runner without walking functions.
## Graph persistence
Gortex snapshots the graph to disk on shutdown and restores it on startup, with incremental re-indexing of only changed files:
```bash
# Default cache directory: ~/.gortex/cache/
gortex mcp --index /path/to/repo
# Custom cache directory
gortex mcp --index /path/to/repo --cache-dir /tmp/gortex-cache
# Disable caching
gortex mcp --index /path/to/repo --no-cache
```
The persistence layer uses a pluggable backend interface (`persistence.Store`). The default backend serializes as gob+gzip. Cache is keyed by repo path + git commit hash, with version validation to invalidate on binary upgrades.
**Warm restarts are incremental.** On restart, each tracked repo is reconciled against disk independently and routed down one of three paths: `incremental` (no on-disk changes — the repo is not re-parsed, re-resolved, or re-enriched at all), `scoped` (only the changed/deleted files are re-parsed and cross-file resolution re-runs against just that delta), or `full_retrack` (a whole-repo evict-and-re-parse, reserved for when the change census can't be taken, churn exceeds ~40% of the repo, or the operator forces it — see `GORTEX_WARMUP_FULL_RETRACK` / `GORTEX_WARMUP_FULL_RESOLVE` in [multi-repo.md](multi-repo.md#daemon-tuning-optional)). Semantic-enrichment completion is persisted per `(repo, provider, commit)` in the graph store, so a repo whose marker still matches HEAD on a clean tree skips re-running its LSP hover pass on the next restart too. Each repo's reconcile logs its route (`route=incremental|scoped|full_retrack`) and an honest `full_retrack` flag; the daemon closes out warmup with a single `daemon: warmup summary` line recapping parse/resolve/enrichment timings for the whole restart.
**XDG base directories.** Gortex honors the XDG Base Directory spec — `XDG_CONFIG_HOME` for configuration, `XDG_DATA_HOME` for durable data (memories, notes, feedback), and `XDG_CACHE_HOME` for the graph snapshot, token cache, and savings ledger. When set to an absolute path, the variable wins on every platform (Linux / macOS / Windows alike). When unset, every path resolves to its prior default — so upgrading never orphans an existing install's config, memories, or cache. `--cache-dir` still overrides the cache location explicitly.
## Scale — battle-tested on large repos
Measured on an Apple Silicon laptop with the default CGO build:
| Repository | Files | Nodes | Edges | Index time | Throughput | Peak heap |
| ---------- | ----: | ----: | ----: | ---------: | ---------: | --------: |
| [torvalds/linux](https://github.com/torvalds/linux) | 70,333 | 1,690,174 | 6,239,570 | ~3 min | 300 files/s | 5.07 GB |
| [microsoft/vscode](https://github.com/microsoft/vscode) | 10,762 | 204,501 | 808,902 | ~1 min | 143 files/s | 580 MB |
| zzet/gortex (self) | 430 | 5,583 | 53,830 | 3.4s | 127 files/s | 52 MB |
Parsing dominates wall time (6580%); reference resolution and search-index build scale sub-linearly. The indexing and parsing pipeline runs entirely in-process — no external services, no database, no network. Optional features that *do* reach the network (LLM providers, first-run model downloads, PR-review forge calls, the remote-daemon roster) are off by default; anonymous usage telemetry is likewise off by default and transmits nothing unless an endpoint is configured (see [telemetry.md](telemetry.md)).
+337
View File
@@ -0,0 +1,337 @@
# CLI reference
```
gortex install One-time machine-wide setup (user-level MCP, skills, hooks, daemon wiring)
gortex init [path] Per-repo setup (.mcp.json, hooks, community routing, per-community SKILL.md)
gortex init --dry-run-intake Emit a privacy-safe intake manifest and exit before parsing/writes
gortex init doctor Zero-op drift report across all detected agents (human or --json)
gortex mcp [flags] Start the MCP stdio server (auto-detects daemon; --no-daemon / --proxy; --server adds HTTP API)
gortex daemon start [flags] Start the daemon; --http-addr <addr> serves the HTTP/JSON API under /v1/* plus the MCP /mcp transport (--http-auth-token, --cors-origin)
gortex daemon <sub> start / stop / restart / reload / status / logs / install-service / service-status / uninstall-service / server (multi-server roster)
gortex eval <sub> Retrieval + coverage benchmarks — recall / embedders / pack / swebench / stdbench / tokens / baselines / quality / parity (substrate; prefer `gortex bench` for the user-facing surface). `parity` measures per-language cross-file coverage against the committed baseline
gortex eval-server [flags] HTTP server used by the swebench harness
gortex bench <sub> User-facing benchmark suite — recall / tokens / tokens-efficiency / embedders / perf / daemon-latency / swebench / all
gortex audit [flags] A-F repo health grade derived from per-symbol complexity-axis health score
gortex gain [flags] Forward-looking per-call USD savings projection from the latest bench tokens output
gortex context [flags] Generate portable context briefing for a task
gortex savings [flags] Token-savings dashboard (Today / Last 7 days / All time + USD avoided)
gortex status [flags] Show index status (per-repo and per-project in multi-repo mode)
gortex repos [--json] List every tracked repo with git head-commit SHA, last-indexed time, and a staleness flag
gortex track <path> Add a repository to the tracked workspace
gortex untrack <path> Remove a repository from the tracked workspace
gortex workspace <sub> list [--json] / set / set-all — manage workspace + project slugs across tracked repos
gortex config exclude ... add / list / remove entries in the effective ignore list
gortex query <sub> Query the knowledge graph from the CLI
gortex prs [number] List open PRs with a one-shot review-state classification, or deep-dive one PR's blast radius (gortex prs bundle <n> writes a reviewer graph bundle)
gortex review [path] Review a changeset and print line-anchored inline comments + a BLOCK/REVIEW/APPROVE verdict (--diff, --base, --audience, --post)
gortex wiki [path] Generate a multi-page markdown wiki (per-community + processes + analysis)
gortex docs [path] Generate a "living docs" bundle (recent changes + ownership + stale + blame)
gortex export [path] Export the graph to Cypher, GraphML, or Mermaid (--format mermaid --scope all)
gortex githook <sub> install / uninstall / status — manage the post-commit hook
gortex clean Remove Gortex files from a project
gortex telemetry <sub> on / off / status — control anonymous, opt-in usage telemetry (off by default; honours DO_NOT_TRACK)
gortex guide [topic] Print the reference guide (providers, capabilities, tokens, analyze, search_ast, resources, workflow) — same content as the gortex://guide resource
gortex version Print version
```
## One-time machine setup
```bash
gortex install # interactive-free: MCP + skills + slash commands + sub-agents at ~/.claude/
gortex install --start --track # also spawn the daemon and track the current directory
gortex install --no-hooks # skip user-level hook installation
# Daemon lifecycle (also spawned by `gortex install --start`):
gortex daemon start --detach # spawn in background
gortex daemon status # PID, uptime, memory, tracked repos, sessions, server roster, search backend, warmup + enrichment progress
gortex daemon stop # graceful shutdown + final snapshot
gortex daemon restart # stop + start
gortex daemon reload # re-read config, pick up new/removed repos
gortex daemon logs -n 50 # tail the log file
# Multi-server roster — let the daemon route to additional Gortex servers (local sockets or remote HTTPS):
gortex daemon server list # show ~/.gortex/servers.toml
gortex daemon server add work --url https://gortex.work.example --auth-token-env WORK_TOK
gortex daemon server remove work
# Auto-start at login (launchd on macOS, systemd --user on Linux):
gortex daemon install-service
gortex daemon service-status
gortex daemon uninstall-service
# Track / untrack repos (daemon-first dispatch; falls back to config-only when no daemon):
gortex track ~/projects/backend
gortex untrack backend
# Per-repo status + daemon-wide status share the same command — it picks:
gortex status
```
## Per-repo setup
```bash
cd ~/projects/myapp
gortex init # writes .mcp.json, .claude/settings.*, CLAUDE.md with community routing
gortex init --analyze # also index first for a richer CLAUDE.md overview
gortex init --no-skills # skip community-routing generation
gortex init --skills-min-size 5 --skills-max 10 # tune the generator
gortex init --hooks-only # (re)install repo-local hooks only, skip everything else
gortex init --no-hooks # full init but skip hook installation
gortex init --dry-run-intake --json # inspect admitted/skipped corpus buckets; no parsing/storage/writes,
# no raw paths or file contents in the report
# Run the MCP server standalone (auto-detects daemon via stdio; --no-daemon forces embedded):
gortex mcp --index /path/to/repo --watch
gortex mcp --no-daemon --watch # explicit embedded mode
```
## Query subcommands
```
gortex query symbol <name> Find symbols matching name
gortex query deps <id> Show dependencies
gortex query dependents <id> Show blast radius
gortex query callers <func-id> Show who calls a function
gortex query calls <func-id> Show what a function calls
gortex query implementations <iface> Show interface implementations
gortex query usages <id> Show all usages
gortex query stats Show graph statistics
```
All query commands support `--format text|json|dot` (DOT output for Graphviz visualization).
## Pull-request review
```bash
# Triage the review queue — open PRs with CI rollup, review decision, age, and a
# one-shot review-state label (DRAFT / BASE_MISMATCH / CHANGES_REQUESTED / APPROVED
# / STALE / READY). Needs a GitHub token (GH_TOKEN / GITHUB_TOKEN).
gortex prs
gortex prs --worktrees # flag PRs whose head branch is checked out locally
gortex prs --base main --format json # override the base branch; machine-readable output
# Deep-dive one PR: join its changed files against the graph for blast radius + risk.
gortex prs 1234 # needs a running daemon that tracks the repo
# Write a reviewer graph bundle (impact + privacy-safe receipt + ranked reviewers).
gortex prs bundle 1234 -o pr-1234.json # deterministic for an unchanged PR — diffable in CI
# Review a changeset → verdict (BLOCK / REVIEW / APPROVE) + line-anchored inline comments.
gortex review # unstaged changes (default scope)
gortex review --base main # compare HEAD against a ref
gortex review --diff - < patch.diff # review a pasted unified diff from stdin
gortex review --use-llm # fold in LLM-found findings (needs a configured provider)
gortex review --audience agent # terse machine-first packet (vs the default human render)
gortex review --base main --post --pr 1234 # post the gated findings as inline PR comments (secrets redacted)
```
The deterministic correctness rulepack always runs (graph-grounded to drop false positives); `--use-llm` adds LLM findings relocated to exact lines. Posting to a public / fork PR is opt-in via `--confirm-public`; `--dry-run` prints the already-redacted payloads without any network call. The same surface is exposed to agents over MCP — see [mcp.md](mcp.md#pr-review).
## Full tool surface from the CLI
The verbs below give the `gortex` CLI parity with the daemon's MCP tool surface — the same handlers back both front doors. Each verb is a thin shell over one MCP tool on the daemon that owns the repo, so a skill (or a shell script) can drive the whole graph-query, edit-safety, and memory workflow with **no MCP transport mounted and no tool schemas loaded into the model's context**. Every group accepts `--index`/`--repo <path>` (default `.`) to name the repository the daemon must track, and `--format` to pick the wire format.
Because the daemon dispatches a tool call **by name** regardless of which tools are eagerly published, every MCP tool is reachable from the CLI even under the lean `core` preset — including tools that are otherwise [deferred behind `tools_search`](mcp.md#tool-discovery-lazy-mode). `gortex call <tool>` is the generic escape hatch; the dedicated verb groups are ergonomic front-ends over the most-used tools.
### Shared structured-input convention
The edit verbs that take a JSON-shaped parameter (an array of changes, a WorkspaceEdit, a steps/edits array, a ranges array) accept it three interchangeable ways — pick whichever suits the caller:
- **inline** — `--<name> '<json>'` (e.g. `--workspace-edit '{…}'`)
- **file** — `--<name>-file <path>` (e.g. `--edits-file ./edits.json`)
- **stdin** — `--<name> -` (a lone `-` reads the JSON from stdin)
The bytes are validated as well-formed JSON before the call. The same `inline / -file / -` triad covers `verify`'s `--changes`, `preview`/`contract`'s `--workspace-edit`, `simulate`'s `--steps`, `batch`'s `--edits`, and `contract`'s `--ranges`.
### `--arg` coercion (`call` and `analyze`)
`gortex call` and `gortex analyze` assemble their argument object from `--arg key=value` pairs (repeatable). Coercion is deterministic:
| Token | Lowered value |
|---|---|
| `key=true` / `key=false` | bool |
| `key=42` / `key=1.5` | number |
| `key=null` | null |
| `key=[…]` / `key={…}` | parsed JSON array / object (falls back to the literal string if it doesn't parse) |
| `key:=<raw>` | the right-hand side is parsed as raw JSON — `version:="1.0"` stays the **string** `"1.0"` |
| `key=` | the empty string |
| anything else | string |
Repeating a key replaces the earlier value. For `call`, a base object can also come from `--json '<obj>'`, `--json-file <path>`, or `--json -` (stdin); precedence is **file < `--json` < `--arg`** (last wins per key).
### `gortex call <tool>` — invoke any tool by name
The generic relay: invoke any tool the daemon's MCP surface registers, even one with no dedicated verb. Best-effort name validation runs against the live catalog (an unknown name lists the nearest matches and points at `gortex tools search`); calling a mutating tool prints a one-line stderr note unless `--quiet`.
| Flag | Meaning |
|---|---|
| `--arg key=value` | one argument, repeatable; coercion table above |
| `--json '<obj>'` / `--json -` | base object inline or from stdin |
| `--json-file <path>` | base object from a file |
| `--format json\|gcx\|toon\|text` | wire format forwarded to the tool (default `json`) |
| `--dry` | print the lowered argument object + target tool **without** calling the daemon (works offline) |
| `--quiet` | suppress the mutating-tool stderr note |
```bash
gortex call explore --arg task="login handler 500s when the session cookie is expired"
gortex call smart_context --arg task="add rate limiting to the login handler"
gortex call find_usages --arg id="internal/auth/login.go::Login" --format gcx
gortex call overlay_push --json-file ./buffer.json # reach a deferred tool by name
gortex call edit_file --arg path=README.md --arg old_string=foo --arg new_string=bar --dry
```
### `gortex tools …` — discover & describe the surface
| Verb | MCP tool | Key flags |
|---|---|---|
| `tools list` | `tool_profile` | `--category <c>`, `--mutating`, `--preset core\|edit\|nav\|readonly`, `--format text\|json` |
| `tools search <q>` | `tools_search` | `--limit <n>` (default 10) — ranks the deferred surface |
| `tools describe <name>` | `tools_search select:<name>` | prints the tool's full parameter schema |
| `tools receipt` | `tool_profile` | `--format yaml\|json` (default `yaml`) |
`tools list` prints a `NAME · CATEGORY · R/W · PRESETS · SUMMARY` table. `tools receipt` emits a **context-budget receipt** — transport, advertised vs deferred tool counts, and `registered_tool_schemas: 0` — the auditable record that driving Gortex through the CLI mounts no tool schemas into the model's context. Searches and describes are inspection-only: they never promote a tool into the live set.
### `gortex edit …` — edit-safety verbs
The daemon's safe-edit surface as CLI verbs. The read-only verbs (`context`, `verify`, `plan`, `preview`, `simulate`, `guards`, `tests`, `contract`, `rename`) never write; the mutating verbs (`apply`, `symbol`, `batch`, `safe-delete`) touch the working tree.
| Verb | MCP tool | Key flags |
|---|---|---|
| `edit context <file>` | `get_editing_context` | `--detail brief\|full`, `--compress` |
| `edit verify` | `verify_change` | `--change 'id=newsig'` (repeatable sugar) **or** `--changes` / `--changes-file` / `-`; `--compact` |
| `edit plan` | `get_edit_plan` | `--ids <csv>` (required), `--depth` (default 3) |
| `edit preview` | `preview_edit` | `--workspace-edit` / `--workspace-edit-file` / `-`; `--no-diagnostics`, `--inherit-overlay` |
| `edit simulate` | `simulate_chain` | `--steps` / `--steps-file` / `-`; `--keep`, `--no-stop-on-error`, `--inherit-overlay` |
| `edit batch` | `batch_edit` | `--edits` / `--edits-file` / `-`; `--dry-run`, `--compact` |
| `edit apply <file>` | `edit_file` | `--old`, `--new` (required), `--replace-all`, `--dry-run`, `--allow-parse-errors`, `--expected <n>` |
| `edit symbol <id>` | `edit_symbol` | `--old`, `--new` (required), `--dry-run` |
| `edit rename <id>` | `rename_symbol` | `--to <name>` (required) — **plan-only, never writes** |
| `edit guards` | `check_guards` | `--ids <csv>` (required), `--compact` |
| `edit tests` | `get_test_targets` | `--ids <csv>` (required), `--depth` (default 3) |
| `edit contract` | `change_contract` | `--source auto\|diff\|edit\|symbols\|ranges`, `--lens api`, `--risk-gate`, `--ack`, `--base <ref>`, `--workspace-edit*`, `--symbols <csv>`, `--ranges*` / `--path` + `--start-line` + `--end-line` |
| `edit safe-delete <id>` | `safe_delete_symbol` | **dry run unless `--apply`**; `--force`, `--cascade off\|preview\|apply`, `--cascade-into-tests`, `--propagate` |
```bash
gortex edit context internal/auth/login.go --compress
gortex edit verify --change 'internal/auth/login.go::Login=func(ctx context.Context) error'
gortex edit apply README.md --old "old text" --new "new text" --dry-run
gortex edit safe-delete 'internal/legacy/old.go::Unused' --propagate # dry run; add --apply to commit
```
### `gortex memory …` — session & durable memory
Session notes are scoped to a session and survive context compactions; durable memories are workspace-wide and survive daemon restarts and team rotation.
| Verb | MCP tool | Key flags |
|---|---|---|
| `memory note` | `save_note` | `--body`, `--symbol`, `--file`, `--tags`, `--links`, `--pin`, `--id`, `--no-autolink` |
| `memory notes` | `query_notes` | `--symbol`, `--file`, `--tag`, `--text`, `--session`, `--since`, `--limit`, `--pinned` |
| `memory distill` | `distill_session` | `--session`, `--max-symbols`, `--max-files`, `--max-tags`, `--max-recent` |
| `memory store` | `store_memory` | `--body`, `--title`, `--symbols`, `--files`, `--tags`, `--kind`, `--source`, `--importance`, `--confidence`, `--pin`, `--supersedes`, `--scope`, `--id`, `--no-autolink` |
| `memory recall` | `query_memories` | `--symbol`, `--file`, `--tag`, `--kind`, `--source`, `--author`, `--text`, `--since`, `--min-importance`, `--pinned`, `--include-superseded`, `--limit`, `--scope` |
| `memory surface` | `surface_memories` | `--task`, `--symbols`, `--files`, `--limit`, `--min-score`, `--include-superseded`, `--scope` |
```bash
gortex memory surface --task "fix the auth bug" --symbols internal/auth/login.go::Login
gortex memory store --kind invariant --importance 5 --symbols pkg/foo.go::Bar \
--body "Bar must hold the lock before mutating the cache"
gortex memory note --tags decision --body "chose token bucket over leaky bucket because of burst tolerance"
```
### `gortex instructions …` — instruction profiles
An **instruction profile** bundles four agent-facing surfaces, generated from one table so they cannot drift: the instructions body (`~/.gortex/instructions/<name>.md`, @-included into `~/.claude/CLAUDE.md` through an `active.md` byte copy), the MCP tool preset sessions default to, the installed skills subset, and the hook-verbosity tier. Three profiles ship: `core` (balanced default), `localization` (lean code-finding surface: ~2 KB body, ~10 read-only tools, 3 skills, lean hooks), `full` (maximum guidance: the ~34-tool dev-cycle preset eager).
| Verb | Effect |
|---|---|
| `instructions list` | profiles with the active marker, body bytes, preset, skills count, hook tier |
| `instructions show <name>` | print one profile's instructions body |
| `instructions switch <name>` | atomically repoint `active.md`, record the state, reconcile installed skills |
| `instructions regen` | re-derive the profile files from the running binary (selection unchanged) |
Switching applies to **new sessions only** — the @-include, `tools/list`, and skills all load at session start. A forwarded `GORTEX_TOOLS` or an operator-pinned `mcp.tools` config always beats the profile's preset (see [`mcp.md`](mcp.md#restricting-the-tool-surface-presets)); `GORTEX_INSTRUCTIONS_PROFILE=<name>` pins the profile per process tree (benchmarks, CI).
```bash
gortex instructions list
gortex instructions switch localization # lean machine: diet body + 10-tool surface + lean hooks
gortex instructions switch core # back to the balanced default
```
### `gortex analyze` — unified analysis dispatcher
`gortex analyze --kind <k>` runs the daemon's `analyze` dispatcher. `gortex analyze kinds` lists the valid kinds (no daemon needed). Universal typed flags (`--limit`, `--compact`, `--path-prefix`, `--format json|gcx|toon|text`) cover the common parameters; kind-specific parameters ride on `--arg key=value` (same coercion as `call`, and a `--arg` pair overrides the matching typed flag).
```bash
gortex analyze kinds
gortex analyze --kind hotspots --arg threshold:=0.8 --limit 5
gortex analyze --kind coverage_gaps --path-prefix internal/auth/ --arg max_pct:=80 --format gcx
gortex analyze --kind todos --arg tag=FIXME --arg has_assignee=true
```
### `gortex flow` / `taint` / `clones` / `feedback`
| Verb | MCP tool | Key flags |
|---|---|---|
| `flow` | `flow_between` | `--from` / `--to` (required), `--max-depth`, `--max-paths`, `--min-tier`, `--format` |
| `taint` | `taint_paths` | `--source` / `--sink` (required, pattern syntax), `--max-depth`, `--limit`, `--min-tier`, `--format` |
| `clones` | `find_clones` | `--dead-only`, `--min-similarity`, `--path-prefix`, `--repo-filter`, `--limit`, `--format` |
| `feedback record` | `feedback action=record` | `--task`, `--useful`, `--not-needed`, `--missing`, `--tool-source` |
| `feedback query` | `feedback action=query` | `--tool-source`, `--top-n`, `--compact` |
`taint`'s `--source` / `--sink` take a pattern (a bare token is a case-insensitive substring on the symbol name; `exact:Foo`, `path:dir/`, `kind:method`, combined with spaces).
```bash
gortex flow --from pkg/a.go::Input --to pkg/b.go::Sink --max-depth 6
gortex taint --source 'path:handlers/' --sink 'exact:Exec' --limit 30
gortex clones --dead-only --path-prefix internal/
gortex feedback record --task "fix the auth bug" --useful pkg/a.go::Foo,pkg/b.go::Bar
```
### Choosing a consumption path
The CLI verbs and the MCP install are two front doors to the **same** handlers. Pick by how much of the agent's context budget you want to spend on tool schemas versus what transport-level features you need:
| | MCP install (full / core) | skill + CLI |
|---|---|---|
| Baseline context | high (full surface) / ~34 schemas (core) | zero schemas |
| Push notifications | yes | no |
| Overlay sessions | yes | no (use `gortex call overlay_*`) |
| Edit-safety + memory workflow | yes | yes |
| Discovery | `tools_search` | `gortex tools search` |
The skill-driven CLI path trades the live transport features (server-pushed `notifications/*`, session-bound overlay shadow graphs) for a zero-schema baseline: nothing is loaded into the model's context until a verb is actually run. Overlay tools remain reachable by name through `gortex call overlay_*`, but without a persistent MCP session they don't compose into a per-session shadow graph the way the MCP transport does. The edit-safety and memory workflows are fully available on both paths.
## Other commands
```bash
gortex track . && gortex daemon start --http-addr 127.0.0.1:7411 # HTTP/JSON API on :7411 (/v1/* + /mcp). UI lives at github.com/gortexhq/web.
gortex savings [--verbose] [--json] # Today / Last 7 days / All time bar-chart dashboard + $ avoided
gortex bench <sub> # user-facing benchmark suite (recall / tokens / tokens-efficiency / perf / daemon-latency / embedders / swebench / all)
gortex audit [--badge|--format svg|json|text] # A-F repo health grade + README-ready SVG shield
gortex gain [--since 7d] # forward-looking per-call USD savings + optional history slice
gortex version
```
## Generated wiki + living diagrams
Run `gortex wiki .` to produce a Markdown wiki under `wiki/<repo-slug>/`:
```
wiki/
index.md # top-level (single repo today, multi-repo extension point)
<repo>/
index.md # community navigation
architecture.md # community-level system overview
communities/<n>-<slug>.md # one page per detected community
processes/<slug>.md # one page per discovered execution flow (Mermaid sequenceDiagram)
contracts/api-surface.md # HTTP / gRPC / GraphQL contracts
analysis/{hotspots,cycles,semantic}.md
_assets/community-graph.mermaid
_workspace/ # reserved for multi-repo pages
```
Pair with `gortex githook install post-commit --regen-mermaid --regen-wiki` to keep diagrams and docs in sync after every commit. The hook is idempotent and preserves any non-gortex content in the existing hook file.
For CI, drop `examples/.github/workflows/gortex-architecture.yml` into your repo: it re-runs `gortex export --format mermaid --scope all` on every push and opens a PR when the diagrams drift.
`gortex wiki --enhance` enables LLM-augmented narrative summaries via the configured `llm.provider` (claudecli for MVP — uses your local Claude Code subscription). Results are cached by `(node, content_hash)` so re-runs on unchanged inputs produce byte-identical output without re-invoking the LLM.
+40
View File
@@ -0,0 +1,40 @@
# Cross-repo API contracts
Gortex detects API contracts across repos and matches providers to consumers:
```bash
# After indexing, contracts are auto-detected
gortex track .
# Via MCP tools
contracts # list all detected contracts (default action)
contracts {action: "check"} # find mismatches and orphans
```
| Contract type | Detection | Provider | Consumer |
|--------------|-----------|----------|----------|
| **HTTP routes** | Framework annotations (gin, Express, FastAPI, Spring, etc.) | Route handler | HTTP client calls (fetch, http.Get) |
| **gRPC** | Proto service definitions | Service RPC | Client stub calls |
| **GraphQL** | Schema type/field definitions | Schema | Query/mutation strings |
| **Message topics** | Pub/sub patterns across Kafka, RabbitMQ, NATS, and Redis (`KindTopic` nodes, `produces_topic` / `consumes_topic` edges); dynamic topic names suppressed | Publish calls | Subscribe calls |
| **WebSocket** | Event emit/listen patterns | `emit()` | `on()` |
| **Env vars** | `os.Getenv`, `process.env`, `.env` files | `Setenv` / `.env` | `Getenv` / `process.env` |
| **OpenAPI** | Swagger/OpenAPI spec files | Spec paths | (linked to HTTP routes) |
| **Temporal workflows** | Go SDK `worker.RegisterActivity(WithOptions)` / `RegisterActivities` / Java `@ActivityInterface` / `@WorkflowInterface` annotations | Activity / workflow function (carries `temporal_role` Meta) | `workflow.ExecuteActivity` / `ExecuteChildWorkflow` / `client.ExecuteWorkflow` / handler & signal/query calls |
Contracts are normalized to canonical IDs (e.g., `http::GET::/api/users/{id}`) and matched across repos to detect orphan providers/consumers and mismatches.
## Temporal edge taxonomy
The Go and Java extractors tag Temporal call sites with a `via` Meta value on the `EdgeCalls` edge (plus `temporal_kind` and `temporal_name`); `ResolveTemporalCalls` rewrites the resolvable ones (`temporal.stub` / `temporal.start`) to the registered handler / workflow node. Because they are ordinary `EdgeCalls`, `find_usages` / `get_callers` / `explain_change_impact` traverse them with no temporal-specific code.
| `via` | Direction | Emitted from | `temporal_kind` | Resolved? |
|-------|-----------|--------------|-----------------|-----------|
| `temporal.register` | provider tag | `worker.RegisterActivity(WithOptions)` / `RegisterWorkflow(WithOptions)` / `RegisterActivities` | `activity` / `workflow` | indexed, not rewritten |
| `temporal.stub` | workflow → activity / child-workflow | `workflow.ExecuteActivity` / `ExecuteLocalActivity` / `ExecuteChildWorkflow` | `activity` / `workflow` | yes → registered handler |
| `temporal.start` | service → workflow | `client.ExecuteWorkflow` / `SignalWithStartWorkflow` | `workflow` | yes → registered workflow |
| `temporal.handler` | workflow exposes | `workflow.SetQueryHandler` / `GetSignalChannel` / `SetUpdateHandler` (+`WithOptions`) | `query` / `signal` / `update` | provider edge |
| `temporal.signal-send` | sender → running workflow | `workflow.SignalExternalWorkflow` / `client.SignalWorkflow` | `signal` | consumer edge |
| `temporal.query-call` | caller → running workflow | `client.QueryWorkflow` | `query` | consumer edge |
Extra Meta on these edges: `temporal_registered_name` (the `RegisterOptions{Name}` override that is the actual dispatch key), `temporal_register_plural` (a `RegisterActivities(&Struct{})` registration whose exported methods are each promoted), and `temporal_name_origin=env_default` (a dispatch name resolved from an env-var-with-literal-default, landed at the speculative tier). Node roles are stamped as `temporal_role` (`activity` / `workflow` / `activity_interface` / `workflow_interface` / `signal` / `query` / `update`). Aliased `import wf "go.temporal.io/sdk/workflow"` receivers are canonicalised before detection.
+142
View File
@@ -0,0 +1,142 @@
# Feature catalog
The full surface, grouped by concern. Each item links to the deeper reference where one exists.
- [Graph & resolution](#graph--resolution)
- [Search & navigation](#search--navigation)
- [Dataflow, structure, concurrency](#dataflow-structure-concurrency)
- [Refactoring, simulation, overlays](#refactoring-simulation-overlays)
- [Safety, guards, agent config](#safety-guards-agent-config)
- [Notifications & live signals](#notifications--live-signals)
- [Coverage, churn, ownership](#coverage-churn-ownership)
- [Framework, infra, contracts](#framework-infra-contracts)
- [Persistence, scale, isolation](#persistence-scale-isolation)
- [Agent ergonomics](#agent-ergonomics)
- [Token economy](#token-economy)
## Graph & resolution
- **Knowledge graph** — every file, symbol, import, call chain, and type relationship in one queryable structure.
- **Multi-repo workspaces** — index multiple repos into one graph with cross-repo symbol resolution, project grouping, reference tags, and per-repo scoping. A dedicated cross-repo edge layer materialises `cross_repo_calls` / `cross_repo_implements` / `cross_repo_extends` whenever a relation's endpoints live in different repos — surfaced via `analyze kind: "cross_repo"`. Resolution is evidence-gated so unrelated same-named symbols don't get wired together. See [multi-repo.md](multi-repo.md).
- **Per-session workspace isolation** — under the daemon, each MCP session's queries are scoped to the workspace it connected from. Sessions on different repos never see each other's graph slices.
- **257 languages** across three tiers — bespoke tree-sitter extractors (~30) for the deep-resolution tier, regex extractors (~60) for niche/legacy, forest-backed signature-only (~165 via `alexaandru/go-sitter-forest`, extended by drop-in user grammars via `.gortex.yaml`'s `index.grammars` key) for the long tail. Plus notebook-style sources: Jupyter `.ipynb` (nbformat 3+4) and Databricks notebooks (`.dbc` archives + source-format `.py` / `.scala` / `.sql` / `.R` with `# COMMAND ----------` separators) extract each cell as its own graph node, tagged with `cell_index` / `cell_kind` / `cell_language`. See [languages.md](languages.md).
- **Type-aware resolution** — infers receiver types from variable declarations, composite literals, and Go constructor conventions to disambiguate same-named methods across types.
- **Scope-based static resolver for C / C++ / Java / PHP** — disambiguates same-named symbols using language scope rules before falling back to directory-locality. C prefers same-file `static` over same-name extern; C++ prefers same-namespace then walks ADL namespaces; Java pins to the enclosing class, then walks `extends` up to 8 hops; PHP resolves `parent::`/`self::`/`static::` against the inheritance chain. Scope-resolved edges stamp `OriginASTResolved` + `Meta["resolution"]="scope"`.
- **LSP-enriched call-graph tiers** — every edge carries an `origin` tier (`lsp_resolved` / `lsp_dispatch` / `ast_resolved` / `ast_inferred` / `text_matched`) plus a coarse `tier` label (`lsp` / `ast` / `heuristic`). Pass `min_tier` to `get_callers`, `find_usages`, `find_implementations`, `flow_between`, etc. for compiler-verified-only edges. For TS/JS/JSX/TSX and Python, the cross-file resolver consults `typescript-language-server` or `pyright` on the hot path. See [lsp.md](lsp.md).
- **Sub-millisecond impact analysis** — `explain_change_impact`, `detect_changes`, `flow_between` step-impact, and the `safe_to_change` / `pre_commit` prompts share a precomputed reach index walking every node's incoming edges to depth 3 at index time. Blast-radius queries become O(seeds × reach) map lookups. The daemon snapshot persists the index so warm starts skip the build.
- **Semantic enrichment** — pluggable SCIP, go/types, and LSP providers upgrade edge confidence from ~70-85% (tree-sitter) to 95-100% (compiler-verified). Additive: graceful degradation when external tools are unavailable. Per-language `connect: { network, address, fallback_spawn }` dials an already-running language server instead of spawning a duplicate.
- **IMPLEMENTS inference** — structural interface satisfaction for Go, TypeScript, Java, Rust, C#, Scala, Swift, Protobuf. C# bases in another compilation unit are split into `extends` vs `implements` via a local-interface prescan + the `I`-prefix convention.
- **Framework dynamic-dispatch synthesis** — a provenance-tagged synthesizer engine materialises the call edges frameworks wire at runtime that static resolution can't see: gRPC stub→handler, Temporal workflow→activity, in-process / native event channels, and the cross-language native bridges — Swift↔Objective-C selectors, React Native (`RCT_EXPORT_*` / `@ReactMethod` ↔ JS `NativeModules`), Expo (`Function(...)` DSL ↔ `requireNativeModule`), and Fabric codegen view managers. Every synthesized edge carries `synthesized_by` provenance; `analyze kind: "synthesizers"` rolls them up.
- **Language-specific resolution** — Rust impl-block method-owner / `self`-receiver / `crate::`-`super::` module-path resolution; Kotlin companion-object static dispatch + lambda-parameter scoping. `analyze kind: "resolution_outcomes"` explains *why* a call/reference edge was left unresolved (`ambiguous_multi_match` / `candidate_out_of_scope` / `cross_language_only` / `stub_only` / `no_definition`).
- **Per-reference contexts** — `find_usages` classifies every usage by the role it plays (`parameter_type` / `return_type` / `field` / `value` / `type` / `attribute` / `call`) and accepts a `context:` filter — e.g. "every place this type is used as a parameter".
- **External-package call qualification** — calls into un-indexed third parties are retargeted onto stable per-package identity nodes (`dep::` / `stdlib::` / `external::`, per-language: Go / Rust / Java / Python / C# / TS), so call chains keep the external hop and a service's external surface aggregates across repos. On by default, incremental on the reindex hot path; opt out with `index.synthesize_external_calls: false`.
## Search & navigation
- **Semantic search default-on** — hybrid BM25 + vector with RRF fusion, baked GloVe-50d (~3.8 MB embedded in the binary, top 20k tokens), CPU-only, zero native deps. Large symbols are split into AST-aware windows and de-chunked at query time. Equivalence-class vocabulary expansion bridges `auth ≈ authentication ≈ login` without an LLM. A HITS authority/hub signal feeds the rerank pipeline. A keyword-soup query defense detects degenerate OR-soup *and* operator-free phrasing and skips wasted LLM expansion. Markdown documentation is a first-class corpus with its own retrieval channel and prose-tuned ranking. Opt-in `embedding.provider: local` (Hugot MiniLM-L6-v2) or `api` (Ollama / OpenAI). See [semantic-search.md](semantic-search.md).
- **Provenance-aware ranking** — the BM25↔vector balance is scored continuously from query shape; edge-resolution provenance attenuates LSP-inflated framework wiring in centrality and rerank; generated files are ranked below a real same-named implementation; the implementation is lifted above its own test; and a post-rerank pass recovers exact embedding cosine. Zero-result identifier queries are auto-decomposed into leaf terms.
- **`context_closure`** — given seed files/symbols, walks the transitive import/dependency closure and packs it under one `token_budget`, ranked by graph distance or seeded random-walk proximity.
- **Code search beyond symbols** — `search_text` is a trigram-indexed literal/regex search; `search_ast` runs structural tree-sitter queries; `analyze kind=sast` is a 190-rule, CWE/OWASP-tagged security scan across 8 languages.
- **Multi-axis structured retrieval (`winnow_symbols`)** — constraint-chain filter over `kind`, `language`, `community`, `path_prefix`, `min_fan_in`, `min_fan_out`, `min_churn`, `text_match` with per-axis score contributions.
- **Token-budgeted free traversal (`walk_graph`)** — walks arbitrary `edge_kinds` outward / inward / both from a starting symbol; auto-stops at `token_budget`.
- **Ad-hoc graph query (`graph_query`)** — small read-only DSL with `nodes` / `traverse` / `filter` stages, bounded by `limit` and a five-stage cap.
- **Per-session symbol cursor (`nav`)** — verb-dispatched (`goto` / `into` / `up` / `sibling` / `back` / `where` / `read`); adjacency preview on every response.
- **Opening-move routing (`plan_turn`)** — ~200-token ranked list of next calls with pre-filled args for a task description.
- **Narrative repo overview (`get_repo_outline`)** — single-call: top languages, communities, hotspots, most-imported files, entry points.
- **Cold-start query suggestions (`suggest_queries`)** — 5-10 starter queries from entry points, hubs, bridges, subsystems.
- **Use-site → declaration resolver (`find_declaration`)** — literal substring or regex over use sites; trigram-prefiltered.
## Dataflow, structure, concurrency
- **CPG-lite dataflow** — `value_flow` (intra-procedural assignment / return / range), `arg_of` (caller arg → callee param), `returns_to` (callee → assignment LHS) built at index time. `flow_between` returns ranked dataflow paths; `taint_paths` does pattern-driven source→sink sweeps for security audits.
- **Near-duplicate clone detection** — every substantial function body is reduced to a 64-slot token-normalised MinHash signature at index time; LSH banding finds candidate pairs; a Jaccard threshold filter keeps the true clones — emitted as `similar_to` edges. `find_clones` surfaces clusters; `dead_only: true` yields dead duplicates of live code.
- **Bundled unsafe-patterns scan** — `analyze kind=unsafe_patterns` fans out seven tree-sitter detectors in one call: Go `panic`, Rust `.unwrap` / `.expect` / `panic!` / `todo!` / `unimplemented!` / `unreachable!` / `assert!` / `unsafe { }` / `unsafe fn`, Python `assert` (stripped by `-O`), JS/TS `throw`. Each rule is also invocable individually via `search_ast detector=…`.
- **Language-agnostic concurrency analyzers** — `analyze kind=race_writes` flags struct-field writes from inside a goroutine-reachable function whose writer has no detected lock acquisition (`Lock` / `RLock` / `Acquire` / `WithLock` / `synchronized` / `Do` recognised across Go, Rust, Java, TS, Python, C#). `analyze kind=unclosed_channels` flags channels with sends but no `close()` call, classified high / medium / low risk. Rides existing `EdgeSpawns` / `EdgeSends` / `EdgeRecvs` / `EdgeWrites` / `EdgeCalls`. `get_callers` and `analyze kind=goroutine_spawns` annotate each result with `sync_guarded` and `cross_concurrent` plus a human-readable explanation.
- **Composite code-health score** — `analyze kind=health_score` aggregates `coverage_pct` + complexity + recency + churn into one `0..100` value per symbol plus an A..F grade. Population distribution (mean / median / std-dev / Gini / per-grade counts). `roll_up=file` or `roll_up=repo` for per-file / per-repo averages.
- **Composite change-impact score** — `analyze kind=impact` blends PageRank centrality + transitive reach + cyclomatic complexity + co-change coupling + community span into one 0..100 score and risk label.
- **`analyze` is a 59-kind dispatcher** — beyond structural kinds, covers `impact`, `health_score`, `sast` / `named` / `unsafe_patterns`, `clusters`, `connectivity_health`, `tests_as_edges`, `channel_ops`, `goroutine_spawns`, `field_writers`, `config_readers`, `event_emitters`, `error_surface`, `routes`, `models`, `components`, `k8s_resources`, `images`, `kustomize`, `cross_repo`, `dbt_models`, `env_var_users`, `sql_call_sites`, `fixes_history`, `edge_audit`, `domain`, `synthesizers` (framework-dispatch-synthesized edges grouped by pass), `resolution_outcomes` (why the resolver left an edge unresolved), more.
## Refactoring, simulation, overlays
- **Atomic refactors** — `edit_symbol` (edit by ID, optional `base_sha` drift guard, returns `new_sha` for pipelined edits), `edit_file` (any file, no graph required, kills Read-before-Edit), `write_file` (atomic temp+rename, re-indexes on write), `rename_symbol` (coordinated multi-file rename), `move_symbol` (relocate function/method/type/variable/const across files — same-package leaves callers untouched, cross-package rewrites every qualified reference, drops/adds imports, synthesises the target file; Go for now), `inline_symbol` (replace every callsite of a trivial callee with the body; refuses cleanly on defer/spawn/close-over-scope/multi-return/side-effecting args; `delete_after: true` removes the declaration), `safe_delete_symbol` (atomic dead-code removal with graph-aware safety gate and a fixed-point orphan-propagation pass).
- **Speculative execution** — `preview_edit` and `simulate_chain` answer "what would change if I applied this WorkspaceEdit?" without touching disk or mutating the base graph. Standard LSP `WorkspaceEdit` input. Per-step impact: touched files, added/removed/renamed symbols, broken callers, broken interface implementors, blast-radius rollup, suggested test targets, round-trip LSP diagnostics. `simulate_chain` with `keep: true` promotes the final state into a real overlay.
- **Live editor overlays (shadow graph)** — `overlay_register` / `overlay_push` / `overlay_list` / `overlay_delete` / `overlay_drop` / `overlay_keepalive` / `compare_with_overlay`. Editor extensions push in-flight (unsaved) buffers; every subsequent tool call in the same MCP session reads through the shadow. Base graph is never mutated. Concurrent sessions each see their own view. Overlays are bound to the MCP session lifecycle; idle TTL via `GORTEX_OVERLAY_IDLE_TTL` (default 30 m).
- **Overlay branching** — N parallel speculative sessions off one baseline. `overlay_fork` / `overlay_branches` / `overlay_switch` / `overlay_merge` / `overlay_drop_branch` / `compare_branches`. Hold strategy A and strategy B simultaneously, evaluate each, merge the winner.
- **Dependency-ordered batch refactors** — `get_edit_plan` returns the file order; `batch_edit` applies atomically, re-indexing between steps.
- **Scaffolding** — `scaffold` generates code, registration wiring, and test stubs from an example symbol.
- **Pattern extraction** — `suggest_pattern` extracts the code pattern from an example (source, registration, tests).
## Safety, guards, agent config
- **Proactive safety** — `verify_change` checks proposed signature changes against all callers and interface implementors; `check_guards` evaluates project guard rules (`.gortex.yaml`) against changed symbols.
- **Graph-validated config hygiene (`audit_agent_config`)** — scans `CLAUDE.md`, `AGENTS.md`, `.cursor/rules`, `.github/copilot-instructions.md`, `.windsurf/rules`, `.antigravity/rules` for stale symbol references, dead file paths, and bloat — validated against the live graph.
- **Guard rules** — project-specific constraints (co-change, boundary) enforced via `check_guards`. The `architecture:` block carries declarative `rules:` (`max_fan_out` dependency-cone limits, `deny_callers_outside` caller boundaries) and named `layers:` (path globs with directional `allow` / `deny` dependency lists).
- **Graph-grounded PR review** — `review` / `review_pack` run a deterministic correctness rulepack (NPE / thread-safety check-then-act / N+1 / logic-error, Go + Python; also `analyze kind=review`) over a changeset, graph-grounded to drop false positives, and return a BLOCK/REVIEW/APPROVE verdict with line-anchored inline comments; `critique_review` adds an adversarial LLM false-positive pass and `post_review` posts the gated findings as inline PR/MR comments (secrets redacted before egress). PR triage rides the graph: `list_prs` / `triage_prs` / `pr_risk` / `get_pr_impact` (blast radius + risk score), `conflicts_prs` (merge-order hotspots), `suggest_reviewers`, `suggested_review_questions`. Exposed on the CLI as `gortex prs` / `gortex review`. See [cli.md](cli.md#pull-request-review) and [mcp.md](mcp.md#pr-review).
- **Phase-enforcement workflow** — `set_planning_mode` switches the session between a no-writes planning phase (every editing tool removed and hard-blocked) and editing mode. `workflow` drives a phase-enforcement state machine (explore → implement → verify); editing tools are gated until the implement phase.
- **Diagnostics & code actions** — `subscribe_diagnostics` / `unsubscribe_diagnostics` push LSP `publishDiagnostics`; `get_diagnostics` reads the current state. `get_code_actions` / `apply_code_action` / `fix_all_in_file` wire LSP code actions (quickfix / organizeImports / refactor / source) across every running language server. Server-driven capability registration (`client/registerCapability`) is honoured live.
- **Prompt-injection screening** — every tool call is screened for injection patterns; non-blocking `_meta.gortex_security` advisory on hit. Disable with `GORTEX_MCP_SANITIZE=0`.
## Notifications & live signals
Five proactive push channels — per-session opt-in, delta-filtered, initial replay, auto-cleanup on disconnect. Subscriber counts surface in `graph_stats`.
- **`notifications/diagnostics`** — LSP `publishDiagnostics` fan-out. Filter by `min_severity` / `path_prefix`.
- **`notifications/workspace_readiness`** — daemon warmup phase transitions (snapshot_loaded → parallel_parse → deferred_passes_all → global_resolve → end_batch → watcher_started → ready). Late subscribers get the last-known phase replayed.
- **`notifications/daemon_health`** — periodic ticker (default 15 s, clampable 1 s..5 min). Snapshots uptime, alloc/sys/heap, num_goroutine, num_gc, tracked_repos, sessions, lsp_alive, graph nodes/edges. Only runs while ≥1 subscriber is attached.
- **`notifications/stale_refs`** — per-session intersect of watcher symbol-change events against the session's viewed/modified working set. Fires only when a change actually touches what *this* session has consumed.
- **`notifications/graph_invalidated`** — coarse "the graph was rebuilt, drop cached results" signal. `{node_count, edge_count, reason, ts}`. Unfiltered.
Plus **MCP progress notifications** on long-running indexing / `track_repository` calls (`notifications/progress` with stage messages walking files → parsing → resolving → semantic enrichment → search index → contracts → done).
## Coverage, churn, ownership
- **Test taxonomy** — functions/methods in test files carry `Meta["is_test"]` + `Meta["test_role"]` (`test` / `benchmark` / `fuzz` / `example`) + `Meta["test_runner"]`. Runner identifier resolved from parser-stamped imports across gotest / pytest / unittest / rspec / minitest / test-unit / jest / vitest / mocha / bun-test / node-test / playwright / cypress.
- **Stratified test classification** — `tests` edges carry the role so `winnow_symbols` can filter "production functions only" and coverage analyzers can reason per-tier.
- **Test-coverage gaps (`get_untested_symbols`)** — inverse of `get_test_targets`. Functions/methods not reached from any test file, ranked by fan-in.
- **`get_churn_rate`** — per-symbol commit density from blame.
- **`find_co_changing_symbols`** — ranked git co-change neighbours over mined cosine-weighted `co_change` edges.
- **Enrichment CLI** — `gortex enrich blame | coverage | releases | all` hydrates the graph with the metadata `stale_*` / `coverage*` / `ownership` / `releases` analyzers need.
- **Per-language parity coverage** — `gortex eval parity` clones a frozen per-language benchmark corpus (one canonical public repo per language), indexes each, and measures the share of symbol-bearing source files that have at least one *resolved cross-file dependent* — a single resolution-quality number per language. Default (assert) mode compares the measurement against the committed baseline (`internal/eval/parity/baseline.json`) and exits non-zero if any language regressed beyond `--epsilon`; `--lang` scopes to one language and `--update` reprints the measured coverage as a fresh baseline document to capture or refresh it.
- **Parity-regression fence** — the parity eval is locked into CI three independent ways so resolution quality can only hold or improve: a **per-language coverage floor** (`baseline.json`) that `gortex eval parity` refuses to let any language fall below; a **frozen at-or-beyond-parity language count** plus a baseline-vs-corpus exhaustiveness check, so no language can be silently dropped from the fence; and **per-feature extraction goldens** that assert each ported capability still produces its exact nodes and edges (annotation types → interfaces, anonymous classes → synthetic types with an extends edge, arrow-valued class fields → methods, per-binding imports + alias-aware re-exports, …) — a refactor that drops one fails immediately, naming the missing node or edge.
## Framework, infra, contracts
- **Framework graph layer** — handler→route edges from HTTP / gRPC / GraphQL / WebSocket / Phoenix / Kafka topic registrations. ORM model→table edges across GORM, SQLAlchemy, Django, ActiveRecord, JPA, TypeORM, Ecto. Component-tree edges for JSX/TSX and Phoenix HEEx. `analyze kind=routes/models/components`.
- **Cross-file contract resolution** — router-mount prefixes are folded into HTTP contract paths (FastAPI `include_router(prefix=)`, Express `app.use`, NestJS `@Controller`); project-specific HTTP client wrappers register as consumers via a configurable alias list; Symfony `services.yaml` / Spring beans XML emit interface→implementation DI bindings; MyBatis mapper XML is indexed (a node per `<select|insert|update|delete>`, `<namespace>::<id>`) and linked from the Java DAO methods that execute it; `supabase.rpc` / SQLAlchemy `func.*` call sites link to SQL `CREATE FUNCTION` nodes.
- **Finer import / export & type edges** — JS/TS `import { foo, bar as baz }` emits one `imports` edge per named binding (the rename recorded on `Edge.Alias`; capped at 64 bindings/statement before falling back to a module-level edge); barrel re-exports (`export { a, b as c } from`, `export * as ns from`, `export * from`) emit `re_exports` edges so a dependency walk separates forwarding hops from consumption. Anonymous classes/types (Java `new T(){…}`, C# `new { … }`) become synthetic type nodes with an `extends` edge; Java `@interface` annotation types index as interfaces; JS/TS arrow-valued class fields (`handleClick = () => {…}`) become callable methods in the call graph.
- **Fluent / factory-chain receiver typing** — call edges through method chains and static factories (`New().Router().Build()`, `svc.GetUser().Save()`) carry an accurate `receiver_type`, walked hop-by-hop from each segment's declared return type — seeded from a typed variable or a free-function / constructor return. Shared across Go, TypeScript, C#, Java, Kotlin, Python, Rust.
- **Real-time transport edges** — WebSocket / SSE client constructors (`new WebSocket` / `new EventSource`) and server upgrade handlers (Go gorilla / gobwas / coder) surface as channel edges, reusing the pub/sub event model.
- **Infrastructure graph layer** — `KindResource` (K8s Deployments, Services, Ingresses, ConfigMaps, Secrets, CronJobs), `KindKustomization` (overlay tree), `KindImage` (Dockerfile FROM and K8s `container.image`) with `depends_on` / `configures` / `mounts` / `exposes` / `uses_env` edges. Cross-references with code-side `os.Getenv` automatically. `analyze kind=k8s_resources/kustomize/images`.
- **Framework-aware extraction** — first-class entry points (Alembic migrations, Next.js pages / App-Router files, ASP.NET host files) stamped so the dead-code analyzer never flags runtime-invoked symbols. Swift HTTP routes (Vapor + Alamofire). XAML / AXAML extractor (`x:Class` code-behind link, named controls, `{Binding}` expressions). .NET DI registrations + COM-interop flags. Lombok / MapStruct / Kotlin / CommunityToolkit.Mvvm source-generated members surfaced via `has_generated_members`.
- **Cross-repo API contracts** — auto-detection across HTTP routes, gRPC services, GraphQL schemas, Kafka/RabbitMQ/NATS/Redis pub/sub topics, WebSocket events, env vars, OpenAPI specs, Temporal workflows. Normalised to canonical IDs (e.g. `http::GET::/api/users/{id}`) and matched across repos to detect orphan providers/consumers and mismatches. See [contracts.md](contracts.md).
- **Artifacts** — non-code knowledge files (DB schemas, API specs, ADRs, infra configs) declared in `.gortex.yaml::artifacts` are indexed as `artifact` nodes. `search_artifacts` / `get_artifact` surface them; `EdgeReferences` links code to spec.
## Persistence, scale, isolation
- **On-disk persistence** — snapshots the graph on shutdown, restores on startup with incremental re-indexing of only changed files (~200 ms vs 35 s full re-index). Snapshots keyed by `(repo, branch)` so branch-switches reuse each branch's cached index and git worktrees of one repo share the base. Change detection is mtime-based by default, opt-in BLAKE3 Merkle-tree mode (`index.merkle` / `GORTEX_MERKLE`) diffs by content hash. Cross-process advisory lock guards the store. A repo with no on-disk changes since shutdown is not re-parsed, re-resolved, or re-enriched — enrichment completion is persisted per `(repo, provider, commit)` so restarts don't re-run finished LSP hover passes. See [architecture.md](architecture.md).
- **Anonymous telemetry, off by default** — opt-in coarse, bucketed counts of which tools/commands run (a 4-key allow-list: `mcp_tool_call` / `cli_command` / `index` / `daemon_session`); never code, paths, names, or exact counts. `gortex telemetry on|off|status`; precedence `GORTEX_TELEMETRY` > `DO_NOT_TRACK` > saved choice > off. Nothing is transmitted unless `GORTEX_TELEMETRY_ENDPOINT` is set. See [telemetry.md](telemetry.md).
- **Crash-resilient indexing** — opt-in (`index.crash_isolation`) tree-sitter extraction in worker subprocesses, so a grammar SIGSEGV / OOM / hang on one pathological file is contained. The worker pool is long-lived. Per-file extraction budget, size cap, content-based bundled/minified detection — each leaves a synthetic node carrying `skipped_due_to_*` telemetry. Pluggable pre-ingestion content transforms (`index.transforms`) — BOM stripping plus user external-command processors (minified-bundle expansion, SVG/TOON, PDF→markdown).
- **Long-living daemon (optional)** — `gortex daemon start` runs a single shared process holding the graph for every tracked repo. Each Claude Code / Cursor / Kiro window connects as a thin stdio proxy over a Unix socket with per-client session isolation. Live fsnotify on every tracked repo. `gortex install` sets up user-level config; `gortex daemon install-service` installs a LaunchAgent (macOS) or systemd `--user` unit (Linux). Binaries fall back to embedded mode if the daemon isn't running.
- **MCP 2026 Streamable HTTP transport (`/mcp`)** — the wire format the June 2026 MCP release locks in. Opt-in on the daemon via `gortex daemon start --http-addr <addr>` (non-localhost binds require `--http-auth-token`); served on the same address as the `/v1/*` JSON API. See [server.md](server.md).
- **Watch mode** — surgical graph updates on file change across all tracked repos, live sync with agents.
## Agent ergonomics
- **PreToolUse + PostToolUse + PreCompact + Stop hooks** — two postures picked at install (`gortex install --hook-mode={deny,enrich}`). `deny` (default) has PreToolUse enrich Read/Grep/Glob/Bash with graph context and redirect by deny to Gortex MCP tools. `enrich` never denies — PreToolUse downgrades to soft `additionalContext`, and a PostToolUse hook augments the actual tool output with graph context. PreCompact injects a condensed orientation snapshot before context compaction. Stop runs post-task diagnostics (`detect_changes``get_test_targets`, `check_guards`, `analyze dead_code`, `contracts check`).
- **Agent feedback loop** — unified `feedback` tool (`action: "record"` / `"query"`). Cross-session persistence improves future `smart_context` quality via feedback-aware reranking.
- **Per-community skills** — `gortex init --skills` (default on) auto-generates SKILL.md per detected community with key files, entry points, cross-community connections, and MCP tool invocations. The same routing table lands in every detected agent's per-repo instructions file. See [skills.md](skills.md).
- **Session memory** — `save_note` / `query_notes` / `distill_session` persist agent-authored notes per repo, auto-linked to symbols mentioned in the body. Notes survive daemon restarts and context compactions.
- **Development memories** — `store_memory` / `query_memories` / `surface_memories` / `edit_memory` / `rename_memory` — cross-session, symbol-linked durable knowledge with `kind` (invariant / constraint / convention / gotcha / decision / incident / reference), `importance` (1..5), `confidence` (0..1). Surfaced proactively by `surface_memories` when anchor symbols / files enter the working set.
- **Repository-local persistent notebook** — `notebook_save` / `notebook_find` / `notebook_list` / `notebook_show` / `notebook_used`. Markdown entries committed to git so agent journal entries surface in PR review.
- **Context export** — `export_context` tool + `gortex context` CLI render graph context as portable markdown/JSON briefings for sharing outside MCP.
- **Composed workflow primitives** — `get_architecture` (single-shot snapshot; pass `resolution` for a hierarchical file→package→service→system rollup), `replay_episode` (incident investigation from a symptom anchor), `get_knowledge_gaps`, `get_surprising_connections`, `verify_citation`, `check_onboarding_performed`, `check_references`, `generate_skill`, `gortex_wakeup`.
## Token economy
- **GCX1 compact wire format** — published, round-trippable text format. Opt-in per call via `format: "gcx"` on every list-shaped tool. Auto-served as the default for known clients (Claude Code, Cursor, VS Code, Zed, Aider, Kilo Code, OpenCode, OpenClaw, Codex) when no `format` is passed. **Median 27.4 % savings vs JSON**, best case 38.3 %, 100 % round-trip integrity. Spec: [wire-format.md](wire-format.md). Standalone MIT-licensed reference implementations: [`github.com/gortexhq/gcx-go`](https://github.com/gortexhq/gcx-go) and [`github.com/gortexhq/gcx-ts`](https://github.com/gortexhq/gcx-ts) (npm [`@gortex/wire`](https://www.npmjs.com/package/@gortex/wire)).
- **TOON fallback wire format** — second-tier compact text (~1015 % smaller than JSON, lossy but human-friendly). `format: "toon"`.
- **Budget-by-default MCP responses** — list-shaped tools cap each page at the project default budget and return `next_cursor` for the tail. Per-call caps via `max_bytes` *and* `max_tokens` (composable — tighter wins). Truncation markers ride on the response.
- **Graded-fidelity context economy** — `smart_context fidelity: "graded"` returns a `context_manifest` that tiers symbols by graph distance: focus symbols at full source, caller/callee ring as signature stubs, keyword-match remainder as outline — packed under one `token_budget` (which, like the seed count, scales with project size when unset). Large interchangeable symbol families are skeletonized to one representative. Every call also emits a `blast_radius` (callers by file + covering tests + a no-tests warning) and a file-clustered `working_set`. `estimate: true` projects a call's token cost before fetching. `if_none_match` turns a repeated call on unchanged code into a near-zero-token `not_modified` no-op. `compress_bodies` takes a `keep` predicate; `fidelity_globs` sets a per-glob full/compress/omit tier. `max_lines` does AST-aware salience truncation (control-flow skeleton kept, leaf runs collapse to `… N lines elided …`).
- **ETag conditional fetch** — content-hash `if_none_match` on source-reading tools avoids re-transmitting unchanged symbols.
- **Response post-filter re-cutting** — every large tool response captured into a bounded per-session ring; `ctx_grep` / `ctx_slice` / `ctx_peek` / `head_results` / `ctx_stats` re-cut a prior result without re-issuing the original query.
- **Token-savings tracking** — per-call `tokens_saved` field; session-level metrics in `graph_stats`; `gortex savings` three-bucket dashboard (Today / Last 7 days / All time) with USD avoided priced per model. See [savings.md](savings.md).
+167
View File
@@ -0,0 +1,167 @@
# Installing Gortex
Pre-built binaries are published to [GitHub Releases](https://github.com/zzet/gortex/releases) for linux/amd64, linux/arm64, darwin/amd64 (Intel Mac), darwin/arm64 (Apple Silicon), and windows/amd64. Every release is **cosign-signed**, ships **SLSA-3 provenance**, and is **VirusTotal-scanned** — see [Verifying releases](#verifying-releases-supply-chain-security) below.
**New to Gortex?** After installing, see [onboarding.md](onboarding.md) for the 15-minute walkthrough: `gortex install` (once per machine) → `gortex init` (once per repo) → verify your AI assistant uses graph tools → what to do if it doesn't.
## One-line install (Linux / macOS)
```bash
curl -fsSL https://get.gortex.dev | sh
```
Detects OS/arch, downloads the signed release tarball, verifies the SHA256 against `checksums.txt` (and the cosign signature if `cosign` is installed), drops the binary in `$HOME/.local/bin`, and adds that directory to your shell rc (`.zshrc` / `.bashrc` / fish config) inside an idempotent `# >>> gortex installer >>>` block. Re-runs upgrade in place and back up the previous binary as `gortex.previous`. No silent sudo.
**On macOS, if `brew` is on `PATH`, the script delegates to Homebrew automatically** (`brew install zzet/tap/gortex`, or `brew upgrade --cask gortex` on re-runs) so updates flow through `brew upgrade`. Set `GORTEX_NO_BREW=1` to force the tarball path instead.
Override defaults via env vars: `GORTEX_VERSION=v0.15.0` (pin a version — also disables Homebrew), `GORTEX_INSTALL_DIR=/usr/local/bin` (system-wide; you'll need write permission — also disables Homebrew), `GORTEX_NO_BREW=1` (skip Homebrew on macOS), `GORTEX_NO_PATH=1` (skip rc edits), `GORTEX_NO_VERIFY=1` (skip checksum + cosign), `GORTEX_FORCE=1` (overwrite without backup). Source: [`scripts/install.sh`](../scripts/install.sh).
## macOS — Homebrew
```bash
brew install zzet/tap/gortex
```
Homebrew strips the `homebrew-` prefix from tap repositories, so `zzet/homebrew-tap` is installed as `zzet/tap`. Updates via `brew upgrade`. No Gatekeeper prompt — `brew` doesn't set the quarantine attribute on downloads.
## Windows — one-line install (PowerShell)
```powershell
irm https://get.gortex.dev/install.ps1 | iex
```
Detects the architecture, downloads the signed `gortex_windows_amd64.zip`, verifies the SHA256 against `checksums.txt`, installs `gortex.exe` to `%LOCALAPPDATA%\Programs\gortex`, and adds that directory to your user `PATH`. Re-runs upgrade in place and back up the previous binary as `gortex.exe.previous`.
Override defaults via environment variables: `GORTEX_VERSION=v0.15.0` (pin a version), `GORTEX_INSTALL_DIR` (custom install directory), `GORTEX_NO_PATH=1` (skip the PATH update), `GORTEX_NO_VERIFY=1` (skip checksum verification), `GORTEX_FORCE=1` (overwrite without backup). Source: [`scripts/install.ps1`](../scripts/install.ps1).
Windows on ARM is supported via x64 emulation — the installer downloads the amd64 build there too. The daemon uses an AF_UNIX socket, which requires Windows 10 1803 or newer.
## Windows — Scoop
```powershell
scoop bucket add gortex https://github.com/gortexhq/scoop-bucket
scoop install gortex
```
The bucket holds the `gortex` manifest, which points at the signed GitHub Release archives. `scoop update gortex` upgrades in place.
## Linux — Debian / Ubuntu (.deb)
```bash
ARCH=$(dpkg --print-architecture) # amd64 or arm64
curl -LO "https://github.com/zzet/gortex/releases/latest/download/gortex_linux_${ARCH}.deb"
sudo dpkg -i "gortex_linux_${ARCH}.deb"
```
## Linux — RHEL / Fedora / CentOS (.rpm)
```bash
ARCH=$(uname -m); [ "$ARCH" = x86_64 ] && ARCH=amd64; [ "$ARCH" = aarch64 ] && ARCH=arm64
curl -LO "https://github.com/zzet/gortex/releases/latest/download/gortex_linux_${ARCH}.rpm"
sudo rpm -ivh "gortex_linux_${ARCH}.rpm"
```
## Linux — Alpine (.apk)
```bash
ARCH=$(uname -m); [ "$ARCH" = x86_64 ] && ARCH=amd64; [ "$ARCH" = aarch64 ] && ARCH=arm64
curl -LO "https://github.com/zzet/gortex/releases/latest/download/gortex_linux_${ARCH}.apk"
sudo apk add --allow-untrusted "gortex_linux_${ARCH}.apk"
```
## Direct binary download (any Linux or macOS)
```bash
# Pick the right asset for your OS/arch
OS=$(uname -s | tr '[:upper:]' '[:lower:]') # linux or darwin
ARCH=$(uname -m)
case $ARCH in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
esac
curl -LO "https://github.com/zzet/gortex/releases/latest/download/gortex_${OS}_${ARCH}.tar.gz"
tar -xzf "gortex_${OS}_${ARCH}.tar.gz"
sudo mv gortex /usr/local/bin/
```
On macOS, if you downloaded via browser (not `curl`), remove the quarantine flag once:
```bash
xattr -d com.apple.quarantine /usr/local/bin/gortex
```
## Verify the install
```bash
gortex version
```
## Verifying releases (supply-chain security)
Every GitHub release is:
- **Signed with [cosign](https://github.com/sigstore/cosign)** — keyless via GitHub's OIDC identity. Each artifact ships with matching `.sig` and `.pem` files that cryptographically prove the binary came from this repo's release workflow.
- **Attested with [SLSA-3 provenance](https://slsa.dev/spec/v1.0/levels#build-l3)** — a `multiple.intoto.jsonl` file attached to each release records the exact commit, builder, and workflow that produced every artifact. Tamper-evident and non-forgeable.
- **Scanned against ~72 AV engines via [VirusTotal](https://virustotal.com)** — the detection count (e.g. `0 / 72`) is posted in each release's notes, with a link to the full per-engine report.
You don't need to verify manually if you're installing via `brew` / `dpkg` / `rpm` — those paths go through package managers that check integrity themselves. Verification matters when you're redistributing Gortex downstream, running it inside a locked-down enterprise environment, or writing your own installer.
**cosign** — install once via `brew install cosign`, `apt install cosign`, or from [the cosign releases page](https://github.com/sigstore/cosign/releases). Then:
```bash
TAG=v0.14.0 # replace with the release you downloaded
FILE=gortex_linux_amd64.tar.gz # pick your artifact
BASE="https://github.com/zzet/gortex/releases/download/${TAG}"
curl -LO "${BASE}/${FILE}"
curl -LO "${BASE}/${FILE}.sig"
curl -LO "${BASE}/${FILE}.pem"
cosign verify-blob \
--certificate "${FILE}.pem" \
--signature "${FILE}.sig" \
--certificate-identity-regexp 'https://github\.com/zzet/gortex/\.github/workflows/.+@refs/tags/v.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"${FILE}"
```
Expected output: `Verified OK`. Anything else — stop and delete the binary.
**SLSA-3** — install [`slsa-verifier`](https://github.com/slsa-framework/slsa-verifier/releases) once. Then:
```bash
curl -LO "${BASE}/multiple.intoto.jsonl"
slsa-verifier verify-artifact "${FILE}" \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/zzet/gortex \
--source-tag "${TAG}"
```
Expected output ends with `PASSED: SLSA verification passed`.
**VirusTotal** — open the release page on GitHub. The notes include a per-asset scan table like `gortex_linux_amd64.tar.gz — 0 / 72` with a link to the full report. A non-zero detection on a Go binary is usually a false positive (Go's static linking + stripped symbols trips heuristics), but you should still compare against prior releases before trusting the download.
## From source
Requires Go 1.26+ and a C toolchain (the tree-sitter extractors are CGO — no way around it).
```bash
git clone https://github.com/zzet/gortex.git
cd gortex
go build -o gortex ./cmd/gortex/
sudo mv gortex /usr/local/bin/
```
Or without cloning:
```bash
go install github.com/zzet/gortex/cmd/gortex@latest
```
`go install` drops the binary into `$(go env GOBIN)` (default `~/go/bin`) — make sure that's on your `PATH`.
## Telemetry
Gortex's anonymous usage telemetry is **off by default** and fully opt-in. The first run of `gortex init` prints a one-time notice. Enable, disable, or inspect it with `gortex telemetry on | off | status`, or disable globally with `DO_NOT_TRACK=1`. Full details — what is and isn't collected, where state lives, and transmission gating — are in [telemetry.md](telemetry.md).
+253
View File
@@ -0,0 +1,253 @@
# Supported Languages
Gortex currently indexes **256 languages**. Each language has an extractor that
walks the source, emits symbols (functions, methods, types, interfaces,
variables) into the graph, and records `imports` / `calls` edges.
Three engine tiers are used, in order of decreasing extraction depth:
- **bespoke tree-sitter** (~30 languages) — full concrete syntax tree via a
vendored grammar with hand-tuned S-expression queries. Produces high-fidelity
symbols, resolved call edges, ORM/contract/dataflow extraction, and accurate
node ranges. Languages: Go, TypeScript, JavaScript, Python, Rust, Java, C#,
Kotlin, Swift, Scala, PHP, Ruby, Elixir, C, C++, Dart, OCaml, Lua, Bash, SQL,
HTML, CSS, Markdown, OrgMode, Protobuf, YAML, TOML, HCL, Dockerfile.
- **regex** (~60 languages) — pattern-matched line scanning with indent / brace /
keyword block heuristics. Captures top-level symbols and imports; call edges
vary per language. Used where no upstream tree-sitter grammar is available
(Verse, AL, SAS, Stata, AutoHotkey, CoffeeScript) or for legacy / niche
languages where the regex path was sufficient (ABAP, COBOL, Fortran, …).
- **forest signature-only** (~165 languages) — generic `*ts.Language`-driven
extractor wrapping `github.com/alexaandru/go-sitter-forest`. Reads the
grammar's bundled `tags.scm` (nvim-treesitter convention) when present and
falls back to a node-kind heuristic walker otherwise. Emits definitions
(function / method / type / interface / variable / constant / module) plus
`EdgeDefines` from the file. `@reference.call` / `@reference.function`
captures route to the enclosing function. **No** ORM / contract / dataflow /
scope-aware resolution — graduate a language to the bespoke tier when those
matter.
For sixteen of these languages an LSP server can additionally upgrade
edges from `ast_inferred` to `lsp_resolved` and unlock the
on-demand action tools (`get_diagnostics`, `get_code_actions`,
`apply_code_action`, `fix_all_in_file`). See **[lsp.md](lsp.md)** for the
server matrix, install commands, lifecycle knobs, and config schema.
## At a glance
| Category | Count | Languages |
|---|---|---|
| Core programming | 10 | Go, TypeScript, JavaScript, Python, Rust, Java, C#, C, C++, Kotlin |
| JVM, .NET, systems | 10 | Scala, Swift, PHP, Ruby, Groovy, F#, D, Zig, Vala, Objective-C |
| Scripting & shell | 10 | Bash, PowerShell, Batch, Perl, Raku, Lua, Tcl, VimScript, AutoHotkey, CoffeeScript |
| Functional | 8 | Haskell, OCaml, Elixir, Clojure, Erlang, Racket, Gleam, Emacs Lisp |
| Systems / emerging | 8 | Nim, Crystal, Mojo, Odin, V, Hare, Carbon, ReScript |
| Scientific & enterprise | 12 | Julia, R, MATLAB, Mathematica, SAS, Stata, Fortran, COBOL, Ada, Pascal, ABAP, Apex |
| Mobile & game | 4 | Dart, GDScript, Verse, ActionScript |
| Blockchain / smart contracts | 6 | Solidity, Move, Cairo, Noir, Tact, Ballerina |
| Template engines | 8 | Blade, EJS, Handlebars, Jinja, Twig, ERB, Liquid, Pug |
| Data, config, build | 12 | JSON, YAML, TOML, HCL/Terraform, SQL, Protobuf, Markdown, HTML, CSS, Dockerfile, Makefile, CMake |
| Niche / domain | 4 | Nix, AL (Business Central), Assembly (NASM/GAS/ARM/WLA-DX/CA65), Shaders (GLSL/HLSL) |
| Forest — frontend / templates | ~16 | Vue, Svelte, Astro, htmldjango, gotmpl, Haml, Slim, Glimmer, Razor, Templ, Tera, Mustache, Vento, SuperHTML, HEEx |
| Forest — schemas / IaC / IDLs | ~25 | GraphQL, Prisma, Jsonnet, Dhall, CUE, Pkl, Nickel, KCL, Bicep, Smithy, Cap'n Proto, Thrift, KDL, RON, TypeSpec, DBML, HJSON, HOCON, INI, JSON5, JSONC, Properties, SCFG, YANG, XML, DTD, EditorConfig, dotenv, Desktop, Devicetree, Kconfig, Linker script |
| Forest — shaders / hardware | ~14 | WGSL, GLSL, HLSL, CUDA, ISPC, VHDL, SystemVerilog, MLIR, LLVM, Jasmin, QBE, FIRRTL, PIO ASM, GDShader |
| Forest — docs / typesetting | 8 | LaTeX, Typst, AsciiDoc, Djot, Mermaid, Norg, BibTeX, PlantUML |
| Forest — functional / niche | ~26 | Agda, Idris, PureScript, Roc, Gren, Elm, Fennel, Janet, Hack, Haxe, Pony, C3, Aiken, Effekt, Eiffel, Jule, Koka, Luau, MoonBit, Motoko, Ralph, Scheme, SML, Wing, Common Lisp |
| Forest — build / DSL / testing | ~16 | Meson, Just, Beancount, Ledger, Gherkin, Hurl, Robot, Earthfile, Ninja, BitBake, Caddy, Snakemake, GN, Cooklang, Requirements, Cedar, CEL, Circom, Clarity, Rego, TLA+, Quint, Structurizr, GritQL, QL |
| Forest — DB / query | 8 | SPARQL, SurrealQL, PromQL, Kusto, SOQL, SOSL, PRQL, Turtle |
| Forest — data / lockfiles / shells / configs | ~28 | TSV, PSV, textproto, .po, PGN, todo.txt, go.mod / go.sum / go.work, godot_resource, Fish, Nushell, jq, Awk, Elvish, gitconfig / gitattributes / gitcommit / gitignore, Hyprlang, nftables, passwd, PEM, PoE filter, Puppet, ssh_config, sxhkdrc, tmux |
| Forest — misc | ~14 | DOT, gnuplot, GPG, Strace, VRL, Zeek, Ziggy + Schema, Starlark, SourcePawn, SCSS, RBS, OCamllex, DataWeave, USD, WIT |
| **Total** | **256** | |
## Core programming — deep extraction
Tree-sitter-backed languages with the most thorough extraction. `Meta["methods"]`
on interface nodes stores the expected method set for implementation matching.
| Language | Functions | Methods + MemberOf | Types | Interfaces | Imports | Calls | Variables |
|----------|-----------|-------------------|-------|------------|---------|-------|-----------|
| Go | Full | Full (receiver) | Full | Full + Meta["methods"] | Full | Full | Full |
| TypeScript | Full | Full | Full | Full + Meta["methods"] | Full | Full | Full |
| JavaScript | Full | Full | Full | - | Full | Full | Full |
| Python | Full | Full | Full | - | Full | Full | Partial |
| Rust | Full | Full (impl blocks) | Full | Full + Meta["methods"] | Full | Full | Full |
| Java | Full | Full | Full | Full + Meta["methods"] | Full | Full | Fields |
| C# | Full | Full | Full | Full + Meta["methods"] | Full | Full | Fields |
| Kotlin | Full | Full | Full | Full | Full | Full | Properties |
| Scala | Full | Full | Full | Full + Meta["methods"] | Full | Full | - |
| Swift | Full | Full | Full | Full + Meta["methods"] | Full | Full | - |
| PHP | Full | Full | Full | Full | Full | Full | - |
| Ruby | Full | Full | Full | - | Full | Full | Constants |
| Elixir | Full | Full (defmodule) | Modules | - | Full | Full | Attributes |
| C | Full | - | Structs/Enums | - | Full | Full | Globals |
| C++ | Full | Full | Classes/Structs | - | Full | Full | - |
| Dart | Full | Full | Classes/Enums/Mixins/Extensions | Abstract interface | Full | Full | Full |
| OCaml | Full | Full (class) | Types/Modules | Module types | open | Full | Full |
| Lua | Full | Full (M.func/M:method) | - | - | require() | Full | Full |
Recent extraction refinements (each covered by a per-feature CI golden): Java `@interface` annotation types index as interfaces and participate in implementation matching; Java `new T(){…}` and C# `new { … }` anonymous classes/types become synthetic type nodes with an `extends` edge (to the instantiated type, or to `object` for C#); JS/TS arrow-valued class fields (`x = () => {…}`) are emitted as callable methods; JS/TS named imports emit one `imports` edge per binding (alias-aware via `Edge.Alias`) and barrel re-exports emit `re_exports` edges; JS/TS cross-file imports resolve onto the target file/symbol for relative specifiers (`./x`, `../x`) and for `tsconfig.json` / `jsconfig.json` `compilerOptions.paths` / `baseUrl` path aliases (`@/lib/x`), so callers / usages / blast-radius span aliased imports the same as relative ones; chained / factory-call receivers (`New().Build()`) carry an inferred `receiver_type`. See [features.md](features.md) and [architecture.md](architecture.md) for the edge semantics.
## Data, config, build
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| JSON | `.json`, `.json5`, `.jsonc` | Top-level keys |
| YAML | `.yaml`, `.yml` | Top-level keys |
| TOML | `.toml` | Tables, key-value pairs |
| HCL / Terraform | `.tf`, `.tfvars`, `.hcl` | Resource / data / module / variable / output blocks |
| SQL | `.sql` | Tables (with columns), views, functions, indexes, triggers |
| Protobuf | `.proto` | Messages (with fields), services + RPCs, enums, imports |
| Markdown | `.md` | Headings, local file links, code-block languages |
| HTML | `.html`, `.htm` | Script / link references, element IDs |
| CSS | `.css` | Class selectors, ID selectors, custom properties, `@import` |
| Dockerfile | `Dockerfile`, `Containerfile`, `.dockerfile` | `FROM` (base images), `ENV` / `ARG` variables |
| Makefile | `Makefile`, `GNUmakefile`, `.mk`, `.make` | Targets, `define…endef`, `VAR = …`, `include` / `-include` |
| CMake | `CMakeLists.txt`, `.cmake` | `function(…)`, `macro(…)`, `add_library`, `add_executable`, `include(…)`, `set(…)` |
## Template engines
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| Blade (Laravel) | `.blade`, `.blade.php` | `@section` / `@yield` / `@component` / `@include`; `@extends` → import |
| EJS | `.ejs` | JS `function` / arrow inside `<% … %>`; `include('x')` → import |
| Handlebars / Mustache | `.hbs`, `.handlebars`, `.mustache` | `{{#block}}` as function; `{{> partial}}` → import; helper calls as edges |
| Jinja | `.jinja`, `.jinja2`, `.j2` | `{% block %}` / `{% macro %}`; `extends` / `include` / `import` / `from … import` |
| Twig | `.twig` | Same shape as Jinja |
| ERB | `.erb`, `.rhtml`, `.html.erb`, `.js.erb`, `.css.erb`, `.json.erb` | Ruby `def` / `class` inside `<% … %>`; `render 'x'` → import |
| Liquid | `.liquid` | `{% capture %}` as function; `{% assign %}` as variable; `{% include/render %}` → import |
| Pug | `.pug`, `.jade` | `mixin` / `block NAME` as function; `extends` / `include` → import |
## Blockchain / smart contracts
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| Solidity | `.sol` | Contracts, functions, events, modifiers, structs |
| Move (Sui/Aptos) | `.move` | `module`, `fun` / `public fun` / `entry fun`, `struct`, `use X::Y` |
| Cairo (StarkNet) | `.cairo` | `fn`, `struct` / `enum` / `trait` / `mod`, `use X::Y` |
| Noir (Aztec) | `.nr` | `fn`, `struct` / `trait` / `impl` / `mod`, `use dep::X::Y` |
| Tact (TON) | `.tact` | `contract` / `trait` / `message` / `struct`, `fun` / `receive` / `init`, `import "X"` |
| Ballerina | `.bal` | `function`, `service NAME on …`, `type NAME record {…}`, `class`, `import X/Y` |
## Scientific & enterprise
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| Julia | `.jl` | `function`, `struct`, `module`, `using` / `import` |
| R | `.r`, `.R` | Function defs; `library` / `require` / `source` |
| MATLAB | `.mlx` | `function` (end-terminated), `classdef`, `import a.b.c` |
| Mathematica | `.wl`, `.wls`, `.nb` | `name[args_] := body`, `SetDelayed`, `Get[…]` / `Needs[…]` |
| SAS | `.sas` | `proc` / `%macro` as function, `data` as variable, `%include` / `libname` |
| Stata | `.do`, `.ado` | `program define`, `local` / `global`, `use` / `do` / `include` |
| Fortran | `.f`, `.f90`, `.f95`, `.f03`, `.f08` | `subroutine` / `function` / `module`, `use X` |
| COBOL | `.cob`, `.cbl`, `.cpy` | Programs, paragraphs, sections, `COPY` |
| Ada | `.ada`, `.adb`, `.ads` | Packages, procedures, functions, `with` |
| Pascal / Delphi | `.pas`, `.pp`, `.dpr` | Units, procedures, functions, classes |
| ABAP (SAP) | `.abap` | `FORM` / `FUNCTION` / `METHOD` / `CLASS…DEFINITION`, `INCLUDE` |
| Apex (Salesforce) | `.cls`, `.trigger`, `.apex` | Classes, triggers, methods |
## Emerging languages
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| Mojo | `.mojo`, `.🔥` | `fn` / `def`, `struct` / `trait`, `from … import` / `import` |
| Odin | `.odin` | `name :: proc`, `name :: struct` / `enum` / `union`, `import "X"` / `foreign import` |
| V | `.v`, `.vsh` | `fn`, `struct` / `interface` / `enum` / `type`, `import`, `module` |
| Hare | `.ha` | `[export] fn`, `type X = struct` / `union` / `enum`, `use X;` |
| Carbon | `.carbon` | `fn`, `class` / `interface` / `adapter` / `choice`, `import` |
| ReScript | `.res`, `.resi` | `let` (function / variable), `type`, `module`, `open` / `include` |
| Gleam | `.gleam` | `[pub] fn`, `[pub] type`, `import X/Y` / `import X.{Y}` |
## Scripting & shell
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| Bash / Zsh | `.sh`, `.bash`, `.zsh` | Function defs, `source` / `.` |
| PowerShell | `.ps1`, `.psm1`, `.psd1` | `function`, `class`, `using` |
| Batch | `.bat`, `.cmd` | `:LABEL` as function, `call :LABEL` / `goto` as call edges |
| Perl | `.pl`, `.pm`, `.t` | `sub`, `package`, `use` / `require` |
| Raku | `.raku`, `.rakumod`, `.p6` | `sub`, `class`, `use` |
| Lua | `.lua` | Full tree-sitter (see core matrix) |
| Tcl | `.tcl` | `proc`, `package require`, `source` |
| VimScript | `.vim`, `.vimrc` | `function`, `command`, `source` |
| AutoHotkey | `.ahk`, `.ahk1`, `.ahk2` | Hotkeys, labels, functions (v1 + v2) |
| CoffeeScript | `.coffee` | `name = (args) ->` / `=>`, `class`, `require 'X'` |
## Functional
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| Haskell | `.hs`, `.lhs` | Full (see core matrix) |
| OCaml | `.ml`, `.mli` | Full (see core matrix) |
| Clojure | `.clj`, `.cljs`, `.cljc`, `.edn` | `defn`, `defrecord` / `deftype`, `defprotocol`, `require` / `use` |
| Erlang | `.erl`, `.hrl` | Functions, `-type` / `-record`, `-import` |
| Elixir | `.ex`, `.exs` | Full (see core matrix) |
| Racket | `.rkt`, `.ss` | `define`, `struct`, `require` |
| F# | `.fs`, `.fsi`, `.fsx` | `let`, `type`, `module`, `open` |
| Emacs Lisp | `.el` | `defun`, `defvar`, `defmacro`, `require` |
## Systems, mobile, game, niche
| Language | Extensions | What it extracts |
|----------|------------|------------------|
| D | `.d`, `.di` | `struct` / `class` / `interface` / `enum` / `union` / `template`, `import X.Y` |
| Zig | `.zig`, `.zon` | Structs / enums / unions, `@import`, functions, globals |
| Nim | `.nim`, `.nims`, `.nimble` | `proc` / `func` / `method` / `iterator` / `template` / `macro`, type defs, `import` |
| Crystal | `.cr` | `def`, `class`, `module`, `require` |
| Vala | `.vala`, `.vapi` | `namespace` / `class` / `interface` / `struct` / `enum`, methods, `using X;` |
| Groovy / Gradle | `.groovy`, `.gvy`, `.gy`, `.gradle` | Classes, `def`, imports |
| Objective-C(++) | `.m`, `.mm` | `@interface` / `@protocol` / `@implementation`, method selectors, `#import` / `@import` |
| ActionScript | `.as` | `package`, classes, interfaces, `function`, `import X.Y.*;` |
| Dart | `.dart` | Full (see core matrix) |
| Swift | `.swift` | Full (see core matrix) |
| GDScript | `.gd` | `func`, `class`, signals |
| Verse (UEFN) | `.verse` | `class` / `struct` / `enum` / `interface`, functions with specifier blocks, `using { /Path }` |
| Nix | `.nix` | Attribute sets, functions, `import` / `<nixpkgs>` |
| AL (Business Central) | `.al` | Tables, pages, codeunits, procedures |
| Assembly | `.asm`, `.s`, `.S`, `.nasm`, `.masm`, `.inc`, `.a65` | Labels as functions; `call` / `jsr` / `bl` / `jmp` as edges; NASM/MASM/GAS/WLA-DX/CA65/ARM |
| Shaders | `.glsl`, `.vert`, `.frag`, `.hlsl`, `.compute` | Functions, uniforms, `#include` |
## Extension collisions
A few extensions conflict across languages; the registration order in
`internal/parser/languages/register.go` decides which extractor wins.
| Extension | Registered as | Alternative |
|-----------|---------------|-------------|
| `.m` | Objective-C | MATLAB (uses `.mlx` instead) |
| `.v` | V | Verilog / Coq (not yet supported) |
| `.d` | D | D import files (`.di`) |
| `.as` | ActionScript | AssemblyScript (not supported) |
## Adding a language
Three paths, in order of decreasing effort:
1. **Bespoke tree-sitter** (deep extraction). Add a new sub-package under
[`internal/parser/tsitter/`](../internal/parser/tsitter/) wrapping the C
grammar, then a hand-tuned extractor under
[`internal/parser/languages/`](../internal/parser/languages/) that compiles
per-language S-expression queries. Use [`golang.go`](../internal/parser/languages/golang.go)
as a reference. Justified for languages where you need ORM / contract /
dataflow / scope-aware call resolution.
2. **Regex** (simple structural). Use [`nim.go`](../internal/parser/languages/nim.go)
or [`abap.go`](../internal/parser/languages/abap.go) as templates. Pick this
when no upstream grammar exists and signature-only is acceptable. Shared
helpers in [`helpers_indent.go`](../internal/parser/languages/helpers_indent.go)
(`findBlockEnd`, `findIndentedBlockEnd`, `findKeywordBlockEnd`, `lineAt`).
3. **Forest signature-only** (cheapest, broadest). If the language already has
a grammar in [`alexaandru/go-sitter-forest`](https://github.com/alexaandru/go-sitter-forest),
add `github.com/alexaandru/go-sitter-forest/<lang>` to `go.mod` and append
one row to `forestLanguages` in
[`forest_registrations.go`](../internal/parser/languages/forest_registrations.go):
`{"<name>", []string{".<ext>"}, <pkg>.GetLanguage, <pkg>.GetQuery}`.
`registerForestLanguages` skips the row at runtime if the name or any
extension is already claimed by a hand-written extractor. The framework
reads the grammar's bundled `tags.scm` when present and falls back to a
generic node-kind walker otherwise — see
[`internal/parser/forest/`](../internal/parser/forest/) for the
implementation.
All three paths must ship a `_test.go` with at least a happy-path and
empty-input case.
+143
View File
@@ -0,0 +1,143 @@
# LLM features (optional)
Gortex can delegate code-intelligence work to an LLM. Two features, both **off by default** and gated on configuring a provider:
- **`ask` MCP tool** — a research agent that drives Gortex's own tools (search, callers, contracts, dependencies) to answer an open-ended question and returns a synthesized answer, instead of the calling agent issuing many tool calls itself. `chain: true` traces cross-system call chains.
- **`search_symbols` `assist` arg** — LLM-assisted ranking on `search_symbols`: `auto` (engage on natural-language queries only), `on`, `off`, `deep` (adds a body-grounded verification pass that reads candidate code + callers and honestly drops irrelevant matches).
## Providers
The backend is chosen by the `llm.provider` key. Every provider except `local` is pure Go — available in any build; only `local` needs a `-tags llama` build (it embeds llama.cpp). Any OpenAI-compatible endpoint can also be registered as a custom provider (see below).
| `llm.provider` | Backend | Needs |
|----------------|---------|-------|
| `local` | in-process llama.cpp | a `-tags llama` build + a `.gguf` model file |
| `anthropic` | Anthropic Messages API | `ANTHROPIC_API_KEY` |
| `openai` | OpenAI Chat Completions | `OPENAI_API_KEY` |
| `azure` | Azure OpenAI Service | `AZURE_OPENAI_ENDPOINT` (or `llm.azure.endpoint`) + `AZURE_OPENAI_API_KEY` + a deployment name |
| `ollama` | Ollama daemon | a running Ollama + a pulled model |
| `claudecli` | Claude Code CLI subprocess | the `claude` binary on `$PATH` (signed in once). **No API key — reuses your Claude Code subscription.** |
| `codex` | OpenAI Codex CLI subprocess | the `codex` binary on `$PATH` (signed in once). **No API key — reuses your Codex / ChatGPT sign-in.** |
| `copilot` | GitHub Copilot CLI subprocess | the `copilot` binary on `$PATH` (signed in via `gh`). **No API key.** |
| `cursor` | Cursor Agent CLI subprocess | the `cursor-agent` binary on `$PATH` (signed in once). **No API key.** |
| `opencode` | opencode CLI subprocess | the `opencode` binary on `$PATH` (signed in once). **No API key.** |
| `gemini` | Google Gemini `generateContent` REST | `GEMINI_API_KEY` |
| `bedrock` | AWS Bedrock Converse API (SigV4-signed, no AWS SDK) | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (+ optional `AWS_SESSION_TOKEN`) |
| `deepseek` | DeepSeek Chat Completions (OpenAI-compatible) | `DEEPSEEK_API_KEY` |
| _`<custom>`_ | any OpenAI-compatible endpoint | registered with `gortex provider add` — see [Custom providers](#custom-providers) |
## Configuration
The `llm:` block goes in `~/.gortex/config.yaml` or a per-repo `.gortex.yaml` (repo-local wins per field, global fills the rest). Configure only the provider you use:
```yaml
# ~/.gortex/config.yaml (or per-repo .gortex.yaml)
llm:
provider: local # local | anthropic | openai | azure | ollama | claudecli | codex | copilot | cursor | opencode | gemini | bedrock | deepseek | <custom>
max_steps: 16 # agent tool-loop cap (provider-agnostic)
local: # provider: local — requires a `-tags llama` build
model: ~/models/qwen2.5-coder-7b-instruct-q4_k_m.gguf
ctx: 4096 # context window in tokens
gpu_layers: 999 # layers to offload to GPU (0 = CPU-only)
template: chatml # chatml | llama3
anthropic: # provider: anthropic
model: claude-sonnet-4-6 # or a tier sentinel: claude-haiku | claude-sonnet | claude-opus
api_key_env: ANTHROPIC_API_KEY # env var holding the key (this is the default)
# base_url: https://api.anthropic.com
# prompt_caching: true # opt-in ephemeral caching of the system prompt + tool (off by default)
# cache_ttl: 5m # 5m (free refresh) | 1h (2x write cost)
# thinking_mode: auto # off | auto | manual | adaptive (freeform requests only)
# thinking_budget_tokens: 8000 # manual-mode budget (min 1024)
# effort: high # output_config.effort: low|medium|high|max|xhigh (model-gated)
openai: # provider: openai
model: gpt-4o
api_key_env: OPENAI_API_KEY
# effort: high # optional reasoning_effort (minimal|low|medium|high)
azure: # provider: azure — Azure OpenAI Service
deployment: my-gpt4o # the Azure deployment name (selects the model)
endpoint: https://my-resource.openai.azure.com # or set AZURE_OPENAI_ENDPOINT
api_version: "2024-10-21"
api_key_env: AZURE_OPENAI_API_KEY
ollama: # provider: ollama
model: qwen2.5-coder:7b
host: http://localhost:11434
claudecli: # provider: claudecli — spawns the `claude` CLI per call
# binary: claude # binary name or absolute path (resolved via $PATH; default "claude")
model: sonnet # optional — forwarded as `--model`; empty = CLI default
# args: ["--allowed-tools", ""] # extra args appended after our flags (disable tools, etc.)
# timeout_seconds: 180 # cap per Complete call; 0 → 120s
codex: # provider: codex — spawns the OpenAI `codex` CLI per call
# binary: codex # binary name or absolute path (resolved via $PATH; default "codex")
model: gpt-5-codex # optional — forwarded as `--model`; empty = CLI default
# args: ["--sandbox", "workspace-write"] # extra args inserted before the prompt
# timeout_seconds: 180 # cap per Complete call; 0 → 180s
copilot: # provider: copilot — spawns the GitHub `copilot` CLI per call
model: claude-opus-4.1 # optional — forwarded as `--model`; empty = CLI default
# timeout_seconds: 180
cursor: # provider: cursor — spawns the `cursor-agent` CLI per call
model: sonnet # optional — forwarded as `--model`; empty = CLI default
# timeout_seconds: 180
opencode: # provider: opencode — spawns the `opencode` CLI per call
model: anthropic/claude-sonnet-4-6 # opencode's provider/model form
# timeout_seconds: 180
gemini: # provider: gemini — Google Gemini generateContent REST
model: gemini-2.5-pro
api_key_env: GEMINI_API_KEY
# base_url: https://generativelanguage.googleapis.com
bedrock: # provider: bedrock — AWS Bedrock Converse API (SigV4-signed)
model_id: anthropic.claude-sonnet-4-20250514-v1:0
region: us-east-1
# access_key_env: AWS_ACCESS_KEY_ID
# secret_key_env: AWS_SECRET_ACCESS_KEY
# session_token_env: AWS_SESSION_TOKEN # optional — for STS-issued creds
# base_url: https://bedrock-runtime.us-east-1.amazonaws.com # override for VPC endpoints
deepseek: # provider: deepseek — OpenAI-compatible Chat Completions
model: deepseek-chat
api_key_env: DEEPSEEK_API_KEY
# base_url: https://api.deepseek.com
routing: # optional — model routing for the `ask` agent
enabled: false # off by default; every run uses the provider's model
simple_model: claude-haiku-4-5 # low-complexity runs (empty = configured model)
complex_model: claude-opus-4-7 # multi-hop / refactor-scale runs
```
**Local provider idle unload.** The in-process llama.cpp model is unloaded after sitting idle (freeing its memory), and reloaded lazily on the next call. Default idle TTL is 10 minutes; override with `GORTEX_LLM_IDLE_TTL` (a verbatim Go duration, e.g. `5m`); `0` / `off` / `none` disables idle unloading, keeping the model resident once loaded.
When `llm.routing.enabled` is true, each `ask` run is scored by graph-derived task complexity — chain-tracing mode, multi-hop keywords, and how broad a slice of the multi-repo graph is in scope — and dispatched to `simple_model` or `complex_model` *within the active provider* (a cheap single-hop lookup costs less; a cross-system trace gets the capable model). The chosen `model` and `complexity` ride on the `ask` response. An empty tier model falls back to the provider's configured model.
Env overrides: `GORTEX_LLM_PROVIDER`, `GORTEX_LLM_MODEL` (targets the active provider's model — for `azure` it sets the deployment), `GORTEX_LLM_MAX_STEPS`, `GORTEX_LLM_{CLAUDECLI,CODEX,COPILOT,CURSOR,OPENCODE}_BINARY`, `GORTEX_LLM_BEDROCK_REGION`, `GORTEX_LLM_AZURE_{ENDPOINT,DEPLOYMENT,API_VERSION}`, `GORTEX_LLM_EFFORT`, `GORTEX_LLM_IDLE_TTL` (`local` provider only), and `GORTEX_LLM_ANTHROPIC_{PROMPT_CACHING,THINKING_MODE,HAIKU_MODEL,SONNET_MODEL,OPUS_MODEL}`. API keys are read from the env var named by `api_key_env` — never stored in the config file.
If the active provider can't be constructed (missing model or API key, `local` without a `-tags llama` build, `claudecli` / `codex` without the `claude` / `codex` binary on `$PATH`, `bedrock` without AWS credentials), the daemon logs a warning and the LLM features stay absent — the rest of Gortex is unaffected. If the `ask` tool isn't in `tools/list`, no provider is configured.
The `assist` prompts are tiered automatically — terser for hosted frontier models, rule-heavy for small local ones. `deep` mode in particular benefits from a 7B-class or hosted model; small local models are unreliable on its disambiguation cases.
## Custom providers
Any OpenAI-compatible Chat Completions endpoint — OpenRouter, Groq, Together, a self-hosted vLLM, an internal gateway — can be registered by name and then selected like a built-in:
```bash
gortex provider add groq \
--base-url https://api.groq.com/openai/v1 \
--model llama-3.3-70b-versatile \
--api-key-env GROQ_API_KEY \
--price-input 0.59 --price-output 0.79
gortex provider list
gortex provider show groq
gortex provider remove groq
```
Then set `llm.provider: groq` (or `GORTEX_LLM_PROVIDER=groq`). Entries are stored in `providers.json` next to your config; a repo-local `.gortex/providers.json` is loaded only when `GORTEX_ALLOW_LOCAL_PROVIDERS=1` (so a cloned repo can't silently repoint your LLM calls). A custom provider may not shadow a built-in name. Per entry: `base_url` (http/https, including any version segment — gortex appends `/chat/completions`), `model`, optional `api_key_env` (omit for keyless local endpoints), `schema_mode` (`json_schema` (default) | `json_object` | `prompt` — use the looser modes for gateways without strict structured-output support), extra headers, and informational USD pricing.
+431
View File
@@ -0,0 +1,431 @@
# LSP Routing
Gortex uses the Language Server Protocol (LSP) for two things:
1. **Compiler-grade resolution during enrichment.** When the resolver
leaves an edge as `ast_inferred` or `text_matched`, an LSP server
(`textDocument/definition` + `textDocument/implementation`) upgrades
it to `lsp_resolved` / `lsp_dispatch`. This raises precision on
`find_usages`, `get_callers`, `find_implementations`, and the
contract pipeline's binding resolver.
2. **On-demand actions** via the four MCP tools that wrap the LSP
action surface: `get_diagnostics`, `get_code_actions`,
`apply_code_action`, `fix_all_in_file`.
Both paths route through the same per-daemon `lsp.Router` — one
subprocess per language server, lazy-spawned on first request, idle
reaper at ten minutes, LRU eviction at six concurrent.
## Server registry
Sixteen servers ship in the registry today
(`internal/semantic/lsp/registry.go`):
| Spec name | Command | Languages | Default priority |
| ---------------------------- | -------------------------------- | --------------------------- | ---------------- |
| `gopls` | `gopls` | go | 3 |
| `typescript-language-server` | `typescript-language-server` | typescript, javascript | 5 |
| `pyright` | `pyright-langserver` | python | 5 |
| `rust-analyzer` | `rust-analyzer` | rust | 5 |
| `clangd` | `clangd --background-index` | c, c++, objc, objc++ | 5 |
| `jdtls` | `jdtls` | java | 6 |
| `kotlin-language-server` | `kotlin-language-server` | kotlin | 6 |
| `omnisharp` | `omnisharp -lsp` | csharp | 5 |
| `ruby-lsp` | `ruby-lsp` | ruby | 5 |
| `phpactor` | `phpactor language-server` | php | 5 |
| `lua-language-server` | `lua-language-server` | lua | 5 |
| `sourcekit-lsp` | `sourcekit-lsp` | swift | 5 |
| `haskell-language-server` | `haskell-language-server-wrapper`| haskell | 5 |
| `elixir-ls` | `elixir-ls` | elixir | 5 |
| `ocamllsp` | `ocamllsp` | ocaml | 5 |
| `zls` | `zls` | zig | 5 |
Several specs declare `AlternativeCommands` — Gortex picks the first
binary on `PATH`:
- `pyright` → falls back to `jedi-language-server` or `pylsp`.
- `ruby-lsp` → falls back to `solargraph stdio`.
- `phpactor` → falls back to `intelephense --stdio`.
Lower priority numbers win when more than one provider serves the same
language. `gopls` is `3` so it beats SCIP-based providers (`5`) for Go;
`jdtls` is `6` so it's lower-priority than the SCIP-java path that
ships separately.
`clangd` is the one server that needs a compilation database for full
enrichment. Point it at a `compile_commands.json` (or a `compile_flags.txt`
/ `.clangd`) at the repo root; without one, gortex degrades that repository's
enrichment to reference confirmation — see
[Enrichment cost model](#enrichment-cost-model).
## Enabling a server
Add it to `.gortex.yaml`:
```yaml
semantic:
enabled: true
mode: typecheck # or "callgraph"
providers:
- name: gopls
enabled: true
- name: rust-analyzer
enabled: true
- name: pyright
enabled: true
```
Names match the **Spec name** column above. The router pre-registers
every enabled spec at boot but does not spawn anything yet —
subprocesses start the first time a tool calls into them.
## Installing the underlying servers
Gortex does not ship the LSP binaries. Install the ones you want to
use; the router falls back gracefully when a binary is missing
(`SpecAvailable(name)` returns false → tool returns a structured
`no_lsp_for` error instead of hanging).
```bash
# Go
go install golang.org/x/tools/gopls@latest
# Rust
rustup component add rust-analyzer
# Python (pick one)
npm install -g pyright # recommended
pip install jedi-language-server # alt
pip install python-lsp-server # alt (pylsp)
# TypeScript / JavaScript
npm install -g typescript typescript-language-server
# C / C++ / Objective-C
brew install llvm # ships clangd
# or apt install clangd
# Java
brew install jdtls
# Kotlin
brew install kotlin-language-server
# C#
dotnet tool install --global Microsoft.OmniSharp
# Ruby (pick one)
gem install ruby-lsp # recommended
gem install solargraph # alt
# PHP (pick one)
composer global require phpactor/phpactor
npm install -g intelephense # alt
# Lua
brew install lua-language-server
# Swift
# Bundled with Xcode toolchain on macOS; no separate install.
# Haskell
ghcup install hls
# Elixir
brew install elixir-ls
# OCaml
opam install ocaml-lsp-server
# Zig
brew install zls
```
Verify with `gortex daemon status` — the LSP-router section lists
`alive`, `last_used`, and the resolved command for each running
server. Newly enabled specs show up only after the first request that
needs them.
## Lifecycle
The router applies these defaults in `cmd/gortex/server.go` and
`cmd/gortex/mcp.go`:
| Knob | Default | What it does |
| ----------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
| `WithIdleTimeout` | 10 minutes | Subprocess closes if no `For()` / `ForSpec()` call lands in this window. |
| `WithReaperInterval` | 1 minute | Background tick invokes `Reap()` to enforce the idle timeout. |
| `WithMaxAlive` | 6 servers | LRU eviction kicks in when a seventh distinct server would spawn — the least-recently-used one closes. |
These defaults suit a polyglot workspace where most languages are
touched only intermittently. Override them by editing the
`lsp.NewRouter(...).With...` chain in your build if you need a longer
warm pool or a tighter memory bound.
## Enrichment cost model
The resolution path (use 1 at the top of this page) runs as a batch
**enrichment pass** — one pass per (repository × language server) — that walks
the repo's nodes for that language and upgrades edges the AST resolver left
ambiguous. A pass runs up to five phases:
1. **Interface pass** — for each interface / abstract declaration, asks the
server for its implementations (`textDocument/implementation`) and links the
dispatch edges.
2. **Confirm pass** — for every ambiguous edge (one whose confidence the
resolver could not raise to certainty), asks for the referent's references
(`textDocument/references`) and confirms or refutes the edge. Grouped by
referent file so each file opens once.
3. **Definition-rebind fallback** — for edges the confirm pass could not settle
from references alone, asks for the call site's definition
(`textDocument/definition`) and rebinds the edge to the concrete target.
4. **References-add pass** — only for servers that expose references but not a
call hierarchy; recovers the caller edges a declaration's references imply.
5. **Per-file sweep** — the whole-repo hover / hierarchy phase. Per function or
method it prepares a call hierarchy and reads outgoing (and, where it helps,
incoming) calls; per type or interface it reads the super/subtype hierarchy;
per symbol it hovers for a type string.
Each phase opens the files it touches on the language server (`didOpen`) and
closes them when done (`didClose`). The first four phases carry most of the
precision gain; the sweep carries most of the cost — on a warm restart, where
every node already reloads with its resolved edges and type stamp, the sweep is
almost pure churn.
### Bounded document session
All five phases share one **document session** per pass. It opens each file on a
server at most once while the file is in use, keeps recently-closed files warm
in an LRU tail so a later phase reuses the open document instead of reopening
it, and closes everything at pass end. The simultaneously-open ceiling is
`2 × max_parallel` documents per server (floor 4): the pinned working set stays
within `max_parallel` (bounded by the pass's file semaphore) and the extra
headroom is the warm tail. `didOpen` / `didClose` stay paired per (file,
server) — a file opened on a server that later dies still gets its close attempt
on that server — while the server's open-document set stays bounded.
This matters most for `clangd` without a compilation database: every `didOpen`
triggers a full fallback-preamble + AST rebuild, so reopening the same file
across phases multiplies that cost.
### Sweep modes
The per-file sweep (phase 5) is gated by a **sweep mode**:
| Mode | Behaviour |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `demand` | **Default.** Sweep a file only when its declarations still carry unresolved same-name call candidates (enrichment demand) or it declares a type / interface whose super/subtype hierarchy the sweep recovers. A file with neither signal is skipped, so a warm restart pays no sweep for it. Within a swept file, a node that already carries a `semantic_type` stamp from an earlier pass is not re-hovered. |
| `full` | Sweep every file of the language — maximal hover and hierarchy coverage, the right choice for a first cold index. |
| `off` | Skip the per-file sweep entirely. The confirm / rebind / references-add / interface passes still run, so edge tiers and recall are unaffected — only hover type strings and sweep-recovered hierarchy edges are dropped. |
Resolution precedence, highest first:
1. The `GORTEX_LSP_SWEEP` environment variable (`demand` / `full` / `off`, with
`none` accepted as an alias for `off`; case- and whitespace-insensitive). Set
it where you launch `gortex daemon` / `gortex server` to dial one run without
editing config.
2. The `.gortex.yaml` key `semantic.lsp_sweep` (same values).
3. A per-server default. Most servers have none and fall through to the global
default; **rust-analyzer defaults to `full`**. Rust method calls bind
overwhelmingly to standard-library receiver types the graph never indexes, so
static confirmation leaves rust-analyzer's net-new call-hierarchy edges on the
table — its recall lives in the full sweep. Either operator source above
overrides it, so `semantic.lsp_sweep: demand` (or the env) puts rust-analyzer
back on the demand gate.
4. The `demand` default.
An unrecognised value at any level is ignored and the next source applies.
```yaml
semantic:
enabled: true
lsp_sweep: demand # demand (default) | full | off
```
### Incoming-calls policy
In the sweep's call-hierarchy step, outgoing calls are always fetched — the
sweep visits every caller, so a declaration's outgoing hops alone reconstruct
every intra-repo static call edge. Incoming calls are fetched only where the
outgoing side is blind: a dispatch-relevant declaration (whose concrete callers
land on the incoming side of an interface method, not its outgoing side) or one
that still carries unresolved demand — or under `full` mode. Files are swept
demand-first, so a declaration whose incoming calls are skipped at a deadline
cut still loses no reachable edge. The count of declined round trips rides on
the pass-complete log as `incoming_calls_skipped`.
### Compile-database preflight (clangd)
Before enriching a repository with a server that needs a compilation database
(`clangd`), gortex checks the workspace root for one of:
- `compile_commands.json` — canonical CMake / Bear output
- `build*/compile_commands.json` — out-of-tree build directories
- `compile_flags.txt` — clangd's flat-flags fallback
- `.clangd` — a hand-written clangd config
If none is present, clangd rebuilds a full fallback AST on every `didOpen`, and
opening a header directly makes it a standalone translation unit with no
cross-file signal to show for the cost. Rather than drive that churn, the pass
**degrades to reference confirmation**: it runs the confirm and rebind passes
(which work inside the fallback translation unit on fallback flags) and skips
the interface pass, the references-add pass, the entire per-file sweep, and all
header files. Edge tiers and confirmed / refuted edges are unaffected; hover
type strings and call / type-hierarchy edges are absent for that pass.
A degraded pass warns once with the remediation and marks its result
`degraded`. `index_health` surfaces a recommendation naming the repository and
provider:
> generate compile_commands.json (cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON,
> bear -- make, or meson) at the repo root, then reindex
Degradation is deliberate, not a failure — `semantic_enrichment_ok` stays true.
### Telemetry
The pass-complete log line (`LSP enrich: hover phase complete`) carries the cost
accounting for the pass:
| Field | Meaning |
| ----- | ------- |
| `sweep_mode` | The effective sweep mode for the pass. |
| `did_opens` | Total `didOpen` calls across every phase. |
| `reopened_files` | Files opened more than once (a warm-tail miss). |
| `doc_evictions` | LRU evictions — a `didClose` forced to make room. |
| `peak_open_docs` | Peak simultaneously-open documents on any one server. |
| `req_references`, `req_implementations`, `req_definitions`, `req_hovers` | Per-method request counts. |
| `req_prepare_call_hierarchy`, `req_outgoing_calls`, `req_incoming_calls` | Call-hierarchy request counts. |
| `incoming_calls_skipped` | Incoming-calls round trips the policy above declined. |
| `req_prepare_type_hierarchy`, `req_supertypes`, `req_subtypes` | Type-hierarchy request counts. |
| `skipped_already_stamped` | Nodes whose hover was skipped because they already carried a `semantic_type`. |
A degraded pass logs `LSP enrich: degraded pass complete (reference
confirmation only)` instead, with `degraded=true` and the document-open counters
but no hover / hierarchy counts.
## Diagnostics
Two surfaces:
### Pull (poll-based)
`get_diagnostics` returns the most recent `publishDiagnostics` payload
the LSP server produced for a file. Use it for one-shot reads, batch
checks, or contexts where the agent doesn't maintain a long-lived
session.
### Push (opt-in)
`subscribe_diagnostics` opts the calling MCP session into
`notifications/diagnostics` push events. After subscribing, every LSP
`publishDiagnostics` for any router-managed server is forwarded to the
session as an MCP notification with this shape:
```json
{
"method": "notifications/diagnostics",
"params": {
"uri": "file:///abs/path/to/main.go",
"path": "/abs/path/to/main.go",
"server": "gopls",
"diagnostics": [
{ "range": {"start": {"line": 41, "character": 4}, "end": {...}},
"severity": 1, "message": "missing return", "source": "gopls" }
]
}
}
```
Push semantics:
- **Opt-in per session.** Sessions that never call `subscribe_diagnostics`
receive nothing — no broadcast spam.
- **Delta-only.** Identical re-publishes (which some servers emit on
every save even when nothing changed) are suppressed at the
broadcaster — your subscribers only see real changes.
- **All-router-managed servers.** One subscription covers every spec
the user enabled in config. The `server` field on each notification
identifies which LSP produced the payload.
- **Non-blocking.** Notifications use `SendNotificationToAllClients`
which drops to an error hook when a session's notification channel
is full — slow consumers don't block the LSP message-pump.
Call `unsubscribe_diagnostics` to opt back out (idempotent).
Pair with `get_code_actions` + `apply_code_action` + `fix_all_in_file`
for the full edit-time diagnostic loop without polling.
## Language-specific behaviour
A few servers need per-language handling beyond the generic router. These
knobs are **environment variables** read by the daemon process — set them
where you launch `gortex daemon` / `gortex server`. There is no `.gortex.yaml`
key for them today.
### C# — NuGet audit advisories (`omnisharp` / `csharp-ls`)
The Roslyn `MSBuildWorkspace` these servers build escalates a NuGet audit
*warning* (the `NU19xx` family — e.g. a transitively vulnerable package) to a
**fatal** project-load failure and drops every project that references the
flagged package. Those projects then have no compilation, so the server emits
false `CS####` "unresolved type / namespace" diagnostics — even though
`dotnet build` / `dotnet test` keep `NU19xx` a non-fatal warning and succeed.
Gortex applies two complementary, C#-scoped fixes, **both on by default**:
| Env var | Default | Effect |
| --- | --- | --- |
| `GORTEX_LSP_CSHARP_RESTORE` | on | Before spawning the C# server, run `dotnet restore -p:NuGetAudit=false` in the workspace so the MSBuild workspace loads every project (root-cause fix). Best-effort: a failure logs and falls through to a normal spawn; skipped on passive IDE attach and when `dotnet` is not on `PATH`. |
| `GORTEX_LSP_CSHARP_DIAG_FILTER` | on | Strip diagnostics whose code is the `NU####` NuGet family from `publishDiagnostics` before storing / fanning out (symptom fix). Deliberately narrow — real `CS####` compiler diagnostics always pass through. |
Set either to a falsey value (`0` / `off` / `false` / `none`) to disable it —
e.g. `GORTEX_LSP_CSHARP_RESTORE=0` for offline / air-gapped indexing or to
keep indexing off the network. Restore is on by default because gortex only
indexes repositories you explicitly add (it never auto-discovers), and
spawning the C# server already evaluates the project's MSBuild graph — so the
restore adds no execution surface beyond the workspace load it precedes. A
successful restore logs `lsp: csharp pre-restore complete (NuGetAudit
suppressed)`; a failure logs `lsp: csharp pre-restore failed; spawning server
anyway` with the restore output tail.
### Java — build-backed resolution (`jdtls`)
By default `jdtls` runs in a **no-build** mode (JRE-only classpath, Maven /
Gradle import and autobuild disabled) so indexing an untrusted Java repo never
runs its build. Resolution is more limited in this mode (jdtls falls back to
an "invisible project"). Set `GORTEX_LSP_JDTLS_TRUST_BUILD=1` (or `true`) to
allow full Maven / Gradle import + autobuild for higher-fidelity resolution —
**opt-in**, because it executes the repository's build tooling. Enable it only
for repositories you trust.
## Troubleshooting
- **`no_lsp_for` error:** the file extension didn't match any
registered spec. Either the spec isn't enabled in `.gortex.yaml`, or
the binary isn't on `PATH`. Run the spec's `--version` directly to
confirm install.
- **`router spawn <name>: ...` error:** the binary was on `PATH` at
boot but the subprocess failed to initialise (commonly a missing
dependency such as `node` for `pyright`, or a workspace-config
mismatch). The error surfaces the LSP server's stderr.
- **Server keeps restarting:** the idle reaper closed it, then the
next request re-spawned. Increase `WithIdleTimeout` if this hurts
warm-cache benchmarks.
- **High memory under polyglot load:** lower `WithMaxAlive` from 6 to
3-4. The LRU evicts the least-recent server transparently.
## Implementation notes
- The router lives at `internal/semantic/lsp/router.go`. It satisfies
the `semantic.LSPRouter` interface so `semantic.Manager` can drive
batch enrichment without taking a hard import dependency on the lsp
package (which would create a cycle — lsp already imports semantic
for the Provider interface).
- `tools_lsp.go::lspProviderForPath` queries the router first; if no
router is wired (legacy boot paths, tests), it falls back to a scan
through `Manager.AllProviders()` so user-defined daemons (specs not
in the registry) still work.
- One `*lsp.Provider` per spec, regardless of how many MCP sessions
hit it. Concurrency is bounded by `ServerSpec.MaxParallel` (6-10
inflight requests per server depending on the spec).
+395
View File
@@ -0,0 +1,395 @@
# MCP surface
Gortex exposes a knowledge-graph query surface over the [Model Context Protocol](https://modelcontextprotocol.io): **100+ tools, 18 resources, 3 prompts**. Agents call the same surface from stdio, the daemon Unix socket, or the MCP 2026 Streamable HTTP endpoint.
- [Tool discovery (lazy mode)](#tool-discovery-lazy-mode)
- [Restricting the tool surface (presets)](#restricting-the-tool-surface-presets)
- [Core navigation](#core-navigation)
- [Graph traversal](#graph-traversal)
- [Search & traversal extensions](#search--traversal-extensions)
- [Dataflow (CPG-lite)](#dataflow-cpg-lite)
- [Structural search](#structural-search)
- [Diagnostics & code actions](#diagnostics--code-actions)
- [Proactive notifications](#proactive-notifications)
- [Coding workflow](#coding-workflow)
- [Agent-optimized (token efficiency)](#agent-optimized-token-efficiency)
- [Response re-cutting](#response-re-cutting)
- [Analysis](#analysis)
- [Proactive safety](#proactive-safety)
- [Code quality](#code-quality)
- [Code generation](#code-generation)
- [PR review](#pr-review)
- [Multi-repo management](#multi-repo-management)
- [Live editor buffers (overlay sessions)](#live-editor-buffers-overlay-sessions)
- [Speculative execution](#speculative-execution)
- [MCP resources (18)](#mcp-resources-18)
- [MCP prompts (3)](#mcp-prompts-3)
## Tool discovery (lazy mode)
By default the server ships a curated **`core`** preset in **`defer`** mode (see [presets](#restricting-the-tool-surface-presets)): ~34 dev-cycle workhorse tools are published eagerly in the initial `tools/list`, and the rest of the ~180-tool catalogue is deferred — fetched on demand through `tools_search`. A cold session therefore pays for the workhorse schemas, not the whole surface. Opt back into the full eager surface with preset `full` (`GORTEX_TOOLS=full`).
`tools_search` returns each deferred tool's schema **inline** (in a `<functions>{…}</functions>` block) and promotes it into `tools/list`, firing `notifications/tools/list_changed`. Clients that honour that notification (or read the inline schema) reach deferred tools transparently. `GORTEX_LAZY_TOOLS=1` is the older, all-or-nothing switch that defers everything except a hard-coded hot set regardless of preset; the `core`/`defer` default supersedes it for the common case.
```jsonc
// With GORTEX_LAZY_TOOLS=1 set:
// Browse — list deferred tool names without schemas.
{"name":"tools_search","arguments":{}}
// Fetch schemas for specific tools by name (auto-promotes them into tools/list).
{"name":"tools_search","arguments":{"query":"select:flow_between,taint_paths,find_clones"}}
// Keyword search with required-token filter, ranked, capped at max_results.
{"name":"tools_search","arguments":{"query":"+overlay drop","max_results":5}}
// Fuzzy keyword match across name + description.
{"name":"tools_search","arguments":{"query":"memories invariants"}}
```
Returned tools are auto-promoted (`promote:false` opts out) and the server fires `notifications/tools/list_changed`. The `tool_profile` tool reports the active surface — which tools are live vs. deferred, their scopes and categories, the active preset (below), and (with a `tool` argument) a single tool's enabled status.
**Two front doors over one set of handlers.** The same tool handlers back both the MCP transport and the `gortex` CLI. The daemon routes a tool call **by name** over its socket, independent of which tools a given client eagerly published in `tools/list` — so the CLI reaches the **full** surface, including tools that are deferred under the `core` preset, with no `tools_search` round-trip. `gortex call <tool>` invokes any tool by name, and the dedicated CLI verb groups (`gortex edit …`, `gortex memory …`, `gortex analyze`, `gortex flow` / `taint` / `clones` / `feedback`) are ergonomic front-ends over the most-used tools. Driving Gortex through those verbs mounts no tool schemas into the model's context — see [`cli.md`](cli.md#full-tool-surface-from-the-cli) and the consumption-path trade-off in [`cli.md`](cli.md#choosing-a-consumption-path).
## Restricting the tool surface (presets)
The full ~180-tool surface is more than many agents need. A **tool preset** picks what the server publishes — the basis both for the lean shipped default and for a minimal, headless editing harness (an agent on a trusted box driving a remote daemon through a small, fixed tool set).
Seven built-in presets:
| Preset | Surface |
|--------|---------|
| `agent` (**default for known coding-agent clients**) | the lean coding-agent working set (~20 tools): `explore` (the one-shot localization verb) + search/navigate + read (incl. `batch_symbols`) + orient + edit/verify. Parameter descriptions are compacted (the full prose is one `tools_search` / `full` hop away). Aliases: `coding-agent` |
| `core` (**default for editors / unknown clients**) | the curated dev-cycle set (~35 tools): orient (incl. `explore`) + search/navigate + read + edit + verify/test + `analyze` + review + the memory workflow. Aliases: `default`, `classic` |
| `full` | every tool (the pre-`core` behaviour — opt back in here) |
| `readonly` | everything except the mutating tools (`edit_file`, `write_file`, `index_repository`, …) |
| `edit` | the minimal headless editing set — orient + navigate + mutate + verify (`smart_context`, `search_symbols`, `find_files`, `edit_file`, `verify_change`, `get_test_targets`, …) |
| `nav` | read-only navigation / exploration; no editors |
| `localization` | the diet "where is the code that does X" set (~10 tools, read-only, compacted descriptions): `smart_context` + search + trace + read. The eager list is sourced from the instruction-profile table, so this surface and the `localization` profile's instructions body cannot drift. Aliases: `locate`, `find` |
`tool_profile` and `tools_search` are always kept. Layer per-tool deltas on any preset with `allow` / `deny`.
**Client-aware default.** With no `GORTEX_TOOLS` / config preset, the server picks the default per connection: a **known coding-agent client** (the same set that defaults the wire format to GCX — `claude-code`, `cursor`, `vscode`, `zed`, `aider`, `kilocode`, `opencode`, `openclaw`, `codex`, `omp-coding-agent`) gets `agent`; every other client keeps `core`. `GORTEX_TOOLS` always overrides. The `gortex mcp` proxy forwards its `GORTEX_TOOLS` / `--tools` to the daemon in the handshake, so a client's preset applies over the shared daemon (it can both narrow and widen the surface, not just subtract).
**Instruction profiles.** The machine's active instruction profile (`gortex instructions switch <core|localization|full>` — see [`cli.md`](cli.md#gortex-instructions--instruction-profiles)) can carry a tool preset; sessions pick it up between the forwarded spec and the client-aware default. Full precedence: **forwarded spec (`GORTEX_TOOLS` / `--tools`) > operator-pinned `mcp.tools` config > active instruction profile > client-aware default > server default**. The shipped `core` profile carries no preset, so nothing changes until a machine explicitly switches; profile changes apply to new sessions only.
**Two modes** (`mode`):
- `defer` (the default mode for `core`) — non-allowed tools are kept out of the cold `tools/list` but stay reachable through `tools_search`, which returns their schema inline and promotes them (firing `notifications/tools/list_changed`). The lean-but-complete surface: nothing is lost, the rare tool is one discovery call away.
- `hide` (the default mode for the explicit `edit` / `nav` / `readonly` harness presets) — non-allowed tools are removed from `tools/list` **and** calls to them are hard-blocked. The locked-down surface; works identically on every client.
Select a preset three ways (precedence: **env > flag > config > default**):
```yaml
# .gortex.yaml — config file
mcp:
tools:
preset: full # agent | core (default) | full | readonly | edit | nav
mode: defer # defer | hide
allow: [find_files] # add tools on top of the preset
deny: [write_file] # remove tools from the preset
```
```bash
# env (overrides config) — spec is "preset,+add,-remove"
export GORTEX_TOOLS="edit,+analyze,-write_file"
export GORTEX_TOOLS_MODE=hide
# CLI flags (override config; env still wins)
gortex mcp --tools edit --tools-mode hide
gortex daemon start --tools readonly # propagates to the detached child
```
**Per-connection (client-driven) scoping.** The selectors above applied to `gortex daemon start` narrow the *whole daemon* for every client. To let one client pick its own surface while the daemon keeps serving the full set to everyone else, set `--tools` / `GORTEX_TOOLS` on that client's **`gortex mcp`** invocation — the stdio proxy filters just that connection's `tools/list` and blocks calls to tools outside the set. Because the filter applies from the first `tools/list`, it works on **every** MCP client (no `tools/list_changed` dependency).
```jsonc
// An MCP client config giving this client a minimal editing surface,
// against a daemon that still serves the full catalogue to others:
{
"command": "gortex",
"args": ["mcp", "--tools", "search_symbols,find_files,edit_file,verify_change"],
// or: "args": ["mcp", "--tools", "edit"] (a named preset)
// or: "env": { "GORTEX_TOOLS": "readonly" }
}
```
A spec whose first token isn't a known preset (`search_symbols,find_files,…`) is an **explicit allow list** — exactly those tools — for experts who want to hand-pick the surface. A known preset followed by names (`edit,find_files`) keeps preset semantics plus the extra tools.
`tool_profile` reports the active `preset` / `preset_mode`, the narrowed `live` set, and a `categories{}` map grouping every tool into a functional family (nav / read / edit / analysis / review / pr / memory / overlay / subscription / enrich / workspace / admin) for prefix-style filtering.
**Prompt-injection screening.** Every tool call is screened by middleware that scans arguments and result text for injection patterns. On a hit it attaches a non-blocking `_meta.gortex_security` advisory — the call still succeeds and the result body is never mutated. Disable with `GORTEX_MCP_SANITIZE=0`.
## Core navigation
| Tool | Description |
|------|-------------|
| `graph_stats` | Node/edge counts by kind, language, per-repo stats, session token savings, and an `edge_identity_revisions` counter (edges re-keyed when their provenance changed) |
| `search_symbols` | Find symbols by name (replaces Grep). Inline `kind:`/`flavor:`/`lang:`/`path:` field clauses (also a top-level `flavor` param) + `query_class` / `max_per_file` tuning; accepts `repo`, `project`, `ref`, `scope` params. `flavor:` filters type nodes by their structural shape — `class`/`struct`/`enum`/`interface`/`trait`/`protocol`/`object`/`record`/`type_alias`/`newtype`/`message`/`service`/`table`/`view`/`module`/… — with `flavor:component` spanning every UI component (React / Vue / Svelte / SwiftUI / Compose / Flutter / Angular / …); a `kind:class`-style value that is only a flavor routes to this filter automatically. `corpus: code\|docs\|all` selects the corpus (`docs` has its own retrieval channel + prose-tuned ranking); `vocab_anchored: true` constrains LLM expansion to the repo's own vocabulary; a zero-result identifier query is auto-decomposed into leaf terms (`decomposed: true`) |
| `search_text` | Trigram-accelerated literal (or `regexp: true`) code search across the repo — the alt grep backbone. Returns file/line/text rows, each carrying the enclosing symbol (`symbol_id` / `symbol_name`) |
| `find_files` | Find source files by **name** — the file-name counterpart of `search_symbols`. `query` (basename/path substring, ranked exact > prefix > substring) and/or `glob` (e.g. `internal/**/*_test.go`), with optional `fuzzy` subsequence matching and `path` / `repo` scoping. File nodes are excluded from the symbol index, so `search_symbols kind:file` cannot return them — use this |
| `winnow_symbols` | Structured constraint-chain retrieval — `kind`, `language`, `community`, `path_prefix`, `min_fan_in`, `min_fan_out`, `min_churn`, `text_match` with per-axis score contributions |
| `get_symbol` | Symbol location and signature (replaces Read). Accepts `repo`, `project`, `ref` params |
| `get_file_summary` | All symbols and imports in a file. Accepts `repo`, `project`, `ref`, `max_bytes` / `max_tokens` budget caps |
| `get_editing_context` | **Primary pre-edit tool** — symbols, signatures, callers, callees. Accepts `max_bytes` / `max_tokens` budget caps; `compress_bodies` stubs bodies, and `fidelity_globs` (e.g. `internal/**:full,*_test.go:omit,vendor/**:compress`) sets a per-glob full/compress/omit tier |
| `get_repo_outline` | Narrative single-call repo overview — top languages, communities, hotspots, most-imported files, entry points |
| `plan_turn` | Opening-move router — returns ranked next calls with pre-filled args for a task description (~200 tokens) |
## Graph traversal
| Tool | Description |
|------|-------------|
| `get_dependencies` | What a symbol depends on |
| `get_dependents` | What depends on a symbol (blast radius) |
| `get_call_chain` | Forward call graph. Accepts `max_bytes` / `max_tokens` budget caps |
| `get_callers` | Reverse call graph. A zero-caller result carries a caveat distinguishing "likely unused" from "possible extraction gap" so a pre-edit safety check isn't silently disarmed |
| `find_usages` | Every reference to a symbol. Each usage carries its reference `context` (parameter_type / return_type / field / value / type / attribute / call); pass `context:` to filter (e.g. "where is this type used as a parameter?"). `flavor:` filters by where a usage originates — a type flavor resolves the usage's enclosing owner type ("usages from inside a struct"), and `flavor:component` keeps usages originating inside a UI component; each usage surfaces the resolved `from_type_flavor` / `from_ui_component`. Accepts `max_bytes` / `max_tokens` budget caps. Zero-edge results carry the same "likely unused" vs "possible extraction gap" caveat |
| `find_implementations` | Types implementing an interface |
| `find_overrides` | Methods that override (children) or are overridden by (parents) a method — backed by `EdgeOverrides` |
| `get_class_hierarchy` | Multi-hop inheritance subgraph around a type, interface, or method. Walks `EdgeExtends` + `EdgeImplements` + `EdgeComposes` (type nodes) and `EdgeOverrides` (method nodes); `direction` ∈ up / down / both, `include_methods` pulls members + their override chain |
| `get_cluster` | Bidirectional neighborhood |
## Search & traversal extensions
| Tool | Description |
|------|-------------|
| `find_declaration` | Use-site → declaration resolver. Accepts a literal substring or (with `regex: true`) a regex matching a use site like `fooBar(`; returns the declaration node plus the matching use locations. Trigram-prefiltered. Optional `path_prefix` / `kind` filters |
| `walk_graph` | Token-budgeted free-form graph traversal — walks arbitrary `edge_kinds` (CSV) outward / inward / both from a starting symbol; auto-stops at `token_budget`. Surfaces `budget_hit` / `stopped_at_depth` on the response. `community` (ID or label) confines the walk to a detected community |
| `context_closure` | Dependency-closure context selection — given a set of seed files / symbols, walks the transitive import / dependency closure and packs it under one `token_budget` (reusing the graded-manifest tiers), ranked by graph distance from the nearest seed or, with `rank: "proximity"`, by seeded random-walk proximity |
| `graph_query` | Ad-hoc graph-query escape hatch — small read-only DSL with `nodes` / `traverse` / `filter` stages joined by `\|`, e.g. `nodes kind=interface name~Handler \| traverse implements in \| filter path=internal/mcp/`. Bounded by `limit` and a five-stage cap |
| `nav` | Per-session symbol cursor — verb-dispatched via `action`: `goto` / `into` (a callee) / `up` (a caller) / `sibling` / `back` / `where` / `read`. Adjacency preview rides on every response; the cursor lives in session state and resets on disconnect |
## Dataflow (CPG-lite)
| Tool | Description |
|------|-------------|
| `flow_between` | Ranked dataflow paths between two symbols — walks `value_flow` / `arg_of` / `returns_to` edges |
| `taint_paths` | Pattern-driven source→sink dataflow sweep for security and architecture audits |
## Structural search
| Tool | Description |
|------|-------------|
| `search_ast` | Cross-language structural search by AST shape — raw tree-sitter S-expression `pattern` or a bundled `detector` (e.g. `sql-string-concat`, `weak-crypto`, `hardcoded-secret`) |
## Diagnostics & code actions
Wired across every running language server (gopls, tsserver, pyright, rust-analyzer, …). Server-driven capability registration via `client/registerCapability` / `client/unregisterCapability` is honoured live, so servers (jdtls, tsserver, rust-analyzer) that announce features *after* `initialize` no longer return empty results.
| Tool | Description |
|------|-------------|
| `subscribe_diagnostics` | Opt the session into push `notifications/diagnostics`; initial state replays immediately, deltas thereafter. Filter by `min_severity` / `path_prefix` |
| `unsubscribe_diagnostics` | Opt back out — idempotent, fires automatically on session disconnect |
| `get_diagnostics` | Latest stored diagnostics for a file; `wait: true` blocks on the first publish |
| `get_code_actions` | LSP code actions (quickfix / organizeImports / refactor / source) at a file location |
| `apply_code_action` | Apply a single code action to disk — atomic temp+rename |
| `fix_all_in_file` | Loop codeAction → apply → re-collect until convergence over the whole file |
## Proactive notifications
Four additional push channels modeled on `subscribe_diagnostics` — per-session opt-in, delta-filtered, initial replay, auto-cleanup on disconnect.
| Tool | Description |
|------|-------------|
| `subscribe_workspace_readiness` | `notifications/workspace_readiness` — daemon warmup phase transitions (snapshot_loaded → parallel_parse → deferred_passes_all → global_resolve → end_batch → watcher_started → ready). Last-known phase replayed to late subscribers. A graph tool *called during warmup* does not need this subscription to cope: it returns an in-band `warming` block plus best-effort partial results instead of blocking or erroring |
| `unsubscribe_workspace_readiness` | Opt back out — idempotent |
| `subscribe_daemon_health` | `notifications/daemon_health` — periodic ticker (default 15 s, `interval_ms` clamped to 1 s..5 min) snapshots uptime, alloc/sys/heap, num_goroutine, num_gc, tracked_repos, sessions, lsp_alive, graph nodes/edges. Ticker only runs while ≥1 subscriber is attached |
| `unsubscribe_daemon_health` | Opt back out — idempotent |
| `subscribe_stale_refs` | `notifications/stale_refs` — per-session intersect of watcher symbol-change events against the session's viewed/modified working set. Fires only when a change actually touches what *this* session has consumed |
| `unsubscribe_stale_refs` | Opt back out — idempotent |
| `subscribe_graph_invalidated` | `notifications/graph_invalidated` — coarse "the graph was rebuilt, drop cached results" signal. `{node_count, edge_count, reason, ts}`; unfiltered |
| `unsubscribe_graph_invalidated` | Opt back out — idempotent |
## Coding workflow
| Tool | Description |
|------|-------------|
| `get_symbol_source` | Source code of a single symbol (80% fewer tokens than Read). Returns `tokens_saved` per call. `compress_bodies` stubs bodies (with an optional `keep` subset); `max_lines` salience-truncates to a control-flow skeleton |
| `batch_symbols` | Multiple symbols with source/callers/callees in one call |
| `find_import_path` | Correct import path for a symbol |
| `explain_change_impact` | Risk-tiered blast radius with affected processes. A zero-edge target carries the "likely unused" vs "possible extraction gap" caveat |
| `get_recent_changes` | Files/symbols changed since timestamp |
| `edit_symbol` | Edit a symbol's source directly by ID — no Read needed. Line-ending tolerant: an LF-authored `old_source` matches a CRLF file (and vice versa) and the replacement adopts the file's endings (`eol_normalized: true` rides on the response). Optional `base_sha` content-hash guard refuses the write when the on-disk SHA has drifted; every success carries `new_sha` so the next edit can pipeline without re-reading |
| `edit_file` | Edit any file (markdown, config, spec, template, source) by exact string replacement — accepts absolute paths or repo-rooted paths. Line-ending tolerant: an LF-authored `old_string` matches a CRLF file (and vice versa) and the replacement is written with the file's own endings (`eol_normalized: true` rides on the response). Same `base_sha` / `new_sha` drift guard. Kills Read-before-Edit for files not in the graph |
| `write_file` | Create or overwrite any file — atomic temp+rename, re-indexes on write. Same `base_sha` / `new_sha` drift guard |
| `rename_symbol` | Coordinated multi-file rename with all references |
| `move_symbol` | Relocate a function / method / type / variable / const to another file. Cross-package moves rewrite every qualified reference, drop the source import, add the target import, synthesise the target file if missing. Go for now |
| `inline_symbol` | Replace every callsite of a trivial single-statement / single-expression callee with the body — refuses cleanly on defer, spawn, close-over-scope, multi-return, or side-effecting arg. `delete_after: true` removes the declaration. Go for now |
| `safe_delete_symbol` | Atomic dead-code removal with a graph-aware safety gate. A `cascade` parameter (`off` / `preview` / `apply`) drives a fixed-point orphan-propagation pass; cross-workspace and out-of-closure callers (and, by default, test-only callers) disqualify a candidate |
| `set_planning_mode` | Switch the session between a guaranteed no-writes planning phase and editing mode |
| `workflow` | Drive a phase-enforcement state machine (explore → implement → verify) — editing tools are gated until the implement phase |
## Agent-optimized (token efficiency)
| Tool | Description |
|------|-------------|
| `explore` | One-shot localization: free task/bug text in, the ranked neighborhood out — likely symbols with source + call paths (1-hop callers/callees), a file map, and a completeness cue, packed under a `token_budget` (default 9000; bodies demote to signatures past it, truncation reported honestly). The opening move for any task-shaped request — folds the whole search/read/callers exploration phase into one call |
| `smart_context` | Task-aware minimal context — replaces 5-10 exploration calls. The working set is ranked through the full rerank pipeline. Always emits a `blast_radius` block (callers grouped by file + covering tests + a `no covering tests found` warning) and a file-clustered `working_set`; seed count and `token_budget` scale with graph size when unset. `fidelity: "graded"` returns a graph-distance-tiered `context_manifest` (large interchangeable symbol families are skeletonized to one representative) under one `token_budget`; `estimate: true` projects token cost without fetching; `if_none_match` dedups an unchanged pack to `not_modified` |
| `get_edit_plan` | Dependency-ordered edit sequence for multi-file refactors |
| `get_test_targets` | Maps changed symbols to test files and run commands |
| `get_untested_symbols` | Inverse of `get_test_targets` — functions/methods not reached from any test file, ranked by fan-in |
| `suggest_pattern` | Extracts code pattern from an example — source, registration, tests |
| `export_context` | Portable markdown/JSON context briefing for sharing outside MCP |
| `feedback` | `action: "record"`: report useful/missing symbols. `action: "query"`: aggregated stats — most useful, most missed, accuracy metrics |
| `ask` | Optional in-process LLM research agent (`-tags llama` + `llm.model`) — navigates the graph and returns a synthesized answer; `chain: true` for cross-repo call-chain tracing |
## Response re-cutting
Gortex captures every large tool response into a bounded per-session ring; these tools re-cut a captured response without re-issuing the original query.
| Tool | Description |
|------|-------------|
| `ctx_stats` | List the session's buffered responses — handles, tools, line / byte / token counts |
| `ctx_grep` / `grep_results` | Regex (or literal) search over a buffered response — structured `matches[]` plus a grep-style block with `-A`/`-B`/`-C` context |
| `ctx_slice` | An explicit line range of a buffered response |
| `ctx_peek` | Head + tail preview of a buffered response |
| `head_results` | The first N lines of a buffered response |
## Analysis
| Tool | Description |
|------|-------------|
| `get_communities` | Functional clusters (Louvain). Without `id`: list all. With `id`: members and cohesion for one community |
| `get_processes` | Discovered execution flows. Without `id`: list all. With `id`: step-by-step trace |
| `detect_changes` | Git diff mapped to affected symbols |
| `index_repository` | Index or re-index a repository path |
| `reindex_repository` | Incrementally re-index a tracked repository — whole-root, or scoped to an optional `paths` subset. Multi-repo aware |
| `contracts` | API contracts. `action: "list"` (default): detected HTTP/gRPC/GraphQL/topics/WebSocket/env/OpenAPI. `action: "check"`: orphan providers/consumers |
| `find_co_changing_symbols` | Ranked git co-change neighbours for a symbol — over the mined cosine-weighted `co_change` edge layer |
| `search_artifacts` | Full-text search over the context-artifacts manifest — DB schemas, API specs, infra configs, ADRs registered via `.gortex.yaml::artifacts` |
| `get_artifact` | Fetch one context artifact by id, with its content and the symbols it references |
## Proactive safety
| Tool | Description |
|------|-------------|
| `verify_change` | Check proposed signature changes against all callers and interface implementors |
| `check_guards` | Evaluate project guard rules (`.gortex.yaml`) against changed symbols |
| `audit_agent_config` | Scan CLAUDE.md / AGENTS.md / `.cursor/rules` / `.github/copilot-instructions.md` / `.windsurf/rules` / `.antigravity/rules` for stale symbol references, dead file paths, and bloat — validated against the live graph |
## Code quality
| Tool | Description |
|------|-------------|
| `analyze` | Unified graph analysis dispatcher. `kind``dead_code`, `hotspots`, `cycles`, `would_create_cycle`, `connectivity_health`, `todos`, `blame`, `coverage`, `coverage_gaps`, `coverage_summary`, `stale_code`, `stale_flags`, `ownership`, `releases`, `cgo_users`, `wasm_users`, `orphan_tables`, `unreferenced_tables`, `channel_ops`, `goroutine_spawns`, `field_writers`, `race_writes`, `unclosed_channels`, `unsafe_patterns`, `health_score`, `impact`, `annotation_users`, `config_readers`, `env_var_users`, `sql_call_sites`, `fixes_history`, `edge_audit`, `domain`, `named`, `tests_as_edges`, `clusters`, `event_emitters`, `pubsub`, `string_emitters`, `error_surface`, `log_events`, `sql_rebuild`, `external_calls`, `routes`, `models`, `components`, `k8s_resources`, `images`, `kustomize`, `cross_repo`, `dbt_models`, `synthesizers`, `resolution_outcomes`. `clusters` takes an `algorithm` arg (`leiden` / `louvain` / `spectral`). `synthesizers` rolls up every framework-dispatch-synthesized edge by the pass that produced it; `resolution_outcomes` classifies unresolved call/reference edges by why the resolver gave up (`ambiguous_multi_match` / `candidate_out_of_scope` / `cross_language_only` / `stub_only` / `no_definition`) |
| `find_clones` | Near-duplicate function/method clusters from the MinHash + LSH `similar_to` layer; `dead_only: true` finds dead duplicates of live code |
| `index_health` | Health score, parse failures, stale files, language coverage, per-(repo, provider) semantic-enrichment lifecycle (`semantic_enrichment`: running / completed / partial / abandoned / failed with edge counts, plus a `semantic_enrichment_ok` rollup) — a green file count with a `partial` enrichment state means LSP-tier edges are incomplete |
| `get_symbol_history` | Symbols modified this session with counts; flags churning (3+ edits) |
The in-graph coverage tools above (`analyze kind=coverage*`, `index_health` language coverage) have an offline, whole-corpus counterpart for regression testing: the `gortex eval parity` CLI benchmarks per-language *resolved cross-file-dependent* coverage against a frozen baseline and is CI-fenced three ways — a per-language coverage floor, a frozen at-or-beyond-parity language count, and per-feature extraction goldens. See [features.md](features.md#coverage-churn-ownership).
## Code generation
| Tool | Description |
|------|-------------|
| `scaffold` | Generate code, registration wiring, and test stubs from an example symbol |
| `batch_edit` | Apply multiple edits in dependency order, re-index between steps |
| `diff_context` | Git diff enriched with callers, callees, community, processes, per-file risk |
| `prefetch_context` | Predict needed symbols from task description and recent activity. Accepts `max_bytes` / `max_tokens` budget caps |
## PR review
A graph-grounded pull-request review surface. The forge-data tools self-serve PR data via the daemon's own forge client (needs `GH_TOKEN` / `GITHUB_TOKEN` in the daemon environment), or accept caller-supplied data to skip the network; all are read-only. The review gate is AST-grounded — the deterministic correctness rulepack runs over the changeset and a graph-grounding pass drops false positives, with an opt-in LLM fold-in. The CLI exposes the same surface as `gortex prs` / `gortex review` ([cli.md](cli.md#pull-request-review)).
| Tool | Description |
|------|-------------|
| `list_prs` | List a repo's PRs with a one-shot review-state classification — a state label (DRAFT / BASE_MISMATCH / CHANGES_REQUESTED / APPROVED / STALE / READY), a normalized CI rollup (NONE / FAILURE / PENDING / SUCCESS), and merge blockers. Pass `prs` to classify an already-fetched set with no network call |
| `get_pr_impact` | Graph-joined blast radius + risk score for one PR — maps the PR's changed files to symbols, scores five risk axes (blast-radius flow, caller fan-in, coverage gap, security keywords, community span), groups the affected surface by community and caller/test file. `receipt: true` emits a privacy-safe review receipt |
| `triage_prs` | Rank a repo's open PRs by graph-derived review priority — `get_pr_impact` per PR ordered by composite risk (deterministic; `use_llm` re-ranks with one compact LLM pass + per-PR rationale). Decides which PR to review first |
| `pr_risk` | PR-level composite risk score for a set of changed symbols — five 0-100 axes into one score + a LOW/MEDIUM/HIGH/CRITICAL level and an ordered `review_priorities` list. Pass `ids` (mapped symbol IDs) or `base` (a git ref — changed set from the diff) |
| `conflicts_prs` | Surface merge-order conflict risk — maps each open PR to the graph communities it touches and reports the communities touched by more than one PR, with colliding PR numbers, a suggested safe merge order, and a conflict-risk score. Plan a merge train that minimises rebases |
| `suggest_reviewers` | Rank the people / teams best placed to review a changeset — blends CODEOWNERS matches, recent authorship of the changed symbols, and co-change experts into one ranked list with per-reviewer reasons. Pass `ids`, `base`, or `number` |
| `suggested_review_questions` | Prioritised, symbol-anchored review questions mapping the changeset to graph anomalies — bridge / hub_risk / surprising / thin_community / untested_hotspot — each tied to a symbol id + file + line with a HIGH/MEDIUM/LOW severity |
| `pr_review_context` | Deterministic, LLM-free PR-review rollup in one call — composes `diff_context`, `verify_change`, `simulate_chain` (gated on an explicit overlay session), and `audit_agent_config` into a composite PASS / WARN / BLOCK verdict. The cheap counterpart to `review_pack` |
| `sibling_diff_context` | Raw unified diff of the OTHER changed files in a changeset — the sibling changes a per-symbol / per-file review view filters out, ranked by relatedness to the focus (shared community/process → co-change → directory proximity) |
| `review` | Review a changeset → line-anchored inline comments + a BLOCK/REVIEW/APPROVE verdict. Runs the deterministic correctness rulepack (graph-grounded to drop false positives) over the changeset (`base` / `scope`, or a pasted `diff`); `use_llm` folds in LLM findings relocated to exact lines |
| `review_pack` | The single AST-grounded PR-review entrypoint — folds the graph-grounded review, per-symbol semantic classification, per-file risk, contract-impact + guard/architecture checks, and impacted test targets into one envelope, with a derived `verification_command` and a privacy-safe receipt |
| `critique_review` | Second, adversarial self-critique pass over a prior review's findings — asks the LLM (grounded in the diff) which findings are genuine vs false positives, returns the kept set, the dropped set each with a reason, and a revised verdict. Conservative: a disabled LLM keeps everything |
| `post_review` | Post review findings as inline comments on a GitHub PR / GitLab MR — each anchored to its file + line, batched into one review. Every body is secret-redacted before any payload is built; public / fork PRs require `confirm_public: true`; `dry_run: true` returns the would-post payloads with no network call |
| `suppress_finding` | Durably silence a review finding as a false positive (or `list` / `remove`) for the current repo — keyed over rule / category / symbol / file / source text so it survives the finding shifting lines. A permanent per-repo never-flag-again list (sidecar-backed) |
`analyze` also takes `kind: "review"` — the idiomatic/correctness rulepack (NPE / thread-safety check-then-act / N+1 / logic-error, Go + Python) with the same graph-grounded false-positive-reduction post-pass that backs the `review` tool.
## Multi-repo management
| Tool | Description |
|------|-------------|
| `track_repository` | Add a repo at runtime — indexes immediately, persists to global config |
| `untrack_repository` | Remove a repo — evicts nodes/edges, persists to global config |
| `set_active_project` | Switch active project scope for all subsequent queries |
| `get_active_project` | Return current project name and its member repositories |
| `list_repos` | List every project/repo in the active workspace |
| `workspace_info` | Workspace identity — bind mode, root directory, marker contents, discovered member set |
| `query_project` | Search symbols in another project or repo without a `set_active_project` switch — read-only cross-project lookup |
| `save_scope` | Save a named, reusable set of repository prefixes — accepted by `search_symbols` / `smart_context` via `scope` |
| `list_scopes` | List every saved repository scope |
| `delete_scope` | Delete a saved repository scope by name |
## Live editor buffers (overlay sessions)
Editor extensions push in-flight (unsaved) buffers as **overlays**. Gortex composes a per-request **shadow view** on top of the immutable base graph and threads it through the tool dispatch context — every subsequent `tools/call` from the same MCP session reads through the shadow. Graph-walking tools (`find_usages`, `get_call_chain`, `analyze`, …) and source-reading tools (`get_symbol_source`, `get_editing_context`, …) all see the editor-buffer state without per-tool changes.
**Base is never mutated by overlay flow.** Concurrent sessions each see their own view; the file watcher's reindex passes don't race with overlay queries; cross-file edges from non-overlaid files into overlaid symbols are preserved.
| Tool | Description |
|------|-------------|
| `overlay_register` | Bind an overlay session to the current MCP session ID (idempotent) |
| `overlay_push` | Push (or update) a single file overlay; `base_sha` enables drift detection, `deleted: true` previews a delete |
| `overlay_list` | List every overlay attached to the session — path / size / deleted / base_sha |
| `overlay_delete` | Remove one overlay from the session |
| `overlay_drop` | Tear down the session and discard every overlay |
| `overlay_keepalive` | Refresh the session's idle timer without re-pushing buffer content; cheap option for debugger / wizard pauses |
| `compare_with_overlay` | Run `find_usages` / `get_callers` / `get_call_chain` / `get_dependencies` / `get_dependents` against base AND overlay; returns added / removed / common ID sets |
**Branching — N parallel speculative sessions off one baseline.** Each overlay session carries an active-branch pointer plus a branches map; every legacy overlay tool operates on the active branch, so callers that never touch branches see exactly one implicit `main` branch and behave unchanged. With branches, an agent can hold strategy A and strategy B simultaneously off the same baseline, evaluate each, and merge the winner.
| Tool | Description |
|------|-------------|
| `overlay_fork` | Clone the active (or named) branch into a new branch; optional `activate: true` flips the session pointer |
| `overlay_branches` | List every branch with active flag, file count, `base_sha` anchor count, parent, and `created_at` |
| `overlay_switch` | Flip the session's active branch |
| `overlay_merge` | Fold one branch into another (default target: `main`) or write the branch to disk through the same atomic-write + `base_sha` drift guard as `edit_file`. Same-path divergent content is refused without `force: true`; `force` resolves last-writer-wins |
| `overlay_drop_branch` | Delete a named branch — refuses to drop the active branch or the implicit `main` |
| `compare_branches` | Run `find_usages` / `get_callers` / `get_call_chain` / `get_dependencies` / `get_dependents` against two branches and report each side plus the delta |
HTTP transport mirrors the surface at `/v1/overlay/sessions/*`; the `/v1/tools/<name>` entry point reads the overlay session from `Mcp-Session-Id` (preferred), `X-Gortex-Overlay-Session`, or `?session_id=`. Overlays are bound to their MCP session — when the session ends the overlay is dropped synchronously. Idle TTL is a fail-safe (default 30 m, configurable via `GORTEX_OVERLAY_IDLE_TTL`); every tool call against a live overlay refreshes it.
## Speculative execution
Built on the same shadow-graph substrate, `preview_edit` and `simulate_chain` answer **"what would change if I applied this WorkspaceEdit?"** without ever touching disk or mutating the base graph. The input is a standard LSP `WorkspaceEdit` (`changes` / `documentChanges`), so any agent that already produces WorkspaceEdits for code actions can speculate on them directly. Per-step impact: touched files, added / removed / renamed symbols (non-trivial-signature rename heuristic), broken callers, broken interface implementors, blast-radius rollup, suggested test targets, and (when an LSP is configured) round-trip diagnostics restored to the on-disk state at simulation end.
| Tool | Description |
|------|-------------|
| `preview_edit` | Single-shot WorkspaceEdit → impact report. Optional `diagnostics: false` skips the LSP round-trip. `inherit_overlay: true` layers on top of the caller's current overlay |
| `simulate_chain` | Ordered sequence of WorkspaceEdits applied in order with per-step impact + cumulative rollup + per-step diagnostics delta. `stop_on_error: true` (default) aborts on the first new ERROR-severity diagnostic. `keep: true` promotes the final simulated state into a real overlay session bound to the caller |
## MCP resources (18)
Read-only, URI-addressable, no args. Clients that speak resources can `resources/subscribe` once and receive `notifications/resources/updated` after each graph re-warm — no polling.
| Resource | Description |
|----------|-------------|
| `gortex://session` | Current session state and activity |
| `gortex://stats` | Graph statistics (node/edge counts) |
| `gortex://schema` | Graph schema reference |
| `gortex://guide` | On-demand reference: LLM-provider matrix, capabilities, token-economy, analyze/search_ast catalogs, workflow |
| `gortex://guide/{topic}` | One guide section by topic (providers, capabilities, tokens, analyze, search_ast, resources, workflow) |
| `gortex://index-health` | Health score, parse failures, stale files |
| `gortex://workspace` | Workspace identity and discovered member set |
| `gortex://repos` | Tracked repo / project list |
| `gortex://active-project` | Active project name and member repos |
| `gortex://communities` | Community list with cohesion scores |
| `gortex://community/{id}` | Single community detail |
| `gortex://processes` | Execution flow list |
| `gortex://process/{id}` | Single process trace |
| `gortex://report` | High-level orientation — graph size, top languages/kinds, hotspot / dead-code / todo counts |
| `gortex://god-nodes` | Top 20 hotspots |
| `gortex://surprises` | Cycles + dead code + cross-community call hubs |
| `gortex://audit` | `audit_agent_config` with discovery defaults |
| `gortex://questions` | TODO / FIXME / XXX / HACK / QUESTION rollup grouped by tag and assignee |
## MCP prompts (3)
| Prompt | Description |
|--------|-------------|
| `pre_commit` | Review uncommitted changes — shows changed symbols, blast radius, risk level, affected tests |
| `orientation` | Orient in an unfamiliar codebase — graph stats, communities, execution flows, key symbols |
| `safe_to_change` | Analyze whether it's safe to change specific symbols — blast radius, edit plan, affected tests |
+174
View File
@@ -0,0 +1,174 @@
# Multi-repo workspaces
Gortex can index multiple repositories into a single shared graph, enabling cross-repo symbol resolution, impact analysis, and navigation.
## Workspace boundary
Every node and contract is keyed on a **workspace slug**, which is the hard graph boundary for cross-repo work. Two repos that should pair their contracts (an HTTP server and the client that calls it, a Kafka producer and its consumer, etc.) must declare the same `workspace:` in their `.gortex.yaml` — otherwise contract matching stops at the boundary and they look like orphans.
Slug resolution precedence (first match wins):
1. `RepoEntry.workspace` in `~/.gortex/config.yaml` — overrides everything, ideal for OSS / read-only repos where you don't want to leave an artifact in the tree
2. `workspace:` in the repo's own `.gortex.yaml` — the default for first-party repos
3. The repo prefix — fallback when neither is set, so each unconfigured repo gets its own isolated workspace
The same chain applies to the optional `project:` slug (a sub-bucket inside a workspace). The daemon loads every tracked repo into one shared graph; you scope a query to a single workspace or project at request time rather than at startup. Over the HTTP surface (`gortex daemon start --http-addr ...`) the `/v1/graph` route accepts `?project=` and `?repo=` to narrow the dump, so a typo'd value returns an empty result for that request instead of bringing the whole index up empty.
## Configuration
Two-tier config hierarchy:
- **Global config** (`~/.gortex/config.yaml`) — projects, repo lists, active project, reference tags
- **Workspace config** (`.gortex.yaml` per repo) — guards, excludes, local overrides
Excludes are layered — builtin → repo's own `.gitignore` → global → per-repo entry → workspace — with gitignore semantics. The repo's `.gitignore` is respected by default so you don't have to re-declare entries already curated for git; opt out per-workspace with `respect_gitignore: false` in `.gortex.yaml`. Use `!pattern` in a later layer to re-include something an earlier layer excluded. Beyond `.gitignore`, the index walk also honors per-directory `.gortexignore` files (Gortex's own ignore file, a sibling to `.gitignore`) and ripgrep's `.ignore` / `.rgignore` — each scoped to the directory that contains it.
```yaml
# ~/.gortex/config.yaml
active_project: my-saas
exclude: # Applies to every tracked repo
- "**/*.generated.*"
- "node_modules/" # Already in the builtin baseline
repos:
- path: /home/user/projects/gortex
name: gortex
exclude: # Extra patterns just for this repo
- "results/**"
projects:
my-saas:
repos:
- path: /home/user/projects/frontend
name: frontend
ref: work
- path: /home/user/projects/backend
name: backend
ref: work
- path: /home/user/projects/shared-lib
name: shared-lib
ref: opensource
```
`synthesize_external_calls: true` (opt-in, default off — set in `.gortex.yaml` or the global config) makes the resolver synthesize placeholder nodes for calls into un-indexed external packages or sibling services, so call-chains keep the external hop instead of terminating at the indexed boundary.
## Daemon tuning (optional)
The daemon's defaults handle typical workflows without configuration. These knobs exist for monorepos, branch-heavy workflows, or filesystems without fsnotify support.
```yaml
# ~/.gortex/config.yaml (or per-repo .gortex.yaml)
watch:
debounce_ms: 150 # per-file patch debounce (default 150)
# Storm mode — when more than N events land within the window,
# switch from per-file debounced patching to a batched reconcile
# that defers cross-file resolver + search work until a quiet
# period has passed. Amortises the cost of bulk operations
# (rsync, npm install, branch checkout, bulk format-on-save,
# find-and-replace). Zero = disabled (default).
storm_threshold: 0 # 0 disables; try 50 on monorepos
storm_window_ms: 500
storm_quiet_period_ms: 500
```
Environment variables:
- `GORTEX_RECONCILE_INTERVAL` — janitor tick that walks every tracked repo and runs `IncrementalReindex` against disk. Insurance against fsnotify gaps on NFS/SMB mounts, inotify watch-limit exhaustion, or daemon downtime where edits happened offline. Default `1h`; `"0"` or `"off"` disables; otherwise any Go duration string (e.g., `15m`).
- The daemon also watches each tracked repo's `.git/HEAD`, so branch switches and rebases reconcile incrementally (via `git diff --name-status`) rather than by re-indexing every changed file individually — no configuration needed.
- `GORTEX_WARMUP_FULL_RETRACK=1` — force every repo through a whole-repo re-track (evict + re-parse every file) on the next warm restart instead of the default scoped reconcile. An escape hatch for when the on-disk change census itself is suspect.
- `GORTEX_WARMUP_FULL_RESOLVE=1` — force the warm-restart master resolve to re-examine the whole graph instead of scoping to changed repos; also makes the resolver ignore the durable terminal-edge stamp and re-attempt every previously-given-up-on edge. Use when a scoped resolve is suspected of missing edges.
- `GORTEX_WARMUP_FORCE_ENRICH=1` — bypass the persisted per-repo enrichment-completion markers and re-run semantic enrichment for every repo on warm restart, even ones whose marker already matches HEAD on a clean tree.
- `GORTEX_DAEMON_MEMLIMIT` — standing soft memory limit installed at daemon boot, as a human size (`4GiB`, `2048MiB`, `2G`) or `off` / `0` to disable. The daemon is a long-lived background service; a soft limit makes the GC pace against a ceiling and resist heap balloon growth rather than letting the high-water climb toward machine RAM. Overrides the `daemon.memory_limit` config value; an explicit `GOMEMLIMIT` overrides both (the runtime already honors it). Unset applies the default policy: a quarter of host RAM, clamped to `[1GiB, 8GiB]`. The cold-index window temporarily raises this to a larger budget and restores it afterward.
- `GORTEX_DAEMON_MEMRELEASE=0` — disable the post-burst heap-to-OS release. By default the daemon calls `debug.FreeOSMemory()` at allocation-burst boundaries (warmup completion, a reconcile-janitor tick that reindexed something, the close of a cold-index window, and a whole-graph analysis pass) so a burst's high-water footprint is returned to the OS promptly instead of pinning resident memory at the peak. It only ever fires at those boundaries, never on a timer.
## CLI
```bash
gortex track /path/to/repo # Add a repo to the workspace
gortex untrack /path/to/repo # Remove a repo from the workspace
gortex mcp --track /path/to/repo # Track additional repos on startup
gortex mcp --project my-saas # Set active project scope
gortex status # Per-repo and per-project stats
gortex repos # List tracked repos — head-commit SHA, last-indexed time, staleness flag
gortex repos --json # Same, machine-readable (for scripts / CI)
# Stamp workspace / project slugs across tracked repos (migration helper)
gortex workspace list # Show what each tracked repo currently declares
gortex workspace list --json # Same, machine-readable
gortex workspace set backend api # Write workspace=api to backend's .gortex.yaml
gortex workspace set upstream-lib api --global # OSS-friendly: pin to api in ~/.gortex/config.yaml
gortex workspace set-all api --root ~/projects/work --yes # Bulk: stamp every tracked repo under a prefix
# Manage the effective ignore list used by indexing + watching
gortex config exclude list # Show all layers (builtin, global, repo entry, workspace)
gortex config exclude add pkg/generated # Default target: workspace .gortex.yaml
gortex config exclude add '**/*.bak' --global # Write to ~/.gortex/config.yaml
gortex config exclude add testdata/ --repo backend # Write to a RepoEntry
gortex config exclude remove pkg/generated # Remove from the same target
```
## MCP tools
Agents can manage repos at runtime without CLI access:
| Tool | Description |
|------|-------------|
| `track_repository` | Add a repo, index immediately, persist to config |
| `untrack_repository` | Remove a repo, evict nodes/edges, persist to config |
| `set_active_project` | Switch project scope for all subsequent queries |
| `get_active_project` | Return current project name and repo list |
Locate, reach, and analyze query tools uniformly accept `repo`, `project`, `workspace`, and `scope` parameters for scoping (plus `ref` where reference tags apply). All are clamped to the session workspace — the hard isolation boundary. Default breadth now follows **tool intent** when `scope.intent_defaults` is enabled (the default); see [Tool scoping by intent](#tool-scoping-by-intent) below.
For `analyze`, the overrides genuinely narrow its **graph-node** kinds — `dead_code`, `hotspots`, `cycles`, `health_score`, `todos`, `stale_code`, `ownership`, `coverage_gaps`, `coverage_summary`, `impact`, `bottlenecks`, `role`, `k8s_resources`, `images`, `kustomize`, `dbt_models`, `external_calls`, and the like — and, since v1, its **edge-walk / graph-algorithm / framework / file-AST-scan** kinds too (`channel_ops`, `pubsub`, `routes`, `models`, `pagerank`, `kcore`, `edge_audit`, `tests_as_edges`, `sast`, `review`, …), which prune their rows / re-tally their counts against the same workspace + repo allow-set. The narrowing also resolves the two kind-specific collisions: `kind=cross_repo` keeps `repo` as its boundary filter and `kind=cycles` keeps `scope` as a file-path / package prefix (both are stripped from the uniform scope-resolution view). **v1 caveat:** the remaining long-tail kinds — community detection (`clusters`, `concepts`, `suggest_boundaries`), git/disk-mining (`blame`, `coverage`, `fixes_history`, `retrieval_log`, `temporal_verify`), per-id (`would_create_cycle`, `def_use`), `synthesizers` / `resolution_outcomes`, and `sql_rebuild` — remain workspace-bound but are **not** repo-narrowed — passing a narrowing arg on such a kind stamps a `scope_note` on the response disclosing the no-op.
## Tool scoping by intent
Tools are split by intent — each group has a different default scope:
| Intent | Tools | Default scope |
|--------|-------|---------------|
| **Locate** ("where is X defined") | `search_symbols`, `search_text`, `find_files` | current repo |
| **Reach** ("who consumes X") | `find_usages`, `get_callers`, `get_call_chain`, `contracts` | workspace |
| **Analyze** | `analyze`, `review`, sast | workspace (graph-node + edge-walk / algorithm / framework / scan kinds narrow to `repo`/`project`/`scope`; community / git-mining / per-id kinds stay workspace-bound — see the caveat above) |
Other query tools (`get_symbol`, `get_file_summary`, `smart_context`, etc.) keep their existing per-tool scope classification; the intent defaults above apply to the locate/reach/analyze groups listed in the table.
### `scope.intent_defaults` config flag
- Controls the intent-based default scoping described above
- **Defaults ON** (enabled out of the box — this is the new behavior after upgrade)
- **Narrow-only invariant:** the intent defaults only ever *narrow* within the session workspace (the hard isolation boundary); they never widen past it, and an explicit `repo` / `project` / `workspace` / `scope` arg always overrides the default
- Opt out: set `scope.intent_defaults: false` in `.gortex.yaml`, or set env var `GORTEX_SCOPE_INTENT_DEFAULTS=0`
**⚠ Upgrade note (behavior change):** When upgrading to this version:
- Locate tools narrow their default: project → repo (you now need `repo:"*"` to search the whole workspace)
- Reach tools widen their default: project → workspace (cross-repo callers surface automatically)
- Restore the old behavior with `scope.intent_defaults: false` or `GORTEX_SCOPE_INTENT_DEFAULTS=0`
### Widen sentinels
When intent defaults are on, you can still widen or narrow explicitly:
- `repo:"*"` — widen a locate tool back to the whole workspace
- `project:<name>` — select the middle rung (explicit project scope)
- `scope:<name>` — select a named saved scope
### Uniform parameter set
Every locate/reach/analyze tool now uniformly accepts `repo`, `project`, `workspace`, and `scope` parameters. All are clamped to the session workspace (the hard isolation boundary). For `analyze` this narrows the graph-node, edge-walk, graph-algorithm, framework, and file/AST-scan kinds; the remaining community / git-mining / per-id / synthesizer kinds are workspace-bound but not repo-narrowed in v1 (see the [MCP tools](#mcp-tools) caveat above).
### Response metadata
Scoped tool responses carry a `scope_applied` meta field plus a one-line widen hint naming an explicit override that re-broadens the result (e.g. `repo:"*"` for the whole workspace, or `project:<name>` / `scope:<name>` to re-scope to a deliberate rung). `analyze` additionally stamps a `scope_note` when a narrowing arg is passed to a kind that does not repo-narrow its rows in v1, so the no-op is self-documenting rather than silent.
## How it works
- **Qualified node IDs** — in multi-repo mode, IDs become `<repo_prefix>/<path>::<Symbol>` (e.g., `frontend/src/app.ts::App`). Single-repo mode keeps the existing `<path>::<Symbol>` format.
- **Cross-repo edges** — the resolver links symbols across repo boundaries with same-repo preference. Cross-repo edges carry a `cross_repo: true` flag.
- **Impact analysis** — `explain_change_impact`, `verify_change`, and `get_test_targets` follow cross-repo edges automatically, grouping results by repository.
- **Shared repos** — the same repo can appear in multiple projects with different reference tags. It's indexed once and shared across projects.
- **Auto-detection** — set `workspace.auto_detect: true` in `.gortex.yaml` to auto-discover Git repos in a parent directory.
+315
View File
@@ -0,0 +1,315 @@
# Onboarding — First 15 Minutes with Gortex
Just installed Gortex? This is the shortest path from "it's on my machine" to "it's helping my AI agent work faster." Follow it top-to-bottom the first time; skip sections you've already done on return visits.
## Prerequisites
- Gortex is installed (run `gortex version` — if that prints a version, you're good). Not installed? → `curl -fsSL https://get.gortex.dev | sh`, or see [docs/installation.md](installation.md) for Homebrew, package managers, and supply-chain verification.
- A repository you want to work in. We'll call it `~/projects/myapp` in the examples — substitute your own path.
- An AI coding assistant installed. Gortex auto-integrates with Claude Code, Cursor, Kiro, Windsurf, GitHub Copilot (VS Code), Continue.dev, Cline, OpenCode, and Antigravity.
## Two Commands
Setup is split into two commands so codebase-agnostic machinery lives once per user, not once per repo:
- **`gortex install`** — run **once per machine**. Writes user-level artifacts: `~/.claude.json` (MCP config), `~/.claude/skills/gortex-*` (tool-usage skills), `~/.claude/commands/gortex-*.md` (slash commands), `~/.gemini/antigravity/` Knowledge Items, and (optionally) user-level hooks. Also sets up the daemon — pass `--start` to spawn it, `--track` to register the current directory.
- **`gortex init`** — run **once per repo**. Writes per-repo artifacts: `.mcp.json`, `.claude/settings.{json,local.json}`, `CLAUDE.md` with the codebase overview and community routing, `.claude/skills/generated/` per-community SKILL.md files, and a marker-guarded community-routing block in every other detected agent's per-repo instructions file.
You can run them independently — `gortex init` doesn't require `gortex install` first; it just writes less if the user-level wiring isn't there.
```bash
# One-time: machine-wide user-level setup
gortex install --start --track # install, start daemon, track current dir
# Per repo: drop into the repo and wire it up
cd ~/projects/myapp
gortex init # default: indexes repo, generates community routing
```
For CI, scripts, or explicit control, both commands accept the usual flags (`--yes`, `--json`, `--dry-run`, `--agents`, `--agents-skip`, `--force`).
## 30-Second Version
```bash
# Once per machine
gortex install --start --track
# Once per repo
cd ~/projects/myapp
gortex init
```
With `--start`, the daemon is already running and Claude Code will find Gortex on its next run. If you skipped `--start`, you can either spawn the daemon (`gortex daemon start --detach`) or run a per-repo server (`gortex mcp --index . --watch`).
Open your AI assistant in that repo and ask it to do something real. It'll use Gortex tools automatically. If that worked, the rest of this document is optional detail.
## Step-by-Step
### 1. One-time: user-level setup
```bash
gortex install # MCP config, skills, slash commands, KIs at ~/
gortex install --start --track # also spawn the daemon + track current dir
gortex install --no-hooks # skip user-level hooks
```
This writes under `$HOME` only. It's idempotent — re-running it is safe. Think of it like `brew install`.
### 2. Per repo: wire it up
```bash
cd ~/projects/myapp
gortex init
```
`gortex init` creates tool-specific config files (auto-detecting which tools you have installed) and runs community detection on the graph so each agent gets codebase-specific routing. Commit the output — your teammates get Gortex for free when they pull.
**Key files `gortex init` creates:**
- `.mcp.json` — tells MCP clients (Claude Code, Cursor, VS Code) how to start the Gortex server
- `CLAUDE.md` — codebase overview (with `--analyze`) plus a marker-guarded community-routing block
- `.claude/settings.local.json` — installs three hooks: `PreToolUse` (redirects `Read` / `Grep` / `Glob` / `Task`), `PreCompact` (injects orientation snapshot before context compaction), `Stop` (post-task diagnostics)
- `.claude/skills/generated/<DirName>/SKILL.md` — one per detected community (via `--skills`, default on)
- `.cursor/mcp.json`, `.kiro/settings/mcp.json`, `.vscode/mcp.json`, etc. — per-agent MCP configs
- Marker-guarded "Gortex Communities" routing block in each detected agent's per-repo instructions file (`AGENTS.md`, `.windsurfrules`, `GEMINI.md`, `.cursor/rules/gortex-communities.mdc`, etc.)
**Tune the community generator:**
```bash
gortex init --analyze # include a richer codebase overview in CLAUDE.md
gortex init --no-skills # skip community generation entirely
gortex init --skills-min-size 5 --skills-max 10 # raise the floor / lower the ceiling
```
### 3. Start the MCP server
Two ways — pick whichever fits your workflow.
**Option A — you start it and leave it running.** Useful when multiple AI tools point at the same graph, or when you want the web UI:
```bash
gortex mcp --index . --watch
```
`--watch` re-indexes changed files live via fsnotify. `--cache-dir ~/.gortex/cache` (default) saves snapshots between restarts so subsequent starts are ~200ms instead of 3-5s.
To also get the HTTP server API (the UI is a separate Next.js app in `web/` that talks to it over HTTP), add `--server` to the same process:
```bash
gortex mcp --index . --watch --server
```
`--server` listens on `http://localhost:8765` and exposes `/v1/*` (including `/v1/graph` and `/v1/events` for force-directed rendering). The long-living daemon serves the same surface — run `gortex daemon start --http-addr 127.0.0.1:7411` to expose `/v1/*` and `/mcp` for every tracked repo at once.
**Option B — your IDE starts it automatically.** The `.mcp.json` that `gortex init` created tells the IDE how to spawn `gortex mcp`. You don't run anything yourself. Claude Code, Cursor, and VS Code all work this way. Downside: each tool gets its own server process (memory cost scales with number of tools).
If you're unsure, start with Option A. You can always remove the `.mcp.json` → switch to Option B later.
### 4. Verify the integration
Open your AI assistant in the repo. Ask it something concrete that requires understanding the code:
> "What does the authentication flow look like? Trace it from the HTTP handler through to the database."
**What should happen:**
- The assistant calls `graph_stats` or `get_repo_outline` to orient itself
- Then `search_symbols "auth"` or `smart_context "authentication flow"` to find relevant code
- Then `get_call_chain` or `find_usages` on the specific handler
- Finally `get_symbol_source` on the specific functions — not `Read` on whole files
**What should NOT happen:**
- The assistant calls `Read` on 5 files and hunts for auth logic manually. If you see this, the hooks aren't wired up — run `gortex init --hooks-only` to reinstall just the hooks.
**Quick sanity check from the CLI:**
```bash
gortex status --index .
```
Prints node/edge counts, language breakdown, and per-repo stats. If this shows 0 nodes, the index didn't build — check for errors in `gortex mcp` output.
### 5. Your first calls (if you're driving Gortex directly)
For debugging, writing custom agents, or working with the bridge HTTP API — the "good first calls" in order:
1. **`get_repo_outline`** — zero-arg narrative overview: primary languages, top communities, load-bearing hotspots, most-imported files, entry points. Takes ~1k tokens, covers "what is this repo?"
2. **`plan_turn` with your task description** — returns ranked recommended next calls. Example:
```json
{"tool": "plan_turn", "args": {"task": "add rate limiting to auth handler"}}
```
You get back a list like "smart_context → get_editing_context → find_usages" with pre-filled args.
3. **`smart_context` with the task** — does what `plan_turn` recommended as step 1, but assembles the actual context (relevant symbols, entry file structure, related tests) rather than just pointing at tools.
4. **Before editing any file — `get_editing_context` on its path.** Returns all symbols, signatures, direct dependencies, immediate callers. You don't need to read the file.
### 6. What the hooks do automatically
Once installed, three things happen without you lifting a finger:
- **PreToolUse on `Read` / `Grep` / `Glob`** — Gortex suggests the right graph tool instead and, for indexed source files, blocks whole-file reads.
- **PreToolUse on `Task`** — spawned subagents get a task-scoped briefing with `smart_context` results + a tool-swap table, so they don't inherit the bad habit of reaching for `Read`.
- **PreCompact** — just before Claude Code compacts the conversation, Gortex injects an orientation snapshot (recent edits, hotspots, feedback-ranked symbols) so the agent survives compaction without re-exploring.
- **Stop** — after the agent finishes a turn, Gortex runs `detect_changes` → `get_test_targets`, `check_guards`, `analyze dead_code`, `contracts check` on the symbols that changed, and feeds the results back so the agent self-corrects before handoff.
All four degrade silently when the bridge is unreachable — they never block your normal flow.
## Troubleshooting
**"Gortex MCP server failed to start" in the IDE.**
Check that `gortex` is on your `PATH` (`which gortex` should resolve). If you installed via Homebrew, restart the IDE — it caches PATH at launch.
**The AI still uses `Read` / `Grep` on source files.**
The hooks didn't install. Re-run `gortex init --hooks-only` and restart the AI tool. On Claude Code, also check that `.claude/settings.local.json` exists and contains `"gortex hook"` invocations under `hooks`.
**`graph_stats` returns `total_nodes: 0`.**
The index is empty. Either `gortex mcp` isn't watching the right directory, or `.gortex.yaml` excludes everything. Run `gortex status --index /absolute/path/to/repo` to verify the paths.
**Indexing a big repo takes forever.**
First-time index of a 100k-symbol repo is ~20-30 seconds. On restart, it's ~200ms because the snapshot gets restored and only changed files re-index. Make sure `--cache-dir` isn't being deleted between runs.
**Semantic search isn't working.**
On first use, Gortex downloads the MiniLM-L6-v2 model (~90 MB) to `~/.gortex/models/`. Needs network the first time; after that, fully offline. Check `~/.gortex/models/sentence-transformers_all-MiniLM-L6-v2/` exists.
**"Cannot be opened because Apple cannot check it for malicious software" on macOS.**
You bypassed the curl installer and downloaded the binary by hand — `curl -fsSL https://get.gortex.dev | sh` strips the quarantine xattr automatically (and on macOS routes through Homebrew when `brew` is on PATH). To fix an existing manual install, re-run the installer, reinstall via Homebrew (`brew install zzet/tap/gortex`), or run once: `xattr -d com.apple.quarantine /usr/local/bin/gortex`.
## Next Steps
Once the basics are working:
- **Multi-repo workspaces** — index several repos into one graph for cross-repo impact analysis. See [Multi-Repo Workspaces](#multi-repo-workspaces) below.
- **Guard rules** — add `.gortex.yaml` to declare architectural invariants (e.g., "UI must not import DB directly"). `check_guards` enforces them on every change. See `.gortex.yaml` in this repo for an example.
- **Per-community skills** — already generated by `gortex init --skills` (default on). Each skill auto-activates when the agent asks about that area. Re-run `gortex init` to regenerate after the graph changes; pass `--no-skills` if you want to skip that step.
- **Token savings + cost tracking** — `gortex savings` prints cumulative tokens saved + dollars avoided per model across all sessions. Accumulates automatically; no setup.
- **Compact wire format (GCX1)** — every list-shaped tool accepts `format: "gcx"` for a round-trippable compact response. Median **27.4% tokens** vs JSON on the benchmark, 100% round-trip integrity. Spec: [docs/wire-format.md](wire-format.md). TypeScript decoder on npm: [`@gortex/wire`](https://www.npmjs.com/package/@gortex/wire). Agents pick it up automatically — the PreToolUse and subagent hooks surface the opt-in. Applies to: `search_symbols`, `find_usages`, `analyze`, `contracts`, `batch_symbols`, `get_callers` / `get_call_chain` / `get_dependencies` / `get_dependents` / `find_implementations`, `get_file_summary`, `get_editing_context`, `smart_context`.
- **Feedback loop** — after a successful task, call the `feedback` MCP tool with `action: "record"`. Future `smart_context` results rerank based on what was actually useful.
- **Custom HTTP integration** — `gortex daemon start --http-addr 127.0.0.1:7411 --cors-origin '*'` exposes every MCP tool as HTTP (`/v1/*` + `/mcp`) for the tracked repos. Good for editor plugins, CI hooks, custom dashboards.
## Daemon Mode
The daemon is a long-living process that holds the graph for every tracked repo. All MCP clients (Claude Code windows, Cursor, Kiro, etc.) connect to it via a Unix socket, so:
- Memory scales with workspace size, not open-editor count — one process instead of one per project.
- Cross-repo queries work by default: an agent in `frontend` can find callers in `backend` without extra config.
- Each session gets isolated per-client state (recent activity, token stats) via handshake-assigned session IDs.
### Setup
```bash
# One-time: user-level MCP config, skills, slash commands, hooks, daemon spawn + track.
gortex install --start --track
# Track additional repos any time:
gortex track ~/projects/backend
gortex track ~/projects/shared-lib
# Remove a repo from the workspace:
gortex untrack backend # by prefix, or by absolute path
# See state:
gortex status # tracked repos, node/edge counts, memory, sessions (via daemon if running)
gortex daemon status # PID, uptime, socket path
gortex savings # cumulative tokens saved + $ avoided across all sessions
```
### Daemon lifecycle
```bash
gortex daemon start --detach # spawn in background
gortex daemon stop # graceful shutdown + final snapshot
gortex daemon restart # stop + start
gortex daemon reload # re-read config, pick up new/removed repos
gortex daemon logs -n 50 # tail the log
```
### Auto-start at login (optional)
Let the OS supervise the daemon so it starts at login and restarts on crash. No sudo required — the unit lives under `$HOME`.
```bash
gortex daemon install-service # launchd (macOS) or systemd --user (Linux)
gortex daemon service-status # check installed state + active/inactive
gortex daemon uninstall-service # remove unit, stop service
```
On macOS the unit lands at `~/Library/LaunchAgents/com.zzet.gortex.plist`; on Linux at `~/.config/systemd/user/com.zzet.gortex.service`. After `install-service`, plain `gortex daemon start / stop` still work — they just fight the service for socket ownership, so prefer `gortex daemon service-status` and `launchctl` / `systemctl --user` commands for lifecycle.
If you run an XDG layout (any absolute `XDG_CONFIG_HOME` / `XDG_DATA_HOME` / `XDG_CACHE_HOME`), `install-service` captures those values into the unit so the supervised daemon resolves the same paths as your shell — service supervisors otherwise start with a near-empty environment and the daemon would fall back to `~/.gortex`. Re-run `install-service` if you later change where those variables point.
### How it works
- `gortex mcp` (what Claude Code spawns via `.mcp.json`) auto-detects the daemon. If reachable, it acts as a thin stdio ↔ socket proxy (~5 MB per client). If not, it falls back to the embedded server — global mode is never "required."
- Every tracked repo gets its own fsnotify watcher so edits on disk flow into the graph live; no manual reload needed. `gortex track` attaches a watcher as part of the track operation; `gortex untrack` detaches it before evicting nodes.
- Graph state is snapshotted to `~/.gortex/cache/daemon.gob.gz` on shutdown and every 10 minutes. Daemon restarts load it back and re-index only changed files.
- Opening Claude Code in an untracked directory returns a structured `repo_not_tracked` error on every tool call. The agent surfaces it; you run `gortex track .` to include it.
- Per-session state is isolated by a handshake-assigned session ID — two Claude Code windows see their own recent-activity and token-savings counters, not a merged view. Cumulative savings in the sidecar ledger (`~/.gortex/sidecar.sqlite`) are still shared.
### Fallback rules
| Invocation | Daemon running | Daemon not running |
|---|---|---|
| Claude Code spawns `gortex mcp` | Proxies through daemon | Embedded server (current behavior) |
| `gortex track /path` | Immediate re-index + watcher attached via daemon | Writes config; takes effect on next daemon/server start |
| `gortex untrack /path` | Immediate graph eviction + watcher detached | Removes from config |
| `gortex status` | Aggregate across tracked repos | One-shot local index |
| `gortex daemon status` | PID, uptime, memory, sessions | "not running" |
Full architectural notes live under `specs/` in the repo.
## Multi-Repo Workspaces
When you have related repos (frontend + backend, service + SDK, producer + consumer) and want cross-repo `find_usages` / `get_call_chain` / contract matching, Gortex indexes them all into one shared graph.
### Add another repo
```bash
gortex track ~/projects/backend
gortex track ~/projects/shared-lib
gortex status # confirms both repos appear
```
Both repos now show up in every query tool. Pass `repo: "backend"` (or `project:` / `ref:`) on any MCP query to scope it; with no scope, the agent sees the full union.
### Workspace slug — make two repos count as one project
By default each tracked repo lives in its own isolated **workspace** — the hard graph boundary. So a server in one repo and the client that calls it in another look like orphans to `contracts check` (and to anything that walks contract pairs). Pin them to the same workspace slug to get cross-repo contract matching:
```bash
gortex workspace list # what each tracked repo declares today
gortex workspace set backend my-saas # write workspace=my-saas to backend/.gortex.yaml
gortex workspace set-all my-saas --root ~/work --yes # bulk-stamp every repo under ~/work
```
For OSS / read-only repos where you don't want a `.gortex.yaml` artifact in the tree, pass `--global` to record the slug in `~/.gortex/config.yaml` instead.
### Projects (optional sub-buckets) and active scope
A **project** is a sub-bucket inside a workspace, useful when you have many repos but a given task only touches a few:
```bash
gortex mcp --project my-saas # only loads repos in this project
```
The daemon, by contrast, loads every tracked repo and scopes at query time: over its HTTP surface (`gortex daemon start --http-addr ...`) the `/v1/graph` route takes `?project=` / `?repo=` to narrow a single request to one workspace or repo.
Inside the agent, `set_active_project` switches the default scope for every subsequent query — no need to repeat the `project:` parameter on each call.
### Federate to remote daemons (multi-server roster)
If a heavyweight repo lives on a build machine or behind a VPN, your local daemon can route queries to a remote Gortex server transparently:
```bash
gortex daemon server list
gortex daemon server add work --url https://gortex.work.example --auth-token-env WORK_TOKEN
gortex daemon server remove work
```
Stored in `~/.gortex/servers.toml`. Local-socket and remote-HTTPS targets both work. The auth token is read from the named env var on demand — never written to disk.
For the full configuration reference (slug precedence chain, `projects:` block, exclude layering, daemon tuning knobs), see [README → Multi-Repo Workspaces](../README.md#multi-repo-workspaces).
## Getting Help
- File issues and feature requests at [github.com/zzet/gortex/issues](https://github.com/zzet/gortex/issues).
- Full tool reference lives in `CLAUDE.md` (created by `gortex init`). Your AI agent already reads it; you can too.
+44
View File
@@ -0,0 +1,44 @@
# Token savings
Gortex tracks how many tokens it saves compared to naive file reads — per-call, per-session, and cumulative across restarts:
- **Per-call:** every source-reading tool — `read_file`, `get_file_summary`, `get_editing_context`, `get_symbol_source`, `batch_symbols` (with `include_source`), `smart_context` — books an observation server-side: tokens actually returned vs the full-file read the response stands in for. The per-call value is deliberately not echoed in responses (agents don't act on it and it would burn tokens on every reply); it lands in the ledger.
- **Session-level:** `graph_stats` returns a `token_savings` object with `calls_counted`, `tokens_returned`, `tokens_saved`, `efficiency_ratio`.
- **Cumulative (cross-session):** `graph_stats` also returns `cumulative_savings` when persistence is wired — includes `first_seen`, `last_updated`, and `cost_avoided_usd` per model — Anthropic (Opus/Sonnet/Haiku), OpenAI (GPT-5, GPT-4.1, GPT-4o, o3/o4-mini), Google Gemini (3.x and 2.5), and DeepSeek. Backed by the machine-global sidecar database (`~/.gortex/sidecar.sqlite` — the same file that holds notes/memories): `savings_totals` carries top-line + per-repo + per-language aggregates and `savings_events` one session-tagged row per call, powering the windowed buckets and the per-tool breakdown. Each observation commits transactionally, so the ledger survives SIGKILLed MCP servers and concurrent writer processes. Flat-file ledgers from older releases (`~/.gortex/cache/savings.json` + `savings.jsonl`) are imported once on first open and renamed `*.bak`.
`gortex savings` renders a three-bucket dashboard:
```text
Gortex Token Savings
====================
Cost avoided: $168.69 (claude-opus-4) across 1,878 calls · 11,246,094 tokens saved
Today ████████░░░░░░░░ 50.0% saved 9,200 / 18,400 tokens $0.14
Last 7 days ██████████░░░░░░ 62.5% saved 60,100 / 96,200 tokens $0.90
All time ███████████████░ 93.3% saved 11,246,094 / 12,050,716 tokens $168.69
```
```bash
# Three-bucket dashboard with USD on top
gortex savings
# Per-tool breakdown inside each bucket
gortex savings --verbose
# Headline a single model (fuzzy match: "opus" → claude-opus-4)
gortex savings --model opus
# Bucket "Today" by UTC instead of local time
gortex savings --utc
# Machine-readable output (mirrors the dashboard structure: buckets[].per_tool, cost_avoided_usd, etc.)
gortex savings --json
# Wipe cumulative totals and the event history
gortex savings --reset
# Override pricing (JSON array of {model, usd_per_m_input})
GORTEX_MODEL_PRICING_JSON='[{"model":"mycorp","usd_per_m_input":5}]' gortex savings
```
Token counts use **tiktoken (`cl100k_base`)** — the tokenizer Claude and GPT-4 actually use — via `github.com/pkoukk/tiktoken-go` with an embedded offline BPE loader, so no runtime downloads. The BPE is lazy-loaded on first call. If init fails for any reason, the package falls back to the legacy `chars/4` heuristic so metrics stay usable.
+121
View File
@@ -0,0 +1,121 @@
# Semantic search
**Default-on.** A baked GloVe-50d table (~3.8 MB embedded in the binary, top 20k tokens) gives every install hybrid BM25 + vector search out of the box — no flag, no model download, no native dependency. Reciprocal Rank Fusion blends the two channels, and the BM25↔vector balance is scored *continuously* from the query's shape (identifier density, separators, stopwords) rather than bucketed into a discrete class — so a half-identifier query lands between the symbol and natural-language blends instead of jumping a whole tier. After ranking, an optional pure-cosine refinement pass re-scores the top results with the exact embedding distance the rank-based fusion discards.
## Configuration
Switch or tune providers in `.gortex.yaml`:
```yaml
embedding:
enabled: true # default — pass `false` to disable
provider: static # static | local | api (default: static)
variant: "" # optional named local model (e.g. a Hugot variant); empty = the provider default
api_url: http://localhost:11434
api_model: nomic-embed-text
chunk_threshold_lines: 60 # symbols longer than this get split
chunk_window_lines: 40 # AST-aware window size
api_concurrency: 4 # bounded worker pool for hosted providers
```
Selecting a different embedding model (`variant`, or `GORTEX_EMBEDDINGS_VARIANT`) that changes vector dimensionality re-embeds the graph on next index; the persisted index guards against a dimension mismatch.
### Where the config lives — and what wins
The `embedding:` block can sit in more than one place; precedence, highest first:
1. **`--embeddings` / `--embeddings-url` / `--embeddings-model` flags** and the **`GORTEX_EMBEDDINGS*` env vars** — one-shot overrides. A URL forces the `api` provider; `GORTEX_EMBEDDINGS=0/1` toggles the vector channel; `GORTEX_EMBEDDINGS_VARIANT` pins a local model.
2. **Repo-local `.gortex.yaml`** — the per-project `embedding:` block (loaded by viper, so `GORTEX_EMBEDDING_*` env keys also merge here).
3. **Global `~/.gortex/config.yaml`** — a user-level `embedding:` block layered *under* the repo-local one: every field the repo leaves unset inherits the global value (the tri-state `enabled:` too), so one block can serve every repo. An unrecognised top-level key in this file is ignored with a startup warning (`contains keys gortex does not recognize`) rather than silently dropped — the usual cause of an `embedding:` block that "does nothing" is placing it under the wrong key here.
| Provider | Quality | Offline | Native deps | Notes |
|---|---|---|---|---|
| `static` (default) | Good for identifier-shaped queries | Yes | None | Baked GloVe-50d table, CPU-only, zero setup |
| `local` (Hugot MiniLM-L6-v2) | Better for NL queries | After first run | None | Auto-downloads ~90 MB to `~/.gortex/models/` |
| `api` (Ollama / OpenAI) | Best | No | None | Bounded concurrent worker pool — tune via `api_concurrency` |
## AST sub-chunking
Symbols longer than `chunk_threshold_lines` are split into AST-aware windows (block statements, case clauses, field groups) before embedding; each window is vectorised independently and de-duplicated back to the parent symbol at query time, so a large function lands as one hit grounded in the specific chunk that matched — chunk IDs never leak into results.
## Input truncation
Every embedding input is capped at the model's positional budget — `max_position_embeddings` from the model's `config.json` minus the two special-token slots (510 for MiniLM/BERT; larger for wide-context variants) — before it reaches inference. A transformer cannot attend past that window, so trimming the tail is lossless by construction, and it is load-bearing: the pure-Go tokenizer path does not enforce the limit itself, so a single over-budget input would otherwise reach inference at full length and abort the *entire* vector-index build with a tensor shape mismatch — dropping the daemon to text-only search. AST chunking already splits long symbols into sub-budget windows, so truncation only ever trims a pathological single chunk. If the model directory lacks a readable `config.json` the budget falls back to 510; if the tokenizer itself can't load, truncation degrades to a rune clamp rather than disabling the backend.
## Persistent index
The vector index and the chunk → symbol map are persisted in the daemon snapshot; restarts re-warm in milliseconds without re-embedding the graph. Daemon snapshot schema is forward-compatible — older snapshots load with an empty vector layer and rebuild incrementally.
## Vocabulary bridging without an LLM
A curated equivalence table (`auth``authentication``login`, `delete``remove``destroy`, …) plus per-repo auto-concept mining from symbol-name token co-occurrence expands queries deterministically — runs alongside (and dedup against) any LLM expansion. Toggle via `search.equivalence_classes`. A weighted concept-relatedness layer sits on top of the flat classes (e.g. `auth` pulls in `token` / `session` at lower priority) without merging the distinct concepts. When an LLM expander *is* configured, `vocab_anchored: true` constrains its invented terms to tokens that actually occur in the repo's symbol vocabulary.
## HITS reranking
A hubs-and-authorities pass over the reference/call graph contributes a `hits` signal to the rerank pipeline — heavily-referenced symbols outrank shallow utility nodes, and the hub penalty (`authority / (1 + hub)`) demotes called-by-everything infra so it doesn't drown the result page.
### Edge-provenance attenuation
Centrality (HITS + PageRank) and a dedicated rerank signal weight call/reference edges by how they were resolved: the abundant LSP-dispatch / framework-wiring tier — and the weak name-only tier — are attenuated relative to the structurally-unambiguous tier. Dense LSP enrichment otherwise inflates the apparent centrality of utility and framework code over genuine domain authorities. The weighting is a no-op on graphs with no resolution provenance recorded, so it never changes ranking where the data is absent.
### Other rerank refinements
- **Generated-file demotion** — a generated file (`*.pb.go`, `mock_*.go`, `*_pb2.py`, …) is ranked below a real same-named hand-written implementation, but only when one exists.
- **Source over test** — when a query surfaces both an implementation and its test, the implementation is lifted above the test (only when both co-occur, so it never shifts the rest of the page).
### Sparse sub-word tokenization (opt-in)
An optional tokenizer stage emits sub-word n-grams whose split points come from a per-repo boundary table learned from symbol names at index time, trading exact-identifier precision for recall on typo/fragment queries. Off by default (it is reindex-required and precision-sensitive); enable with `GORTEX_SPARSE_NGRAM=1`. Applies to the BM25 backend.
## Keyword-soup defense
Boolean / OR-soup queries (`A OR B OR 'no access' OR …`) — and operator-free keyword lists (`parse decode unmarshal token jwt cache`) and comma-enumerations — defeat embedding retrieval. The query classifier detects all three, skips wasted LLM expansion, and splits the soup into terms fused via the existing BM25 expansion path; a `query_advice` nudge rides on the response. Genuine natural-language questions stay classified as concept. Tune via `search.keyword_soup_rewrite: split | nudge | off`.
## Prose corpus
Markdown headings + section bodies become first-class searchable nodes (`KindDoc`) — `search_symbols corpus: "docs"` returns ranked README / ADR / design-doc sections; `corpus: "all"` mixes them with code hits. A docs query runs its own retrieval channel (a parallel doc-biased fetch, not merely a post-filter over the code fetch) and applies a prose weight profile that suppresses code-structural rerank signals (API/type-signature, definition-bias) which are meaningless for prose. Section node IDs are derived from the heading path, so incremental reindex of a touched markdown file produces stable IDs.
## Per-keyword TaskMemory
The combo store now keys symbol associations both on the whole query and per keyword, so a new task with similar keywords inherits learned ranking from prior searches even when the exact phrasing differs. Exact-query matches still dominate; per-keyword evidence is the lower-confidence generalisation.
## Build-tag backends
Opt-in faster local backends via build tags:
```bash
go build -tags embeddings_onnx ./cmd/gortex/ # needs: brew install onnxruntime
go build -tags "embeddings_gomlx XLA" ./cmd/gortex/ # needs libtokenizers.a on the linker path — use `make build-gomlx` (see below)
```
The `embeddings_onnx` backend (GTE-small) **never auto-downloads**: place `model.onnx` and `vocab.txt` in `~/.gortex/models/gte-small/` yourself and install the ONNX Runtime native library (`brew install onnxruntime`, or the distro equivalent). Without both, the backend reports "ONNX model not found" and the local chain falls through to the pure-Go Hugot backend.
The GoMLX/XLA backend requires **both** tags — `embeddings_gomlx` alone links a disabled XLA stub and always falls through to the pure-Go backend; the `XLA` tag is what compiles the real XLA session. It also statically links the rust tokenizer, so the build needs `libtokenizers.a` on the linker path (a prebuilt archive from [daulet/tokenizers](https://github.com/daulet/tokenizers) releases, at `/usr/lib` or `/usr/local/lib`); `make build-gomlx` downloads it for you. At runtime the XLA/PJRT plugin auto-downloads (~100 MB). XLA/PJRT runtime viability is platform-dependent and still experimental — if the plugin fails to load, the local chain degrades to the pure-Go Hugot backend and the startup log names the failed backend (see Troubleshooting). The default pure-Go backend needs no tags and no native libraries, and is the reliable path.
| Build tag | Backend | Model | Extra dependency | Status |
|---|---|---|---|---|
| _(none)_ | Hugot pure-Go | MiniLM-L6-v2 (auto-download) | none | **default — reliable path** |
| `embeddings_gomlx XLA` | Hugot + XLA/GoMLX | MiniLM-L6-v2 (auto-download) | libtokenizers.a (build) + PJRT plugin (runtime download) | experimental — XLA/PJRT runtime is platform-dependent |
| `embeddings_onnx` | ONNX Runtime | GTE-small (manual placement) | libonnxruntime + hand-placed model | manual setup — never auto-downloads |
The legacy `--embeddings` / `--embeddings-url` / `--embeddings-model` CLI flags and the `GORTEX_EMBEDDINGS*` env vars still take precedence over the config block — useful for one-shot overrides without editing `.gortex.yaml`.
## Troubleshooting
Semantic search degrading to text-only (BM25 / FTS5) is always logged — match the daemon log line to the cause:
- **`embeddings enabled ... provider: local (hugot/fp32), dim: 384`** — working as intended: the transformer backend is active at its true width.
- **`embeddings enabled ... provider: local → static fallback, dim: 50`** — a `local` config could not construct any transformer backend and fell back to static GloVe. The preceding `embedding backend unavailable — degraded to static fallback` warnings name each backend and why (an uncached model with downloads disabled, a missing `embeddings_onnx` model, …). Fix the named backend or accept static; the width and provider name now tell the truth rather than echoing the configured name.
- **`vector index aborted on chunk failure`** — an embedding call failed (API timeout / auth, or an over-long input on a build without truncation) and the whole vector index was dropped to avoid a half-embedded, mis-scoring index. Text search stays live. `gortex eval embedders` reports the concrete cause as `vector build failed: …` instead of a bare "no vector data".
- **`vector index built ... dropped: N`** (N > 0) — N malformed vectors (nil / wrong width) were skipped; the `sample_ids` warning names the first few. A non-zero `dropped` from a healthy provider is worth investigating.
- **`vector index disabled — embedding text count exceeds threshold`** — the corpus is larger than `embedding.max_symbols`; raise it if you have the memory headroom.
- **`~/.gortex/config.yaml contains keys gortex does not recognize`** — a top-level key (commonly an `embedding:` block nested one level too deep, or a typo) is being ignored; move it to a recognised key.
## `search_symbols` `assist:` modes
- `auto` (default) — skips LLM for identifier queries, expands NL queries
- `on` — forces expansion + rerank
- `off` — pure BM25
- `deep` — adds a body-grounded verification pass; +1.54 s; quality is highly model-dependent — unreliable on 3B local models, fine on 7B+ or hosted
See [llm.md](llm.md) for provider configuration.
+84
View File
@@ -0,0 +1,84 @@
# HTTP server, MCP 2026 transport, and Web UI
Gortex exposes three transports — stdio MCP (the default `gortex mcp`), a Unix-socket daemon, and an HTTP API. The HTTP layer is what IDE plugins, CI, the web UI, and remote agents talk to.
- [Server mode (`/v1/*` JSON API)](#server-mode-v1-json-api)
- [MCP 2026 Streamable HTTP transport (`/mcp`)](#mcp-2026-streamable-http-transport-mcp)
- [Web UI](#web-ui)
## Server mode (`/v1/*` JSON API)
The daemon exposes all MCP tools as an HTTP/JSON API under versioned `/v1/*` routes once you give it an HTTP address with `--http-addr`. The daemon serves the repos you track, so add the repo first, then bring the HTTP surface up:
```bash
# Track the repo (or run from inside it — the cwd's repo auto-tracks), then start the HTTP backend
gortex track /path/to/repo
gortex daemon start --http-addr 127.0.0.1:7411
# Non-localhost bind requires an auth token
gortex daemon start --http-addr 0.0.0.0:7411 --http-auth-token "$(openssl rand -hex 32)"
# HTTP API alongside MCP stdio (same process)
gortex mcp --index /path/to/repo --server --port 8765
```
**Endpoints (all under `/v1/`):**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/health` | GET | Status, node/edge counts, uptime |
| `/v1/tools` | GET | List all available tools with descriptions |
| `/v1/tools/{name}` | POST | Invoke any MCP tool with JSON arguments. Accepts `?format=gcx` or top-level `"format"` in the body |
| `/v1/stats` | GET | Graph statistics by kind and language, plus `server_id` + `started_at` |
| `/v1/graph` | GET | Full brief-graph dump (nodes + edges + stats); accepts `?project=` and/or `?repo=` for scoping |
| `/v1/events` | GET | SSE stream of graph-change events (the daemon watches tracked repos by default). Accepts `?token=<t>` for `EventSource` auth |
**Auth & binding.** A localhost `--http-addr` (e.g. `127.0.0.1:7411`) runs unauthenticated. Pass `--http-auth-token <token>` or set `$GORTEX_DAEMON_HTTP_TOKEN` to require `Authorization: Bearer <token>` on every `/mcp` and `/v1/*` request (constant-time compare; CORS preflights bypass). Non-localhost binds without a token are rejected at startup. CORS origin is configurable via `--cors-origin` (default `*`); it applies to both `/mcp` and `/v1`. `/healthz` is exempt from auth so liveness probes work.
**Scoping the graph.** The daemon serves every tracked repo from one shared index, so the HTTP surface spans all of them. Add or remove repos from the served set with `gortex track <path>` / `gortex untrack <path>`; query-time `?project=` / `?repo=` parameters (see the `/v1/graph` row above) narrow a request to a single workspace or repo without restarting the daemon.
**Multi-server roster.** When the daemon is running, it can route MCP traffic across multiple Gortex servers — a local Unix socket for the repos on this machine, plus one or more remote HTTPS servers for shared / cloud indexes. The roster lives at `~/.gortex/servers.toml`; manage it with `gortex daemon server list / add / remove`. Auth tokens can be embedded directly (`--auth-token`) or pulled from an env var the daemon reads at request time (`--auth-token-env`, preferred). Restart the daemon to pick up roster changes.
## MCP 2026 Streamable HTTP transport (`/mcp`)
`gortex daemon start --http-addr <addr>` exposes the **MCP 2026 Streamable HTTP transport** — the wire format the June 2026 MCP release locks in — on the same TCP address as the `/v1/*` JSON API.
| Verb | Path | Behaviour |
|------|------|-----------|
| `POST` | `/mcp` | One or more JSON-RPC frames in, one or a JSON-RPC array out. Notification-only batches return 202. |
| `GET` | `/mcp` | Opens an SSE stream the server uses to push server-initiated notifications (progress, sampling) onto the bound session. |
| `DELETE` | `/mcp` | Terminates a session. Idempotent — returns 204 even when the id is unknown. |
| `OPTIONS` | `/mcp` | CORS preflight; advertises the allowed methods. |
**Stateless per request.** Every POST carries `Mcp-Session-Id`; the transport replays the matching state out of a `streamable.SessionStore` (the default in-memory `MemoryStore` is TTL-evicted; swap for a Redis-backed adapter to share state across replicas behind a load balancer). `initialize` mints the id and returns it on the response header; an unknown id replies with a JSON-RPC `-32001 session not found` envelope. The `Mcp-Protocol-Version` header is echoed when provided; absent, the transport advertises its default. `tools/call` frames flow through the same multi-server router that serves `/v1/tools/<name>`, so workspace scoping carries over unchanged.
**Daemon enablement.** `gortex daemon start --http-addr 127.0.0.1:7411 [--http-auth-token <token>]` brings the transport up alongside the unix-socket dispatcher. Non-localhost binds require an auth token (or `$GORTEX_DAEMON_HTTP_TOKEN`). `/healthz` is exempt so liveness probes work. Once `--http-addr` is set, the daemon mounts `/mcp` alongside the `/v1/*` surface on the same address — no extra flag needed.
## Web UI
The web UI lives in its own repo at [`gortexhq/web`](https://github.com/gortexhq/web) so it can be deployed independently of the backend (Vercel / a static host / your own Next.js deployment). It's a standalone Next.js 15 app that talks to the daemon's HTTP surface over `/v1/*`:
```bash
# 1) Track the repo and start the HTTP backend (bearer-auth required for non-localhost binds)
gortex track /path/to/repo
gortex daemon start --http-addr 127.0.0.1:7411
# 2) Clone and run the UI in another terminal
git clone https://github.com/gortexhq/web.git gortex-web && cd gortex-web
echo 'NEXT_PUBLIC_GORTEX_URL=http://localhost:7411' > .env.local
npm install && npm run dev
# Open http://localhost:3000
```
| Page | Features |
|------|----------|
| **Dashboard** | Health, stats, language pie chart, node kind bar chart |
| **Graph Explorer** | Sigma.js 2D + five react-three-fiber 3D modes (City / Strata / Galaxies / Constellation / Graph3D), node filters, selection, detail panel |
| **Search** | Semantic + BM25 search via `/v1/*`, results grouped by kind |
| **Symbol Detail** | Source code, signature, callers/callees/usages/deps tabs |
| **Communities** | Community cards with cohesion bars, expandable members |
| **Processes** | Collapsible call-tree steps, product vs test process split |
| **Analysis** | Dead code, hotspots, cycles, index health — 4 tabs |
| **Contracts** | API contracts (HTTP, gRPC, GraphQL, topics, WebSocket, env vars) with provider/consumer matching, request/response type tracing, `yours / tests / deps / all` scope filter |
| **Services** | Service-level graph visualization with per-repo stats |
| **AI Chat** | LLM-powered chat with code context (placeholder) |
+63
View File
@@ -0,0 +1,63 @@
# Per-community skills & agent usage
## Usage with Claude Code
After `gortex install` (once per machine) and `gortex init` (once per repo), Claude Code automatically starts Gortex via `.mcp.json`. The agent gets:
- **Slash commands (19):** installed to `~/.claude/commands/` by `gortex install`. Three groups:
- *Discovery & analysis (8)* — `/gortex-guide`, `/gortex-explore`, `/gortex-debug`, `/gortex-impact`, `/gortex-dataflow-trace`, `/gortex-cross-repo-usage`, `/gortex-co-change`, `/gortex-onboarding`
- *Refactor & edit (enforce tool-call order) (6)* — `/gortex-refactor`, `/gortex-safe-edit`, `/gortex-rename`, `/gortex-extract-function`, `/gortex-fix-all`, `/gortex-add-test`. These wrap the speculative-execution (`preview_edit` / `simulate_chain`) and LSP code-actions (`get_code_actions` / `apply_code_action` / `fix_all_in_file`) paths so the agent does not bypass the safety steps by calling `Edit` / `Write` directly.
- *Review & operate (graph-grounded playbooks) (5)* — `/gortex-pr-review`, `/gortex-architecture-review`, `/gortex-quality-audit`, `/gortex-incident-investigation`, `/gortex-episode-replay`. These wrap the discovery + impact + memory surfaces into ordered playbooks so postmortems, audits, and PR reviews are graph-grounded.
- **Tool-usage skills:** the same 19 are installed as model-invoked skills to `~/.claude/skills/` by `gortex install` — one copy per user, used across every repo
- **Sub-agents (2):** installed to `~/.claude/agents/` by `gortex install`. Claude Code auto-routes matching prompts to them; each runs in a fresh context window and returns a single summary, keeping the parent's context clean. Tool allowlists are pinned to gortex graph tools only — Bash / Grep / Glob are unavailable to the sub-agent by construction.
- `gortex-search` — locate code, trace call paths, explore architecture
- `gortex-impact` — assess blast radius before editing (`verify_change`, `simulate_chain`, `check_guards`, `get_test_targets`)
- **PreToolUse hook:** automatic graph context + graph-tool suggestions on Read/Grep/Glob. The posture is selectable via `gortex install --hook-mode``deny` (default), `enrich`, `consult-unlock` (deny fallback reads only until the graph has been queried once this session), or `nudge` (a rate-limited soft reminder instead of a hard deny). Gortex's own MCP tools are auto-approved under the host's permissive permission modes
- **PreCompact hook:** condensed orientation snapshot injected before context compaction so the agent resumes without re-exploring
- **Stop hook:** post-task diagnostics — tests to run, guard violations, dead code, and contract issues on the changed symbols — injected as context before the agent hands off
- **CLAUDE.md:** per-repo codebase overview (via `--analyze`) plus a marker-guarded community routing block written by `gortex init --skills`
## The `gortex-cli` skill — a zero-schema consumption path
Alongside the MCP transport, `gortex install` writes a single user-level skill to `~/.claude/skills/gortex-cli/SKILL.md` that drives the same Gortex workflow entirely through `gortex` **shell verbs** — with no MCP server mounted and no tool schemas loaded into the model's context. It is a first-class consumption pattern, not a fallback:
- **One copy per user.** Like the other model-invoked skills it lives once at `~/.claude/skills/`, used across every repo.
- **No transport, no baseline tax.** Because nothing is published over MCP on this path, the agent pays zero context for tool schemas until it actually runs a verb. `gortex tools receipt` is the auditable record of that — it reports `registered_tool_schemas: 0`.
- **The full workflow in shell.** The skill routes the agent through `gortex` verbs that map 1:1 onto the MCP tools: `gortex call <tool>` (any tool by name), `gortex tools search` / `tools list` (discovery), `gortex edit context|verify|plan|preview|simulate|batch|apply|symbol|rename|guards|tests|contract|safe-delete` (the edit-safety surface), `gortex memory surface|store|recall|note|notes|distill` (session + durable memory), and `gortex analyze` / `flow` / `taint` / `clones` / `feedback`. The verb reference lives in [`cli.md`](cli.md#full-tool-surface-from-the-cli).
- **Same handlers, same daemon.** Each verb is a thin shell over one MCP tool on the daemon that owns the repo; the daemon dispatches the call by name, so the CLI reaches the **full** surface (including tools that are otherwise deferred behind `tools_search`) even under the lean `core` preset.
The trade-off versus an MCP install — push notifications and per-session overlay shadow graphs in exchange for a zero-schema baseline — is laid out in [`cli.md`](cli.md#choosing-a-consumption-path). The edit-safety and memory workflows are identical on both paths.
## Usage with other agents
`gortex install` (user-level) and `gortex init` (repo-level) together auto-detect and configure 14 other AI coding assistants — Kiro, Cursor, VS Code / Copilot, Windsurf, Continue.dev, Cline, OpenCode, Antigravity, Codex CLI, Gemini CLI, Zed, Aider, Kilo Code, OpenClaw. Each adapter writes only when its host is present on the machine, and every re-run is idempotent.
Tool-usage guidance for agents that have a user-level surface (Claude Code, Antigravity) lives once per user; for the rest, MCP tool descriptions carry the teaching and `gortex init` adds only a per-repo community-routing block — no more duplicated instructions blocks in every repo.
- **Adapter matrix + per-agent schema notes:** [`agents.md`](agents.md)
- **Audit what's currently configured:** `gortex init doctor` (zero-op; `--json` for CI consumers)
- **Constrain setup:** `gortex init --agents=claude-code,cursor` or `--agents-skip=antigravity` (same flags accepted by `gortex install`)
- **CI / scripted install:** `gortex install --yes --json` then `gortex init --yes --json --dry-run`
## Per-community skills
`gortex init --skills` (default on) analyzes your codebase, detects functional communities via Louvain clustering, and generates targeted SKILL.md files that Claude Code auto-discovers:
```bash
# Runs as part of `gortex init` by default — community generation is folded in
gortex init
# Tune or disable:
gortex init --skills-min-size 5 --skills-max 10
gortex init --no-skills
```
Each generated skill includes:
- **Community metadata** — size, file count, cohesion score
- **Key files table** — files and their symbols
- **Entry points** — main functions, handlers, controllers detected via process analysis
- **Cross-community connections** — which other areas this community interacts with
- **MCP tool invocations** — pre-written `get_communities`, `smart_context`, `find_usages` calls
For Claude Code, skills are written to `.claude/skills/generated/<DirName>/SKILL.md`, and a routing table is inserted into `CLAUDE.md` between `<!-- gortex:communities:start/end -->` markers. Every other detected agent gets the same routing table inside its per-repo instructions surface (`AGENTS.md` for Codex/OpenCode, `.windsurfrules` for Windsurf, `GEMINI.md` for Gemini CLI, `.cursor/rules/gortex-communities.mdc` for Cursor, etc.) — so the routing is consistent across tools on the same repo.
+89
View File
@@ -0,0 +1,89 @@
# Telemetry & privacy
Gortex can collect **anonymous usage statistics** — coarse, bucketed counts of *which* tools and commands run. It is **opt-in and OFF by default**: nothing is recorded, buffered, or sent until you explicitly enable it, and even when enabled nothing is transmitted unless an ingest endpoint is configured. Telemetry never sees your code, file paths, file names, symbol names, or repository names.
## Quick control
```bash
gortex telemetry status # is it on/off, why, what is collected, the anonymous install id
gortex telemetry on # enable anonymous tool/command counts
gortex telemetry off # disable and delete any buffered, unsent data
```
- `telemetry on` prints `Telemetry enabled — anonymous tool/command counts only (no code, paths, or names).`
- `telemetry off` prints `Telemetry disabled — buffered data cleared.` — it deletes all buffered daily rollups and the send marker (the anonymous install id is intentionally kept).
- `telemetry status` prints the state and the precedence rung that decided it (`decided by: env | do_not_track | config | default`), the ingest endpoint (`not configured — nothing is transmitted` when unset), the anonymous install id, and a one-line summary of what is collected.
`gortex install` also offers the choice: pass `--telemetry` / `--no-telemetry`, or use the **Anonymous telemetry** toggle in the interactive wizard. A toggle takes effect on a running daemon within a few seconds (it re-reads consent on the record and send paths) — no restart needed.
## How consent is decided
Consent resolves through a fixed four-rung precedence (**highest wins**):
| Rung | Signal | Effect |
| --- | --- | --- |
| 1 | `GORTEX_TELEMETRY` | Explicit per-process override — can force **on or off**. ON values `1/true/on/yes/enable/enabled`; OFF values `0/false/off/no/disable/disabled` (case-insensitive, trimmed). Highest, so it overrides even a global `DO_NOT_TRACK` for one invocation. |
| 2 | `DO_NOT_TRACK` | The cross-tool standard ([consoledonottrack.com](https://consoledonottrack.com)). Any set value other than `0`/`false` forces **off**. Can only ever disable, never enable. |
| 3 | Saved choice | The persisted `telemetry.enabled` value from `gortex telemetry on/off`. |
| 4 | Default | **Off.** |
An unrecognised or empty value at a rung falls through to the next rung rather than being treated as a decision. To turn telemetry off everywhere, set `DO_NOT_TRACK=1` or `GORTEX_TELEMETRY=0`.
## What is collected
Exactly **four** metric keys can ever be recorded — a hard allow-list; the aggregator physically cannot record anything else:
| Key | Meaning | Dimension |
| --- | --- | --- |
| `mcp_tool_call` | an MCP tool was invoked | tool name (e.g. `search_symbols`) |
| `cli_command` | a CLI subcommand ran | dotted command path (e.g. `daemon.start`, `review`) |
| `index` | an index pass completed | file-count bucket |
| `daemon_session` | a daemon session started | backend kind (e.g. `sqlite`) |
A recorded counter is `key` or `key:dimension` (e.g. `mcp_tool_call:search_symbols`, `index:1k-10k`). Values are **bucketed**, never exact — exact counts can narrow identification:
- File counts → `<100`, `100-1k`, `1k-10k`, `10k+`
- Durations → `<10s`, `10-60s`, `1-5m`, `5m+`
A dimension guard (`^[A-Za-z0-9_.<>+-]{1,32}$`) drops any token containing a path separator, whitespace, or over 32 characters, so even a caller that mistakenly passed a path or symbol name as a dimension cannot leak it — only the bare metric key is recorded.
### Never collected
Code or source content; file paths or names; symbol or repository names; IP-derived data; hostnames, usernames, or MAC addresses; exact counts.
## The anonymous install id
A random UUIDv4 minted on first use and stored at `~/.gortex/telemetry/install-id`. It is the only stable identifier telemetry carries, derived from nothing about your machine (no hostname, user, MAC, or path) — it merely ties one machine's daily aggregates together. `gortex telemetry status` prints it.
## Where state lives
All telemetry state is under `~/.gortex/telemetry/` (`<data-dir>/telemetry`; honours XDG / data-dir overrides). It lives under the durable data dir, not the cache, so a cache wipe never resets the install id or drops unsent days. Contents:
- per-UTC-day rollup files — the buffered daily aggregates
- `install-id` — the anonymous id
- `consent.json` — the persisted choice
- `last-send` — the once-per-day send marker
## Transmission
There is **no built-in default endpoint**. With `GORTEX_TELEMETRY_ENDPOINT` unset, telemetry is aggregated locally but **never transmitted** — the whole pipeline runs end-to-end except the final POST. When the endpoint is configured, and only while consent is enabled, Gortex sends:
- a single JSON `POST` to the endpoint, 5s timeout, **no retries** (a failed send leaves the days buffered)
- **at most once per UTC machine-day**
- only **completed** UTC days — never the day still accumulating; on a successful (`2xx`) response the sent days are deleted
Payload shape:
```json
{ "install_id": "…", "schema_version": 1, "gortex_version": "…",
"os": "darwin", "arch": "arm64", "ci": false,
"days": [ { "day": "2026-06-18", "counts": { "cli_command:review": 3, "index:1k-10k": 1 } } ] }
```
`os`/`arch` are the Go `runtime.GOOS`/`GOARCH`; `ci` reflects whether a CI environment was detected (`CI` env var set and not `0`/`false`); `counts` holds only the allow-listed, bucketed keys.
## First-run notice
The first time you run `gortex init` (non-dry-run), Gortex prints a one-time notice to stderr and records the default (off) choice so the notice fires at most once. It never enables anything:
> Gortex can collect anonymous usage stats (tool/command counts only — no code, paths, or names). It is OFF by default; enable with `gortex telemetry on`. See `gortex telemetry status`.
+103
View File
@@ -0,0 +1,103 @@
# Versioning
Gortex follows [SemVer 2.0.0](https://semver.org/). Every release tag is a SemVer identifier, and the binary carries the full canonical form — including the build-metadata slot — for traceability.
## Format
```
vMAJOR.MINOR.PATCH[-PRERELEASE][+COMMITSHA]
```
- **MAJOR**, **MINOR**, **PATCH** — integers, no leading zeros.
- **PRERELEASE** (optional) — dot-separated identifiers; alphanumeric and hyphens. Common values: `rc.1`, `alpha.2`, `beta`.
- **COMMITSHA** (build slot, optional) — the short git SHA the binary was built from. Injected by the release pipeline; local `make build` also injects it.
Examples:
| String | Meaning |
|---|---|
| `v0.1.0` | Tagged release from a clean commit. Build slot not shown because the tag alone is authoritative. |
| `v0.1.0+abc1234` | A build of `v0.1.0` made from commit `abc1234`. |
| `v0.2.0-rc.1+def5678` | First release candidate for `v0.2.0`, built from `def5678`. |
| `v0.1.0-4-g63d6c43-dirty+63d6c43` | Local dev build 4 commits past `v0.1.0`, with uncommitted changes. `git describe` generates this; the SemVer parser still accepts it. |
| `v0.0.0-dev` | Built with `go build` (no ldflags). Scripts can detect this sentinel. |
Run `gortex version` for the multi-line summary (commit + build date + Go toolchain + os/arch) or `gortex version --short` for just the canonical string.
## When to bump
Gortex exposes two user-visible surfaces — **the MCP tool API** and **the CLI**. SemVer rules apply to both.
**MAJOR** — breaking changes:
- Removing or renaming an MCP tool.
- Removing or renaming a required argument on any MCP tool.
- Changing the shape of a tool's return value in a way that would break an existing agent parsing it.
- Removing or renaming a CLI subcommand or a flag that existed in the previous MAJOR.
- Protocol-breaking daemon changes (e.g., bumping `daemon.ProtocolVersion`).
**MINOR** — additive changes:
- New MCP tools, new optional tool arguments, new fields in a tool's response object.
- New CLI subcommands or new optional flags.
- New config keys with backwards-compatible defaults.
- New backend features that don't alter existing outputs.
**PATCH** — no user-visible API or CLI changes:
- Bug fixes.
- Performance improvements.
- Internal refactors.
- Documentation updates.
- Dependency bumps that don't change observable behavior.
## Tagging a release
Recommended workflow — `gortex version bump` + `make tag-release`:
```bash
# 1. Bump the source-of-truth version in cmd/gortex/main.go
gortex version bump minor # or major / patch
# (for a release candidate: gortex version bump minor --pre rc.1)
# 2. Commit the bump
git add cmd/gortex/main.go
git commit -m "Bump version to v0.2.0"
# 3. Create the tag from the freshly-bumped value
make tag-release # annotated git tag, name pulled from ./gortex
# 4. Push
git push && git push origin v0.2.0
```
`make tag-release` rebuilds the binary without VERSION ldflags so `gortex version --short` reflects the literal value in `main.go`, strips the `+<commit>` build slot (git tags don't carry it), and creates an annotated tag. It refuses to run on a dev build, a duplicate tag, or a dirty working tree — so a misfire doesn't silently create a broken release.
The push triggers the release workflow at **Actions → Release**. Goreleaser picks up the tag, builds the matrix, publishes to GitHub Releases, and updates the Homebrew tap.
If you want to do the tagging by hand instead (e.g., to sign the tag):
```bash
git tag -s v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
```
## Programmatic access
The `internal/version` package parses and emits the canonical form. Callers that need structured access:
```go
import "github.com/zzet/gortex/internal/version"
v, err := version.Parse("v1.2.3-rc.1+abc1234")
// v.Major == 1, v.Minor == 2, v.Patch == 3
// v.Prerelease == "rc.1"
// v.Build == "abc1234"
// v.String() == "v1.2.3-rc.1+abc1234"
```
The daemon exposes its running version in the handshake ACK as `DaemonVersion` and on the control surface's `status` response — clients can feature-gate or warn on mismatch.
## 0.x caveat
Until Gortex reaches `v1.0.0`, the MAJOR rule relaxes slightly: breaking changes may ship in a MINOR bump when they fall under a clearly-communicated rework (with changelog entry). That's the standard SemVer 0.x behavior. From `v1.0.0` onward, the rules above apply strictly.
+471
View File
@@ -0,0 +1,471 @@
# GCX1 — Gortex Compact Wire Format
**Status:** Draft v1. Shipped in Gortex v0.9.0.
GCX1 is a tab-delimited, line-oriented, round-trippable wire format
for Gortex MCP tool responses. It is an opt-in alternative to JSON
selected per-call via `format: "gcx"`. On the benchmark bundled at
`bench/wire-format/` it yields a **median 27.4 % tiktoken savings**
vs JSON with **100 % round-trip integrity** across 20 representative
tool responses.
## Goals
- **Round-trippable.** Every GCX payload decodes back to an
equivalent Go value. No lossy text.
- **Tokenizer-aware.** Field delimiters, escape sequences, and
header syntax are chosen so tiktoken (cl100k_base) counts them as
whitespace or single tokens — matching the LLM budget users care
about, not just raw bytes.
- **Per-tool tunable.** Hot-path tools (`search_symbols`,
`find_usages`, `analyze`, ...) ship hand-tuned encoders with fixed
field layouts. Everything else falls through to a generic
fallback so no tool ever produces invalid GCX.
- **Versioned.** The header carries a protocol version. Decoders
reject unknown versions and agents can fall back to JSON
transparently.
## Non-goals
- Binary encoding. GCX1 is text-only; a future `GCX2` may carry
binary payloads (CBOR / MessagePack) under the same version
prefix, but v1 stays text so agents can read raw payloads during
debugging.
- Schema evolution inside a major version. The field layout for a
given tool is fixed for the lifetime of `GCX1`. New fields ship
as `GCX2`.
- Streaming. GCX1 is full-response. `GCX1-stream` is a reserved
future extension.
## Grammar (EBNF)
```
payload = section { section } ;
section = header row-line { row-line | comment } ;
header = TAG SP "tool=" token { SP key-value } SP "fields=" field-list LF ;
key-value = token "=" value ;
field-list = token { "," token } ;
row-line = value { TAB value } LF | LF ;
comment = "#" [ SP text ] LF ;
value = { escaped-char | safe-char } ;
escaped-char = "\\" ( "\\" | "t" | "n" ) ;
safe-char = any UTF-8 codepoint except TAB, LF, "\\" ;
TAG = "GCX1" ;
TAB = U+0009 ;
LF = U+000A ;
SP = U+0020 ;
```
## Header
Each section begins with a single-line header:
```
GCX1 tool=<name> fields=<a>,<b>,... [k=v]...
```
- `tool=` is the MCP tool name (or a dot-suffixed sub-section name
like `get_callers.edges`).
- `fields=` is a comma-separated list declaring the column order for
subsequent rows. At least one field is required.
- Additional space-separated `k=v` pairs carry metadata (`total`,
`truncated`, `etag`, `rows`, `ms`, ...). Keys are emitted in
sorted order so fixtures stay deterministic.
Header values that contain spaces, `=`, tabs, newlines, or backslashes
must be escaped exactly as row values are escaped.
Example:
```
GCX1 tool=search_symbols fields=id,kind,name,path,line,sig rows=3 total=7 truncated=false
```
## Rows
After the header, each non-blank, non-comment line is a row of
tab-separated values in the order declared by `fields=`.
- Fewer values than declared fields: missing trailing columns default
to `""`.
- More values than declared fields: decoder returns an error.
- Blank lines between rows are ignored.
## Comments
Lines beginning with `#` are comments. Comments carry no data; any
intermediary may drop them. The encoder uses them to annotate the
first row of a section (e.g. `# 3 matches`).
## Escape rules
A row value may contain the following characters by escaping them:
| Character | Escape |
|-----------|:------:|
| `\` (backslash) | `\\` |
| TAB (U+0009) | `\t` |
| LF (U+000A) | `\n` |
Any other `\x` sequence decodes to the literal byte `x` so a
pathological payload cannot wedge the decoder. Callers should treat
decoded values as untrusted input.
CR (U+000D) is stripped on encode so Windows CRLF input round-trips as
`\n`-only output.
## Multi-section payloads
A GCX1 payload may contain multiple sections concatenated back-to-back.
Each new section begins with its own `GCX1` header line. Decoders
detect section boundaries by scanning for the header tag after the
current section's rows exhaust.
Multi-section is used by:
- `get_callers`, `get_call_chain`, `get_dependencies`,
`get_dependents`, `find_implementations` — emit `<tool>.nodes`
then `<tool>.edges`.
- `get_editing_context` — emits `target`, `callers`,
`dependencies`, `tests` sections.
- `get_repo_outline` — one section per top-level key
(`languages`, `communities`, `hotspots`, `most_imported`,
`entry_points`).
## Per-tool field layouts (GCX1 v1)
### `search_symbols`
| field | type | description |
|-------|------|-------------|
| id | string | node ID |
| kind | string | `function`, `method`, `type`, `interface`, `variable`, `contract` |
| name | string | short name |
| path | string | file path |
| line | int | start line |
| sig | string | extracted signature, optional |
Header meta: `total`, `truncated`.
### `get_symbol_source`
| field | type | description |
|------------|--------|-------------|
| id | string | |
| kind | string | |
| name | string | |
| path | string | |
| start_line | int | |
| end_line | int | |
| from_line | int | first line of returned source (may precede `start_line` by `context_lines`) |
| sig | string | |
| etag | string | content hash for `if_none_match` caching |
| source | string | full source text, tab/newline-escaped |
Exactly one row.
### `batch_symbols`
| field | type |
|------------|--------|
| id | string |
| kind | string |
| name | string |
| path | string |
| start_line | int |
| end_line | int |
| sig | string |
| source | string | *(present only when `include_source=true`)* |
| error | string | non-empty when the symbol could not be resolved |
### `find_usages`
| field | type | description |
|------------------|--------|-------------|
| from | string | caller symbol ID |
| to | string | called symbol ID (the query subject) |
| edge_kind | string | `calls`, `references`, `implements`, ... |
| context | string | reference role at the usage site: `parameter_type`, `return_type`, `field`, `value`, `type`, `attribute`, `generic_arg`, `call` |
| return_usage | string | how a call site consumes the return value: `discarded`, `assigned`, `partially_ignored`, `returned`, `goroutine`, `deferred`, `argument`, `condition`; empty when unclassified |
| origin | string | provenance: `lsp_resolved`, `lsp_dispatch`, `ast_resolved`, `ast_inferred`, `text_matched` |
| tier | string | coarse provenance label derived from origin |
| confidence | float | 0..1 |
| from_name | string | caller short name |
| from_path | string | usage-site file path |
| from_line | int | call-site line (falls back to the caller's start line) |
| from_is_test | bool | caller is a test symbol |
| from_test_role | string | `test`, `benchmark`, `fuzz`, `example` when applicable |
| from_test_runner | string | detected JS/TS test runner when applicable |
### `get_file_summary`
| field | type |
|-------|--------|
| id | string |
| kind | string |
| name | string |
| line | int |
| sig | string |
Header meta: `total_nodes`, `total_edges`, `truncated`, `etag`.
### `get_callers` / `get_call_chain` / `get_dependencies` / `get_dependents` / `find_implementations`
Two sections: `<tool>.nodes` then `<tool>.edges`.
- `.nodes` fields: `id`, `kind`, `name`, `path`, `line`.
- `.edges` fields: `from`, `to`, `kind`, `origin`, `confidence`, `label`.
`get_callers` emits a third section, `get_callers.caller_notes`, only
when at least one caller carries a concurrency-safety annotation —
fields `id`, `sync_guarded`, `sync_guarded_why`, `cross_concurrent`,
`cross_concurrent_why`. The section is absent entirely when no caller
is flagged, so the other traversal tools' output is unchanged.
### `get_editing_context`
Four sections. Fields:
- `.target`: `id`, `kind`, `name`, `path`, `start_line`, `end_line`,
`sig`, `etag`. One row.
- `.callers`: `id`, `kind`, `name`, `path`, `line`.
- `.dependencies`: same as `.callers`.
- `.tests`: `path`.
### `smart_context`
Two sections: `.task` (one row, field `task`) and `.symbols` with
fields `id`, `kind`, `name`, `path`, `line`, `score`, `reason`.
### `analyze`
Kind-polymorphic header tag (`analyze.dead_code`,
`analyze.hotspots`, `analyze.cycles`, `analyze.<other>`):
- `analyze.dead_code`: `id`, `kind`, `name`, `path`, `line`, `reason`.
- `analyze.hotspots`: `id`, `name`, `path`, `line`, `fan_in`,
`fan_out`, `cross_cut`, `score`.
- `analyze.cycles`: `size`, `severity`, `nodes` (comma-separated).
- Anything else falls through to the generic fallback encoder.
### `contracts`
- `contracts.list`: `id`, `type`, `method`, `path`, `service`,
`providers`, `consumers` (comma-separated lists).
- `contracts.orphans` (only when `action=check`): `contract_id`,
`side`, `repo`, `symbol`.
## Workspace-aware MCP shapes
GCX1 v1 also defines three protocol-level shapes that travel alongside
tool responses: a **tool-definitions** registry section, a
**tool-request** envelope, and an **error** envelope. Every MCP
tool definition carries an explicit `scope`, and so the legality of an
inbound call can be decided by combining that scope with the request's
`repo` parameter. All three shapes are first-class GCX1 sections and
must round-trip byte-identically across `gcx-go` and `gcx-ts`.
[adr2]: ../../adr/0002-workspace-aware-mcp-bind.md
### `tool_definitions`
Section for the per-tool scope registry. Layout:
```
GCX1 tool=tool_definitions fields=name,scope
<name>\t<scope>\n
...
```
- `name` is the MCP tool name (one row per tool).
- `scope` is one of the three string literals `repo`, `workspace`,
`fan-out`. Anything else is a schema error in both codecs.
- Rows are emitted in ascending `name` order so the bytes are
reproducible regardless of the encoder's input order.
A definition without `scope` (empty cell, missing column, or unknown
value) is a schema error and both codecs reject it on encode and on
decode.
### `tool_request`
Envelope for one inbound MCP call. Layout:
```
GCX1 tool=tool_request fields=tool,scope,repo
<tool>\t<scope>\t<repo-cell>\n
```
Exactly one row. The `repo` cell is a **union shape decided by
`scope`**:
| scope | `repo` cell |
|--------------|-----------------------------------------------------------------------------------|
| `repo` | a non-empty repo name (plain string, e.g. `gortex`) |
| `workspace` | empty string (the `repo` parameter is absent) |
| `fan-out` | a compact JSON-array literal, e.g. `["*"]` or `["gortex","gortex-cloud"]` |
Rationale for the cell encoding choices:
- **scope=repo → plain string.** A single repo name is the most
common case and never needs structure; a plain string keeps the cell
tokenizer-friendly.
- **scope=workspace → empty.** The `repo` parameter MUST NOT be
present for workspace-level tools. The empty
cell — already how GCX1 represents an absent column under the
"fewer values than declared fields default to empty" rule — is the
correct on-wire signal for that absence.
- **scope=fan-out → compact JSON array.** This re-uses the
generic-fallback nested-value rule already used elsewhere in GCX1
("nested values inside a cell serialise to compact JSON"). Callers
decode the cell with `JSON.parse` (TypeScript) or `json.Unmarshal`
(Go) without learning a new escape format. Alternative encodings
considered:
- *Comma-joined string* (e.g. `gortex,gortex-cloud`): rejected
because some namespaces legitimately contain commas (gRPC method
paths, generic type parameters).
- *Repeated cells across multiple rows*: rejected because the
request envelope is single-row by contract; multi-row would
overload the section's identity.
- *Tab-joined string*: rejected because tab is the GCX1 column
delimiter; any in-cell use would force an escape and break the
"tabs never appear in cells" property the format relies on for
fast scanning.
Compact JSON wins on three axes simultaneously: it is unambiguous
(every list value round-trips), it composes with the existing
generic-fallback rule, and it stays on a single physical line.
The `["*"]` sentinel is a literal two-character string `*` inside a
JSON array — it is the **only** legal way to spell "fan out across
every repo in this workspace". Omitting `repo` for a fan-out tool is a
protocol error, surfaced as an `error` section with code
`missing_repo_list` (see below).
### `error`
Envelope for protocol-level rejections returned by the server in lieu
of a tool result. Layout:
```
GCX1 tool=error fields=code,message,detail
<code>\t<message>\t<detail>\n
```
Exactly one row. `code` MUST be non-empty; `message` and `detail` are
free-form strings (escape rules apply per the standard table). The
codes defined in GCX1 v1:
| code | when |
|---------------------|-------------------------------------------------------------------------------------|
| `unknown_repo` | a fan-out request lists a name not present in the active workspace (resolved Q1) |
| `missing_repo_list` | a `scope: fan-out` request omits `repo` in workspace mode |
| `missing_repo` | a `scope: repo` request omits `repo` in workspace mode |
| `repo_not_allowed` | a `scope: workspace` request includes `repo` (any value) |
| `wrong_repo_shape` | the `repo` parameter has the wrong type for the tool's declared scope |
Both codecs expose these as named constants
(`ErrCodeUnknownRepo` / `ERR_CODE_UNKNOWN_REPO`, etc.) so call sites
do not stringly type the code value.
### Conformance
The fixtures under `gcx-ts/test/golden/scope_*.gcx` cover one fixture
per scope kind (repo, workspace, fan-out with `["*"]`, fan-out with a
named subset) plus the two named protocol-error shapes. The Go-side
`gcx-go` parity test (`scope_golden_test.go`) re-encodes the same
logical inputs and asserts byte-for-byte equality against the
committed fixtures. Any drift between `gcx-go` and `gcx-ts` MUST fail
that test before any other CI step.
## Generic fallback
Any tool without a hand-tuned encoder routes through the generic
fallback. The fallback inspects the canonical JSON shape:
| Input shape | Output |
|-------------|--------|
| `{}` object | one section, one row, fields = sorted keys |
| `[]` array of objects | one section, one row per element, fields = union of keys (sorted) |
| `[]` array of scalars | one section, field `value`, one row per element |
| scalar | one section, field `value`, one row |
Nested values (arrays / objects) inside a cell serialise to compact
JSON so the cell stays on a single physical line. Decoders may
re-hydrate by `JSON.parse` on such cells.
## Versioning
- The literal header prefix `GCX1` is stable for the lifetime of
version 1.
- A decoder that sees a different prefix (e.g., `GCX2`) must
treat the payload as unknown and MAY fall back to JSON by
re-issuing the MCP call without `format: "gcx"`.
- Field layouts for declared tools are frozen within `GCX1`.
Additions ship as `GCX2` — renaming a tool's field set is
a breaking change.
## Rationale
- **Tab delimiter (not comma):** symbol names routinely contain
commas (`(int, string)`) and parentheses. Tab is rare in source
and absent from identifiers. Escape pressure stays low.
- **Newline-terminated rows:** tokenizer-friendly and
transport-transparent (no binary framing). SSE / chunked HTTP
can forward one row per frame without re-parsing.
- **Minimal escape alphabet:** two-byte `\t` / `\n` / `\\` keeps
the hot path cheap. Code payloads rarely contain raw tabs or
unescaped backslashes, so escape overhead is a rounding error
in practice.
- **Header-based metadata:** `total`, `truncated`, `etag` live on
the header rather than a per-row phantom column. That keeps the
row schema flat and lets the encoder skip meta work when the
tool doesn't care.
## Reference implementations
- **Go encoder / decoder:** MIT-licensed standalone module at
[`github.com/gortexhq/gcx-go`](https://github.com/gortexhq/gcx-go)
(`go get github.com/gortexhq/gcx-go`) — header + row + escape
primitives + generic fallback. Per-tool hand-tuned encoders live in
`internal/mcp/gcx.go`.
- **TypeScript decoder:** MIT-licensed standalone package at
[`github.com/gortexhq/gcx-ts`](https://github.com/gortexhq/gcx-ts)
(npm: [`@gortex/wire`](https://www.npmjs.com/package/@gortex/wire)).
## Benchmark
See `bench/wire-format/`. The harness scores bytes, tokens, gzip
bytes, and round-trip integrity across 20 representative tool
responses and emits a markdown scorecard. Rerun after any change to
the upstream `gcx-go` module or `internal/mcp/gcx.go` to catch
regressions.
### Dual tokenizer scorecard
The scorecard renders one or two tables depending on `--tokenizer`:
- `cl100k` — tiktoken `cl100k_base` only. The historical default;
matches Claude 3 / Opus 4 / Sonnet 4 / Haiku 4.5 / GPT-4o token
budgets.
- `opus47` — Claude Opus 4.7 input-token counts only.
- `both` (default) — stacks the two tables so the same fixtures show
up under each tokenizer.
The Opus 4.7 column has two data sources:
1. **Scalar estimate (default, offline).** Each cl100k_base count is
multiplied by an empirical inflation factor (~1.35×) and labeled
`estimated` in the table footer. Per-fixture variance runs
28-42%; the median across the 20-case suite is honest.
2. **Exact counts via Anthropic's `messages/count_tokens` API**
(`--use-api`). Requires `ANTHROPIC_API_KEY`. Successful calls
populate `bench/wire-format/opus47-counts.json` so subsequent
runs are deterministic without re-hitting the API. Network
failures degrade gracefully to the scalar with a single warning
on stderr.
The headline median token-savings figure stays around 27% under
both tokenizers — the wire format's advantage compounds with the
tokenizer change rather than being amplified by it.