Commit Graph
529 Commits
Author SHA1 Message Date
Colby McHenryandClaude Opus 4.8 80a1044d3d feat(resolution): bridge Vuex string dispatch/commit to actions and mutations
Completes the Vue store dispatch family (the Pinia bridge was 8ea3205). Vuex
dispatches by a runtime STRING key — `dispatch('user/login')` /
`commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')` — with no
static edge to the handler.

vuexDispatchEdges (callback-synthesizer.ts): the last `/` segment of the key is
the action/mutation name, the preceding segment is the namespace (≈ the module
file). Resolve the name to a function node IN A STORE FILE — the ≥2-signal
store-file gate excludes a same-named `api/` helper (`getInfo`/`login` collide in
practice) — disambiguated by the immediate namespace segment appearing in the
path (handles deep nesting like `d2admin/user/set`), or the same file for a root
local `commit('M')` inside an action. The .vue component is a dispatcher fallback
for top-level setup calls. Surfaces in explore as `dynamic: vuex dispatch`.

Also extracts the canonical Vuex MODULE shape `export default { namespaced,
actions: {…}, mutations: {…} }` (tree-sitter.ts: extractStoreCollectionMethods
off the export_statement, store-file gated) — its object-literal methods were
otherwise never nodes, so d2-admin's actions couldn't be bridged.

Validated 100% precision on three repos — vue-element-admin (55 edges),
vue-admin-template (12), d2-admin (63): 0 non-store targets, 0 namespace
mismatches (54/54 namespaced edges route to the correct module despite 6
colliding `load` actions in d2-admin), 0 on Redux controls (basetool/uwave —
non-string `dispatch()` correctly ignored). Suite green (1613); new
__tests__/vuex-dispatch-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:49:06 -05:00
Colby McHenryandClaude Opus 4.8 8ea32059b6 feat(resolution): bridge Pinia useStore().action() calls to the action
The dispatch bridge for Pinia, on top of the store-action extraction foundation
(cc9c2f7). A consumer does `const store = useXStore()` then `store.action()` —
a method-on-instance call with no static edge to the action, which lives in the
store module. So tracing "what does this view do when it loads" stopped at the
`store.fetchUser()` line.

piniaStoreEdges (callback-synthesizer.ts): map each `const useXStore =
defineStore(...)` factory → its file; per consumer file, bind `const s =
useXStore()` vars; link the enclosing function (or the .vue component, via a
fallback) → the `s.method()` action node IN THE STORE'S FILE. The same-store-file
gate is the precision lever — a Pinia built-in (`$patch`) or an unrelated
same-named method resolves to nothing. Covers the options and setup store forms
uniformly (the action is a function node in the store file either way) and
surfaces in explore as `dynamic: pinia store`.

Validated 100% precision (Geeker 41 edges, MallChat 64; 0 targets outside a
store file), 0 on the Vuex-only element-admin control (no defineStore), n=2 in
hand. Suite green (1612); new __tests__/pinia-store-synthesizer.test.ts. The
Vuex string-key dispatch bridge (`dispatch('ns/action')`) remains a follow-up
(n=1 in hand — needs a 2nd string-literal Vuex repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:30:13 -05:00
Colby McHenryandClaude Opus 4.8 cc9c2f7420 feat(extraction): index Vuex/Pinia store actions, mutations, and getters
A Vue store's callable surface — Vuex `actions`/`mutations`/`getters` and Pinia
store actions — lived only as object-literal properties, so the symbols an agent
looks for (`login`, `getSessionList`, `getAuthMenuList`) were never nodes:
`codegraph search`/`codegraph_node` returned "not found" and the agent had to
read the store by hand. This extracts them as function nodes (with their real
bodies + callees), the foundation under any later dispatch-bridge synthesis.

A corpus probe (vue-element-admin, vue2-elm, Geeker-Admin, MallChatWeb) showed
Vue store dispatch is NOT one clean string-keyed shape but ~5; extraction here
covers the three dominant definition forms:
  - Vuex MODULE: non-exported `const actions/mutations = {…}` collections
    (gated by a ≥2-signal looksLikeVueStoreFile + the object-of-functions shape,
    so a Redux file's stray `const actions` is a 0-node no-op).
  - Pinia OPTIONS: `defineStore({ actions: {…}, getters: {…} })` — methods of
    the actions/mutations/getters properties of a store-factory config.
  - Pinia SETUP: `defineStore('id', () => { const foo = …; return {…} })` — the
    body-local function consts (findPiniaSetupFn + extractPiniaSetupBody; the
    generic body walk doesn't reach nested function scopes). Distinguished from
    an inline action map via objectHasInlineFunctions so zustand/SvelteKit
    extraction is unchanged.

Validated findable on element-admin (50 fns), Geeker (21), MallChat (68);
0-node no-op on a non-Vue control (uwave-web, unchanged at 4496 nodes). Deferred
(documented in the backlog): vue2-elm's `export default {…}` split-file +
computed-key `commit(CONST)` form (n=1), and the dispatch BRIDGE synthesis
(Vuex string-key + Pinia useStore().action()). Suite green (1610); new
__tests__/vue-store-extraction.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:08:34 -05:00
Colby McHenryandClaude Opus 4.8 e9f7422223 feat(resolution): synthesize RTK Query hook→endpoint dispatch edges
Adds the RTK Query member of the dispatch-through-indirection family
(synthesizedBy:'rtk-query'). An RTK Query endpoint defined inside
`createApi({ endpoints })` and the `useGetXQuery`/`useUpdateYMutation` hook it
generates were both invisible to static extraction, so a `component →
useGetXQuery → getX → queryFn` flow had nothing to connect and explore
dead-ended on the API slice.

Extraction (tree-sitter.ts): mint a function node per endpoint — named by its
key, spanning the queryFn/query handler so its calls attribute — handling both
the `endpoints: build => ({...})` arrow and `endpoints(builder){ return {...} }`
method forms, with a bare-node fallback for factory handlers
(`queryFn: makeFn(url)`); and a function node per generated-hook binding from
`export const {...} = api`, carrying a sentinel signature.

Resolution (callback-synthesizer.ts): rtkQueryEdges bridges each generated-hook
node to its same-file endpoint by the naming convention (strip use + optional
Lazy + Query|Mutation, lowercase head). Component→hook is normal import/call
resolution; the hook→endpoint hop surfaces in explore as `dynamic: rtk query`.

Validated 100% precision (hooks == synth edges, 0 cross-file) on basetool (54),
minusx-metabase (11), shapeshift (13); 0 on the uwave-web control (no createApi
→ a complete no-op). The sentinel gate correctly ignores hand-written
look-alikes (shapeshift's useFoxyQuery is a real custom hook, never bridged).
Full suite green (1608); new __tests__/rtk-query-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:23:37 -05:00
Colby McHenryandClaude Opus 4.8 7f970296cf feat(resolution): synthesize object-literal registry dispatch edges
Adds `objectRegistryEdges` — a dynamic-dispatch synthesizer for the command/handler
registry pattern: an object literal maps string keys → handler classes/functions, then
dispatches by a RUNTIME key static parsing can't follow:

    this.commands = { [Cmd.ADD]: AddObjectCommand, ... }    // registration
    new this.commands[command](args).execute()              // dynamic dispatch

It links each dispatching function → each registered handler's callable entry (a class's
execute/run/handle method — preferring the method chained at the dispatch site — or the
function value), like the gin-middleware-chain fan-out. Same-file registry+dispatch only.

Validated precise on 3 real repos (the discipline that caught redux-thunk's n=1 overfit):
EtherealEngine's CommandManager (64 edges, class registry → .execute), Prebid.js (7:
builder/consent/message dispatch, function registry), warp-drive (1). Zero false positives
after several precision gates found during validation:
- skip minified/generated bundles (avg line length > 200) — draco/three.min were a
  false-positive minefield of `h[x](...)` calls + `{a:b}` literals;
- DEPTH-AWARE entry parsing (top-level `key: Identifier` only) so method-shorthand bodies
  and nested objects don't leak their inner `k: v` pairs as bogus handlers;
- callable-only targets (drop data `constant`s — a `{x: URL}` entry resolving to the global);
- dynamic-dispatch gate (a statically-accessed look-alike object yields nothing).
Handles constructor and field-initializer registry forms (this. normalized). Surfaces in
codegraph_explore via the existing Dynamic-dispatch-links section.

Deferred (recall, documented in dispatch-synthesizer-backlog.md): assign-then-call dispatch,
augmentation registration (reg[k]=H), and the cross-file barrel-namespace variant
(trezor getMethod) — the hard tier.

Full suite green (1606); new __tests__/object-registry-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:17:31 -05:00
Colby McHenryandClaude Opus 4.8 270e50655a fix(explore): surface synth constant-endpoint edges + precise redux-thunk dispatch resolution
Two fixes hardening the redux-thunk dynamic-dispatch synthesizer, found by
validating it on real RTK repos beyond its trezor origin (uwave-web,
session-desktop, octo-call):

- Surfacing: buildFlowFromNamedSymbols filtered its named set to CALLABLE
  kinds, so synthesized edges between `constant` nodes (RTK thunks are
  `const X = createAsyncThunk(...)`) never entered the Flow / Dynamic-dispatch
  links scan — invisible at every tier, while the kind-agnostic Relationships
  section is off below 500 files. Add a `dynNamed` set (named constant/variable/
  field nodes with a heuristic edge) feeding a shared collectSynthLinks into the
  "## Dynamic-dispatch links" section, threaded through the named.size<2
  early-out (both-endpoints-constant hit return EMPTY first) and the main path.
  Main call-chain stays callable-only; the <500 budget tiers are untouched.
  No-op for callable flows. Plus a generic synthEdgeNote fallback so any synth
  hop reads "dynamic: <kind> @site", not a bare "[calls]".

- Precision: reduxThunkEdges resolved a dispatched name by first-match-by-kind,
  so a thunk name colliding with a same-named service function linked to the
  wrong node (octo-call `leaveCall`). Prefer thunk-signature const > other
  const > same-file callable > first match.

Tests: new explore-synth-constant-endpoints.test.ts (surfacing on a small repo)
+ a collision case in redux-thunk-synthesizer.test.ts. Full suite green (1605).
Rationale + coverage backlog in docs/design/dispatch-synthesizer-backlog.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:45:51 -05:00
Colby McHenry e5897d0334 feat: remove reasoning offload / CodeGraph AI managed reasoning feature
Strips the bring-your-own-model reasoning offload and managed CodeGraph AI
integration (login/logout/usage commands, offload config/credentials/reasoner
modules, and the synthesizeOffload call in codegraph_explore). The eval findings
showed raw source output outperformed the synthesized path on accuracy, so
codegraph_explore reverts to returning verbatim retrieved source exclusively.

CHANGELOG and README sections for reasoning offload are removed; test comments
and DEFAULT_MCP_TOOLS description are updated to drop offload references.
2026-06-20 13:23:16 -05:00
Colby McHenry e7d9f8c6fa feat(explore): surface interface/registry dispatch boundaries and window oversize spine methods
Two gaps closed in `codegraph_explore` output quality:

**Interface/registry dispatch (#687 extension).** When a named token resolves to
a large same-name family (≥8 members) that doesn't land on the connected flow, the
static path truly ends there — the target is chosen at runtime from N implementations
(plugin/strategy/handler interface). `buildPolymorphicBoundaries` detects this via
`implements`/`extends` edges, ranks candidate supertypes by their TRUE graph-wide
implementer count (not FTS sample frequency, which is biased), and emits a
"## Interface dispatch" section naming the supertype, implementer count, and a few
concrete targets. Fires only for uncovered named tokens; a connected flow stays silent.

**Oversize spine method windowing.** A flow entry that is a god-method (e.g. n8n's
962-line `processRunExecutionData`) previously lost the per-file budget to denser
peripheral blocks and was dropped, forcing the agent to `Read` it back. The spine
call site (edge line to the next hop) is now tracked via `spineCallSites` and used
to window the method to its signature head + a ±28-line band around the call, keeping
it under the OVERSIZE_SPINE_LINES threshold. Spine clusters also rank first in the
budget sort and may exceed the per-file cap up to a 2.5× ceiling so they can never be
starved by co-flow files.

Test suite gains an `interface dispatch` describe block (announce, silent-on-connected,
silent-below-threshold) and uses `beforeAll`/`afterAll` to pin `CODEGRAPH_OFFLOAD_DISABLE=1`
so structural assertions are hermetic regardless of machine config.
2026-06-20 12:32:05 -05:00
Colby McHenry 4f8782cbe5 test(agent-eval): add output-style A/B harness, cost/token analyzer, and DISALLOW/REP_START controls
Three additions to tighten the eval loop:

- offload-eval-styles.sh: new 4-arm eval (raw/refs/map/src) isolating the Worker's
  output shape's effect on main-session tokens, latency, and accuracy. Delegation
  blocked by default (DISALLOW=Agent) so variance from Haiku subagent spawning doesn't
  contaminate the measurement.
- offload-eval-cost.mjs: cost/token analyzer that reads Claude Code's own per-model
  accounting (modelUsage.costUSD) rather than re-deriving from raw token counts,
  giving a correct main(Sonnet)/sub(Haiku) split with proper per-tier pricing.
- offload-eval-3arm.sh: adds DISALLOW env to block sub-agent delegation across all
  arms, and REP_START to append reps to an existing run without clobbering earlier
  jsonls (e.g. REP_START=4 REPS=3 → reps 4,5,6).

Also adds CODEGRAPH_OFFLOAD_STYLE forwarding to the managed gateway so the styles
eval can drive output shape end-to-end; the field is stripped before the upstream
model call and never sent to BYO endpoints.
2026-06-19 16:43:10 -05:00
Colby McHenry 291b200ece chore: stop tracking .claude/handoffs (local session notes only) 2026-06-19 02:16:50 -05:00
Colby McHenry f82a662ddb feat(mcp): pare default tool surface to codegraph_explore alone + redux-thunk synthesizer 2026-06-19 02:15:14 -05:00
Colby McHenryandClaude Opus 4.8 7ddd3fa7eb test(agent-eval): persist offload accuracy/adoption eval harness + front-load hook
Reproducible suite measuring the managed CodeGraph AI offload and the front-load
UserPromptSubmit hook (approach 1) vs raw codegraph and no-codegraph, across repo
sizes, on time / main-session tokens+cost / CodeGraph-AI tokens+cost / accuracy.
All agent arms run claude -p sonnet --effort high; eval-only, nothing shipped.

- offload-eval-setup.sh: clone + index 4 memory-probe-verified "not-trained-on" repos
  (mtkruto/postybirb/shapeshift/trezor — small→large) so the no-codegraph baseline is honest.
- offload-eval-3arm.sh / -frontload.sh: one repo, the arms (offload/raw/nocg, frontload).
- offload-eval-matrix.sh / -frontload-matrix.sh: drive all 4 tiers.
- offload-eval-hook.mjs: the front-load hook (self-locates its engine; CG_FRONTLOAD_DEBUG to log).
- offload-eval-metrics.mjs / -judge.mjs (Sonnet) / -summarize.mjs: extract, score, aggregate.
- offload-eval-ground-truth.json: source-verified canonical flows (the judge's reference).
- offload-eval.md: usage + the 2026-06 findings (raw = the win; offload least-accurate;
  front-load solves adoption but exposes explore's dynamic-dispatch gaps).

Scripts are path-portable (self-locating $HERE/$ENGINE; AGENT_EVAL_OUT scratch dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:30:30 -05:00
Colby McHenry 6d5cb6b25c feat(reasoning): add CODEGRAPH_OFFLOAD_DISABLE kill-switch and per-call usage log
`CODEGRAPH_OFFLOAD_DISABLE=1` immediately disables the offload for the current
process without touching the persisted config or stored login — useful for A/B
arms or sessions where raw source is preferred.

`CODEGRAPH_OFFLOAD_USAGE_LOG=` appends one JSONL entry per call with token
counts, charged credits, and derived cost (`creditsCharged / 100_000`) so a
harness can attribute CodeGraph AI spend to a single run independently of the
server's cumulative totals. Both features are best-effort and never disrupt the
degradable offload path.

Also fixes the `login` credit display to check `unlimited` before the numeric
balance, so comped/internal accounts don't incorrectly show "0 remaining".
2026-06-18 21:10:10 -05:00
Colby McHenry c9e207a0f2 feat(cli): add codegraph usage command to show AI balance and recent usage
Adds a `usage` subcommand that pings `/v1/usage` with the stored token and
displays balance, plan, 30-day explore/token counts, and allowance reset date.

Degrades quietly in all non-happy-path states — signed out, BYO endpoint, or
unreachable server — so managed reasoning remaining optional doesn't change.

Also extends `OffloadUsage` with the fields the endpoint already returns
(`unlimited`, `banned`, `tokensLast30`, `callsLast30`, `creditsLast30`) that
were previously untyped.
2026-06-18 01:22:58 -05:00
Colby McHenry 193722de45 feat(cli): replace offload subcommands with browser device-authorization login / logout
The old `offload` command family required users to paste a token manually (`offload login --token `) and exposed bring-your-own-endpoint plumbing (`set-endpoint`, `status`, `disable`) as top-level CLI surface. This replaces it with a standard OAuth device flow (RFC 8628 shape) against the CodeGraph dashboard.

`codegraph login` calls `/api/cli/device/start`, opens the browser to the returned URL, polls `/api/cli/device/token` until the user approves, then stores the minted token and enables managed reasoning. `codegraph logout` clears it. BYO-endpoint configuration moves entirely to env vars (`CODEGRAPH_OFFLOAD_URL` / `CODEGRAPH_OFFLOAD_KEY` / `CODEGRAPH_OFFLOAD_MODEL`), keeping the CLI surface minimal.
2026-06-18 00:15:40 -05:00
Colby McHenry 8aa05380a2 Merge branch 'feat/offload-byo' into codegraph-ai 2026-06-17 23:48:10 -05:00
Colby McHenry 3ba82681ea Merge branch 'fix/explore-corroboration-ranking' into codegraph-ai 2026-06-17 23:47:50 -05:00
Colby McHenryandClaude Opus 4.8 da5c6c2f79 feat(offload): managed tier (CodeGraph AI) — metered reasoning via org token [WIP]
Adds the managed offload mode: point codegraph_explore at the CodeGraph AI metered
gateway (https://ai.getcodegraph.com) with an org token instead of a BYO provider key.
Same synthesis client, pointed at codegraph-ai-proxy (a metered OpenAI-compatible gateway).

- credentials.ts — org token in ~/.codegraph/credentials.json (0600); unlike a BYO
  provider key it's a revocable org-scoped auth token (gh/npm-login style), kept out
  of config.json
- config.ts — managed branch in resolveOffload: default gateway URL + public model id
  (openai/gpt-oss-120b) + login token as bearer; managed requires a token to be enabled
- reasoner.ts — fetchUsage() reads the credit balance from /v1/usage
- bin/codegraph.ts — `codegraph offload login --token <t>` / `logout`; status shows the
  managed tier + live balance

Proven GREEN end-to-end against a local wrangler-dev of the proxy: org token validated,
credits prechecked, real Cerebras synthesis returned, and credits metered + charged
(250,000 → 248,473). Graceful degrade on upstream failure; balance via /v1/usage.
Phase 3 (codegraph login device flow) replaces the manual --token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:06:42 -05:00
Colby McHenryandClaude Opus 4.8 db4c9f3641 feat(offload): reasoning offload for codegraph_explore (bring-your-own endpoint)
codegraph_explore can now hand the source it retrieved to a reasoning model you
point at — any OpenAI-compatible endpoint (Cerebras, OpenAI, a local vLLM/Ollama)
with your own key — and return that model's tight, cited answer instead of the
raw source dump. The agent's main context gets the answer in far fewer tokens, at
the cost of one network round-trip.

Off by default. Configure with `codegraph offload set-endpoint <url> --model <m>
--key-env <ENV>` (or the CODEGRAPH_OFFLOAD_* env vars); status/disable manage it.
The API key is never written to disk — the config stores the NAME of an env var
and the key is read from it at call time. Strictly degradable: any failure
(no endpoint, network, timeout, empty answer) returns null and the call falls
back to the local source, so the offload can never surface an error to the agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:18:22 -05:00
Colby McHenryandClaude Opus 4.8 798cd0e21c fix(explore): keep multi-term backend files from being buried by a denser frontend layer
codegraph_explore's file sort is primarily driven by Random-Walk-with-Restart
graph-centrality mass, seeded from the query's text matches. In a cross-layer
monorepo (an API server alongside a much larger, internally dense frontend that
mirrors the same domain words), that mass skews to the bigger layer — so a
backend service/handler that genuinely matches several query terms, even when
it's the #1 search hit, sorts below hits=0 frontend files and gets truncated out
of the response, and the agent reads it back.

Add a corroboration tier above the graph signal: a file that is BOTH an
entry/central file AND matched by >=2 distinct query terms is kept in. The
entry/central guard prevents an incidental multi-term file (a type/util file
that isn't the flow) from displacing a graph-central answer file — a blunt
hits-only tier regressed that case. Single-layer repos are unaffected. Gated by
CODEGRAPH_RANK_NO_MULTITERM=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:44:39 -05:00
Colby MchenryandGitHub f34f606342 feat(extraction): same-file value-reference edges for impact analysis — 15 languages (#897)
Adds same-file value-reference edges (reader symbol → const/var it reads) so impact analysis catches a constant's same-file consumers, closing the 'change this table, break its readers' hole. 15 languages validated S/M/L on public OSS: TS/JS/tsx, Go, Python, Rust, Ruby, C, Java, C#, PHP, Scala, Kotlin, Swift, Dart, Pascal/Delphi (+ Svelte/Vue/Astro inherited). Edges-only — node count identical on/off; default ON, CODEGRAPH_VALUE_REFS=0 opts out.
2026-06-16 12:16:00 -05:00
2f6316500d feat(extraction): enable same-file value-reference edges by default (TS/JS) (#895)
Value-reference edges (same-file `references` edges from a reader to the
file-scope const/var it reads) shipped behind CODEGRAPH_VALUE_REFS pending an
agent A/B. The A/B is in: on excalidraw the edges are correct and precise (node
count unchanged) and they transform the impact/blast-radius API — `impact` on a
const consumed by 103 readers goes from 1 affected symbol to the full radius.
That blast-radius API is what `codegraph impact` and CodeGraph Pro's verdict
engine consume, so the win is impact correctness; the agent path showed no
regression. Flip the default on; CODEGRAPH_VALUE_REFS=0 disables.

Also close the one precision gap the A/B surfaced: a bundled/Emscripten
`const Module` re-declared as an inner `var Module` / param produced false
positives (nested readers resolve to the inner binding). isGeneratedFile() is
path-only and can't catch content-minified bundles, so prune SHADOWED targets at
the syntax level — drop any value-ref target whose name is bound by more than one
`variable_declarator` in the file. On excalidraw this removes the 23 false
positives while preserving every real reader (impact unchanged at 170).

Adds regression coverage (there was none): same-file readers are edged, they
surface in the impact radius, shadowed consts are NOT edged, and
CODEGRAPH_VALUE_REFS=0 emits nothing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:28:00 -05:00
b49147eab0 fix(cli): make codegraph index a full rebuild so it stops reporting 0 nodes (#874) (#894)
`codegraph index` ran extraction against the already-populated DB without
clearing it first. On an unchanged tree every file's content hash still
matched, so the orchestrator skipped re-inserting all of them and the run
reported its delta (after - before = 0) as "0 nodes, 0 edges" — which read as
if `index` had wiped the graph. `init` only ever differed because it runs on a
freshly created, empty DB.

Clear the existing graph before re-indexing so `index` rebuilds from scratch
and reports the same complete result as a fresh `init`. `--force` keeps its
role as the home-dir/root-path override; `sync` stays the incremental path.

Adds an end-to-end regression test driving the built binary (init -> index),
asserting the graph stays populated and the summary is never "0 nodes, 0 edges".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:45:03 -05:00
ab107b325a fix(watcher): warn (don't degrade) on Linux inotify watch exhaustion (ENOSPC) (#893)
On the Linux per-directory watch path, hitting fs.inotify.max_user_watches
surfaces as ENOSPC — which the degrade logic added for #876 (EMFILE/ENFILE
only) did not catch, so it fell through to the silent "skip this directory"
branch: a large repo got a partial watch set with no hint why edits in
unwatched directories stopped auto-syncing.

ENOSPC is non-fatal — raise the limit and partial watching keeps working — so
it now warns ONCE, naming the exact knob (fs.inotify.max_user_watches, with the
sysctl to set it), instead of degrading. It also stops attempting further doomed
watches for the session (every inotify_add_watch would fail too). Installed
watches keep firing; `codegraph sync` / git sync hooks cover the remainder.

Validated on macOS (forced per-directory path) and real Linux (Docker) — the
new test asserts a single warning naming fs.inotify.max_user_watches, no
degrade, and a live partial watch.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:01:05 -05:00
beca7116a0 feat(mcp): surface degraded watcher state to the agent in tool responses (#892)
When live file watching permanently degrades (watch-resource exhaustion, or a
write lock held past the retry budget), getPendingFiles() goes empty — so the
existing per-file staleness banner can't fire even though the index is now
frozen and silently drifting stale. The agent kept getting clean-looking
responses off a no-longer-updating index.

Read-tool responses now lead with a whole-index banner ("CodeGraph auto-sync
is DISABLED…") whenever the watcher is degraded, and codegraph_status gets a
dedicated "Auto-sync disabled" section. Both carry the degrade reason and tell
the agent to Read files directly. Expose isWatcherDegraded() /
getWatcherDegradedReason() on the CodeGraph class, and document the new banner
in the MCP server instructions.

Completes the agent-notification half of #876 (the operator-facing onDegraded
wiring shipped in #891).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:47:31 -05:00
cea4d086f9 fix(watcher): degrade cleanly on watch exhaustion and prolonged lock contention (#891)
The live file watcher could stay "alive" after it had stopped being
trustworthy. EMFILE/ENFILE watch-resource exhaustion only logged (and was
silently tolerated on the Linux per-directory path), and prolonged
LockUnavailableError retried forever at the normal debounce cadence — both
left auto-sync dead while the index silently drifted stale. Especially bad
for long-running MCP/daemon sessions.

Add a one-way degrade(): on watch-resource exhaustion (any watch strategy)
or on lock contention past a bounded exponential-backoff budget, log once,
fire a new onDegraded callback, and stop. start() now returns false
consistently when the per-directory path degrades at startup — it previously
returned true on Linux, so the MCP server reported the watcher "active" when
it had degraded. Wire onDegraded into the MCP server so callers are actually
told, and expose isDegraded()/getDegradedReason().

Builds on the approach in #877 by @thismilktea. Validated on macOS
(recursive), Linux (per-directory, Docker) and Windows (recursive) — 30/30
watcher + watch-policy tests on each.

Closes #876

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:32:54 -05:00
Colby McHenry 1bd9431879 Merge branch 'feat/value-reference-edges' 2026-06-14 18:39:35 -05:00
Colby McHenryandClaude Opus 4.8 ec90ddf79a feat(extraction): same-file value-reference edges (flag-gated)
Emit 'references' edges from a symbol to the file-scope const/var it reads
(TS/JS), so impact analysis catches "change this table, affect its readers".
Off by default behind CODEGRAPH_VALUE_REFS pending the agent A/B; on a real PR:
+3.1% edges, 100% precision on the spot-checked target, 372/372 extraction tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:25:34 -05:00
github-actions[bot] a1489f77a6 docs(changelog): promote [Unreleased] into [1.0.1]
[skip ci] Auto-generated by Release workflow.
2026-06-13 21:08:42 +00:00
github-actions[bot] ceb66d86fa release: sync package-lock.json to 1.0.1
[skip ci] Auto-generated by Release workflow.
2026-06-13 21:08:31 +00:00
b35a292c90 chore(release): bump version to 1.0.1 (#869)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:06:55 -05:00
5cc155ddc4 fix(index): skip nested git worktrees instead of indexing them as duplicate repos (#848) (#868)
A git worktree nested in a project (e.g. Claude Code's gitignored
`.claude/worktrees/<name>/`) was swept into the index as an embedded repo: its
`.git` is a FILE pointing into the host repo's `.git/worktrees/`, and embedded-
repo discovery treated any `.git` (file or directory) as a distinct repo to
index. Each worktree then duplicated the entire graph — one report went from
~1,850 files to 24,533, with search/explore flooded by stale copies.

classifyGitDir() now distinguishes:
- `.git` directory       -> embedded clone, index (#193/#514/#622, unchanged)
- `.git` file → worktrees/ -> worktree, skip (#848)
- `.git` file → modules/   -> submodule, index (unchanged)

Applied at both embedded-repo entry points: findNestedGitRepos discovery (which
also covers the sync/change-detection path) and the untracked-subdir recursion
in collectGitFiles.

Verified: the reproduction drops from 6 files / betaHelper×3 to 3 files / ×1,
with a genuine embedded clone and submodules still indexed. Regression test added.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:01:00 -05:00
64ff7597d0 fix(cli): stop serve --mcp from confusing humans — hide it + explain on a TTY (#867)
`codegraph serve --mcp` is the stdio MCP server an AI agent launches for itself
(the installer wires it into every agent's MCP config), not a command a human
runs. Run by hand in a terminal it just hung waiting for JSON-RPC, looking
broken.

- Hide `serve` from `--help` (commander `{ hidden: true }`); it stays fully
  invocable, so agents are unaffected.
- When stdin is an interactive TTY (a person — never the agent's pipe or the
  detached daemon), print what it is and point to `codegraph status` /
  `codegraph daemon`, then exit instead of hanging.
- README: drop `serve --mcp` from the CLI Reference and stop the troubleshooting
  section from telling users to run it; keep the accurate "your agent launches
  it" note.

Verified: agent path intact (22 MCP handshake/daemon tests pass), `serve` absent
from --help, and the TTY path prints the message and exits cleanly.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 15:18:55 -05:00
f7441f2124 fix(resolution,cli): cross-file static method calls + affected path normalization (#825) (#865)
Cross-file `ClassName.staticMethod()` calls resolved to the class, not the
method: the import resolver matched the receiver `Foo` to the named class
import but dropped the `.bar` member, and createEdges then mis-promoted the
`calls` edge to `instantiates`. So callers/impact for the static method came
back empty. Descend from the resolved class into its `Container::member` so the
call links to the method; fall back to the class when no such member exists
(non-`::` languages and genuine class references are unaffected).

Also normalize `codegraph affected` inputs to the project-relative,
forward-slash form the index stores, so `./src/x.ts`, an absolute path, and a
Windows back-slash path all match (previously silently returned 0).

Validated on luxon (24 files): node/edge totals identical (no explosion), 69
mis-promoted `instantiates` edges become `calls`, and real static factories
(DateTime.fromISO, etc.) resolve their callers. Full suite: 1534 passed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:52 -05:00
fb974552b0 docs(readme): point existing users to codegraph upgrade under the 1.0 banner (#866)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:48 -05:00
070ce4da2b feat(cli): codegraph version command + complete CLI Reference (#864)
* feat(cli): codegraph version command + complete CLI Reference

Add a `codegraph version` subcommand plus the `-v` and `-version`
spellings (commander already wires up `--version`/`-V`), so the version
is easy to reach however a user guesses at it. The `-v`/`-version` forms
are intercepted before commander parses — its version short flag is the
capital `-V`, and its parser rejects a multi-character single-dash flag.
A trailing `-v` on a subcommand still means `--verbose`.

Document the previously-missing commands in the README CLI Reference:
`daemon`/`daemons`, `unlock`, `telemetry`, `version`, and `help`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): reference #864 on the version-command entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:14:32 -05:00
Colby MchenryandGitHub ff288ac711 feat(cli): one interactive codegraph daemon command, replaces stop/list (#863)
Collapses the unreleased daemon controls into a single interactive command.
`codegraph daemon` (alias `daemons`) opens an arrow-key picker (current project's
daemon first, pre-selected), enter stops it, or pick "Stop all"; non-TTY prints a
plain list. Removes stop/list/ps; reuses the unchanged daemon-registry machinery;
the pick->stop loop is in daemon-manager.ts behind an injectable select (unit
tested). Validated live on macOS/Linux (real clack picker driven via pty) and
Windows (real runDaemonPicker + stopDaemonAt against a real daemon). Closes #845
follow-up.
2026-06-13 13:53:38 -05:00
Colby MchenryandGitHub 0f825649a1 feat(cli): codegraph stop / list to manage background daemons (#861)
Adds first-class daemon control (the #845 pain point: no clean way to stop a
runaway daemon). `codegraph stop [path]` stops the current/given project's
daemon (SIGTERM -> SIGKILL fallback, sweeps artifacts); `stop --all` stops every
daemon; `list`/`ps` shows running daemons (--json for scripts).

Discovery via a small self-healing registry: each daemon records its root under
~/.codegraph/daemons/ on start, removes it on graceful shutdown; readers prune
dead pids. Cross-platform by construction (files + process.kill). Validated live
on macOS, Linux (docker), and Windows (VM): registry unit 6/6 and real-daemon
stop/list 6/6 on each.
2026-06-13 12:59:41 -05:00
Colby MchenryandGitHub 2472508549 fix(installer,cli): refuse to index $HOME / filesystem root (#860)
Running the installer or `codegraph init`/`index` from $HOME auto-indexed the
entire home tree (installer indexes process.cwd() with no guard), producing a
multi-GB ~/.codegraph/codegraph.db; the install dir sharing the ~/.codegraph
name then made every home subdir resolve its root to $HOME. On pre-1.0 macOS the
per-file watcher over that tree exhausted kern.maxfiles and crashed the machine
(#845; the fd blowup was fixed in 1.0.0, this fixes the root cause).

Add unsafeIndexRootReason() and refuse the home dir, a parent of home, and
filesystem roots at the installer auto-index, `init`, and `index`. Overridable
with --force. Closes #845.
2026-06-13 12:35:18 -05:00
Colby MchenryandGitHub 484da77296 test(mcp): make liveness-watchdog kill assertions cross-platform (#859)
Validated the watchdog on the Windows VM: it kills a wedged process correctly,
but Windows has no real signals — process.kill(pid,'SIGKILL') maps to
TerminateProcess, seen as signal=null + non-zero code, not 'SIGKILL'. Assert
"killed" platform-agnostically and require the own exit code in the opt-out test.
Source watchdog unchanged. Windows: fatal-handler 8/8, liveness-watchdog 7/7,
mcp-daemon 9/9; mcp-initialize EPERM is pre-existing (identical with watchdog off).
2026-06-13 11:51:57 -05:00
Colby MchenryandGitHub 1702dfc544 fix(mcp): make the liveness watchdog a separate process, not a worker thread (#858)
The worker-thread watchdog from #856 didn't work in the real daemon — caught by
live-testing against a real serve --mcp. V8 isolates coordinate on global
safepoints, so a main thread wedged in a tight non-allocating loop (#850's
SourcePositionTableIterator::Advance) strands the watchdog worker before it can
SIGKILL.

A separate child process shares no isolate/heap with the parent, so the wedge
can't touch it; it kills via the kernel. Parent heartbeats to the child's stdin;
silence past the timeout -> SIGKILL; parent exit closes the pipe -> child exits.
Validated live (real daemon SIGKILLed in ~timeout); regression test covers the
non-allocating-wedge-under-heap-pressure case. API/install points/CHANGELOG
unchanged; the broken worker version was never released.
2026-06-13 10:40:55 -05:00
Colby MchenryandGitHub 576149e062 feat(mcp): worker-thread liveness watchdog to self-kill a wedged main thread (#856)
Belt-and-suspenders follow-up to #855. Any non-yielding sync loop on the main
thread wedges the event loop, and nothing running on that loop (timers, signal
handlers, PPID watchdog) can recover it — only another thread can.

A tiny worker thread (in the detached daemon + direct modes) watches a
shared-memory heartbeat the main thread bumps each event-loop turn; if it stops
advancing across enough consecutive checks (~CODEGRAPH_WATCHDOG_TIMEOUT_MS,
default 60s) the worker SIGKILLs the process so a fresh daemon starts on the next
connection. Counts consecutive stale checks (not wall-clock) so it's immune to
clock jumps / sleep; tuned never to fire on real work; opt out with
CODEGRAPH_NO_WATCHDOG=1.
2026-06-13 10:04:40 -05:00
Colby MchenryandGitHub 3476ac9a27 fix(mcp): exit on uncaught exception instead of orphaning/spinning at 100% CPU (#855)
The process-wide uncaughtException handler logged the error and kept running. For
the detached `serve --mcp` daemon that turned any escaped fault into an
unrecoverable orphan: nothing respawns it, and when logging the raw Error hit a
V8 source-position loop while lazily formatting `.stack`, the main thread wedged
at 100% CPU so even the PPID watchdog / idle-timer could no longer fire. Same
failure mode as #799, which only fixed the stdin-'error' trigger.

Restore Node's default fatal semantics: render a bounded, hang-proof line (name +
message only — never read `.stack`) then exit non-zero, so a fresh daemon starts
on the next connection. Extracted to src/bin/fatal-handler.ts with injectable
seams; unit-tested incl. the never-touch-stack invariant.

Closes #850.
2026-06-13 09:48:44 -05:00
06e03758af docs(readme): collapse the npm-install alternative into a details section (#844)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:01:12 -05:00
13027b0730 docs(readme): auto-sync becomes quick-start step 4 heading (#843)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:00:23 -05:00
eed0b5ae20 docs(readme): init indexes by default (drop -i) + bold auto-sync guarantee (#842)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:57:50 -05:00
github-actions[bot] 3286e9f104 docs(changelog): promote [Unreleased] into [1.0.0]
[skip ci] Auto-generated by Release workflow.
2026-06-12 18:22:16 +00:00
github-actions[bot] 238bd909cb release: sync package-lock.json to 1.0.0
[skip ci] Auto-generated by Release workflow.
2026-06-12 18:22:06 +00:00
b9eff08c77 chore(release): 1.0.0 — README banner + X account (#840)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:21:46 -05:00
06a410e9b4 feat(extraction): R language support (#828) (#839)
R has no declaration syntax — everything is an expression — so the
extractor works through the visitNode hook: functions in every
assignment form (incl. nested, attributed to their enclosing scope),
top-level variables/constants, library()/require() imports and
source() file references (claimed, Lua-style), S4/RefClass/R6/ggproto
classes with their methods and extends edges, setGeneric/setMethod.
Grammar vendored from r-lib/tree-sitter-r v1.2.0 (ABI 14; npm package
is a security placeholder, tree-sitter-wasms has no R).

Benchmarked on AnomalyDetection (8/8 named defs), dplyr (1027 fns),
ggplot2 (150 ggproto classes / 597 methods / 128 extends edges —
adding ggproto mid-bench flipped the large-repo A/B from a regression
to 2.4x faster than the no-codegraph arm).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:17:29 -05:00