Reference

The documentation, unabridged.

The landing page argues a case. This is the README itself — every command, every flag and every captured run, in the order the repository keeps them.

Install and start

Requires Bun 1.4+ and git. From this checkout:

sh
bun install
bun run build                 # standalone binary: dist/sgx, with WASM, workers and UI embedded
bun link                      # registers sgx -> src/cli.ts; requires this checkout and Bun
cd /path/to/your/repository
sgx init --claude              # index and merge repository-local Claude Code configuration
sgx init --codex               # the same, for Codex (.codex/config.toml)
sgx init --fx                  # the same, for fx (.mcp.json and AGENTS.md)
sgx find login
sgx source add https://docs.example.com/api.md --covers 'src/api/**'
sgx ui

While working on sgx itself, run the working tree directly — no build, no bun link, and the source you just edited:

sh
bun run --silent sgx find login   # from this checkout (`--silent` drops bun's banner)
./scripts/sgx find login          # the same
cd /other/repo && /path/to/sgx/scripts/sgx source list   # against another repository

scripts/sgx execs bun <checkout>/src/cli.ts "$@" and changes nothing else, so --root still defaults to the caller's repository.

Use the absolute path to dist/sgx instead of sgx to run the compiled binary. The package name is sgx.sh; the executable is sgx. This private package is installed from source, not from a published registry release. init creates .sgx/graph.db and .sgx/.gitignore; it does not edit your root .gitignore. --claude, --codex and --fx keep existing MCP servers, hooks and instructions, adding only missing sgx entries. All three reject configuration paths that resolve outside the repository, and any of them may be passed on its own or together.

CLI reference

All commands accept --root <dir> and --json. The default root is the nearest ancestor containing .git, otherwise the working directory. Scope flags (--repo, --project, --workspace, --all) widen a command past one repository; see Workspaces. Read commands refresh the index first; --no-refresh skips that pass. --help lists commands and --version reports the version. Watch/UI use JSON readiness/events; MCP always speaks the MCP protocol and hooks retain their Claude Code output contract.

These samples were captured from the compiled binary on 2026-09-19 in a temporary copy of test/fixtures/mini with git history. Paths, ports and timings are the actual run's values; handles remain stable across body edits. Where described as an excerpt, lines were omitted without rewriting the shown output. All ~ token counts are estimates at roughly four characters per token.

init, index, watch, stats

init [--claude] [--codex] creates the graph and runs its first index:

sgx init --claudecaptured output
initialised .sgx/ in /var/folders/k3/bvwcdygn053bktwwbzm9c8vc0000gn/T/sgx-mini-HIiPLr · indexed 23 of 23 files (+23 ~0 -0) · 23 files · 44 symbols · 68 edges · epoch 1 · 66 ms · history mined
.mcp.json: added mcpServers.sgx → /Users/tim/workspace/sgx/dist/sgx mcp
.claude/settings.json: added hooks.SessionStart → /Users/tim/workspace/sgx/dist/sgx hook session-start
.claude/settings.json: added hooks.PostToolUse (Edit|Write|MultiEdit) → /Users/tim/workspace/sgx/dist/sgx hook post-edit

index [--force] refreshes changed files; --force reparses everything. stats reports graph size, resolution confidence and timings.

sgx indexcaptured output
indexed 2 of 25 files (+2 ~0 -0) · 25 files · 52 symbols · 68 edges · epoch 2 · 18 ms
sgx statscaptured output
epoch 2 · 25 files · 293 lines · 52 symbols · 68 edges · 82 refs
languages  typescript 9 · json 5 · python 4 · go 3 · javascript 1 · markdown 1 · other 1 · tsx 1
symbols    function 17 · key 16 · method 6 · interface 5 · class 2 · const 2 · enum 1 · section 1 · struct 1 · type 1
edges      calls 21 · imports 20 · tests 15 · references 12
confidence 44 at 1.0 · 15 at 0.9 · 9 at 0.5 · 0 at 0.2
git        HEAD 388aa7f · 23 files with history · 1 co-change pairs
last index 18 ms (parse 1 ms) · 2 of 25 files parsed · +2 ~0 -0 · epoch 2

watch keeps indexing until Ctrl-C. Edits are debounced into one epoch; failed batches retry with capped backoff. Startup from this run:

sgx watchcaptured output
watching /var/folders/k3/bvwcdygn053bktwwbzm9c8vc0000gn/T/sgx-mini-HIiPLr · up to date · 25 files · 52 symbols · 68 edges · epoch 3 · 14 ms

find, outline, show, map, flavors, context

find <query> [--limit 20] [--kind <kind>] searches names, paths and docs, boosting exact and prefix names before applying the result limit.

sgx find login --limit 3captured output
@twtp1 fn    login(user: User, opts: LoginOpts = {}): Session   src/auth/login.ts:17
@89nj2 iface interface LoginOpts                                src/auth/types.ts:6
@jwcqd fn    testLogin(): void                                  tests/login.ts:3

outline <file|dir> prints signatures, handles and anchored notes, eliding bodies:

sgx outline src/auth/store.tscaptured output
src/auth/store.ts · 23 lines · outline ~53 tok (full ~112)
@atvdc export class SessionStore {
  @rtj1r get(id: string): Session | undefined { … 3 lines }
  @f5zfk set(session: Session): void { … 3 lines }
  @8czjn clear(): void { … 3 lines }
}
@hwz8t export const store

show <handle|path#qualname|unique-name>... [--level 0-3] [--lines] uses the fidelity ladder: L0 location/name, L1 signature, L2 documentation and member outline, L3 full source (default). --lines adds source line numbers. Use --session <id> to deduplicate L3 bodies across CLI calls and --fresh to resend them. Without a session, CLI output is self-contained.

sgx show validate --level 1captured output
@f0daw fn validate(user: User): boolean  src/auth/login.ts:9

map [--budget 1500] ranks central exported symbols and entry points. First lines of a 200-token-estimate map:

sgx map --budget 200captured output
repo map · ~193/200 tok · 25 files · 12 symbols · epoch 2

entry: go/cmd/main.go · py/app/service.py · src/cli.ts

go/greeter/ (1 file)
  @bpayd type Greeter struct  greeter.go:4
  @b32tr fn New() *Greeter  greeter.go:14

flavors [--evidence] lists detected ecosystems by package. The small mini fixture has none:

sgx flavorscaptured output
no flavors detected

context "<task>" [--budget 4000] [--working-set] [--session <id>] packs code into an estimated token budget. Explicit handles, paths, identifier terms and optionally the git working set seed personalized PageRank. The strongest seeds receive source before remaining space goes to neighbours. Generic/common terms carry less weight. JSON and YAML keys are background: a task word landing on a config key is a coincidence, not a request, so the packer skips them unless the caller names one by handle or path — and still falls back to them when a question about configuration has nothing else to answer with. They stay indexed either way, because find and docs want them. Symbols are grouped by path and line; the footer lists what was dropped. Output is deterministic for the same graph, sources and ledger. bun run eval:context runs the relevance evaluation set.

sgx context login --budget 200captured output
context · ~197/200 tok · 7 symbols in 3 files · epoch 2

src/auth/login.ts
@f0daw fn validate  :9
@twtp1 fn login  :14-25
/**
 * Log a user in and remember the session.
 */
export function login(user: User, opts: LoginOpts = {}): Session {
  if (!validate(user)) {
    throw new Error("invalid user");
  }
  const token = `${formatName(user.name)}-${opts.remember ? "long" : "short"}`;
  const session: Session = { token, user };
  store.set(session);
  return session;
}
@5kx7a fn logout  :27

src/auth/types.ts
@rxqer iface User  :1
@89nj2 iface LoginOpts  :6-8
export interface LoginOpts {
  remember?: boolean;
}
@sn2v2 iface Session  :10

tests/login.ts
@jwcqd export function testLogin(): void { … 5 lines }

not shown (12): @atvdc 0.32 · @b0q5q 0.29 · … — expand: sgx show <handle>

impact, tests, check, hotspots

impact <target|--diff> [--depth 3] combines static callers/importers, tests and git co-change evidence. Inferred relationships print ~; syntactic resolution is not a complete, type-checked call graph.

sgx impact login --depth 1captured output
impact @twtp1 login · src/auth/login.ts:17
2 callers in 3 files · 1 test · 1 co-change partner
direct:
  @jb7pz module src/cli.ts:1  imports
  @eqdyv function main  src/cli.ts:3  calls
  @2pnq2 module src/index.ts:1  imports
  @kd19m module tests/login.ts:1  imports
  @jwcqd function testLogin  tests/login.ts:3  calls
tests:
  tests/login.ts
co-change:
  src/auth/store.ts  support 1.00 (4 commits)

tests <target|--diff> prints one test path per line. For example, sgx tests --diff | xargs bun test selects tests reachable from a working diff.

sgx tests logincaptured output
tests/login.ts

check [--diff|--file <path>|--all] reports removed exports, stale callers of changed signatures, cycles, architecture-rule violations, untested changes and (with --all) dead exports. It exits 1 for errors, 0 for warnings/info. The first line from the fixture's --all report:

sgx check --allcaptured output
warn cycle src/core/a.ts import cycle src/core/a.ts -> src/core/b.ts -> src/core/a.ts

Architecture rules and file/test globs live in repository-local sgx.json:

json
{
  "exclude": ["generated/**"],
  "tests": ["tests/**", "**/*.test.ts"],
  "rules": [{"name": "core-no-ui", "from": "src/core/**", "to": "src/ui/**"}]
}

docs [<path>] [--stale] reports which documents cover which code; see Documents. It exits 1 when --stale finds anything behind.

hotspots [--limit 20] ranks normalized 90-day churn × complexity × log(1 + fan-in). This fixture's January commits are outside the run's 90-day window, so its scores are zero:

sgx hotspots --limit 3captured output
0.00  src/auth/login.ts      churn 0 in 90d (4 total) · complexity 6 · fan-in 2
0.00  .claude/settings.json  churn 0 in 90d (0 total) · complexity 5 · fan-in 0
0.00  go/greeter/greeter.go  churn 0 in 90d (1 total) · complexity 5 · fan-in 1

changes and note

changes [--since <epoch>] [--limit 500] prints committed symbol changes. When truncated, text and JSON report omitted rows and text gives a larger-limit command. After adding a parameter to login:

sgx changes --since 2captured output
changes since epoch 2 · 1 symbol change in 1 file · epoch 3
M sig  @twtp1 login(user: User, opts: LoginOpts = {}): Session → login(user: User, opts: LoginOpts = {}, required: boolean): Session  src/auth/login.ts:17

note add <ref> <text> [--author <name>], note list [<ref>], and note rm <id> manage anchored notes. Notes become stale after body edits; list without a ref also shows notes whose symbol no longer resolves. Put global flags before -- to preserve flag-like note text, for example sgx note add login -- --json is an argument, not an output mode.

sgx note add login "Retries twice on 503" --author developercaptured output
#1 @twtp1 login  src/auth/login.ts:17: Retries twice on 503
sgx note list logincaptured output
#1 @twtp1 login  src/auth/login.ts:17: Retries twice on 503
sgx note rm 1captured output
removed note #1

git

sgx git [status|log|show|authors|branches|remotes|owners] reads the history mined into the graph, plus the live working tree for status. History is mined from one git log --raw --numstat pass over the last 2,000 commits when HEAD moves; renames are not followed, so history belongs to the path it was made under.

sgx git statuscaptured output
main · 388aa7f · 5 commits · 1 authors
working tree clean
sgx git log --limit 4captured output
388aa7f 2026-01-06 Test Author      +1/-0 1f  format note
d0ac0ba 2026-01-04 Test Author      +2/-0 2f  auth tweak 3
e496837 2026-01-03 Test Author      +2/-0 2f  auth tweak 2
e99edba 2026-01-02 Test Author      +2/-0 2f  auth tweak 1
sgx git authorscaptured output
    5 Test Author <author@example.com> · 23 files · +257/-0 · 2026-01-01…2026-01-06
sgx git owners src/auth/login.tscaptured output
src/auth/login.ts · 1 author
  100% Test Author <author@example.com> · 4 commits · +30/-0 · last 2026-01-04

log takes --limit, --path and --author; show <sha> accepts an abbreviated sha and lists the commit's files with their line counts.

github

sgx github [sync|repo|prs|issues|pr <n>|review <n>] over the real GitHub GraphQL API.

Token, first hit wins: $SGX_GITHUB_TOKEN, $GITHUB_TOKEN, $GH_TOKEN, then gh auth token. So if you already use gh, there is nothing to configure. The token is never stored, logged or printed, and the only header sgx sends about itself is User-Agent: sgx.

When it goes to the network: sync always; any other github read (and the UI's GitHub screen) when the cache is empty or older than 15 minutes. Never during indexing, watching, the post-edit hook, check or context. --offline or SGX_OFFLINE=1 keeps it off the network entirely and answers from the cache. A failed sync keeps the cache and tells you what failed.

sgx github synccaptured output
cli/cli · api · 94 pull requests (63 open) · 57 issues (40 open) · 4824 API calls left until 2026-09-20 16:16
file lists truncated at 100 by the API for #13017

Each sync makes four passes at once: every open pull request — "what is in flight" has to be complete — a recent window of any state (--limit, default 100), and the same two for issues. Rows are upserted, so syncing repeatedly accumulates history rather than truncating it. files(first: 100) is a hard API page, so a bigger pull request is reported as truncated rather than quietly analysed short.

sgx github prs --state allcaptured output
#14475 open         Add a skill for chaptered CLI recordings                     BagToad        76f +16765/-0 2026-09-19
#14462 merged       Revert "Use internal Apple codesign action"                  williammartin  2f +3/-3 2026-09-15
· cli/cli · api · synced 2026-09-20 15:45

pr <n> is the point of the integration: the pull request's changed paths are resolved into the graph and answered with the same blast radius sgx impact gives, plus the reviewers the file history suggests and any downstream repositories from the registry.

sgx github pr 42captured output
#42 Retry login twice on 503
open · dana · login-retry → main · REVIEW_REQUIRED · 2026-01-01
checks     test failure
files      2 files · +12/-3 · 8 symbols · 3 callers in 4 files · 1 tests

symbols
  @twtp1 function  login src/auth/login.ts:17
  @atvdc class     SessionStore src/auth/store.ts:7

impact     3 callers in 4 files · 1 test · 0 co-change partners
   @eqdyv main src/cli.ts:3 (calls, depth 1)
   @jwcqd testLogin tests/login.ts:3 (calls, depth 1)
tests      tests/login.ts

reviewers  Test Author (2 files, 100%)

review <n> prints the same report followed by check findings scoped to the changed files, and exits 1 on an error finding. And check itself now warns when a file you are editing is also changed by an open pull request — the conflict you would otherwise meet at merge time:

sgx checkcaptured output
warn  open-pr  src/auth/login.ts  also changed by open pull request #42 (dana)

That finding reads only the cache, so it costs nothing on the post-edit hook.

Testing and pinning. $SGX_GITHUB_FIXTURE, else <root>/.sgx/github.json, replays recorded responses and wins over the API. The test suite and bun run e2e use it and never touch the network; bun run scripts/record-github.ts <owner/name> re-records the API payloads in test/fixtures/github/ that the mapping is tested against.

enrich and where

The graph is syntactic, so sgx find auth misses SessionStore and validateCredentials. sgx enrich judges every eligible symbol into a concept — one request per symbol to TypeSafe's System One model, three questions each (concept, public API, change risk), each answer carrying the model's own confidence.

sgx enrich --vocabularycaptured output
40 concepts
  auth               role  authentication, authorization, sessions, tokens, credentials
  resilience         role  retries, timeouts, backoff, circuit breaking and error recovery
  …
  ui                 repo  code under the ui directory (42 files)
  nextjs             repo  Next.js framework code

Half the vocabulary is universal roles; half is derived from the repository's own directories, package names and detected flavors, so nothing is generated and the list is reproducible from the graph. Docs, tests and hidden trees contribute nothing.

sgx enrichcaptured output
jev-latest · judged 1,694 of 1,694 sent (1,714 eligible) · 40 concepts, 37 in use · 20944 ms · 2,311,847 input tokens

That is the whole of sgx: 1,694 symbols in 21 seconds, about ten cents. It is incremental by body hash, so a second run sends only the symbols whose bodies changed, and --limit enriches a large repository in slices.

sgx where "caching"captured output
cache (10) · 4 symbols
@h8kcy 100% cache          perEpoch      src/ui/api.ts:155
@frevc  99% cache          getCsr        src/context/csr.ts:39
@hkymv  88% cache          ensureSynced  src/github/sync.ts:88
@m0s0e  87% cache          REFRESH_MS    src/github/sync.ts:14

None of those four say "cache" in their name. sgx find --concept cache filters lexical search the same way, and sgx enrich --concepts and --status show the vocabulary in use and how much of the repository has been judged.

Concept matching is lexical, and a word is weighted by how many concept descriptions share it — "sessions" routes to auth, "service" routes nowhere. A question that matches nothing says so rather than guessing:

sgx where "where do we prove who the caller is before talking to a hosted service"2 commands
no concept matches '…' — try `--route` to ask the model

$ sgx where "where do we prove who the caller is before talking to a hosted service" --route
auth (7) · 3 symbols
@fzx1a 100% auth           fromGhCli     src/github/token.ts:18
@7nxv1 100% auth           requireToken  src/github/token.ts:35
@8hrb7  98% auth           TokenSource   src/github/token.ts:9

Two confidences, never merged. The percentage on a symbol line is the judge's confidence that the symbol belongs to the concept. Whether the question is about that concept is a separate thing, and it is the one to read first, so it goes in the header. Every description word unique to one concept scores the same, which means the score alone cannot tell a good detail match from a bad one: sessions and session ledger resending bodies both reach auth with exactly 1.5. What separates them is how much of the question the match explains, so a lone word inside a longer question is reported as weak rather than rendered like a verdict:

sgx where "session ledger resending bodies"captured output
auth (7) · 7 symbols
weak match: 1 of 4 question words reached 'auth' — `--route` asks the model instead
@fzx1a 100% auth           fromGhCli      src/github/token.ts:18

Lexical matching genuinely cannot decide that case, and --route is what can. A routed answer is never weak: the model judged the whole question, and its choice probability is calibrated. Routing is also allowed to answer nothing — the vocabulary it is given carries an explicit "none of these" option, because a choice question always picks something, and without somewhere to put "the repository has no concept for this" the model is forced to name one anyway.

Key: $SGX_TYPESAFE_API_KEY or $TYPESAFE_API_KEY. Network: sgx enrich and sgx where --route are the only commands that reach the model; SGX_OFFLINE=1 stops both. Recorded judgements in .sgx/semantic.json replace the model entirely and are how the test suite and bun run e2e run — regenerate them with bun run scripts/record-semantic.ts.

Precision, not just recall. sgx enrich --edges asks about the edges the graph already admits it is guessing at. On this repository that is 1,366 ambiguous edges across 513 call sites and 4,673 tests edges — the latter being three-hop reachability, which is generous. In a sample of 25, 18 tests edges were rejected and the rejections were right: src/github/api.test.ts does not exercise findToken or Sources.get, and the edges it kept (ApiProvider.fetch, ApiProvider.page) are the ones it really does. Verdicts are stored by handle, so they survive re-indexing. sgx enrich --precision shows how much is still a guess.

sgx enrich --precisioncaptured output
1,325 ambiguous edges in 494 call sites · 4,655 tests edges · 92 judged (25 confirmed, 42 rejected, 18 tests edges dropped)

Across a workspace. sgx where --all answers from every repository, and sgx where --all --shared lists the concepts two or more of them share — two repositories both doing auth are related whether or not one imports the other, which is the thing package-name links cannot see.

Before you spend anything. sgx enrich --estimate prices a pass first, and the /concepts screen's button carries the same number.

sgx enrich --estimatecaptured output
~1,245 symbols × 40 concepts ≈ 1,718,100 input tokens ≈ $0.07 and ~39s (estimates)

What it is worth, measured. bun run eval:context --no-semantic packs context without concept seeds. On this repository the semantic layer moves overall recall from 75% to 78% and flips one case outright: "caching computed results so they are not recomputed" finds perEpoch, which the lexical packer never reached. That is a modest gain on the packer; the larger one is sgx where, which answers a question the graph previously could not.

Workspaces, projects and repositories

A workspace holds projects; a project holds repositories. Every repository keeps its own .sgx/graph.db, so nothing about indexing, watching or the ledger changes and a repository still works entirely on its own. The registry that records the structure is one SQLite file outside them all — $SGX_HOME/registry.db, default ~/.sgx — holding no symbols. Delete it and every repository still works. sgx never creates it as a side effect of an ordinary single-repo run.

sgx workspace add acme3 commands
workspace acme · active

$ sgx repo add ~/work/ui --project platform --name ui
added acme/platform/ui · /Users/you/work/ui

$ sgx repo add ~/work/app --project platform --name app
added acme/platform/app · /Users/you/work/app

sgx repo scan <dir> [--depth 3] registers every git checkout it finds and does not descend into one. sgx repo link re-derives the cross-repo links: a repository publishes the package names in its manifests and consumes the import sources that resolved to nothing inside it, and a consumed source that equals, or is a subpath of, another repository's published name is a link. Matching is exact — @acme/ui matches @acme/ui/button and never @acme/ui-kit.

sgx index --all3 commands
app  indexed 2 of 2 files (+2 ~0 -0) · 2 files · 1 symbols · 0 edges · epoch 1 · 58 ms · history mined
ui   indexed 2 of 2 files (+2 ~0 -0) · 2 files · 1 symbols · 0 edges · epoch 1 · 57 ms · history mined

$ sgx repo link
2 repositories · 2 published packages · 1 cross-repo links

$ sgx workspace show
workspace acme · 1 project · 2 repositories · 4 files · 2 symbols · 0 edges

platform/
  app                       2 files       1 symbols  main           epoch 1
                       publishes @acme/app
  ui                        2 files       1 symbols  main           epoch 1
                       publishes @acme/ui

cross-repo imports
  app → ui  @acme/ui  1 file

Scope flags decide what a command runs over:

FlagScope
(none)the repository containing the working directory, registered or not
--repo <slug|project/slug|path>that one repository
--project <slug>every repository in the project
--workspace <slug>every repository in the workspace
--allevery repository in the active workspace

One repository always takes the ordinary single-repo code path, so behaviour with no scope flag is unchanged. A wider scope is federated by index, find, stats, hotspots, changes, impact, context and ui; every other command says it works on one repository at a time rather than quietly answering for one.

sgx find renderButton --all3 commands
ui  @aq0dz fn renderButton(label: string): string   src/button.ts:1

$ sgx impact renderButton --all
ui · 0 callers in 0 files · 0 tests · 0 co-change partners
  downstream app imports @acme/ui in 1 file

$ sgx stats --all
workspace acme · 2 repositories · 4 files · 16 lines · 2 symbols · 0 edges
  app  epoch    1 ·      2 files ·       1 symbols ·       0 edges
  ui   epoch    1 ·      2 files ·       1 symbols ·       0 edges
languages json 2 · typescript 2

Federated answers merge per-repository results; no query joins across SQLite files. find merges by score, hotspots and changes by score and epoch, and context splits the budget by how well each repository matches the task — every repository packs a small probe first and the budget is shared in proportion to the probes' seed scores, so an unrelated repository costs a header and nothing more. impact adds the downstream repositories, which is the reason multi-repo exists: package-level, not type-accurate, and reported as such.

sgx workspace list|add|use|remove|show, sgx project list|add|remove and sgx repo list|add|scan|remove|link manage the registry. $SGX_WORKSPACE overrides the active workspace for one run.

Language support

sgx deeply indexes TypeScript/TSX, JavaScript/JSX, Python, Go, Rust, C#, Java, PHP, and Ruby. It also builds compact structural outlines for Lua, Racket, SQL (.sql/.ddl/.psql), Markdown, JSON, YAML, HTML/HTMX, CSS, and GraphQL (.graphql/.gql). Local imports, module paths, document links, resources, and schema references become graph edges where their repository targets can be resolved. Other files remain searchable by path and participate in churn and co-change analysis.

SQL has no tree-sitter grammar in @vscode/tree-sitter-wasm, so it takes the same text-extractor path as Lua, Racket and GraphQL, and is dialect-agnostic on purpose. CREATE TABLE becomes a table symbol with a column child per definition; views, functions, procedures, triggers, types and indexes each become a symbol; and REFERENCES, ALTER TABLE, FROM and JOIN become ordinary references, so a foreign key resolves through the same rules as a function call. Comments and string literals are blanked before scanning, so a CREATE TABLE inside a default value declares nothing.

sgx outline db/schema.sqlcaptured output
db/schema.sql · 17 lines · outline ~116 tok (full ~118)
@a7f10 table accounts
  @8ccay id BIGSERIAL PRIMARY KEY
  @hd786 email TEXT NOT NULL UNIQUE
  @m0ddr created_at TIMESTAMPTZ NOT NULL DEFAULT now()
@3984n table sessions
  @excsk id UUID PRIMARY KEY
  @2stvy account BIGINT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE
@w1ry2 index sessions_account INDEX sessions_account ON sessions (account)
@73vxx view active_sessions VIEW active_sessions AS SELECT s.id FROM sessions s JOIN accounts a ON a.id = s.account

The foreign key is an edge, so a table has a blast radius like anything else:

sgx impact accounts --depth 1captured output
impact @a7f10 accounts · db/schema.sql:1
2 callers in 1 file · 0 tests · 0 co-change partners
direct:
  @2stvy column sessions.account  db/schema.sql:11  ~0.90 references
  @73vxx view active_sessions  db/schema.sql:16  ~0.90 references
documented by:
  docs/auth.md

YAML keys with scalar values are indexed to two levels of nesting, so runs-on: ubuntu-latest and image: postgres:16 are searchable; deeper leaves are left out rather than flooding the graph. Markdown files carry a real module doc for the first time — front-matter title/description, else the opening paragraph — which is what lets search and context rank a document at all.

Extension mappings and display metadata live in src/parse/language-meta.ts; parser implementations live behind the registry in src/parse/registry.ts.

Documents

A repository is not only code, and the prose beside it is what goes stale silently. A document declares what it governs in its front matter; the graph carries documents edges from it to the files it covers; and check reports the ones a change has left behind. The design is in docs/documents.md.

Front matter is the --- block at the top of a Markdown file. Every top-level key becomes a searchable key symbol, scalars included, and five keys mean something to sgx:

KeyMeaning
titleDisplay name; becomes the module doc, so find and context can rank the document
descriptionAppended to that doc
ownerWho answers for it; printed on every finding
statusdeprecated, obsolete, archived or superseded never raises a staleness finding
coversGlobs or paths: what this document governs

Edges come from three sources, each with its own confidence, and nothing else:

SourceConfidence
A front-matter covers: glob that matches the file1.0
A Markdown link that resolves to a repository file, optionally #symbol0.9
A fenced block whose info string names a repository path0.7

A path or a name merely mentioned in prose is deliberately not a source. Prose-to-code inference is noisy, and a finding is only worth having if it is trusted. covers: is the answer for anything a link cannot express.

sgx docs lists every document, what it covers and whether the code has moved on:

sgx docscaptured output
docs/auth.md  covers   4  up to date  current · platform
1 document · 0 behind

sgx docs <path> answers in both directions — what documents a file, and what a document covers:

sgx docs src/auth/login.tscaptured output
src/auth/login.ts is documented by:
  docs/auth.md → login  How login works

sgx docs --stale is the repository-wide sweep, and exits 1 when anything is behind, so it works as a CI gate. "Behind" means commits that touched a covered file after the document itself last moved:

sgx docs --stalecaptured output
docs/auth.md  2 commits behind src/auth/login.ts  owner platform
1 stale document

The point of all of it is the finding. check raises stale-doc when you change a covered file and leave its document alone, which puts it in the post-edit hook, where an agent that has just edited login is told what it has invalidated:

sgx checkcaptured output
warn stale-doc docs/auth.md documents src/auth/login.ts, which you changed; the document is 2 commits behind it (owner: platform)
0 errors · 1 warning · 0 info

check --all sweeps every documents edge instead of the change set. Both read only tables already in the graph — no network, no extra git process — so the guard costs the hook nothing. sgx impact gains a documented by section for the same reason, and /api/symbol/:handle and /api/file carry documents.

Coverage is a claim the repository makes about itself, not a measurement. A covers: glob can be wrong, a link can point at the wrong file, and a document can be current while its git timestamp is old. The finding says what it saw — which document, which file, how many commits — and lets the reader judge.

Remote sources

A repository depends on things that are not in it: a vendor's API reference, a runbook, a sibling checkout you read constantly. sgx source fetches those into .sgx/sources/<name>/, indexes them with the ordinary pipeline, and gives them the same treatment documents get — symbols, handles, search, covers: and staleness. The design, and what it deliberately refuses, is in docs/remote-sources.md.

sgx source add ../vendor-docs --as vendor --covers 'src/billing/**' --ttl 7d2 commands
vendor: fetched · 1 file · 131 B

$ sgx source list
1 source

vendor  ../vendor-docs
  1 file · 131 B · synced 0s ago · trusted · ttl 1w
  covers src/billing/**

Once it is in, a mirrored page is ordinary graph content:

sgx find charge --limit 52 commands
@qdsrj fn      chargeCustomer(id: string, cents: number)   src/billing/charge.ts:1
@y5mzv section ## charge.failed                            .sgx/sources/vendor/webhooks.md:7
@3kekz section ## charge.succeeded                         .sgx/sources/vendor/webhooks.md:3

$ sgx source show vendor
vendor · ../vendor-docs
trusted · text · ttl 1w · synced 3m ago · changed 3m ago
1 file · 131 B

  .sgx/sources/vendor/webhooks.md · 9 lines

covers src/billing/charge.ts

The last line is the point of the feature. The manifest's covers: becomes a documents edge, so the vendor's reference governs billing code exactly as docs/auth.md would: when the code moves on and the reference does not, it is reported by sgx docs --stale and warned about by sgx check as a stale-doc finding, naming the mirrored page and how many commits it is behind. A mirrored file has no commits of its own, so its last fetch stands in for the timestamp.

What it fetches

URIWhat happens
a path, or file://…a file, or a directory of text files, is mirrored (.md, .txt, .sql, .graphql, .json, .yaml, .toml, .csv, …)
https://… serving Markdown or plain textthe body is mirrored, with ETag/Last-Modified for the next sync
https://… serving HTMLrefused, naming tier 2 as the change that would support it
git+https://…refused, naming tier 4

sgx source add|sync are the only commands in sgx that touch the network. check, context, find, the MCP server and the hooks all read the mirror, so a source that is unreachable today still answers, and a flaky CDN can never block an edit-time guard. A failed sync keeps the previous snapshot and records why.

Refreshing, on purpose

--ttl is not a scheduler. It marks a source due in sgx source list, and sgx source sync --stale fetches those and nothing else — an unchanged page is a conditional request answered 304, so a due source that has not moved costs one round trip and no re-index. Three ways to run it, none of them on by default:

sh
sgx source sync --stale                        # by hand, or from cron / CI
sgx watch --sync-sources [--sync-every 15m]    # while the watcher is already running
sgx init --claude --sync-sources               # a SessionStart hook, for Claude Code, Codex or fx

--sync-sources on init installs exactly one extra hook: sgx source sync --stale --quiet --deadline 10. Bounded and silent by construction — ten seconds for the whole run, whatever it does not reach is reported as skipped and waits for next time, and it prints nothing unless something actually changed or failed. The watcher's timer is the same shape: a check every 15 minutes, a 60-second deadline per tick, and no output when nothing was due. Without the flag, none of sgx's wiring ever reaches the network.

The manifest, and what is committed

.sgx/sources.toml is written by sgx source add and is meant to be committed — it is how a team shares sources. .sgx/.gitignore is rewritten to keep it and ignore everything else, so the mirror and the graph stay out of git. sgx source sync reconciles the graph with the manifest first: a source someone added in a pull request is fetched, and one they removed is dropped.

toml
[[source]]
name = "vendor"
uri = "../vendor-docs"
covers = ["src/billing/**"]
ttl = "1w"

Trust

Mirrored text is the one input to sgx that a third party writes, and it is treated that way:

  • Data, never instructions. Any MCP result containing mirrored content is prefixed with a notice naming the sources it came from and saying exactly that. Nothing fetched is ever executed, and no JavaScript is run to fetch it.
  • context leaves it out by default. --remote opts in, and even then remote text may take at most 25% of the budget, so a documentation page cannot evict the function you are editing.
  • Nothing identifying goes out. The request carries User-Agent: sgx and conditional-request headers. No repository name, no user, no token, no cookie.
  • Bounded. 2 MB per body, 500 files per directory, at most 3 redirects, and a redirect that leaves the original host is refused rather than followed.
  • The worktree pass never reaches the network, and never deletes a mirrored file: files.source_id marks them, and only the sync that owns a source removes one.

Ecosystem flavors

sgx detects package-scoped framework and ecosystem flavors independently of language parsing: React, Next.js, Vite, Svelte, SvelteKit, FastAPI, GraphQL, and HTMX. A package can have several flavors at once, and monorepo packages are reported separately. Detection combines dependencies, config files, file conventions, imports, and syntax evidence; every result retains confidence, version, traits, and its supporting evidence.

Next.js enrichment recognizes App and Pages Router files, route handlers, layouts, modern proxy files, and backward-compatible middleware files. FastAPI routes, GraphQL declarations and operations, and HTMX endpoint references are added to the same symbol graph. Add simple detectors in src/flavors/registry.ts; richer graph adapters live in src/flavors/.

MCP: agent tools and schema cost

Run sgx mcp --root /path/to/repo as a stdio MCP server. It starts a watcher; stdout contains only protocol messages. Any MCP client can use:

json
{
  "mcpServers": {
    "sgx": {"command": "/absolute/path/to/dist/sgx", "args": ["mcp", "--root", "/path/to/repo"]}
  }
}

On initialize the server sends instructions (src/mcp/instructions.ts), which the client puts in the agent's system prompt. They route one habit per tool — where/find instead of grep, outline instead of opening a file to see what is in it, show instead of reading around a symbol, context instead of exploring file by file, impact before editing a shared symbol, check after editing — because an agent that is merely offered the tools keeps reaching for grep and whole-file reads, which it always has. The same first line is prepended to the SessionStart hook output, for clients that ignore server instructions.

The original eight text tools are preserved. Framework flavors add one text tool, Jev adds two structured lookup tools, git/GitHub add two, the semantic layer adds where and documents add docs, for 15 current tools:

ToolInputsResult
findquery, limit=20 (1–200)L1 symbol lines
outlinepathFile/directory skeleton
showhandles[], fresh=falseL3 source with session deduplication
contexttask, budget=4000 (200–50000), working_set=falsePacked text, estimates and dropped handles
impacttarget or diff=true, depth=3 (1–6)Callers, tests and co-change
changesnoneFeed since this session's last call
checkscope="diff", "all", or a pathGuard findings
docstarget?, stale=falseWhich documents cover which code, or what has fallen behind
notehandle, textStored anchored note
flavorsevidence=falseEcosystems by package
wherequery, limit=20, route=falseSymbols by concept with calibrated confidence; route spends one model call
gitwhat="status", "log" or "owners", path, limit=20Working tree, commit log, or a file's authors
prnumber?, state="open"Pull request list, or one with its changed symbols, callers, tests and downstream repositories
lookup_candidatesquery, kind, limit, budget, expandStructured versioned candidates and provenance
lookup_materializeexpected, handle, budgetSource for the selected snapshot, or stale result

sgx mcp --schema-cost measures the actual SDK tools/list response. JSON array punctuation is included in the total; estimates are not tokenizer counts.

sgx mcp --schema-costcaptured output
sgx mcp schema · 15 tools · 6,049 bytes · ~1,513 tok (est.)
lookup_candidates     532 B  ~133 tok
lookup_materialize    697 B  ~175 tok
find                  360 B  ~90 tok
flavors               301 B  ~76 tok
outline               291 B  ~73 tok
show                  403 B  ~101 tok
context               422 B  ~106 tok
impact                404 B  ~101 tok
docs                  389 B  ~98 tok
changes               224 B  ~56 tok
check                 310 B  ~78 tok
where                 488 B  ~122 tok
git                   425 B  ~107 tok
pr                    434 B  ~109 tok
note                  353 B  ~89 tok

Text-tool responses end with · epoch N. show and context remember sent body hashes for this server session: unchanged source becomes a one-line ≡ unchanged since sent reference; changed bodies are resent and marked changed. show's fresh=true bypasses deduplication. Compaction resets the ledger. Separate clients/server processes have separate ledgers.

Usage telemetry: how agents use sgx

Every tool call is recorded locally, in the repository's own .sgx/graph.db (docs/telemetry.md): which tool, with what arguments, how long it took, how many tokens came back, whether the session ledger kept anything out of the response, and what the agent called next. MCP calls are recorded in the one handler every agent call passes through; CLI runs are recorded the same way, in a session shared by one shell ($SGX_SESSION, else cli:<ppid>). SGX_USAGE=0 turns it off; nothing is ever sent anywhere; retention is 50,000 calls.

sgx usagecaptured output
sgx usage · 8 calls · 2 sessions · 0s ago → 0s ago
~371 tok returned · 87 tok saved by the ledger · 25.0% errors · 1 finding · p50 15ms · p95 75ms

tool     calls  err  find   p50   p95  tok  share  saved  last
find         2    —     —  17ms  18ms  212    57%      —  0s ago
show         2    —     —   0ms   1ms  114    31%     87  0s ago
impact       1    1     —  15ms  15ms    0     0%      —  0s ago
index        1    —     —  75ms  75ms   23     6%      —  0s ago
outline      1    1     —   0ms   0ms   11     3%      —  0s ago
where        1    —     1  15ms  15ms   11     3%      —  0s ago

what follows what:
  find → where  ×1
  find → show  ×1
  index → find  ×1
  show → show  ×1
  show → outline  ×1
  where → impact  ×1

most repeated arguments:
  2× show {"handles":["@twtp1"],"fresh":false}
  1× outline {"path":"nope/nope.ts"}
  1× find {"query":"login","limit":20}
  1× where how do we log in
  1× find login

errors:
  1× outline: nothing indexed at 'nope/nope.ts'
  1× impact: sgx impact: needs a target (handle, path#name, name or file) or --diff

sessions:
  session   client           calls  err  find  tok  last    tools
  e4132ffc  mcp/claude-code      4    1     —  232  0s ago  show outline find
  cli:3420  cli                  4    1     1  139  0s ago  where index impact find

A finding is not a failure. check exits 1 because it found drift, where because there is no semantic layer yet: the tool ran, and the answer is negative. Those get their own column and stay out of the error rate, which is reserved for misuse, missing preconditions and crashes. The reason is recorded either way — MCP tools return their message, and a CLI run's is taken from what it printed.

share is the tool's share of every token sgx returned in the window: the fastest way to see which tool is spending the agent's context.

--trace prints the calls themselves, newest first, errors marked ! and findings ~, each carrying the message the agent received:

sgx usage --trace --limit 6captured output
when    session   #  tool        ms  tok  args
0s ago  e4132ffc  4  !outline   0ms   11  {"path":"nope/nope.ts"} → nothing indexed at 'nope/nope.ts'
0s ago  e4132ffc  3  show       0ms   14  {"handles":["@twtp1"],"fresh":false}
0s ago  e4132ffc  2  show       1ms  100  {"handles":["@twtp1"],"fresh":false}
0s ago  e4132ffc  1  find      18ms  107  {"query":"login","limit":20}
0s ago  cli:3420  4  !impact   15ms    0   → sgx impact: needs a target (handle, path#name, name or file) or --diff
0s ago  cli:3420  3  ~where    15ms   11  how do we log in → no semantic layer yet — run `sgx enrich`

--sessions lists sessions, --clear forgets everything, and --json emits the whole summary. Filters: --since 30m|24h|7d|2w, --tool <name>, --session <id>, --surface mcp|cli, --limit <n>. The same numbers are on the /usage screen of the web UI, with charts and a live trace.

The readings worth acting on: which tools an agent actually reaches for, which ones it fails against and with what message, what each one costs its context in tokens and as a share of the total, where latency is high enough that it would rather grep, what the ledger saved, which pairs of calls follow one another (where then show), and which questions it asked twice.

Self-improvement: sgx improve

sgx usage answers how sgx was used. It cannot answer what the agent did instead — an agent that never calls sgx looks perfect there. That answer is in the agent's own transcript, which Claude Code and Codex both write to disk: every grep -rn, every cat src/foo.ts, with the size of the result it received.

sgx improve joins the two (docs/improve.md), ranks what is worth changing, and with --apply rewrites the guidance the next session is given.

sgx improvecaptured output
sgx improve · 52 sgx calls · 948 agent tool calls across 10 sessions
361 of them were questions sgx answers, costing ~195k tok of the agent's context

! displaced: the agent read a whole file 196× here, costing ~152k tok (~774 a call) · `show` cost ~1.4k tok a call here
  → route it to `show` / `outline`
    cat docs/telemetry.md && ls src/telemetry/
    git diff --stat && sed -n '1,80p' src/telemetry/record.ts

! displaced: the agent searched with grep 145× here, costing ~36k tok (~251 a call) · `where` cost ~145 tok a call here
  → route it to `where` / `find`

! stale: `where` was called 5× with no semantic layer built (1,532 symbols eligible)
  → `sgx enrich`

· costly: `check` returned 62% of every token sgx gave the agent (~37k tok over 14 calls)
  → tune its defaults, or give it a smaller budget when the agent calls it

A call counts as displaced only when sgx indexes the file and has a tool for the question. A grep outside the repository, a read of something nothing indexed, a | grep filtering another command's output: all left alone, because a wrong accusation teaches an agent to distrust the report.

--apply writes .sgx/instructions.md — one generated block, regenerated in full each time, capped at 1,200 characters:

text
<!-- generated by `sgx improve --apply` · 2026-09-20 · do not edit -->
Learned in this repository from 10 sessions:

- Do not read a source file whole to find something in it: 196 whole-file reads here cost ~152k tok. `show` / `outline` answer the same question.
- Do not grep this repository: 145 searches here cost ~37k tok. `where` / `find` answer the same question.
- `impact` keeps failing with: impact needs a target (handle, path#name, name or file) or diff=true

Two readers pick it up, both already places an agent is told about sgx: the MCP server appends it to its instructions, and sgx hook session-start prints it under the repo map. That is the loop — measure the workaround, then change the instruction that allowed it.

Flags: --since 24h|7d, --apply, --no-transcripts (telemetry only), --json. Transcripts are read, never written, and nothing read ever leaves the machine.

Structured lookup for Jev loops

sgx mcp --root <worktree> also exposes lookup_candidates and lookup_materialize. These read-only tools return version 1 JSON in both structuredContent and the text content, without an epoch footer. Existing agent tools keep their text formats.

Candidates accept query, kind (symbol or concept), limit (0–32), budget (1–50000 estimated tokens), and expand. A zero limit refreshes and returns provenance only. Exact symbols preserve ambiguity; concept lookup uses indexed terms and graph ranking, and may not find unrelated synonyms. Materialization accepts the returned expected snapshot, a selected handle, and budget. A changed generation or epoch returns status: "stale".

Every result includes the canonical worktree root, generation, epoch, parser coverage, source hashes, and truncation information. Source buffers are checked against their indexed hashes. Lookup refresh hashes every indexed file, including files whose size and mtime are unchanged, so new competing symbols invalidate previous mappings. Results are self-contained across sessions; these tools never suppress a body because another agent saw it. Candidate evidence budgets include serialized candidate metadata; output budgets apply to rendered source text. Cold indexing and freshness scans still cost time: this interface does not promise constant-time search.

Claude Code, Codex and fx

sgx init --claude merges .mcp.json and .claude/settings.json in the indexed repository, using the absolute running binary path (or Bun plus the source entry). Existing entries survive; running it again is idempotent. No home-directory Claude configuration is written.

  • sgx hook session-start is installed for SessionStart from every source. It emits the routing line, then the repo map at ≤1,500 estimated tokens plus open errors. On source: "compact", it resets the ledger because the previous bodies were removed from the model's context.
  • sgx hook post-edit is installed for PostToolUse with matcher Edit|Write|MultiEdit. It reads hook JSON on stdin, reindexes the given file, checks it against HEAD, and prints nothing when clean. Paths outside the indexed repository are ignored.

Both read stdin the way the agents write it: the JSON arrives and stdin closes. A caller that leaves stdin open instead — a shell pipeline, or running the hook by hand — is waited on for two seconds and then treated as sending no input, so a hook never hangs the turn it was meant to inform.

First lines of the session-start output:

sgx hook session-startcaptured output
sgx indexes this repo — prefer its MCP tools (where, find, outline, show, context, impact) over grep, find and whole-file reads.

repo map · ~363/1,500 tok · 25 files · 25 symbols · epoch 2

entry: go/cmd/main.go · py/app/service.py · src/cli.ts

After the signature edit above, post-edit received {"tool_name":"Edit","tool_input":{"file_path":"<fixture>/src/auth/login.ts"}} on stdin and produced this real response:

sgx hook post-editcaptured output
{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"sgx check src/auth/login.ts\nwarn signature-changed src/auth/login.ts:17 @twtp1 login: signature changed (was login(user: User, opts: LoginOpts = {}): Session; now login(user: User, opts: LoginOpts = {}, required: boolean): Session); 2 call sites in unchanged files: src/cli.ts:5, tests/login.ts:4\n0 errors · 1 warning · 0 info"}}

Codex

sgx init --codex writes the same wiring to .codex/config.toml, the project-scoped configuration Codex reads after ~/.codex/config.toml — which sgx never touches. Codex uses the same hook contract as Claude Code: the same stdin JSON, the same hookSpecificOutput.additionalContext on stdout. One hook implementation serves both, so Codex gets the orientation and the edit-time guard, not only the tools.

Three things differ, and the installer handles all three: the file is TOML, MCP servers live under [mcp_servers.<id>], and Codex's edit tool is apply_patch, so the PostToolUse matcher names it alongside Claude's Edit|Write|MultiEdit. Merging appends the missing blocks rather than reformatting the file, so comments and ordering survive; an entry already running sgx hook <sub> is left alone however it was written.

sgx init --codexcaptured output
initialised .sgx/ in /tmp/sgx-mini-0kPqa2 · indexed 23 of 23 files (+23 ~0 -0) · 23 files · 44 symbols · 68 edges · epoch 1 · 98 ms · history mined
.codex/config.toml: added mcp_servers.sgx → /path/to/dist/sgx mcp
.codex/config.toml: added hooks.SessionStart → /path/to/dist/sgx hook session-start
.codex/config.toml: added hooks.PostToolUse (apply_patch|Edit|Write|MultiEdit) → /path/to/dist/sgx hook post-edit
toml
[mcp_servers.sgx]
command = "/path/to/dist/sgx"
args = ["mcp"]
startup_timeout_sec = 30

[[hooks.PostToolUse]]
matcher = "apply_patch|Edit|Write|MultiEdit"

[[hooks.PostToolUse.hooks]]
type = "command"
command = "/path/to/dist/sgx hook post-edit"
statusMessage = "sgx check"

fx

sgx init --fx wires fx 0.0.7 or newer, which reads the repository's own .mcp.json — the same file --claude writes, under the same mcpServers key, which fx takes as an alias of its canonical mcp map. So the tools need no second entry: sgx init --claude --fx writes one server and both agents use it.

fx has no command hooks. Its lifecycle hooks — PreToolUse, Stop, PostTurnEnd, AttentionRequired — are in-process handlers registered through libfx, not commands named in a configuration file, so there is nowhere to install sgx hook session-start or sgx hook post-edit. What those hooks do automatically becomes an instruction instead: --fx appends a marked block to AGENTS.md, fx's project instructions, carrying the routing line and the two commands that stand in for the hooks. That depends on the model following the line, which is weaker than a hook, and it is the most fx allows today.

sgx init --fxcaptured output
initialised .sgx/ in /tmp/sgx-mini-0kPqa2 · indexed 23 of 23 files (+23 ~0 -0) · 23 files · 44 symbols · 68 edges · epoch 1 · 96 ms · history mined
.mcp.json: added mcpServers.sgx → /path/to/dist/sgx mcp
AGENTS.md: added the sgx block (repo map and check, which fx has no hook for)
note: fx (0.0.7+) leaves workspace .mcp.json servers untrusted — approve sgx once with `fx mcp trust`, or /mcp inside fx
markdown
<!-- sgx:start -->
## sgx

sgx indexes this repo — prefer its MCP tools (where, find, outline, show, context, impact) over grep, find and whole-file reads.

fx has no command hooks, so two things sgx does by itself under other agents are shell calls here:
run `/path/to/dist/sgx map` when a session starts, for the repo map, and `/path/to/dist/sgx check --file <path>` after editing a file.
<!-- sgx:end -->

The note matters: fx marks every workspace .mcp.json server pending, and a pending server does not connect. Trust it once with fx mcp trust or /mcp inside fx, or set enabledMcpjsonServers in ~/.fx/settings.json — which sgx never writes, the same rule it follows for Claude Code and Codex. Once connected, fx puts the server's initialize.instructions in front of the model, so the per-tool detail arrives the same way it does everywhere else. docs/fx.md records what was verified against the fx source.

Any other MCP client works from the generic form above; without hooks you get the tools but not the automatic orientation or the edit-time guard.

Web UI: ui and demo

sgx ui [--root <dir>] [--port <n>] indexes, watches and serves the current repository; with a scope flag (--all, --project, --workspace) it indexes, watches and serves every repository in that scope behind one switcher. Port precedence is --port, then $PORT, then 4817, with fallback to the next free port. --port 0 lets the OS choose. It binds only 127.0.0.1. The compiled binary embeds the UI; bun run dev runs from source with HMR. A bounded fixture test captured:

sgx ui --port 0captured output
sgx ui · /var/folders/k3/bvwcdygn053bktwwbzm9c8vc0000gn/T/sgx-mini-HIiPLr · epoch 3
listening on http://127.0.0.1:52232

sgx demo indexes sgx's own checkout and serves those results. From this checkout, bun run demo builds first if the binary is absent and honours $PORT. Rebuild explicitly after source edits when a binary already exists. sgx demo --root /path/to/sgx --port 4817 can locate the checkout explicitly. The command's real help entry is:

text
  demo      index sgx's own repository and serve the UI on $PORT
ScreenWhat it does
/Counts, languages, package flavors/framework symbols, confidence, epoch/history, findings, ledger savings and live feed
/search?q=contextInstant symbol search with links to symbols and files
/symbol/:handleSignature/docs, L0–L3 text and estimates, editable anchored notes, source, callers/callees with confidence, tests and co-change
/file/*File outline and source ladder, symbol links, imports/importers, the pull requests in flight on the file, tests and notes
/graphBounded Cosmos.gl symbol graph; seed, depth, limit and relationship filters in the URL; selection shows visible neighbours and file links
/flowxyflow lenses over the same graph: an architecture map of module containers with imports and cycles, and a feature-flow pipeline staged from entry points; lens, expansion, seed and depth in the URL
/modulesDirectory dependency matrix; rows depend on columns, cycles highlighted, click a count to see contributing edges
/hotspotsTanStack scatter chart and table; churn versus square-root complexity, square-root fan-in radius, directory colors and filters
/usageHow agents are using sgx: calls over time, latency against tokens returned, the per-tool table, what follows what, repeated arguments, errors, sessions and the live trace
/contextTask and budget (200–64,000 estimated tokens), exact agent-facing output, budget usage and dropped candidates
/conceptsThe concept vocabulary, how many symbols carry each, a question box answered from the index, and every assignment with its confidence
/workspaceEvery served repository with counts, branch and published packages, and the cross-repo import links between them
/gitWorking tree, the commit stream with a selected commit's files, and the author table
/githubPull requests and issues from the API; /github/:number opens one pull request's files, symbols, blast radius, tests, downstream repositories and suggested reviewers

All pages refetch on SSE epoch events. The sidebar offers Auto/Light/Dark; mobile navigation becomes a horizontal strip. / opens search; ⌘K focuses the jump box, and g followed by a navigation hint changes screens. Hotspot bubbles can spread horizontally up to 0.32 commits to separate overlaps; tooltips and the table retain exact values.

The typed UI client calls local /api/* endpoints for repos, workspace, stats, overview, feed, changes, search/find, symbol/:handle, file?path=, graph, flow, modules, hotspots, context, impact, check, notes, usage, usage/trace, usage/sessions, git, git/commits, git/commit/:sha, git/authors, git/owners, concepts, concept, where, github, github/pr/:number and github/touching; syncing is a POST to github/sync, so no read ever triggers one by accident. SSE uses /api/events. Request paths cannot escape the repository; notes render as text.

Every endpoint takes ?repo=<slug>, defaulting to the first repository served; /api/repos lists them. The sidebar shows a repository picker when more than one is served, and every repository's epochs stream on the one SSE connection, so the screens refetch only for the repository they are showing.

/flow loads @xyflow/react and elkjs only when the screen is opened, so no other screen pays for them; they add 5.1 MB to dist/sgx (77.3 MB to 82.4 MB, measured on this checkout). The architecture lens groups directories with the same two-level rule as /modules, so the two screens always agree. The feature lens takes framework routes as its entry points where a repository has them and otherwise falls back to source roots: exported callables that call something and that no other file reaches.

End-to-end checks and benchmark

bun run e2e builds dist/sgx if missing, then drives that binary in temporary copies of the fixtures with isolated git history. Each step prints PASS: init with Claude integration, index, find, outline, show, context, impact, tests, git status/log/owners, enrichment and concept lookup from recorded judgements, GitHub sync/prs from recorded responses and a pull request's blast radius, a signature-change guard, post-edit hook JSON, an edit under watch and its persisted feed, MCP initialize/list/call (including the structured lookup tool registrations), UI /api/stats on an OS-assigned port, a two-repository workspace with federated find/stats and the UI serving both, and flavor regressions. The run uses its own $SGX_HOME and sets SGX_OFFLINE=1, so it touches neither your registry nor the network. Children have bounded waits and are stopped before temp repos are removed. Rebuild first when changing application code; an existing binary is used as-is. On macOS, run watcher checks in an environment that permits filesystem events; a sandbox that suppresses those events causes timeouts.

bun run bench generates a deterministic temporary git repo with 2,000 source files and 100,000 lines: 1,600 TypeScript, 200 Python, 200 Go, including 160 TypeScript test files, classes and cross-file calls. It also indexes go.mod. bun run bench --check exits nonzero if any latency reaches twice the spec's target. Core queries run over one opened graph handle; the hook uses the compiled binary and includes process startup. Single-file timings include writing the edit and committing the new epoch, without the watch debounce. Repeated measurements report p50 without discarding warmup samples. Cold indexing includes graph creation, parsing, insertion, resolution and git mining, with a fresh database (not a flushed OS disk cache).

Run the complete command checks in order:

sh
bun run typecheck
bun test
bun run build
bun run e2e
bun run eval:context --min-recall 0.75
bun run bench --check

.github/workflows/ci.yml runs all of these on every push except the benchmark gate: the targets are wall-clock on an M-series Mac, and a shared runner measured the post-edit hook at 416 ms against 140 ms locally. CI records the numbers so a catastrophic regression is visible in the log; --check stays the gate on hardware we control.

Real run, 2026-09-20; machine, runtime and all measurements below are copied from bun run bench --check:

text
machine: Tims-MacBook-Pro-2.local · Apple M5 Pro · darwin/arm64 · Bun 1.4.2
corpus: 2,000 source files · 100,000 lines · TS 1,600 / Python 200 / Go 200 · 160 TS test files · cross-file calls and classes
timings: core library over one graph handle; cold includes parsing, inserts, resolution and git; hook includes binary startup; no warmup samples discarded
cold index: 522.45 ms · target 4000 ms · check < 8000 ms PASS
graph: 2001 files · 5200 symbols · 5500 edges
no-op refresh: 40.64 ms p50 (n=21) · target 150 ms · check < 300 ms PASS
one-file edit → committed epoch: 28.20 ms p50 (n=21) · target 25 ms · check < 50 ms PASS
find: 0.16 ms p50 (n=21) · target 5 ms · check < 10 ms PASS
outline: 0.05 ms p50 (n=21) · target 5 ms · check < 10 ms PASS
show: 0.03 ms p50 (n=21) · target 5 ms · check < 10 ms PASS
impact depth 3: 0.10 ms p50 (n=21) · target 20 ms · check < 40 ms PASS
context 4k: 1.66 ms p50 (n=21) · target 60 ms · check < 120 ms PASS
full files vs outlines (entire corpus): 3637333 → 398650 bytes · saved 3238683 bytes (89.0%) · estimated tokens 910381 → 97890 (~4 chars/token)
naive target + callers vs context (20 targets, depth 3, 4k budget): 145331 → 150653 bytes · saved -5322 bytes (-3.7%) · estimated tokens 36337 → 37393 (~4 chars/token)
30-call show replay (10 targets, one body edit), without vs with ledger: 18933 → 7945 bytes · saved 10988 bytes (58.0%) · estimated tokens 4749 → 1988 (~4 chars/token)
post-edit hook process start → exit: 140.20 ms p50 (n=9) · target 150 ms · check < 300 ms PASS

Two of these were over their nominal targets for a long time, and profiling found one cause rather than many. workingChanges — what check, impact --diff, tests --diff and the post-edit hook all start with — ran four git subprocesses in series at roughly 13 ms each. HEAD now comes from reading .git instead of spawning git rev-parse, and the three remaining commands are independent, so they run together: 53 ms to 13 ms, and check --file from 68 ms to 26 ms. The post-edit hook went from 243 ms to 140 ms and is now inside its 150 ms target.

The one-file edit is 28 ms against a 25 ms target, down from 43 ms. What is left is almost entirely one git ls-files spawn on the full-refresh path; the watcher, which passes the changed paths directly, skips it and indexes in about 3 ms.

Token counts are estimates (~4 characters/token). On this synthetic corpus, outlines save 89.0% of bytes and the replayed ledger saves 58.0%. The 4k context pack is 3.7% larger than the naive caller-file read: it includes additional related context, so savings are not guaranteed. Cold indexing also writes the commit log and per-file line counts now, which this corpus (one commit of 2,000 files) absorbs without leaving its target.

Landing page

bun run site writes the site into site/. site/index.html is the landing page: headlines and section prose are landing-page copy and live in scripts/site-content.ts. Examples never do. Every terminal block on it is resolved out of this README by a command that appears inside it:

ts
terminal("sgx context login --budget 200")

An unresolved or ambiguous reference throws, so a command shown on the page is a command that was really run here, and deleting or editing an example above breaks the build instead of leaving the page stale. Headline figures work the same way — the four latency numbers are read out of the benchmark block by regex, not retyped.

site/reference.html is this README in full: every ## section, in order, with a sticky contents column. The two pages share one header, footer and stylesheet, and link to each other.

Beside them the build writes llms.txt (the project in Markdown: one ## per capability with its captured example), AGENTS.md (install, the smallest real run, the tool table and the three mistakes that waste a context window), index.md and reference.md (each page as Markdown), sitemap.xml, robots.txt and a committed 1200×630 og.png. Both pages carry a canonical URL, Open Graph and Twitter tags and a SoftwareSourceCode JSON-LD block written from the same model.

Neither page has network fonts, a build step to serve it, or any script but that JSON-LD, which executes nothing. bun test scripts/site.test.ts fails if a reference no longer resolves, if a section's prose stops naming a real option, if a README section is missing from the reference page, or if any committed file is out of date with this README. docs/landing-page.md records the composition.

Boundaries and spec reconciliation

Not in v0.1: type-accurate LSP/SCIP resolution, external-package nodes, embeddings, hosted/team servers, or UI authentication. Two things leave the machine and nothing else does: the GitHub sync and sgx enrich (with where --route). Both are explicit, both are stopped by SGX_OFFLINE=1, and both replay from recorded files in tests. Concept assignments are one model's judgement with its own confidence, reported as such — not ground truth. Cross-repo links are package-name matches, not resolution: sgx says which repository imports the package, never which symbol in it resolves where. The UI is local only. Unknown languages remain file nodes; structural extractors do not imply deep semantic analysis. Edges are syntactic with explicit confidence, and false positives/negatives remain possible.

Session-ledger rows and savings counters currently have no automatic retention policy. Handles lengthen on hash collisions; in that rare case allocation can depend on insertion order. Renames/moves create new handles. A parser-engine failure retains the previous graph until a successful retry rather than deleting known symbols.

The current contract is docs/spec.md. Its final exclusion paragraph still says languages beyond TS/JS/Python/Go are absent and Rust has no extractor; that conflicts with its updated §2 and the shipped parsers. The language list above documents the implementation. The older task wording asks for eight MCP tools; current §8 adds flavors, the shipped Jev interface adds two more, and git/GitHub add git and pr. Go same-package cross-file calls resolve at 0.9, extending §3's literal same-file/same-class rule to Go's package scope. These differences are reported explicitly; the completion pass preserves the newer capabilities. §11's command list now also carries docs.

Document coverage is a claim, not a measurement: a covers: glob is whatever the document says, a documents edge is never inferred from prose, and "behind" is a commit count, not a judgement that the text is wrong. SQL is extracted by a dialect-agnostic text scanner, not a grammar, so an exotic dialect degrades to fewer symbols rather than wrong ones.

Spec section ownership: T1 §1; T2 §2; T3 §3; T4 §§4–5; T5 §6; T6 §7; T7 §§8–9; T10 §10; T14 §11; T13 the benchmark. T8/T9 supply visual designs, T11/T12 complete the UI screens, and T15 refines §5 relevance. T14's acceptance covers the CLI audit and integration/documentation checks.