a06f331eb8
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
175 lines
14 KiB
Markdown
175 lines
14 KiB
Markdown
# 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.
|