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
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:
@@ -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.
|
||||
@@ -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)).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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 (65–80%); 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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 3–5 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 (~10–15 % 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).
|
||||
@@ -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).
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.5–4 s; quality is highly model-dependent — unreliable on 3B local models, fine on 7B+ or hosted
|
||||
|
||||
See [llm.md](llm.md) for provider configuration.
|
||||
@@ -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) |
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user