Skip to content

Latest commit

 

History

History
386 lines (370 loc) · 68.1 KB

File metadata and controls

386 lines (370 loc) · 68.1 KB

jcodemunch-mcp — Project Brief

Current State

  • Version: 1.108.192 — #377 Phase 2 P3: ONE IMMUTABLE SNAPSHOT, START TO FINISH. The two edges @mightydanp pinned adversarially after reviewing .183-.188. ⚠⚠ BOTH ARE THE SAME DEFECT: the pipeline READ THE EVIDENCE MORE THAN ONCE and assumed the reads agreed. (1) An absence receipt resolved through a MUTABLE key. A receipt has a full snapshot-bound sha256 identity; its linked absence record did NOT — absent:<sha12> is sha256(tool,repo,query,scope)[:12] with NO SNAPSHOT IN IT, so re-running the same query over a different tree OVERWRITES the record at the same key, and finalization followed envelope.absence_ref to whatever sat there at validation time. ⚠ BOTH DIRECTIONS were reachable and both now have a regression: a provable receipt REFUSED because a later scan went stale, and — the dangerous one — a receipt minted over a STALE scan ATTESTED because a later scan was clean (borrowed proof). Producer now freezes a DEEP COPY into envelope["absence_record"]. ⚠ absence_refusal is STILL THE ONLY implementation of the refusal rules — what changed is the INPUT it is handed, not the rules; a receipt still cannot disagree with the gate that issued it.DEEP copy, not a reference: the live record is a mutable dict in another module's map and a shared nested channels would let a later scan reach into a minted receipt (pinned).Pre-.192 receipts STILL RESOLVE — no frozen record falls back to the legacy lookup rather than refusing, so an existing receipt does not become uncitable. (2) Validation and RENDERING did TWO independent lookups. _validate_evidence resolved the envelope, then _render_receipt_detail called receipts.lookup AGAIN; the store is BOUNDED+LRU, so eviction between them produced a body with NO receipt block and a sha256 OVER THAT BODY while the attestation said it was proved — the rendered artifact and its own receipt disagreed. render_handoff now takes resolved_receipts from validation. ⚠ The regression asserts on the SEAM (lookup( must not appear in the render path), not only the symptom, so it survives an eviction-policy change. ⚠⚠ THIS IS A PUBLISHED-SCHEMA CHANGE AND MUST BE CALLED ONE: schemas/evidence-receipt.schema.json sets additionalProperties: false, so adding absence_record fails validation until the schema is updated — caught by test_v1_108_183.py::test_a_live_absence_receipt_validates, NOT by review. Additive+optional, so pre-.192 receipts still validate; schema STRING stays jcodemunch.evidence/v1 (nothing required changed, no field changed meaning). ⚠⚠ ONE SHIPPED BEHAVIOUR CHANGED: editing the live _absences record can no longer re-judge an already-minted receipt. A .183 test expressed "this scan became unciteable" by MUTATING that record post-mint and now fails BY DESIGN — that technique was never valid and its invalidity IS the fix (the key carries no snapshot, so an in-place edit is indistinguishable from a different scan overwriting it). Its real claim is unchanged and now asserted at MINT time; a new test pins the changed semantics directly. ⚠ This does NOT settle whether a receipt should EXPIRE because the tree moved on — that is a different axis, the expiry taxonomy, still OPEN under Phase 2 P3. New tests/test_v1_108_192.py (10); every fix verified NON-VACUOUS by reverting it (3 failures on the absence half, 2 on the resolution half). No tool-count/INDEX_VERSION change.
  • Prior (1.108.191):The EAGER auto-watch path indexes ONCE. Found while porting #388. ⚠⚠ BOTH arms of _auto_watch_if_needed indexed TWICE and NEITHER was the #384 bug. Takeover arm = a REAL RACE: maybe_takeover returning started spawns a watch task whose initial index runs as a CONCURRENT asyncio task, and the caller THEN awaited ensure_indexed on the same folder — two writers, one indexwrite lock, every acquire wait_seconds=60.0. This is the race the whole #384 discussion warned about, and it was ALREADY IN THE CODE. Fall-through arm = redundant walk on EVERY EAGER AUTO-WATCH (any tool touching an unwatched repo, NOT just index_folder): ensure_indexed awaited, then add_folder's task walked the same tree again — serialized so not a race, but OLDER AND WIDER than #384 ever was. Both collapse to one restructure: index once via ensure_indexed (awaited), then adopt it; maybe_takeover + add_folder both told to skip. ⚠ ORDERING IS THE FIX, NOT JUST THE FLAG — PINNED BY A CALL-ORDER TEST. ensure_indexed must COMPLETE before any watch task starts or the task builds its hash cache from an index about to be rewritten underneath it; the old code started the task FIRST. ⚠ ensure_indexed is the pass kept because it is the only one that is BOTH awaited (the hook exists so the tool runs against fresh data) AND race-safe via the manager's _pending coordination. ⚠⚠ NEW record_index_ready flag, SEPARATE from skip_initial_index: .189 had the skip path ALWAYS write a synthetic reindex record — right for its ONE caller, wrong the moment a second appeared. The index_folder TOOL writes NO record (so the watcher must record on its behalf or get_watch_status never learns), but ensure_indexed writes a REAL record with the REAL result (so a synthetic one over it replaces a MEASUREMENT WITH A PLACEHOLDER). One flag could not serve both without losing the signal or clobbering it. New tests/test_v1_108_191.py (9) incl. a guard that the TWO internal standby-loop callers still pass NO flags — they take over precisely so the folder gets indexed. No tool-count/INDEX_VERSION/schema change.
  • Prior (1.108.190):The TAKEOVER half of the #384 double index, found by @Bortlesboat in PR #388. ⚠⚠ .189 fixed add_folder and LEFT maybe_takeover ALONE, so when ANOTHER PROCESS had been watching the folder and this one adopted it, the watch task STILL ran a full pass over the tree the tool had just indexed — the exact #384 bug survived for that case. maybe_takeover now takes skip_initial_index; _auto_watch_after_tool passes it. ⚠ DEFAULT STAYS False AND IS PINNED BY A TEST — _watch_standby_signal and the fallback retry loop both call it when NOTHING ELSE is indexing, and there the initial index is the WHOLE POINT of taking over.Also adopted from #388: LAZY hash-cache hydration, which fixes something OLDER than either #384 fix — the cache is built only on a SUCCESSFUL initial index, so a FAILED one left it empty for the life of the task, and an empty cache does not raise, it silently drops every old_hash. Guarded on an explicit _hash_cache_built flag, NOT their if not _hash_cache, so a legitimately empty index does not re-read the store on every change. ⚠ PROCESS LESSON: their PR opened 06:51 UTC and we shipped .189 at 12:56 UTC without ever checking open PRs on an issue that had one attached — the cross-reference was on #384's timeline the whole time. CHECK PRs BEFORE WRITING CODE ON AN ISSUE. New tests/test_v1_108_190.py (7). No tool-count/INDEX_VERSION/schema change.
  • Older releases (1.108.182 and earlier): see CHANGELOG.md. The 1.108.182 entry ("a stall has a name and a ceiling", #375) and the 1.108.177-.181 #377 hardening arc are there in full.
  • INDEX_VERSION: 17
  • Tests: 6189 passed, 7 skipped (1.108.192) + the KNOWN 12 local-ONNX test_semantic_search env failures (green in CI on all 4 ubuntu jobs — never read them as a regression)
  • Python: >=3.10
  • Tool count: 90 in full (front door hidden; +1 v1.108.111 get_parity_map, +1 v1.108.112 get_decorator_census, +1 v1.108.113 get_architecture_metrics); tool_surface=counter exposes a 3-tool front door (order/menu/route) instead

Key Files

src/jcodemunch_mcp/
  server.py            # MCP dispatcher (async); CLI subcommand dispatch, auth/rate-limit middleware. v1.108.66: the Counter front door (order/menu/route) — _effective_surface()/_counter_front_door_tools()/_raw_catalog_tools()/_catalog_names() + surface-collapse in _build_tools_list + _handle_order/menu/route + early front-door branch in call_tool
  counter.py           # (v1.108.66) The Counter: adaptive tool surface logic (pure, no server import). FRONT_DOOR set; STATE_CHANGING_ACTIONS + exec/write-verb tripwire (_FORBIDDEN_VERB_RE) → order_gate(); idf-weighted search_catalog() for menu; _INTENT_RULES + classify_intent()/shape_execute_args() for route. v1.108.124: EXAMPLES (curated per-action example arg objects) + example_for() — catalog_entry attaches `example` into menu rows, _handle_route uses it as the args_template fallback; validated against live inputSchemas in test_counter.py. server.py owns Tool registration + call_tool re-dispatch; counter.py is fed plain data
  watcher.py           # WatcherManager class (dynamic folder watching); watch_folders() wrapper
  progress.py          # MCP progress notifications; ProgressReporter (thread-safe, monotonic), make_progress_notify() bridge. v1.108.189 adds HeartbeatReporter (#383) — the token-less fallback: elapsed-time WARNING lines on the LOG channel, duck-typing ProgressReporter so the dispatcher wires either identically. ⚠ Holds NO notify channel/session ref by construction (not in __slots__) and close() yields no futures, so it CANNOT become an unrequested notification; silent until the first JCODEMUNCH_HEARTBEAT_SECONDS elapses, and finish() is silent if it never spoke
  security.py          # Path validation, skip patterns, file caps
  redact.py            # Response-level secret redaction; regex patterns for AWS/GCP/Azure/JWT/GitHub/Slack/PEM/API keys/private IPs; redact_dict() post-processor
  config.py            # JSONC config: global + per-project layering, env var fallback, language/tool gating
  agent_selector.py    # Complexity scoring + model routing (off/manual/auto); default provider batting orders
  cli/
    init.py            # `jcodemunch-mcp init` — one-command onboarding (client detection, config patching, CLAUDE.md, Cursor rules, Windsurf rules, hooks); --demo flag. v1.105.1: `install <agent>` / `uninstall` / `install-status` verbs. v1.107.0: `--skills` flag on install, skills block in install_status report
    skills.py          # v1.107.0: Claude Agent Skill bundle writer. _build_skill_content() composes YAML frontmatter + tier-filtered tool-usage decision tree. install_claude_skill / uninstall_claude_skill / skill_status. Lives at ~/.claude/skills/jcodemunch/SKILL.md (global) or ./.claude/skills/jcodemunch/SKILL.md (project). Reuses _filter_policy_for_tools from init.py for tier awareness
    hooks.py           # PreToolUse (Read interceptor) + PostToolUse (auto-reindex) + PreCompact (session snapshot) + TaskCompleted (post-task diagnostics) + SubagentStart (repo briefing) hook handlers for Claude Code
  groq/
    cli.py             # `gcm` CLI entrypoint — codebase Q&A (single question + --chat mode)
    config.py          # GcmConfig dataclass: GROQ_API_KEY, model, token_budget, system prompt
    retriever.py       # Bridge to jCodeMunch: ensure_indexed(), retrieve_context()
    inference.py       # Groq API streaming + batch via OpenAI-compatible client
  parser/
    languages.py       # LANGUAGE_REGISTRY, extension → language map, LanguageSpec
    extractor.py       # parse_file() dispatch; custom parsers for Erlang, Fortran, SQL, Razor
    imports.py         # Regex import extraction (19 languages); extract_imports(), resolve_specifier(), build_psr4_map()
    fqn.py             # PHP FQN ↔ symbol_id translation (PSR-4); symbol_to_fqn(), fqn_to_symbol()
  encoding/
    __init__.py          # Dispatcher: encode_response(tool, response, format) — auto/compact/json
    format.py            # MUNCH on-wire primitives: header, legends (@N), scalars, CSV tables
    gate.py              # 15% savings threshold (JCODEMUNCH_ENCODING_THRESHOLD override)
    generic.py           # Shape-sniffer fallback encoder (covers all tools w/o custom encoder)
    decoder.py           # Public decode() — rehydrates MUNCH payloads back to dicts
    schemas/             # Per-tool custom encoders (tier-1, phase 2+); auto-discovered registry
  storage/
    sqlite_store.py    # CodeIndex, save/load/incremental_save, WAL-aware LRU cache (_db_mtime_ns); get_source_root(). v1.106.0: save_index + migrate_from_json acquire `indexwrite` process_locks before SQLite writes, body extracted to `_save_index_locked` / `_migrate_from_json_locked`; serialises across MCP processes
    process_locks.py   # v1.106.0: generic multi-process coordination (acquire/release/inspect/held). Atomic O_EXCL + fcntl flock (Unix) + PID liveness + scoped lock files. Scopes: `watcher` (one-watcher-per-repo, shared with watcher.py) + `indexwrite` (save coordination). Metadata: pid/client_id/scope/target/started_at. JCODEMUNCH_CLIENT_ID env var sets friendly client name (defaults to sys.argv[0] basename)
  embeddings/
    local_encoder.py   # Bundled ONNX local encoder (all-MiniLM-L6-v2, 384-dim); WordPiece tokenizer, encode_batch(), download_model()
  enrichment/
    lsp_bridge.py      # LSP bridge — opt-in compiler-grade call graph resolution via pyright/gopls/ts-language-server/rust-analyzer; LSPServer lifecycle, LSPBridge multi-server manager, enrich_call_graph_with_lsp() + enrich_dispatch_edges() (interface/trait dispatch resolution)
  retrieval/
    subject_state.py     # (v1.108.178) #377 item 3: what a scan's answer depends on, cheap enough to re-check. capture() at cache-WRITE (index generation, .db mtime, live git HEAD, + working-tree fingerprint ONLY for an absence) / changed() at cache-READ / revalidate_verdict() downgrades a replayed `absent` and strips the stale evidence token. UNKNOWN is never a change. v1.108.179 adds moved_during_scan() (item 6: before/after identity around a scan, fresh_head bypasses the TTL cache) + changed(when=) so the cached-replay and live-scan refusals read differently. v1.108.181 adds working_tree_state() (item 5: scope-level clean/dirty_in_scope/dirty_outside_scope/unknown/not_applicable; blocks ONLY on in-scope dirt the index has not re-read) + _parse_porcelain/_in_scope/_unreflected_in_index
    signal_fusion.py   # Weighted Reciprocal Rank (WRR) fusion: lexical + structural + similarity + identity channels
    ledger_trust.py    # (v1.108.186/.187) THE ONE RULE for which ranking_events labels are evidence, shared by tuning.py + regret.py + tools/analyze_perf.py instead of copied. semantic_label_is_trustworthy(row) refuses exactly (tool="get_ranked_context_fusion", semantic_used=1) — pre-fix rows from an exit that built no similarity channel. identity_label_is_trustworthy(row) (.187) refuses rows that RETURNED symbols while recording NO top1_score — the only exact signature of the exit that passed no ledger features; ⚠ it deliberately does NOT match on identity_hit itself (pre-fix is always 0 and 0 is an honest post-fix answer), and search_symbols_fusion's history is UNSEPARABLE (no discriminator exists, window is the only remedy). UNKNOWN, not False: consumers put them in a THIRD bucket and disclose the count. A short row is TRUSTED (this refuses a KNOWN lie; refusing the unclassifiable would be silent data loss). ⚠ The semantic rule EXPIRES if that exit ever builds a similarity channel — drift guard in tests/test_v1_108_186.py
    regret.py          # (v1.108.68) analyze_regret: mines the ranking_events ledger for SIX retrieval-regret signals (requery_churn/low_confidence/thin_result/ambiguous_top/stale_at_query/vocabulary_gap) as severity-ranked clusters. Pure read via token_tracker.ranking_db_query; no new tables. Consumed by suggest_corrections + the digest one-liner
  summarizer/
    batch_summarize.py # 3-tier: Anthropic > Gemini > OpenAI-compat > signature fallback
  tools/
    index_folder.py    # Local indexer (sync → asyncio.to_thread in server.py). v1.108.0 adds `paths=[...]` arg via new `resolve_explicit_paths()` helper to skip the directory walk when the caller supplies an explicit file/subdir list; security matches the walk path (outside-root / traversal / symlink-escape / oversize / unsupported-ext all warn-and-skip with per-entry warnings). v1.108.6 adds `identity_mode: "config"|"local"|"git"` arg — delegates to `storage/git_root.resolve_index_identity()` which is the single source of truth for local-folder → repo-ID resolution (replacing duplicated logic across watcher.py / resolve_repo.py / index_folder.py).
    index_repo.py      # GitHub indexer (async, httpx)
    get_symbol.py      # get_symbol_source: shape-follows-input (id→flat, ids[]→{symbols,errors}). v1.108.70 bounded-source mode: optional source_start_line/source_end_line/max_source_lines/max_source_bytes/max_total_source_bytes return an explicitly-labeled slice (source_truncated + range/total metadata, source_is_bounded_view); verify stays full-body; context_lines+bound rejected. Pure helpers _utf8_safe_truncate + _bound_source
    search_columns.py  # Column search across dbt/SQLMesh models
    get_context_bundle.py   # Symbol + imports bundle; token_budget/budget_strategy
    get_ranked_context.py   # Query-driven budgeted context (BM25 + PageRank)
    resolve_repo.py    # O(1) path→repo-ID lookup
    find_importers.py  # Files that import a given file (import graph); cross_repo param
    find_references.py # Files that reference a given identifier. v1.108.96: _attach_scip_to_response unions SCIP compiler-verified reference edges (compile-time evidence P1)
    _scip_consume.py   # (v1.108.118) Shared SCIP-evidence reader for the graph consumers (P2): open_scip_reader (mode=ro, honest-None when scip_edges absent/empty incl. pre-v17) + scip_meta_and_stale + scip_meta_block. Used by get_blast_radius._attach_scip_to_blast + get_call_hierarchy._attach_scip_to_hierarchy
    test_summarizer.py # Diagnostic tool: probe AI summarizer, report status (disabled by default)
    package_registry.py # Cross-repo package registry: manifest parsing, registry building, specifier resolution
    get_cross_repo_map.py # Cross-repo dependency map at the package level
    _call_graph.py       # Shared AST-derived call-graph helpers (callers/callees, BFS)
    get_call_hierarchy.py # get_call_hierarchy: callers+callees for a symbol, N levels deep
    decision_context.py   # (v1.108.59) resolve_decision_context: read-only git-archaeology surfacer. Mines decision-bearing commits (revert/perf/refactor/rename/bugfix) for a set of files, reusing get_symbol_provenance's _run_git/_classify_commit/_extract_intent; dedupes by SHA, ranks by category weight × recency, emits digest + by_category + volatility + summary. Surface-only, nothing persisted. Consumed by get_blast_radius / get_impact_preview via include_decisions
    get_impact_preview.py # get_impact_preview: transitive "what breaks?" analysis. v1.108.59: include_decisions attaches a read-only `decisions` block (decision_context)
    plan_refactoring.py   # plan_refactoring: edit-ready plans for rename/move/extract/signature refactorings
    get_symbol_complexity.py  # get_symbol_complexity: cyclomatic/nesting/param_count for a symbol
    get_churn_rate.py         # get_churn_rate: git commit count for file or symbol over N days
    get_delivery_metrics.py   # (v1.108.69) get_delivery_metrics: durable-change delivery over a window. Classifies each non-merge commit into one bucket (revert_authored/reverted/reworked/durable) via _run_git; commits_durable is the numerator for cost-per-outcome (the `delivery` CLI's --cost divides AI spend by it). Hub files (CHANGELOG/version/monolithic dispatch, co-touched by >=max(4,20%) of commits) excluded from the rework signal (auditable via _meta.hub_files_excluded); commits_provisional flags the trailing tail. Reuses get_symbol_provenance._classify_commit for by_category. Read-only, no new tables
    get_symbol_provenance.py  # get_symbol_provenance: full git archaeology per symbol — authorship lineage, semantic commit classification, evolution narrative. Phase 5: optional stack_frequency block reading runtime_stack_events over a 30-day window — per-severity counts + first/last seen; narrative gains an appended sentence when error count >= 3
    get_pr_risk_profile.py    # get_pr_risk_profile: unified PR/branch risk assessment — fuses blast radius + complexity + churn + test gaps + volume into composite score. Phase 7: when runtime traces have been ingested, adds a 6th signal (runtime_traffic; W=0.15 with the static five rebalanced to 0.85 of their original weights) plus a runtime_dark_code_introduced flag for PRs that add code in files with zero runtime evidence. Static-only callers (no traces) keep the historical 5-signal mix bit-for-bit.
    get_architecture_metrics.py # (v1.108.113) get_architecture_metrics: concentration (Gini over per-file symbols/bytes/fan_in/fan_out + top concentrators) + depth (Lakos levelization, longest chain over SCC-condensed DAG) + modularity (WCC clusters + back_edges = DSM hidden coupling). Reuses _build_adjacency (get_dependency_graph) + _find_cycles. One tool vs their 3; NO N×N matrix; does NOT touch radar composite. Read-only analytics. Standard tier
    get_decorator_census.py   # (v1.108.112) get_decorator_census: repo-wide census of decorators/annotations/attributes. Aggregates the index's stored per-symbol `decorators` (cross-language, no parser work); normalized histogram (_normalize_decorator strips @/args/[]; _short_raw flattens+caps raw_forms), per-bucket symbol_kinds + file count; name_filter/scope_path/kind filters, include_sites. Read-only ANALYTICS (no tokens-saved _meta). Standard tier
    get_parity_map.py         # (v1.108.111) get_parity_map: correspondence-aware migration parity between a SOURCE and TARGET symbol tree (two subpaths of one repo, or two repos). Exact + rename matching (reuses find_similar_symbols _signature_tokens/_callee_set/_jaccard/_byte_ratio), status per source symbol (ported/ported_diverged/unported/orphaned/added), dependency-ordered port_plan (adjacency from _callee_set, SCC grouping via get_dependency_cycles._find_cycles, Kahn topo, unblocked/blocking_deps). Read-only/plan-only; parity_axes reserved for P3 suite axes. Standard tier
    get_hotspots.py           # get_hotspots: top-N high-risk symbols by complexity x churn
    get_repo_map.py           # get_repo_map: query-less, token-budgeted, signature-level repo overview ranked by PageRank — cold-start orientation. Reuses cached PageRank, emits signatures only (no bodies), greedy-packs per-file under token_budget
    find_similar_symbols.py   # find_similar_symbols: multi-signal consolidation detection — semantic (embeddings) + structural (signature/size) + behavioral (callee Jaccard); union-find clustering, verdict tier (near_duplicate / similar_logic / parallel_implementation), canonical pick by PageRank, differs_by breakdown. BM25 inverted-index pre-filter for sub-N^2 cost. Skips tests/dunders/generated by default.
    get_group_contracts.py    # get_group_contracts: cross-repo shared-symbol API surface for a group of indexed repos. Resolves named imports through the package registry, classifies each shared symbol into 4 verdict tiers (de_facto_api / leaky_internal / dead_contract / version_skew), attaches stability score (churn-weighted), last_breaking_change (from provenance), and runtime_hits (when traces exist). Pairs with get_cross_repo_map: that gives repo-level edges; this zooms in to the symbol-level surface.
    find_implementations.py   # find_implementations: multi-source concrete-impl discovery for interfaces/abstracts/methods. Four resolution channels with confidence scoring — LSP dispatch (1.0), AST class hierarchy (0.85), duck-typed name match (0.65), decorator handler (0.45). Classifies each impl (subclass_override / interface_impl / duck_typed / decorator_handler / subclass), ranks by PageRank × byte_length, attaches differs_by breakdown, optional cross_repo discovery.
    check_delete_safe.py      # check_delete_safe: composite preflight — can this symbol be deleted? Combines find_importers (cross_repo) + check_references + find_dead_code + runtime evidence + entry-point heuristics into a single verdict (safe_to_delete / test_coverage_only / internal_only / internal_uses_blocking / external_uses_blocking / cross_repo_blocking / runtime_observed / entry_point) plus top-5 blockers ranked by severity plus a one-line recommended_action. Read-only. Pairs with check_rename_safe for the rename-and-delete refactor flows. v1.104.1: track test_import_count separately from external_import_count so test-only consumption correctly downgrades to test_coverage_only. v1.108.6: honest-hint caveat — when `safe_to_delete` is reached AND `include_runtime=True` AND no traces are ingested for the repo (`_runtime_data_present()` returns False), the `recommended_action` surfaces that the verdict rests on static signals only and points at `import-trace`. `signals.runtime_data_present` surfaced for callers to introspect. Back-ported from `check_column_drop_safe` in jdatamunch-mcp v1.8.0.
    assemble_task_context.py  # assemble_task_context: task-aware single-call context orchestrator. Auto-classifies the task into one of six intents (explore/debug/refactor/extend/audit/review) via keyword scoring, auto-extracts anchor symbol names from the task, runs the intent-appropriate sub-tool sequence (digest + hotspots + tectonic for explore; anchor + callers + callees + blast + runtime for debug; anchor + rename_safe + delete_safe + implementations + similar for refactor; anchor + implementations + similar + decorators for extend; anchor + risk + blast + dead_code + untested for audit; changed + blast + risk + similar_changed for review), packs results into a single source-attributed capsule under token_budget. Each entry tagged with stage + source_tool. Intent classification is explainable (returns intent_keywords_matched + intent_confidence). Caller can override intent and include to force specific stages.
    get_tectonic_map.py       # get_tectonic_map: logical module topology via 3-signal fusion (structural+behavioral+temporal) + label propagation
    get_signal_chains.py      # get_signal_chains: entry-point-to-leaf pathway discovery; traces how HTTP/CLI/task/event signals propagate through the call graph; discovery + lookup modes. v1.108.58: include_flow_edges param consumes flow_edges.py — string-dispatched handlers become http gateways, rendered templates attach as a per-chain `views` list
    get_endpoint_impact.py    # (v1.108.90) Endpoint-centric impact: "what breaks if I change GET /users?" _collect_endpoints unifies flow_edges route edges (string-dispatch) + get_signal_chains decorator gateways (Flask/FastAPI/Spring local path) into one endpoint table; _match_endpoints (verb+path exact→suffix); _impact_for_handler fuses get_blast_radius (importers+callers) + render→view edges. Read-only, standard tier. handler_symbol_id bypasses URL resolution for prefixed routes. First slice of docs/prd-framework-routes-endpoint-impact.md; FastAPI prefix / Spring class-mapping composition is the follow-on
    flow_edges.py             # (v1.108.58) Language-agnostic framework flow-edge resolver. resolve_flow_edges(index, store, owner, name, kinds=("route","render")) emits typed edges the AST call graph misses: route→handler (Django path/re_path/url, Express/Fastify/Koa .get(p,h), Flask add_url_rule view_func=, Rails to:"ctrl#action") resolved to symbols via the import graph; render→view (render/render_template/res.render/view string templates) resolved to the template file when indexed. Shape-keyed (one resolver, not per-framework plugins); reuses _ContentCache/_symbol_body/build_symbols_by_file/resolve_specifier. Pure read path, no reindex. Decorator-bound handlers NOT re-emitted (they already surface as gateways)
    render_diagram.py         # render_diagram: universal Mermaid renderer; auto-detects source tool, picks optimal diagram type (flowchart/sequence), encodes metadata as visual signals; 3 themes, smart pruning; optional `open_in_viewer` (config-gated, spawns mmd-viewer)
    mermaid_viewer.py         # mmd-viewer spawn helper for render_diagram; resolve_viewer_path/open_diagram/cleanup_temp_dir; jcm- prefix for safe cleanup; config-gated via render_diagram_viewer_enabled + mermaid_viewer_path
    get_project_intel.py      # get_project_intel: auto-discover+parse non-code knowledge (Dockerfiles, CI configs, compose, K8s, .env templates, Makefiles, scripts); cross-references to code symbols; 6 categories. v1.108.0 adds `scope_path` arg to restrict discovery to a monorepo subpath (use list_workspaces.path values); validates against source_root (traversal/absolute/non-existent all error).
    list_workspaces.py        # (v1.108.0) Enumerate monorepo workspace members. Detects pnpm (pnpm-workspace.yaml), yarn/npm (package.json `workspaces:`), turborepo (turbo.json), lerna (lerna.json), rush (rush.json), Go (go.work `use (...)`, module name from go.mod), Cargo (Cargo.toml `[workspace] members`). Returns `[{path, package_name, manager}, ...]` plus `is_monorepo` + `managers`. Read-only, dependency-free (hand-rolled minimal TOML/YAML readers).
    get_repo_health.py        # get_repo_health: one-call triage snapshot (delegate aggregator); includes six-axis `radar` field (v1.87.0)
    health_radar.py           # Six-axis health radar (complexity/dead_code/cycles/coupling/test_gap/churn_surface) + diff_health_radar pure-function tool for PR-time diff-grade reporting (v1.87.0). Phase 7 (v1.100.0): optional 7th axis runtime_coverage when caller passes runtime_coverage_pct; axis is omitted otherwise so the composite stays comparable against pre-Phase-7 baselines. diff_radar walks the axes dict generically — picks up the new axis automatically.
    get_untested_symbols.py   # get_untested_symbols: find functions with no test-file reachability (import graph + name matching)
    search_ast.py             # search_ast: cross-language AST pattern matching; 10 preset anti-patterns + custom mini-DSL (call:, string:, comment:, nesting:, loops:, lines:); enriched with symbol context
    winnow_symbols.py         # winnow_symbols: multi-axis constraint-chain query; AND-intersects kind/language/name/file/complexity/decorator/calls/summary/churn in one round trip; ranks by importance/complexity/churn/name
    audit_agent_config.py    # audit_agent_config: token waste audit for CLAUDE.md, .cursorrules, etc.; cross-refs against index. Reused by suggest_corrections (_discover_files / _fuzzy_suggest / stale-config findings)
    suggest_corrections.py   # (v1.108.68) Retrieval-regret synthesis: fuses regret.analyze_regret clusters + audit_agent_config + WeightTuner dry-run into SUGGESTED corrections (routing/vocabulary/index-freshness/stale-config) with difflib unified-diff CLAUDE.md previews. Read-only charter — never writes a user file; apply_weights touches only tuning.jsonc. Honest no-telemetry hint
    analyze_perf.py          # analyze_perf: per-tool latency telemetry (p50/p95/max/error_rate) + cache hit-rate; reads in-memory session ring or persistent telemetry.db (opt-in via perf_telemetry_enabled); compare_release="X" loads benchmarks/token_baselines/vX.json and adds baseline_diff
  runtime/
    __init__.py          # Trace ingestion package (Phases 0-5): re-exports redact_trace_record, resolve_to_symbol_id, parse_otel_file, ingest_otel_file, OtelSpan, parse_sql_log_file, ingest_sql_log_file, SqlQueryRecord, parse_stack_log_file, ingest_stack_log_file, StackEvent, StackFrame, VALID_SOURCES = {'otel','sql_log','stack_log','apm'}
    redact.py            # Single chokepoint redact_trace_record(record, source) — strips emails, IPv4, SQL literals/numerics, JSON value blocks, Python locals reprs, plus all secret patterns from ../redact.py
    resolve.py           # resolve_to_symbol_id(conn, file, line, name) — best-effort (file, line, function) → symbol_id with suffix-match fallback for absolute trace paths against repo-relative index paths
    otel.py              # Phase 1 OTel JSON parser — handles JSON-Lines, single-document JSON, top-level array, and .gz transparently; extracts code.filepath / code.lineno / code.function / duration into OtelSpan
    ingest.py            # Phase 1 orchestrator ingest_otel_file(db_path, file_path, redact_enabled, max_rows) — parse → redact → resolve → upsert; computes per-batch p50/p95 from span durations; FIFO-evicts runtime_calls + runtime_unmapped down to max_rows when exceeded; persists per-pattern redaction counts to runtime_redaction_log
    sql_log.py           # Phase 4 SQL log parser — pg_stat_statements CSV (header autodetect; total_time/total_exec_time + mean_time/mean_exec_time aliases) + generic JSON-Lines (.jsonl/.json/.log) + top-level array fallback + .gz transparent; extracts table refs (FROM/JOIN/UPDATE/INSERT INTO/DELETE FROM/MERGE INTO; schema-qualified names → trailing ident) and column refs (qualified alias.col + bare idents in SELECT/WHERE/ON/HAVING/GROUP BY/ORDER BY)
    sql_ingest.py        # Phase 4 orchestrator ingest_sql_log_file(db_path, file_path, redact_enabled, max_rows) — parse → redact → resolve → upsert; resolver builds a one-shot read-only metadata snapshot (file-stem map, exact-name map, dbt_columns/sqlmesh_columns set); upserts runtime_calls + runtime_columns + runtime_unmapped + runtime_redaction_log under source='sql_log'; FIFO-evicts all three runtime tables
    stack_log.py         # Phase 5 stack-frame parser — Python tracebacks (`File "...", line N, in <name>` pairs), JVM tracebacks (`at pkg.Class.method(File.java:N)` + flattened `Caused by:` chains), Node.js stacks (named `at funcName (file.js:N:N)` + anonymous `at file.js:N:N` + node:events-style module paths). Plain-text + JSON-Lines structured-log + top-level array + .gz. Severity heuristic: looks 3 lines back for FATAL/CRITICAL/ERROR/WARN[ING]/INFO; default 'info'.
    stack_ingest.py      # Phase 5 orchestrator ingest_stack_log_file(db_path, file_path, redact_enabled, max_rows) — parse → redact (event.message) → resolve each frame → upsert; populates BOTH runtime_calls (severity-agnostic rollup so confidence-stamping fires) AND runtime_stack_events (per-severity counts). FIFO-evicts runtime_calls + runtime_unmapped + runtime_stack_events. Phase 6 adds ingest_stack_log_stream() that takes an in-memory text payload via the shared _ingest_stack_iter() pipeline.
    http_routes.py       # Phase 6 Starlette route handlers: POST /runtime/otel, POST /runtime/sql, POST /runtime/stack. Off by default — gated by runtime_ingest_enabled config + JCODEMUNCH_HTTP_TOKEN bearer auth. Per-repo asyncio.Lock serialises writes against the same SQLite DB. Body cap (default 5 MB) checked separately for on-wire and decompressed sizes (gzip-bomb guard). Repo selection via X-JCM-Repo header or ?repo= query. Mounted on both SSE and streamable-http transports.
    confidence.py        # Phase 2 RuntimeConfidenceProbe + attach_runtime_confidence (symbol-keyed) + attach_runtime_confidence_by_file (file-keyed). Stamps `_runtime_confidence` ∈ {confirmed, declared_only, unmapped} on result entries; emits `_meta.runtime_freshness` summary. Read-only connections use ?mode=ro&immutable=1 so they never bump WAL mtime and invalidate the CodeIndex LRU cache. Zero-cost when runtime_calls is empty.
  evidence/
    receipts.py          # (v1.108.183) #377 Phase 2 P1: the `jcodemunch.evidence/v1` envelope + session store. evidence_id() hashes EXACTLY (subject, effective_search, snapshot) — full sha256, never 12 hex; build_envelope/record_receipt (fail-closed on id reuse over differing content: an id that ever named two receipts names NEITHER after); lookup() returns (envelope, reason) with reason naming never_recorded/evicted/collision; PROOF_KINDS holds the jdoc/jdata halves too so parity attaches to ONE enum; coverage_fingerprint() is the OPAQUE Phase-5 (#385) extension point; envelope_json() is deterministic so repeated resource reads are byte-identical; _absence_links maps a Phase-3 `absent:` token to its receipt. Session-scoped, in memory, bounded at 500 + an evicted set
    producers.py         # (v1.108.183) #377 Phase 2 P2 — THE GATE. PRODUCERS registry (4 entries: get_symbol_source symbol_definition only / search_symbols + get_ranked_context symbol_definition+symbol_lookup_absence / search_text literal_text_absence only), each declaring verdict shape, proof kinds, canonical projector arg sets (scope_args NARROW, mode_args change WHICH operation ran), and completeness/freshness/coverage/integrity semantics. mint() is called from the call_tool chokepoint, so it is immune to early returns BY CONSTRUCTION; `_verdict_shape` is the gate — an exit that asserts an answer without the registered build_verdict shape cannot mint (the v1.108.179 class made structural). `_snapshot(trust_channel=)` binds subject_state.capture + repo_freshness + index_coverage_meta + verdict.working_tree; trust_channel=False for the symbol-verdict shape because ITS channels.index says `fresh` for a revisionless folder. `_row_subject` reads the SERVED row only and names what was not served in `limitations`
    scip.py              # (v1.108.96) Hand-rolled SCIP protobuf wire-format reader (no protobuf dep): _read_varint/_iter_fields walk varint + length-delimited fields, unknown fields skipped by construction. Parses Index/Metadata/Document/Occurrence/SymbolInformation/Relationship subset; packed AND unpacked int32 ranges, 3-/4-int range forms, .gz by magic sniff; ValueError (honest) on non-SCIP input. display_name_from_symbol = best-effort last-descriptor name (resolution FALLBACK only; primary channel is (file,line))
    scip_ingest.py       # (v1.108.96) ingest_scip_file: parse → resolve (definition map scip-symbol→(file,line) from Definition-role occurrences; enclosing symbol via runtime/resolve.resolve_to_symbol_id) → persist scip_edges (kinds: reference, implementation) / scip_unmapped (reasoned) / scip_meta (tool, ingested_at, git_head staleness anchor). Skips counted: Import-role occurrences (import graph covers) + `local N` symbols. _ensure_scip_tables covers pre-v17 DBs; FIFO eviction per JCODEMUNCH_SCIP_MAX_ROWS
  tools/
    get_runtime_coverage.py  # Phase 3: coverage histogram for repo or single file. {total_symbols, confirmed, declared_only, coverage_pct, sources, last_seen, unmapped_runtime[]}.
    find_hot_paths.py        # Phase 3: top-N symbols by runtime hit count, with p50/p95, sources, last_seen. Optional name substring filter. Pairs with get_blast_radius.
    find_unused_paths.py     # Phase 3 + 4: symbols with zero/stale runtime hits over the window. Excludes test files and entry-point filenames by default. Refuses when runtime_calls is empty (would trivially flag everything). Phase 4 dbt-aware extension: when context_metadata has *_columns + runtime_columns has rows, rescues SQL-file model symbols that have observed column reads (column-only audit-log shape) and surfaces dbt models whose declared columns have zero hits with reason='dbt_model_no_column_reads' + unused_columns list.
    get_redaction_log.py     # Phase 6: forensic accounting of PII redactions — surfaces per-pattern counts from runtime_redaction_log so operators can verify the redaction chokepoint is firing on production traffic. Filters by source + since_days. Read-only / immutable connection.
  retrieval/
    confidence.py        # compute_confidence/attach_confidence: 0-1 retrieval confidence score (geometric mean of gap, strength, identity, freshness sub-signals); attached to _meta.confidence on search_symbols / plan_turn / get_ranked_context
    freshness.py         # FreshnessProbe: v1.108.180 adds repo_freshness (fresh/stale/unknown/not_tracked, #377 item 4 — the boolean repo_is_stale rendered 'could not find out' as fresh) + _is_git_backed (walks up, so a monorepo subdir is not mislabeled not_tracked). per-result _freshness classification (fresh / edited_uncommitted / stale_index); compares index SHA vs git HEAD + per-file mtime vs CodeIndex.file_mtimes; wired into search_symbols / get_symbol_source / get_context_bundle / get_ranked_context
    tuning.py            # WeightTuner + get_semantic_weight: learns per-repo semantic_weight from v1.78.0 ranking_events ledger; ±0.05 step (clamp 0.1-0.8) when mean confidence between semantic_used groups differs by ≥0.05; persists to ~/.code-index/tuning.jsonc; applied at query time when caller leaves semantic_weight at the default (identity_boost learning removed v1.108.102 — audit W6, was never consumed at query time)
    embed_drift.py       # CANARY_STRINGS (16) + capture_canary/check_drift: pins canary embeddings to ~/.code-index/embed_canary.json, re-checks cosine drift via check_embedding_drift MCP tool; catches silent provider model changes (Gemini/OpenAI/bundled-ONNX); default threshold 0.05 cosine distance

CLI Subcommands

Subcommand Purpose
serve (default) Run the MCP server (stdio, sse, or streamable-http)
init Interactive one-command onboarding: detect MCP clients, write config, install CLAUDE.md policy, hooks, index
install <agent> (v1.105.1) Per-agent shortcut over init; targets: claude-code, claude-desktop, cursor, windsurf, continue, all. install --list enumerates; install --status reports state (JSON via --json). v1.107.0: --skills also emits the Claude Agent Skill bundle (~/.claude/skills/jcodemunch/SKILL.md by default; --skills-scope project for project-local)
install-status (v1.105.1) Read-only report of which clients / policies / hooks currently have jcodemunch wired; --json for scripting. v1.107.0: also reports skills.global.present and skills.project.present
uninstall [target] (v1.105.1) Reverse init / install. Preserves user-authored hook rules and content outside our policy region; removes files only when empty after stripping. --keep-claude-md, --keep-hooks, etc. scope what's reversed
watch <paths> File watcher — auto-reindex on change
watch-claude Auto-discover and watch Claude Code worktrees
watch-all Auto-discover every locally-indexed repo and keep it fresh; rediscovers on interval
watch-install Install watch-all as a login service (systemd / launchd / Task Scheduler)
watch-uninstall Remove the installed watch-all login service
watch-status Print service state + per-repo reindex status (also exposed as MCP tool get_watch_status)
hook-event create|remove Record a worktree lifecycle event (called by Claude Code hooks)
index [target] Index a local folder (default: .) or GitHub repo (owner/repo). One command, no init required
index-file <path> Re-index a single file within an existing indexed folder (used by PostToolUse hooks)
import-trace [--otel <path> | --sql-log <path> | --stack-log <path>] [--repo <id>] [--no-redact] (Phases 1 + 4 + 5) Ingest a runtime trace file into the runtime_* tables. --otel takes JSON / JSON-Lines / .gz and maps spans by (code.filepath, code.lineno, code.function); --sql-log takes pg_stat_statements CSV or generic SQL JSON-Lines and maps queries by referenced tables + dbt/SQLMesh column metadata; --stack-log takes plain-text app log or JSON-Lines record set with Python / JVM / Node.js tracebacks and writes severity-tagged frame counts to runtime_stack_events. Redacts PII at the chokepoint by default. Pass exactly one source flag.
import-scip <path.scip> [--repo <id>] (v1.108.96) Ingest a SCIP index file (compiler-verified cross-references from scip-typescript / scip-python / scip-java / scip-go / rust-analyzer; .gz accepted) into the scip_* tables. Hand-rolled protobuf reader, no deps. find_references then tags compiler_verified refs + appends compiler-only refs. Cap via JCODEMUNCH_SCIP_MAX_ROWS.
config Print effective configuration grouped by concern
config set <key> <value> / config unset <key> (v1.108.51) Write/clear a config key in the global config.jsonc (typed, comment-preserving, validated; --json for tooling)
config --check Also validate prerequisites (storage writable, AI pkg installed, HTTP pkgs present)
config --upgrade Add missing keys from current template to existing config.jsonc, preserving user values
download-model Download bundled ONNX embedding model (all-MiniLM-L6-v2) for zero-config semantic search; --target-dir override
install-pack [id] Download and install a Starter Pack pre-built index; --list for catalog, --license KEY for premium
hook-pretooluse PreToolUse hook: intercept Read on large code files, suggest jCodemunch (reads JSON stdin)
hook-posttooluse PostToolUse hook: auto-reindex files after Edit/Write (reads JSON stdin)
hook-precompact PreCompact hook: generate session snapshot before context compaction (reads JSON stdin)
hook-taskcomplete TaskCompleted hook: post-task diagnostics — dead code, untested symbols, dangling refs (reads JSON stdin)
hook-subagent-start SubagentStart hook: inject condensed repo orientation for spawned agents (reads JSON stdin)
whatsnew Refresh README recency block + write whatsnew.json from CHANGELOG.md (release flow)
receipt Token-economy ledger from Claude transcripts — modeled tokens-saved + dollar value at Fable/Opus/Sonnet/Haiku rates; --explain, --export csv|json, --days (rolling), --model. v1.108.134: --since/--until for calendar windows (local dates; --until exclusive) + --by-day for a per-day series in the JSON export. v1.108.135: --rates dumps the model price table as JSON (scans nothing) so consumers price from the one table instead of a drifting copy
digest Agent stand-up briefing — composes since-last-session delta + risk surface + dead-code candidates; tracks per-repo last-seen SHA at ~/.code-index/digest_state/; also exposed as MCP tool digest. v1.108.68 adds a one-line retrieval-regret summary when the ledger has clusters
reflect (v1.108.68) Surface retrieval regret as SUGGESTED config corrections — reflect [repo] [--project-path] [--window-days N] [--all] [--apply-weights] [--json]. Thin CLI over the suggest_corrections tool; read-only (only --apply-weights writes, and only the tuning.jsonc sidecar)
delivery (v1.108.69) Print durable-change delivery metrics for a window — delivery [repo] [--window-days N] [--rework-horizon-days N] [--cost DOLLARS] [--json]. Thin CLI over get_delivery_metrics; --cost prints the headline cost-per-durable-change (how much got done for how little). Read-only git archaeology
parity (v1.108.111) Map migration parity between two symbol trees — parity <source> <target> [--source-path P] [--target-path P] [--match-threshold F] [--divergence signature|signature+body|name_only] [--no-rename] [--no-port-plan] [--json]. Thin CLI over get_parity_map: ported/diverged/unported/orphaned/added counts + dependency-ordered port plan. Read-only/plan-only
health Print get_repo_health JSON to stdout (includes six-axis radar). For CI/scripting; --radar-only for just the radar sub-field. Used by the v1.88.0 health-radar GitHub Action
file-risk Print per-symbol risk JSON for a file (composite score + four-axis breakdown). Used by the v0.2.0 VS Code risk-density gutter
observatory build|init Public OSS code-health observatory pipeline — clones, indexes, scores a configured repo list; writes static HTML + RSS + JSON to an output dir. v1.90.0; CI repo-id bug fixed in v1.90.1. Live at https://jgravelle.github.io/jcodemunch-observatory/
org-report / org-rollup (v1.108.38/39) Team SKU: record this seat's savings under its org / aggregate across seats. org-rollup is the licensed feature (v1.108.42 gate).
license (v1.108.42) Check jCodeMunch license status — license [--key KEY] [--json]; reports licensed / evaluation / unlicensed, tier, trial days left. Gates org-rollup only.
surface (v1.108.154) Print the tool-surface schema receipt (same block get_session_stats reports as tool_surface) — surface/profile, visible vs catalog counts, schema tokens, avoided, heaviest schemas. --json for tooling (the Console's Tool surface cost card shells it). Scans nothing.

Architecture Notes

  • index_folder is synchronous — dispatched via asyncio.to_thread() in server.py to avoid blocking the event loop
  • index_repo is async (uses httpx for GitHub API)
  • has_index() distinguishes "no file on disk" from "file exists but version rejected"
  • Symbol lookup is O(1) via __post_init__ id dict in CodeIndex

Custom Parsers

Tree-sitter grammar lacks clean named fields for these — custom regex extractors:

  • Erlang: multi-clause function merging by (name, arity); arity-qualified names (e.g. add/2)
  • Fortran: module-as-container, qualified names (math_utils::multiply), parameter constants
  • SQL: _parse_sql_symbols + sql_preprocessor.py strips Jinja (dbt); macro/test/snapshot/materialization as symbols
  • Razor/Blazor (.cshtml/.razor): @functions/@code → C#, @page/@inject → constants, HTML ids

Env Vars

Var Default Purpose
CODE_INDEX_PATH ~/.code-index/ Index storage location
JCODEMUNCH_MAX_INDEX_FILES 10,000 File cap for repo indexing
JCODEMUNCH_MAX_FOLDER_FILES 2,000 File cap for folder indexing
JCODEMUNCH_FILE_TREE_MAX_FILES 500 Cap for get_file_tree results
JCODEMUNCH_GITIGNORE_WARN_THRESHOLD 500 Missing-.gitignore warning threshold (0 = disable)
JCODEMUNCH_USE_AI_SUMMARIES auto AI summarization mode: auto (detect provider), true (use explicit config), false/0/no/off (disable)
JCODEMUNCH_SUMMARIZER_PROVIDER Explicit summarizer provider: anthropic, gemini, openai, minimax, glm, openrouter, none
JCODEMUNCH_SUMMARIZER_MODEL Model name override for the selected summarizer provider
JCODEMUNCH_TRUSTED_FOLDERS Roots trusted for index_folder; whitelist mode by default
JCODEMUNCH_EXTRA_IGNORE_PATTERNS Always-on gitignore patterns (comma-sep or JSON array)
JCODEMUNCH_PATH_MAP Cross-platform path remapping; format: orig1=new1,orig2=new2
JCODEMUNCH_STALENESS_DAYS 7 Days before get_repo_outline emits a staleness_warning
JCODEMUNCH_MAX_RESULTS 500 Hard cap on search_columns result count
JCODEMUNCH_HTTP_TOKEN Bearer token for HTTP transport auth (opt-in)
JCODEMUNCH_RATE_LIMIT 0 Max requests/minute per client IP in HTTP transport (0 = disabled)
JCODEMUNCH_REDACT_SOURCE_ROOT 0 Set 1 to replace source_root with display_name in responses
JCODEMUNCH_SHARE_SAVINGS 1 Set 0 to disable anonymous token savings telemetry
JCODEMUNCH_REDACT_RESPONSE_SECRETS 1 Set 0 to disable response-level secret redaction (AWS/GCP/Azure/JWT/etc.)
JCODEMUNCH_STATS_FILE_INTERVAL 3 Calls between session_stats.json writes; 0 = disable
JCODEMUNCH_PERF_TELEMETRY 0 Set 1 to enable persistent perf SQLite sink at ~/.code-index/telemetry.db (per-tool latency + ok flag + repo). In-memory ring is always tracked; the env var only controls durable persistence.
JCODEMUNCH_PERF_TELEMETRY_MAX_ROWS 100000 Rolling cap on persisted perf rows; oldest rows trimmed in 1k-row batches once exceeded.
JCODEMUNCH_RUNTIME_MAX_ROWS 100000 (Phase 0) Per-repo cap on rows in runtime_* tables (ingested in Phase 1+); FIFO eviction in 1k batches once exceeded.
JCODEMUNCH_RUNTIME_REDACT 1 (Phase 0) Set 0 to disable PII redaction at the runtime trace ingest chokepoint. Off ONLY for offline debugging on synthetic data — never on production traces.
JCODEMUNCH_RUNTIME_INGEST_ENABLED 0 (Phase 6) Set 1 to enable the HTTP live-ingest endpoints (POST /runtime/otel, /runtime/sql, /runtime/stack). Requires JCODEMUNCH_HTTP_TOKEN. Off by default — write endpoints are a deliberate two-key turn.
JCODEMUNCH_RUNTIME_INGEST_MAX_BODY_BYTES 5242880 (Phase 6) Per-request body cap in bytes (post-decompression). Decompressed size is checked separately from on-wire size — gzip-bomb guard. Minimum 1024.
JCODEMUNCH_CLIENT_ID basename(sys.argv[0]) (v1.106.0) Friendly client name recorded in process_locks metadata. Auto-detected for common runtimes (claude, cursor, codex). Override for custom or wrapper runtimes so get_watch_status.watcher_holder.client_id surfaces a meaningful name to other processes.
ANTHROPIC_API_KEY Enables Claude Haiku summaries (pip install jcodemunch-mcp[anthropic])
GOOGLE_API_KEY Enables Gemini Flash summaries (pip install jcodemunch-mcp[gemini])
OPENAI_API_BASE Local LLM endpoint (Ollama, LM Studio)
OPENAI_WIRE_API Set responses to use OpenAI Responses API instead of chat/completions
JCODEMUNCH_OPENAI_EXTRA_BODY JSON object merged into every OpenAI-compatible /chat/completions + /responses summarizer request (config key openai_extra_body, project-overridable). Disable a thinking model's reasoning so the output budget isn't burned on reasoning tokens, e.g. {"chat_template_kwargs":{"enable_thinking":false}} (#323)
OPENROUTER_API_KEY Enables OpenRouter summaries (default model: meta-llama/llama-3.3-70b-instruct:free)
JCODEMUNCH_LOCAL_EMBED_MODEL Override path to bundled ONNX model directory (default: ~/.code-index/models/all-MiniLM-L6-v2/)
GEMINI_EMBED_TASK_AWARE 1 Set 0/false/no/off to disable task-type hints (RETRIEVAL_DOCUMENT / CODE_RETRIEVAL_QUERY) when using Gemini embeddings
JCODEMUNCH_CROSS_REPO_DEFAULT 0 Set 1 to enable cross-repo traversal by default in find_importers, get_blast_radius, get_dependency_graph
JCODEMUNCH_EVENT_LOG Set 1 to write _pulse.json on every tool call (per-call activity signal for dashboards)
JCODEMUNCH_WATCH_POLL_DELAY_MS 1000 (v1.108.83) Poll interval (ms) used ONLY when watchfiles falls back to polling — which it auto-enables under WSL (#356). Default raised from watchfiles' 300ms to cut idle CPU; ignored when native FS events are in use. Falls back to WATCHFILES_POLL_DELAY_MS if set; non-positive/garbage → default. For Linux-filesystem repos under WSL, WATCHFILES_FORCE_POLLING=false opts back into inotify (~0 idle CPU).
JCODEMUNCH_LIVE_JOURNAL 1 (v1.108.57) Set 0/false/no/off to disable the live session-journal write (<CODE_INDEX_PATH>/_session_live.json). On by default so the out-of-process PreCompact hook can read real session state (#334); throttled ≤1/~2s, paths+queries only, no file contents.
JCODEMUNCH_TOOL_SURFACE full (v1.108.66) Tool surface selector (config key tool_surface; env wins). counter collapses list_tools to the 3-tool front door (order/menu/route) + always-present controls. Any other value (default full) preserves existing tiered behavior byte-for-byte — front-door tools stay hidden but callable. Composes with the core/standard/full tier profiles.
JCODEMUNCH_PARSE_CACHE Shared directory for the content-addressed parse cache (v1.108.40). Point all seats on a multi-home-dir box at the same path so identical files parse once across seats. Unset = disabled (no caching).
JCODEMUNCH_PARSE_CACHE_MAX_ROWS 50000 (v1.108.41) Row cap for the shared parse cache; FIFO-trimmed oldest-first by rowid after each write (stale-content/stale-version rows go first). <= 0 disables the cap (unbounded).
JCODEMUNCH_ORG_ID Org identifier for the team-SKU rollup (org-report / org-rollup)
JCODEMUNCH_ORG_ENDPOINT Org host URL that org-report POSTs seat savings to (/org/report); unset = record locally
JCODEMUNCH_ORG_INGEST_ENABLED 0 Set 1 on the org host to accept POST /org/report (two-key turn with JCODEMUNCH_HTTP_TOKEN)
JCODEMUNCH_LICENSE_KEY (v1.108.42) jCodeMunch license key (config key license_key). Gates the org-rollup team feature ONLY; everything else is free. Validated online vs validate.php (sticky-offline cache; 14-day grace for new orgs). Requires a multi-seat tier — Studio or Platform (v1.108.43); Builder doesn't unlock org-rollup. Check with the license CLI.
JCODEMUNCH_INDEX_CACHE_TTL 0 (off) (v1.108.172) Seconds an unused hydrated index may sit in the in-memory cache before being released. OPT-IN: 0/unset/garbage = disabled = today's behavior exactly.Do NOT default this on — cold hydration of a 665k-symbol index was measured at 7.5-11.4 min (#370), so evicting during a quiet spell hands the next query that bill. For hosts whose MCP client leaks stdio servers (#375: 25+ instances, ~17 GB), where each idle process otherwise sits on its own cache. Swept on access, no timer thread.
JCODEMUNCH_PROVIDER_BUDGET_SECONDS 30.0 (v1.108.182) Wall-clock ceiling on ONE context provider's detect()+load(). Discovery runs before a single file is indexed, so an unbounded provider takes the whole index down with it (#375). On overrun the provider is skipped and NAMED in providers_skipped + warnings. 0/negative = no ceiling (pre-.182 inline behaviour). ⚠ A watchdog stops the CALLER waiting; it cannot stop the work — Python cannot preempt a thread, so the abandoned provider keeps burning CPU until it finishes or polls budget_expired(). Only the Express walk polls it so far.
JCODEMUNCH_PARSE_BUDGET_SECONDS 20.0 (v1.108.182) Per-file wall-clock ceiling on parse_file, via parse_file_budgeted. On overrun the file is skipped and named in the index result's warnings instead of the run hanging. ⚠ Armed only at or above 128 KiB (_PARSE_WATCHDOG_MIN_BYTES) so the common path stays inline — a 2 KB file that takes 20s is a bug to see, not to paper over. 0/negative disables. Same no-preemption caveat: tree-sitter is C code.
JCODEMUNCH_HEARTBEAT_SECONDS 30.0 (v1.108.189, #383) Elapsed wall-clock seconds between heartbeat log lines when the client sent no progressToken — the MCP spec makes progress notifications the client's opt-in, so the fallback signal goes to the log instead. Emitted at WARNING (the default log_level, or nobody sees it) and only after the first interval elapses, so a run finishing inside the window is byte-for-byte as silent as before. ⚠ Garbage parses to the DEFAULT, not to 0 — a typo must not reintroduce the silence this exists to fix. 0/negative disables.
JCODEMUNCH_SCIP_MAX_ROWS 200000 (v1.108.96) Row cap for scip_edges / scip_unmapped (compile-time evidence from import-scip); FIFO-evicted oldest-first in 1k batches. Negative disables the cap; env-only, deliberately not a config key.
JCODEMUNCH_LAUNCH_ID (v1.108.152) Opaque host-supplied launch token echoed back as launch_id in the munch://runtime/identity resource (#371). Fallback: suite-generic MUNCH_LAUNCH_ID. Omitted from the payload when unset. Env-only, not a config key.

PR / Issue History

See git log and CHANGELOG.md. Active contributors: MariusAdrian88, DrHayt, tmeckel, drax1222, oderwat, thomasmodeneis, gokhanozdemir, horknfbr.

Merged 2026-07-25: #379 (@oderwat) Gleam import extraction — Gleam was already in LANGUAGE_REGISTRY, so symbols extracted but the import graph stayed EMPTY, leaving find_importers/get_blast_radius/get_dependency_graph silently blind on Gleam projects. Same shape as the week's verdict work: capability present, wiring absent. Verified against a TRIAL MERGE onto current main (branch-green is not merged-green), 210 neighbouring import/language tests green. Landed AFTER the 1.108.170 release commit, so it rides the NEXT release, not that one.

Merged 2026-07-25: #378 (@zuoYu-zzz) TOML symbol extraction — tables → type, array tables → class, key-value pairs → constant. Merged rather than review-round-tripped, then fixed on top in f0eda7b. ⚠ The defect worth remembering: _extract_key scanned a dotted_key's DIRECT children for bare_key/quoted_key, but tree-sitter-toml nests dotted_key LEFT-RECURSIVELY ([tool.ruff.lint] = dotted_key(dotted_key(tool, ruff), lint)), so every segment but the last was dropped. Two-level paths worked, which is exactly why it read as correct — the bug only shows at three-plus, and on jcm's OWN pyproject.toml [tool.hatch.build.targets.wheel] came back as wheel with signature [wheel], a header that appears nowhere in the file (search_symbols would have handed an agent fabricated source text). Fix returns path SEGMENTS and recurses; building from segments also fixed name/ qualified_name, which the PR set to the same value (now leaf / full dotted path, matching every other extractor). New test asserts three- AND five-deep tables plus a signature-occurs-in-source check, proven non-vacuous. The PR's own test used only single-segment headers, so nothing in the suite could have caught it — the general lesson for any new nested-grammar walker. Rides the next release with #379.

Closed 2026-07-25: #380 Atlas Cloud summarizer (@binyangzhu000-sudo). Closed on DEMAND, not quality: CLA unsigned (hard blocker), and the capability is fully reachable today via OPENAI_API_BASE + SUMMARIZER_PROVIDER=openai since Atlas Cloud is OpenAI-compatible. Cost of merging was 8 permanent env-var spellings (ATLASCLOUD_/ATLAS_CLOUD_ × API_KEY/API_BASE/BASE_URL/MODEL) plus 3 aliases, permanent under the 1.x no-removal contract. ⚠ Do NOT re-close a future one of these "we don't take branded providers" — MiniMax/GLM/OpenRouter are exactly this shape and already merged; the comment concedes that on the record. The bar is a user asking, same as platform installers. It correctly added atlascloud to _PAID_CLOUD_PROVIDERS, so the money-safety guard was respected. Open issues (2026-07-27): #375, #377 ONLY. Open PRs: #387 (@nyxst4ck, draft, docs) and #388 (@Bortlesboat) — BOTH BLOCKED ON AN UNSIGNED CLA (license/cla pending), so neither can merge on merit alone.

⚠⚠ PROCESS FAILURE WORTH NOT REPEATING: #388 fixed #384 and was opened 2026-07-27 06:51 UTC. We shipped our own .189 fix and closed #384 at 12:56 UTC having NEVER LOOKED AT OPEN PRs — the cross-reference sat on #384's timeline the whole time. CHECK gh pr list BEFORE WRITING CODE ON AN ISSUE. Their fix then went CONFLICTING/DIRTY because .189 rewrote the same functions. Resolved by PORTING the gap they covered and we missed (maybe_takeover) in v1.108.190 with credit in the CHANGELOG, release notes and close comment, rather than asking a pre-empted first-time contributor to both rebase onto our version of their fix AND sign a CLA. #388 closed 2026-07-27. Cleaned up in v1.108.189 on a standing rule jjg set: an issue opens when work STARTS or when a USER is BLOCKED — an issue is a problem to fix or a feature to build, not a to-do list. #383 and #384 are FIXED (see Current State); #385/#386 (evidence Phases 5 and 6) were CLOSED and moved to ROADMAP.md — accepted design with no start date and an unmet dependency is a plan, not an issue. ⚠ Closing them is NOT a rejection of @mightydanp's design and the close comments say so explicitly; credit and close conditions moved verbatim.The convention that GENERATED the clutter was our own — "new scope gets its own close condition", cited in #385's body. It is right for scope being WORKED and wrong for scope PARKED. Remaining: #375 (needs a re-run from @dkiaulakis at >=1.108.182, not code) and #377 (down to two concrete Phase 2 P3 edges @mightydanp pinned 2026-07-27: an absence receipt still links a MUTABLE absent:<sha> key note_absence can overwrite across snapshots, and validation vs rendering do two SEPARATE receipt lookups instead of one atomic snapshot).

#375 (index_folder silent 1800s+ on Linux) — REOPENED 2026-07-26, and the blocker is a RE-RUN, not code. Closed 2026-07-26 on our own measurement after five releases; @dkiaulakis re-ran at 1.108.176 and the SAME tools/eidos subtree took 268s SIGTERM'd vs a 240s baseline at .169 — no improvement, which is exactly the condition the close comment said would reopen it. ⚠ No py-spy this round: ptrace is restricted in his sandbox and his agent correctly declined to grant itself CAP_SYS_PTRACE mid-task.The 5400s full-repo number is a CLIENT-side MCP timeout and does NOT prove the server job stopped — he flagged that himself; he runs 10+ concurrent stdio servers and had no safe way to identify his own process. What .176 DID deliver, in his words: "we can now see the problem we could not previously see" — index_coverage read ABSENT before and now reports a number plus index_stale: true (git_head_lag). The freshness half stands; the stall is a separate axis. v1.108.182 shipped three bounds in response (provider-discovery budget, walk pruning at iter_source_files, per-file parse_file_budgeted), two of them his own twice-proposed suggestions. ⚠ STATED LIMIT, do not overclaim it: a watchdog stops the CALLER waiting, it cannot stop the WORK — Python cannot preempt a thread and tree-sitter is C, so an abandoned parse keeps burning CPU. It makes the index finish and the gap visible; it does not cap CPU. Sub-problems: A -> #383, FIXED in .189. B closed not-a-defect (default log_level is WARNING, so a healthy run emits nothing). C fixed in .176 (a partial index no longer reports itself fresh; complete is TRI-state and pre-.176 indexes report null, NEVER true — re-index or the signal is not there to see). D near-ruled-out (every indexwrite acquire passes wait_seconds=60.0 and RAISES naming the holder, so it cannot present as unbounded silence). The double-index finding -> #384, FIXED in .189.Next action is a PING, not a patch.

Closed 2026-07-26: #382 "Old tree sitter dependency?" (@kecsap) — asked why we pin tree-sitter-language-pack>=0.7.0,<1.0.0 when "other code parser MCP tools happily use >= 1.0.0". Tested 1.13.3 against the full suite before answering; the pin STAYS, and the rationale now lives as a comment on the dep itself so this is not re-derived. ⚠ The load-bearing reason: 1.x STOPPED BUNDLING GRAMMARS. The wheel ships a single _native.pyd and an empty bindings dir; get_parser downloads the grammar from a remote manifest into %LOCALAPPDATA%\tree-sitter- language-pack\v<ver>\libs on first use (proven by watching that cache go 0 -> 67 shared libs while walking our language list). That is runtime network access plus executable-writes-to-disk in a tool that advertises itself as read-only and local, and it breaks airgapped installs outright — i.e. exactly the class of undisclosed persistent/network behavior that caused the PyPI quarantine, so it could never ride a dependency-housekeeping commit anyway. Two smaller blockers: autohotkey, ejs, verse do not exist in 1.x (DownloadError: not available for download), so bumping silently drops three languages; and the nim grammar was swapped for a different upstream (source_file/proc_declaration/identifier -> module/stmt/routine/symbol/ident), so our nim extractor returns zero symbols. Suite on 1.13.3: 5812 passed, 12 skipped, 1 failed (test_nim_parsing, and only that). ⚠ There is NO API incompatibility to cite — we use exactly one symbol from this package, get_parser — so do not argue the pin on API grounds; the blockers are all behavioral. ⚠ Unrelated pre-existing pathology found while testing, NOT a 1.x regression and NOT filed: get_parser("cobol").parse(b"x") hangs indefinitely on BOTH 0.13.0 and 1.13.3. A 1-byte input, no timeout. Unreachable today (we only feed it real .cbl files) but it invalidated the first version of the compatibility harness, so any future per-language sweep must resolve parsers WITHOUT parsing pathological input.

#381 (MCP Toplist badge) CLOSED by jjg — 120 identical drive-by PRs from that author; the badge renders "Top 1% of 81,432", not the rank the PR body promised, and it is live third-party-controlled content in a README that also renders on PyPI.

Maintenance Practices

  1. Document every tool before shipping. Any PR adding a new tool to server.py must simultaneously update: README.md (tool reference), CLAUDE.md (Key Files), CHANGELOG.md, and at least one test.
  2. Log every silent exception. Every except Exception: block must emit at minimum logger.debug("...", exc_info=True). For user-facing fallbacks (AI summarizer, index load), use logger.warning(...).
  3. CHANGELOG.md is the authoritative version history — update it with every release.
  4. Keep Current State to the 3 newest releases. It is a pointer, not a second changelog. On each release, add the new entry and drop the 4th-oldest — the detail already lives in CHANGELOG.md. (2026-07-25: this section had grown to 157 entries / ~233k chars, loading ~58k est. tokens into every session under this directory.)