# sgx

A live code knowledge graph that lets an agent ask in symbols instead of reading
files: a location, a signature, an outline, a blast radius, a context pack sized
to a budget you set — and, after every edit, what that edit broke elsewhere.

Index a repository once, then ask for symbols, compact outlines, budgeted context,
callers, tests and edit-time checks. A CLI, MCP server, Claude Code and Codex
hooks and a local web UI share one SQLite graph. Indexing, analysis and the UI are entirely
local — the UI uses loopback HTTP and SSE — and the only code that reaches the
network is the GitHub sync, which you can turn off.

Repositories group into projects and projects into workspaces, so the same
questions can be asked of one repository or of everything you work on at once.
Each repository still keeps its own graph; a registry above them adds the
cross-repo import links and lets every command run wide. Local git history is
modelled in full — commits, per-file line counts, authors, ownership, branches —
and GitHub pull requests are fetched from the real API behind a provider
interface, so a pull request can be read as a blast radius rather than a list of
diffs. A semantic pass judges every symbol into a concept with a calibrated
confidence, so "where is auth handled?" finds the code whose names never say it.

It is not only code. A document declares what it governs in its front matter, and
the graph carries `documents` edges from prose to the files it covers — so a change
to `login` reports the design note that describes it, and `check` says when that
note has fallen behind the code.

## 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](#workspaces-projects-and-repositories). 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:

```text
$ sgx init --claude
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.

```text
$ sgx index
indexed 2 of 25 files (+2 ~0 -0) · 25 files · 52 symbols · 68 edges · epoch 2 · 18 ms
```

```text
$ sgx stats
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:

```text
$ sgx watch
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.

```text
$ sgx find login --limit 3
@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:

```text
$ sgx outline src/auth/store.ts
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.

```text
$ sgx show validate --level 1
@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:

```text
$ sgx map --budget 200
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:

```text
$ sgx flavors
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.

```text
$ sgx context login --budget 200
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.

```text
$ sgx impact login --depth 1
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.

```text
$ sgx tests login
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:

```text
$ sgx check --all
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](#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:

```text
$ sgx hotspots --limit 3
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`:

```text
$ sgx changes --since 2
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`.

```text
$ sgx note add login "Retries twice on 503" --author developer
#1 @twtp1 login  src/auth/login.ts:17: Retries twice on 503
```

```text
$ sgx note list login
#1 @twtp1 login  src/auth/login.ts:17: Retries twice on 503
```

```text
$ sgx note rm 1
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.

```text
$ sgx git status
main · 388aa7f · 5 commits · 1 authors
working tree clean
```

```text
$ sgx git log --limit 4
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
```

```text
$ sgx git authors
    5 Test Author <author@example.com> · 23 files · +257/-0 · 2026-01-01…2026-01-06
```

```text
$ sgx git owners src/auth/login.ts
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.

```text
$ sgx github sync
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.

```text
$ sgx github prs --state all
#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.

```text
$ sgx github pr 42
#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:

```text
$ sgx check
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.

```text
$ sgx enrich --vocabulary
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.

```text
$ sgx enrich
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.

```text
$ sgx where "caching"
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:

```text
$ sgx where "where do we prove who the caller is before talking to a hosted service"
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:

```text
$ sgx where "session ledger resending bodies"
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.

```text
$ sgx enrich --precision
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.

```text
$ sgx enrich --estimate
~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.

```text
$ sgx workspace add acme
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`.

```text
$ sgx index --all
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:

| Flag | Scope |
| --- | --- |
| *(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 |
| `--all` | every 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.

```text
$ sgx find renderButton --all
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.

```text
$ sgx outline db/schema.sql
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:

```text
$ sgx impact accounts --depth 1
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](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:

| Key | Meaning |
| --- | --- |
| `title` | Display name; becomes the module doc, so `find` and `context` can rank the document |
| `description` | Appended to that doc |
| `owner` | Who answers for it; printed on every finding |
| `status` | `deprecated`, `obsolete`, `archived` or `superseded` never raises a staleness finding |
| `covers` | Globs or paths: **what this document governs** |

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

| Source | Confidence |
| --- | --- |
| A front-matter `covers:` glob that matches the file | 1.0 |
| A Markdown link that resolves to a repository file, optionally `#symbol` | 0.9 |
| A fenced block whose info string names a repository path | 0.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:

```text
$ sgx docs
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:

```text
$ sgx docs src/auth/login.ts
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:

```text
$ sgx docs --stale
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:

```text
$ sgx check
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](docs/remote-sources.md).

```text
$ sgx source add ../vendor-docs --as vendor --covers 'src/billing/**' --ttl 7d
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:

```text
$ sgx find charge --limit 5
@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

| URI | What 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 text | the body is mirrored, with `ETag`/`Last-Modified` for the next sync |
| `https://…` serving HTML | refused, 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**:

| Tool | Inputs | Result |
| --- | --- | --- |
| `find` | `query`, `limit=20` (1–200) | L1 symbol lines |
| `outline` | `path` | File/directory skeleton |
| `show` | `handles[]`, `fresh=false` | L3 source with session deduplication |
| `context` | `task`, `budget=4000` (200–50000), `working_set=false` | Packed text, estimates and dropped handles |
| `impact` | `target` or `diff=true`, `depth=3` (1–6) | Callers, tests and co-change |
| `changes` | none | Feed since this session's last call |
| `check` | `scope="diff"`, `"all"`, or a path | Guard findings |
| `docs` | `target?`, `stale=false` | Which documents cover which code, or what has fallen behind |
| `note` | `handle`, `text` | Stored anchored note |
| `flavors` | `evidence=false` | Ecosystems by package |
| `where` | `query`, `limit=20`, `route=false` | Symbols by concept with calibrated confidence; `route` spends one model call |
| `git` | `what="status"`, `"log"` or `"owners"`, `path`, `limit=20` | Working tree, commit log, or a file's authors |
| `pr` | `number?`, `state="open"` | Pull request list, or one with its changed symbols, callers, tests and downstream repositories |
| `lookup_candidates` | `query`, `kind`, `limit`, `budget`, `expand` | Structured versioned candidates and provenance |
| `lookup_materialize` | `expected`, `handle`, `budget` | Source 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.

```text
$ sgx mcp --schema-cost
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.

```text
$ sgx usage
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:

```text
$ sgx usage --trace --limit 6
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.

```text
$ sgx improve
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:

```text
$ sgx hook session-start
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:

```text
$ sgx hook post-edit
{"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.

```text
$ sgx init --codex
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](https://fx.sh) 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.

```text
$ sgx init --fx
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:

```text
$ sgx ui --port 0
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
```

| Screen | What it does |
| --- | --- |
| `/` | Counts, languages, package flavors/framework symbols, confidence, epoch/history, findings, ledger savings and live feed |
| `/search?q=context` | Instant symbol search with links to symbols and files |
| `/symbol/:handle` | Signature/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 |
| `/graph` | Bounded Cosmos.gl symbol graph; seed, depth, limit and relationship filters in the URL; selection shows visible neighbours and file links |
| `/flow` | xyflow 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 |
| `/modules` | Directory dependency matrix; rows depend on columns, cycles highlighted, click a count to see contributing edges |
| `/hotspots` | TanStack scatter chart and table; churn versus square-root complexity, square-root fan-in radius, directory colors and filters |
| `/usage` | How 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 |
| `/context` | Task and budget (200–64,000 estimated tokens), exact agent-facing output, budget usage and dropped candidates |
| `/concepts` | The concept vocabulary, how many symbols carry each, a question box answered from the index, and every assignment with its confidence |
| `/workspace` | Every served repository with counts, branch and published packages, and the cross-repo import links between them |
| `/git` | Working tree, the commit stream with a selected commit's files, and the author table |
| `/github` | Pull 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](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](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.
