tensor-grep config and flags
A ground-truthed catalog of every tg config axis — env vars, CLI flags, provider modes — plus the
registration checklist for adding a new one. Verified against source as of 2026-07-23, v1.95.0
(pyproject.toml). Re-verify commands are in Provenance and maintenance
because these drift with every release.
When to use this skill
- You are adding, renaming, or removing an env var or CLI flag.
- A search flag is reaching ripgrep raw (
rg: unrecognized flag at runtime) or a command 404s.
- You need to know whether a knob (
--gpu-device-ids, --provider lsp, TENSOR_GREP_CLASSIFY_PROVIDER=cybert)
is production-safe to recommend to a user, or still experimental/default-off.
- You need the authoritative default value or guard condition for a
TG_* / TENSOR_GREP_* variable
before writing docs, a benchmark harness, or an agent prompt that references it.
When NOT to use this skill (use the sibling instead)
| If you need... |
Use instead |
| The why behind the front-door/routing architecture, not just the flag list |
tensor-grep-architecture-contract |
| The process gates for shipping a flag/command change (PR, CI, one-merge-per-tick) |
tensor-grep-change-control |
| Reproducing/debugging a routing bug once you already know which flag is involved |
tensor-grep-debugging-playbook |
tg doctor output fields, dogfood harness, benchmark scripts |
tensor-grep-diagnostics-and-tooling / tensor-grep-benchmark-and-proof-toolkit |
How to actually use tg commands day to day (not configure them) |
.claude/skills/tensor-grep/SKILL.md |
| Build/toolchain setup (uv, maturin, cargo) |
tensor-grep-build-and-env |
| Release mechanics / positioning claims |
tensor-grep-release-and-positioning |
The two front doors, and why config is split across them
tg has two CLI entry points that both parse flags, and a config change that only lands in one
is a silent bug, not a crash:
- Python bootstrap (
src/tensor_grep/cli/bootstrap.py) — tensor_grep.cli.bootstrap:main_entry
intercepts plain-text searches before the Typer app loads and forwards them straight to rg.
- Rust native front door (
rust_core/src/main.rs) — the standalone tg binary, which re-implements
flag parsing with clap.
The canonical, always-current documentation of every env var and the full search flag surface lives
in two places that are meant to stay in sync — read these first when you need ground truth fast:
tg --help epilog: src/tensor_grep/cli/main.py:187-200 (the app = typer.Typer(help="""...""") block).
- Native
tg --help epilog: ENVIRONMENT_OVERRIDES_HELP const, rust_core/src/main.rs:67.
If those two drift from each other or from this file, trust the source, not this document — see
Provenance and maintenance.
Environment variable catalog
Boolean env vars in tg follow one convention almost everywhere (env_flag_enabled,
grep -n "def env_flag_enabled" src/tensor_grep/cli/runtime_paths.py — was :13-15, now :20-22):
the raw value is lower-cased and stripped, and it is "on" only if it is exactly 1, true, yes,
or on — anything else (including unset) is "off". Two STRICT exceptions compare the raw value
against the literal "1" only, so true/yes/on do NOT enable them: TG_DOCTOR_OFFLINE is
== "1" on the Python side (grep -n "TG_DOCTOR_OFFLINE" src/tensor_grep/cli/main.py — :492),
and TG_RESIDENT_AST is != "1" on the Rust side (grep -n "TG_RESIDENT_AST" rust_core/src/main.rs — :7581).
Routing / launcher
| Var |
Default |
Effect |
Source |
TG_SIDECAR_PYTHON |
sys.executable |
Python executable used for sidecar-backed commands (classify, GPU sidecar). |
main.py:188, main.py:533 |
TG_NATIVE_TG_BINARY (alias TG_MCP_TG_BINARY) |
auto-resolved |
Path to the native tg binary front door used by Python-backed commands. Priority 1 override; stale in-tree dev builds (rust_core/target/{debug,release}/tg.exe) are otherwise skipped unless pinned here. |
main.py:189, runtime_paths.py:238-248 |
TENSOR_GREP_NATIVE_FRONTDOOR_FLAVOR (alias TG_NATIVE_FRONTDOOR_REQUESTED_FLAVOR) |
cpu |
nvidia/cuda prefers the NVIDIA release-native front-door asset (tg-*-nvidia.exe), with CPU fallback; anything else normalizes to cpu. |
main.py (grep -n "def _normalize_native_frontdoor_flavor" src/tensor_grep/cli/main.py -- :550 as of 2026-08-14; the prior :7276/:454-473 cites were stale) |
TG_RG_PATH |
auto-resolved |
Path to the rg executable used for text-search passthrough. |
main.py:191, runtime_paths.py:281 |
TG_FORCE_CPU |
off |
Force CPU routing for search commands (boolean convention). |
main.py:192, main.py:2772 |
TG_RUST_FIRST_SEARCH |
off |
Opt-in: prefer the Rust native front door before Python bootstrap logic for search dispatch. |
bootstrap.py:242 |
TG_RUST_EARLY_RG, TG_RUST_EARLY_POSITIONAL_RG |
off |
Internal early-dispatch toggles surfaced by tg doctor --json; not documented in the public --help epilogs. |
main.rs:53-54, main.py:2775-2762 |
TG_RESIDENT_AST |
off |
Enables the resident AST worker path (see docs/runbooks/resident-worker.md); reported by tg doctor --json. |
main.py:2773, main.rs (search TG_RESIDENT_AST) |
TG_DISABLE_NATIVE_TG |
off |
Kill-switch: forces resolve_native_tg_binary() to return None, fully bypassing the native tg binary front door (Python-backed commands fall back to pure-Python routing even if a compatible native binary is resolvable). |
runtime_paths.py:234 |
TG_DISABLE_RG |
off |
Kill-switch: forces the native binary's ripgrep resolver to return None, so the native front door treats rg as unavailable regardless of TG_RG_PATH/PATH. |
rust_core/src/rg_passthrough.rs:13,477 |
TG_DOCTOR_OFFLINE |
off |
Disables tg doctor's PyPI latest-version probe: pypi_latest becomes None and installation_health reports unknown_pypi instead of a network result. Test/offline escape hatch for the v1.110.14 doctor schema-3 freshness fields; deliberately disclosed (never a silent clean). |
main.py:489-492 |
Timeouts
| Var |
Default |
Effect |
Source |
TG_RG_TIMEOUT_SECONDS |
60.0s (lowered from 600s in #288) |
Ripgrep-passthrough search timeout. Fails fast with a stderr hint to scope the search or raise the timeout, instead of hanging. Overridden by TG_SIDECAR_TIMEOUT_MS when that is set to a positive value. |
grep -n "TG_RG_TIMEOUT_SECONDS" src/tensor_grep/cli/subprocess_policy.py (was :32-44, now :75) |
TG_SIDECAR_TIMEOUT_MS |
unset |
Milliseconds; if set and > 0, takes precedence over TG_RG_TIMEOUT_SECONDS for the ripgrep-passthrough timeout (ms / 1000.0). Also documented as the general sidecar-command timeout. |
subprocess_policy.py:32-40, main.py:193 |
TG_SUBPROCESS_TIMEOUT_SECONDS |
600.0s |
Default timeout for the generic run_subprocess() helper (git ops, MCP validation commands, etc.) unless a call site overrides timeout_env_var. |
subprocess_policy.py:82-87 |
TG_GIT_TIMEOUT_SECONDS |
120.0s |
Timeout for git subprocess calls (checkpoint/session git operations). |
subprocess_policy.py:28-29 |
TENSOR_GREP_TRITON_TIMEOUT_SECONDS |
5.0s |
Timeout for Triton-backed NLP (CyBERT) probes. |
cybert_backend.py:18-19, main.py:196 |
TENSOR_GREP_LSP_OPERATION_BUDGET_SECONDS |
2.0s |
Total per-command budget for optional external LSP provider requests before falling back to native evidence. |
grep -n "TENSOR_GREP_LSP_OPERATION_BUDGET_SECONDS" src/tensor_grep/cli/repo_map.py (was :95-96, now :193-194), main.py:198 |
TENSOR_GREP_LSP_REQUEST_TIMEOUT_SECONDS, TENSOR_GREP_LSP_INITIALIZE_TIMEOUT_SECONDS |
implementation defaults |
Per-request / per-initialize LSP timeouts; reported (not overridden) by tg doctor --json. |
main.py:2777-2764 |
Every non-positive or unparseable value for the float-typed timeout vars above silently falls back to
the compiled-in default (_configured_positive_float, subprocess_policy.py:9-17) — a bad value does
not crash, it just gets ignored. Do not assume "I set it, therefore it changed" without checking
tg doctor --json's env block (see Discovering effective config).
GPU
| Var / flag |
Default |
Effect |
Source |
--gpu-device-ids IDS (CLI flag, e.g. tg search --gpu-device-ids 0,1) |
unset (no GPU routing) |
Explicit, user-intent GPU pin for search / tg agent / benchmark evidence probes. Comma-separated non-negative ints; parse errors raise typer.BadParameter immediately (main.py:4061-4074). |
main.py:5725-5762, main.py:6890-6969 |
TENSOR_GREP_DEVICE_IDS |
unset (all detected devices visible) |
Lower-level env allow-list of GPU IDs available to tensor-grep at all (like CUDA_VISIBLE_DEVICES), consulted by device detection/memory-manager code, not just the CLI flag. |
device_detect.py:30-55, main.py:194 |
--gpu-timeout-s (flag, tg agent only) |
5.0s |
Max seconds for each opt-in agent GPU evidence subcommand. |
grep -n "gpu_timeout_s: float = typer.Option" src/tensor_grep/cli/main.py (was :6970-6975, now :9987-9991) |
Fail-loud contract: an explicit --gpu-device-ids request that cannot be honored raises
ConfigurationError (a RuntimeError subclass, pipeline.py:20-21) — it never silently falls back to
CPU. Example message shape (pipeline.py:26-39):
GPU acceleration is experimental. Explicit GPU device selection [0, 1] could not initialize a
GPU backend: fixed-string (-F) search has no GPU backend
This is deliberate: -F/--fixed-strings GPU search has no kernel yet, so pairing it with an explicit
--gpu-device-ids must error, not silently drop to CPU and report a clean result (see the Backend
Fail-Closed Contract in AGENTS.md:214-224, and tensor-grep-architecture-contract for the general
principle). By contrast, the heuristic (non-explicit) auto-GPU path degrades to CPU with a visible
warnings.warn(...) + fallback_reason, not an exception — only explicit user intent gets fail-loud
treatment (pipeline.py:174-176, _should_honor_explicit_gpu_ids).
GPU remains EXPERIMENTAL end-to-end. GPU Phase-0 SHIPPED (v1.75.0-v1.75.4, PRs #593-#597):
NVIDIA native assets are built and locally correctness-proven (RTX 4070 sm_89 / RTX 5070 sm_120 --
docs/gpu_crossover.md), but gated OFF the public release by the CI Actions var
TENSOR_GREP_RELEASE_NATIVE_ASSET_PROFILE (default native-frontdoor, CPU-only; GPU asset
publishing needs the non-default native-frontdoor-gpu) -- Phase 1 is now a reversible flag-flip, not
a multi-week rebuild. That flip publishes assets only: no speed crossover is proven vs rg/tg_cpu,
GPU auto-recommendation stays false, and the reviewer-gated public-gpu-proof.yml speed-crossover
gate remains unmet (docs/CONTRACTS.md:80-82). Do not market or default-enable it.
Classify provider
| Var |
Default |
Effect |
Source |
TENSOR_GREP_CLASSIFY_PROVIDER |
heuristic (local, deterministic) |
Set to cybert or triton to opt into the CyBERT/Triton NLP classifier for tg classify FILE. Any other value (including unset) uses the local regex-heuristic classifier. |
sidecar.py:16-17, sidecar.py:119-125 |
tg classify always reports provenance in its JSON output (classification_backend /
provider_requested / provider_used / provider_status / fallback_reason / cache,
sidecar.py:57-71) so a caller can distinguish "asked for cybert, got heuristic because it failed"
from "asked for heuristic". Never read a classify result as model-backed without checking
provider_used.
Session / daemon
| Var |
Default |
Effect |
Source |
TG_SESSION_MAX |
64 |
Max on-disk cached sessions retained per root; oldest are pruned past this. |
grep -n "_SESSION_MAX_ENV|_configured_session_max" src/tensor_grep/cli/session_store.py (was :49-51, :120-121, now :88-89 (env+default), :159 (usage)) |
TG_SESSION_NEARBY_LOOKUP |
off |
By default session discovery is confined to the explicit root; set to opt into parent/sibling-directory session discovery. |
grep -n "_SESSION_NEARBY_LOOKUP_ENV" src/tensor_grep/cli/session_store.py (was :52-54, :124-130, now :90-92) |
TG_SESSION_DAEMON_IDLE_SECONDS |
900.0s |
Idle stretch (no requests) after which the warm tg session daemon self-shuts-down. Non-positive disables the idle limit. |
grep -n "_DAEMON_IDLE_SHUTDOWN_SECONDS_ENV|_DEFAULT_DAEMON_IDLE_SHUTDOWN_SECONDS" src/tensor_grep/cli/session_daemon.py (was :67-72, now env-name :93, default :95) |
TG_SESSION_DAEMON_MAX_UPTIME_SECONDS |
86400.0s (24h) |
Hard max daemon lifetime regardless of activity. Non-positive disables the uptime limit. |
grep -n "_DAEMON_MAX_UPTIME_SECONDS_ENV|_DEFAULT_DAEMON_MAX_UPTIME_SECONDS" src/tensor_grep/cli/session_daemon.py (was :67-73, now env-name :94, default :96) |
TG_SESSION_DAEMON_RESPONSE_TIMEOUT_SECONDS |
60.0s |
Client-side socket read timeout for a daemon response (#390, moat P0-6 step 5). Env-configurable so a large repo whose warm-daemon graph query legitimately needs >60s isn't killed by a hard cap that returns a bare "timed out"/exit 1/zero JSON. Does not by itself bound the daemon's own traversal — the served graph commands run on a cached map and are not covered by the scan-side --deadline; see the #390 daemon-path gap in tensor-grep-large-repo-scale-campaign. |
grep -n "_DAEMON_RESPONSE_TIMEOUT_ENV|_DAEMON_RESPONSE_TIMEOUT_SECONDS =" src/tensor_grep/cli/session_daemon.py (was :52,:58, now env-name :68, default :58) |
TENSOR_GREP_SESSION_RESPONSE_CACHE_MAX_BYTES |
8 MiB (8 * 1024 * 1024) |
Byte cap on the in-process session response cache. |
grep -n "_SESSION_SERVE_RESPONSE_CACHE_MAX_BYTES_ENV" src/tensor_grep/cli/session_store.py (was :44-45, now :57-58), main.py:200 |
The daemon binds to 127.0.0.1 only (session_daemon.py:46) — it is not exposed off-host. Operational
detail (starting/stopping the daemon, tg session daemon start|status|stop) lives in
.claude/skills/tensor-grep/REFERENCE.md, not here.
Agent capsule
| Var |
Default |
Effect |
Source |
TG_CAPSULE_INLINE_CALLERS |
off (env_flag_enabled-style on-values: 1/true/yes/on) |
When on, tg agent/tg prepare prepend # tg: callers=N (top: a, b) to the PRIMARY snippet's source, reusing already-collected blast-radius evidence (no new scan). Off by default for a stronger reason than most flags here: it mutates snippets[i].source/line_map/token_estimate on the primary snippet (an inserted line shifts both the displayed source and its line-number mapping, and raises the token estimate ~+2.8%), rather than only adding a new field — a consumer that diffs/re-parses source byte-for-byte will see it change. An additive snippets[i].inline_structural_annotation field is also added. py/js/ts/rs comment syntax only; fails closed (no annotation) for any other language. callers=N is only ever emitted on a verified count; token-budget truncation is fail-closed (never silently drops the annotation without accounting for its cost). |
agent_capsule_constants.py (find it: grep -n "_CAPSULE_INLINE_CALLER_ANNOTATION_ENV = " src/tensor_grep/cli/agent_capsule_constants.py) (_CAPSULE_INLINE_CALLER_ANNOTATION_ENV) |
MCP security gate (default-OFF)
| Var |
Default |
Effect |
Source |
TG_MCP_ALLOW_VALIDATION_COMMANDS |
off |
Gates whether the tg mcp server's tg_rewrite_apply tool may accept and shell-execute lint_cmd / test_cmd (from either the direct call arguments or a loaded apply-policy JSON file — both paths are gated, not just the direct one). Off by default because these commands can be steered by untrusted repo content / prompt injection; the agent-safe edit loop does not require them. Rejected requests return code="unsupported_option". |
mcp_server.py:249-254, apply_policy.py:41-45,226-230 |
This is an Enablement Discipline case: default-OFF, opt-in only, and it is the kind of knob you should
never flip on in a shared/CI MCP server config without an explicit operator decision — see
AGENTS.md "Enablement Discipline (autonomous behaviors)" (referenced from the workspace root
CLAUDE.md) and tensor-grep-change-control for the graduation gate (council-verify → dry-run →
conscious flag-flip).
Evidence signing (tg evidence emit --sign / tg evidence verify)
| Var |
Default |
Effect |
Source |
TG_EVIDENCE_SIGNING_KEY |
unset → ambient default key |
Ed25519 private key path for --sign. Precedence: --signing-key flag > this env var > the ambient per-USER default key ~/.tensor-grep/keys/evidence_ed25519.key. Consequence (A70): clearing/unsetting the env var does NOT disable signing when the ambient default key exists — resolution falls through to the default path and --sign still signs. To force a true no-key fail-closed arm, isolate HOME/USERPROFILE (or remove the default key). With no resolvable key file, --sign fails closed: non-zero exit, no receipt written — never a silent unsigned fallback. |
grep -n "def resolve_signing_key_path" src/tensor_grep/cli/evidence_signing.py — :133-140 (precedence), _default_signing_key_path :127-130, _DEFAULT_KEY_FILENAME = "evidence_ed25519.key" :60 |
TG_EVIDENCE_TRUSTED_KEYS |
unset |
The trust pin: comma-separated base64 Ed25519 public keys, merged with repeatable --trusted-key flag values. verify always reports the signer's fingerprint recomputed from the actual key bytes, but only upgrades key_trusted to true against this out-of-band pinned set (hmac.compare_digest); --require-trusted fails valid closed on an unpinned key. An embedded public key proves internal consistency, never authenticity. |
grep -n "_TRUSTED_KEYS_ENV|def resolve_trusted_public_keys" src/tensor_grep/cli/evidence_signing.py — :57, :143-152 |
Ledger (tg ledger claims / findings)
| Var |
Default |
Effect |
Source |
TG_LEDGER_CLAIM_TTL_SECONDS |
900 |
Claim TTL in seconds; expired claims are pruned. |
grep -n "_TTL_ENV|_DEFAULT_TTL_SECONDS" src/tensor_grep/cli/ledger_store.py — :124-125 |
TG_LEDGER_AGENT_ID |
anonymous sentinel (after fallback) |
Agent identity for claim/release, recorded verbatim (never inferred from process/user identity; do not put secrets in it — it lands in a plaintext, multi-agent-readable per-repo JSON). Precedence: --agent-id flag > this env > TG_EVIDENCE_AGENT_ID > the anonymous sentinel. The sentinel is DELIBERATE: two zero-config agents must both file as anonymous so _find_overlaps shows them each other's overlaps (#845) — do not auto-derive a per-checkout id. |
grep -n "_AGENT_ID_ENV|_FALLBACK_AGENT_ID_ENV|_DEFAULT_AGENT_ID|def resolve_agent_id" src/tensor_grep/cli/ledger_store.py — :127-129, :302 |
TG_LEDGER_FINDING_TTL_SECONDS |
86400 (24h) |
Wall-clock backstop TTL for findings; revision match, not this TTL, is the primary freshness signal. |
grep -n "_FINDING_TTL_ENV|_DEFAULT_FINDING_TTL_SECONDS" src/tensor_grep/cli/ledger_store.py — :168-169 |
TG_LEDGER_MAX_BLOB_BYTES |
256 MiB |
Total on-disk bytes across all DISTINCT (content-addressed, dedup'd) finding blobs for one root — independent of the live-findings count cap, so a flood of small findings cannot accumulate unbounded disk under the count cap. |
grep -n "_MAX_BLOB_BYTES_ENV|_DEFAULT_MAX_TOTAL_BLOB_BYTES" src/tensor_grep/cli/ledger_store.py — :178-179 |
In-process caches (bound long-lived agent-loop state)
These exist so a long-lived tg session daemon / MCP server process doesn't grow unbounded caches.
All are documented together in main.py:199-200; defaults are implementation-internal (read the
cited module if you need the exact number) — this skill's job is to tell you that they exist and
where, not to duplicate the numeric defaults, which drift independently of flags/commands.
TENSOR_GREP_CPU_LITERAL_INDEX_CACHE_MAX_ENTRIES
TENSOR_GREP_STRING_INDEX_CACHE_MAX_ENTRIES
TENSOR_GREP_AST_QUERY_CACHE_MAX_ENTRIES
TENSOR_GREP_AST_NODE_INDEX_CACHE_MAX_ENTRIES
TENSOR_GREP_REPO_CONTEXT_CACHE_MAX_ROOTS
TENSOR_GREP_LSP_PROVIDER_CLIENT_CACHE_MAX_ENTRIES
TENSOR_GREP_LSP_PROVIDER_OPEN_DOCUMENT_MAX_ENTRIES
Internal constants (not env-configurable)
Not every load-bearing bound in tg is an environment variable — some are deliberately hardcoded
constants, single-sourced so multiple call sites cannot drift apart. Distinguish these from the
env-configurable knobs above before assuming a behavior can be tuned at runtime:
IMPLICIT_SEARCH_WALK_FILE_CEILING = 1500 (DEFINED at src/tensor_grep/io/scan_limits.py:106;
io/directory_scanner.py only RE-EXPORTS it. An earlier revision cited directory_scanner, which
would send someone changing the ceiling to edit a re-export and wonder why nothing moved —
matches the sibling tensor-grep-architecture-contract A9 wording) — the
fast-refuse ceiling for an unscoped/defaulted-path search or tg find walk (A9, v1.92.3/#702). It is
imported by both src/tensor_grep/cli/main.py's _LARGE_ROOT_SCAN_FILE_CEILING and
src/tensor_grep/cli/bootstrap.py's _search_paths_include_oversized_implicit_root — one constant,
two Python call sites, so a future change to the ceiling cannot silently desync the Typer-app path
from the flag-less bootstrap-passthrough path. The Rust rust_core/src/rg_passthrough.rs keeps its
own copy of the same numeral, synced by convention (not a shared build-time constant across the
Python/Rust boundary) — if you ever change the Python value, grep rg_passthrough.rs for the
matching literal and update it in the same PR, or the two front doors will silently disagree on
where the ceiling sits.
- This is a distinct axis from
TG_DIR_SCAN_MAX_ENTRIES (env-configurable, a different directory-
scan bound) — do not conflate the two when reading a scan-refusal report; check which constant/env
var actually produced the observed refusal before describing the mechanism.
LSP provider
| Var |
Default |
Effect |
Source |
TG_LSP_PROVIDER |
native |
Overrides the LSP semantic-provider mode for editor/MCP clients; same value space as --provider (native/lsp/hybrid). Set by tg lsp --provider ... before calling run_lsp(). |
main.py:9649-9799, main.rs:51 |
TG_ALLOW_UNVERIFIED_TOOLCHAIN |
off |
Security opt-out: skips checksum verification of downloaded LSP-toolchain archives/binaries (rust-analyzer, etc.) for air-gapped/offline installs — same default-secure/opt-out-to-weaken pattern as TG_MCP_ALLOW_VALIDATION_COMMANDS below. Off by default; fails closed (refuses the unverified binary) unless set. |
lsp_provider_setup.py:229-265,465-480 |
Provider modes: native / lsp / hybrid
The --provider flag appears on every symbol/navigation command (defs, refs, source, impact,
callers, blast-radius*, context-render, edit-plan, agent, lsp) with the same three-way
contract everywhere, default native:
tg defs REPO_PATH SYMBOL --provider lsp
tg blast-radius REPO_PATH SYMBOL --provider hybrid
tg lsp --provider hybrid
native — tg's own tree-sitter/AST-derived symbol graph. Production default.
lsp — routes through an external language server (ExternalLSPProviderManager). EXPERIMENTAL.
hybrid — combines native with LSP evidence when available.
tg lsp validates the value explicitly and exits 2 on anything else
({"native", "lsp", "hybrid"} check, main.py:9656-9785):
Unsupported LSP provider mode; expected one of: native, lsp, hybrid
LSP-availability is not LSP-proof — this is a load-bearing distinction from AGENTS.md:163:
"Treat tg lsp-setup / tg doctor --with-lsp availability as install evidence only; provider-backed
navigation must report health_status, health_check, lsp_proof, lsp_evidence_status, and
not_lsp_proof_reason when it falls back to native evidence. A navigation row counts as LSP proof only
when it carries lsp_provider_response = true from a completed provider request." Do not tell a user
"LSP is working" because tg doctor --with-lsp found a binary on PATH.
Production vs EXPERIMENTAL (default-OFF), and the guard that keeps it off
| Axis |
Status |
Guard |
Why |
Native CPU/rg search, AST search (tg run), symbol nav (native provider) |
Production |
none — default path |
Backbone of the tool. |
tg agent / Actionable Context Capsule |
Production, opt-in by design |
explicit tg agent invocation |
Not a default search mode; it's a distinct command surface, but it is a shipped, supported feature. |
classify local heuristic |
Production |
default |
Deterministic, no model download. |
--gpu-device-ids / GPU backends |
EXPERIMENTAL |
must be explicitly requested; heuristic auto-GPU only fires when rg is unavailable |
Slower than CPU today; no promotion-ready path (AGENTS.md:226-234). |
--provider lsp / --provider hybrid, TG_LSP_PROVIDER |
EXPERIMENTAL |
explicit --provider value or TG_LSP_PROVIDER env |
Availability ≠ working navigation; see LSP-proof contract above. |
TENSOR_GREP_CLASSIFY_PROVIDER=cybert/triton |
EXPERIMENTAL |
explicit env opt-in |
Requires a Triton/CyBERT model deployment; falls back before expensive model load if unavailable. |
TG_MCP_ALLOW_VALIDATION_COMMANDS=1 |
Off by design (security), not "not ready yet" |
explicit env opt-in on the MCP server process |
Shell-executes lint_cmd/test_cmd, a prompt-injection surface. |
| Local hybrid semantic search (BM25 + CPU dense embeddings + RRF) |
SHIPPED, EXPERIMENTAL default-OFF — tg search --semantic (grep -n '"--semantic"' src/tensor_grep/cli/main.py — was :6619, now :7403; core/retrieval_dense.py + core/retrieval_fusion.py) |
explicit --semantic flag; requires the semantic extra (model2vec, pyproject.toml:627), fails closed with a rank_fallback_reason when unavailable |
No API key, no GPU, pure local CPU dense leg fused with BM25 via RRF -- see tensor-grep-semantic-search-campaign for build history and promotion gates. A 2nd consumer of the same dense/fusion core is tg find (below). |
TG_FIND_DENSE_WEIGHT (tg find only) |
unset → adaptive (flip SHIPPED, #191/#634, first released v1.79.0): 5.0 for genuinely multi-word queries, 1.0 for single-token queries |
Query-adaptive dense_weight for tg find's rank_chunks calls ONLY -- gates ONLY tg find, never --semantic. Resolution (_find_dense_weight): unset/empty/malformed/non-finite env → _FIND_DENSE_WEIGHT_ADAPTIVE_DEFAULT (5.0, the ledger-swept 1:5 bm25:dense ratio) for a genuinely multi-word query (len(query.split()) > 1, the whitespace word-count gate, #191/#630); a single whitespace-free token (a literal identifier/symbol lookup) ALWAYS stays pinned at _FIND_DENSE_WEIGHT_DEFAULT (1.0) regardless of the env value; a malformed/non-finite value (nan/inf/garbage) falls to the adaptive 5.0 — treated exactly like unset, NOT clamped to 1.0 — so a typo cannot silently opt an operator out of the improved default (#634 must-fix; a non-finite value still never reaches reciprocal_rank_fusion's sort). Explicit TG_FIND_DENSE_WEIGHT=1.0 is the opt-out back to the old equal-weight fusion; any other finite value (e.g. =3.0) is honored verbatim. |
grep -n "_find_dense_weight|_FIND_DENSE_WEIGHT_ADAPTIVE_DEFAULT" src/tensor_grep/cli/main.py (env/default consts were :4007-4008, then :4271-4272, now :4593-4594; adaptive const now :4600; reader _find_dense_weight was :4176-4227, now :4613-4684). The old "still default-OFF; the flip to a non-1.0 default is a separate CEO checkpoint" framing is SUPERSEDED by the shipped flip — see tensor-grep-semantic-search-campaign STATUS UPDATE 4. |
tg inventory: walk-only repo manifest (v1.19.0, #343)
tg inventory PATH [--json] [--max-repo-files N] [--deadline SECONDS] (src/tensor_grep/cli/inventory.py,
registered main.py:8292-8404) emits a single-pass file/byte/language/category manifest by
reusing the same gitignore-aware walker (repo_map._iter_repo_files) that orient/callers/
blast-radius trust — so counts stay truth-consistent with every other tg command and inherit
its .tensor-grep/.git/vendor exclusions for free.
--max-repo-files defaults to 50_000, still well above the AST map limit — this is a
deliberate, documented divergence, not an oversight. The AST-side number changed underneath this
divergence (backlog #1, 2026-07-06): DEFAULT_AGENT_REPO_MAP_LIMIT was raised from 512 to
2000 (repo_map.py:157), and the CLI-side mirror _DEFAULT_AGENT_REPO_SCAN_LIMIT (main.py:82)
was raised to match — do not describe the AST cap as 512 anymore.
DEFAULT_MAX_INVENTORY_FILES = 50_000 (inventory.py:40), passed to the CLI option as a
literal 50_000 (grep -n "50_000" src/tensor_grep/cli/main.py -- no line range: the old :8406-8413 pin sat INSIDE a --deadline option block that the 2026-08-23 de-duplication deleted outright, so it has no successor line to re-stamp to) rather than importing the constant, so the (heavy)
repo_map import stays lazy. A nearby code comment still says "matching map's 512 pattern" —
that comment is about the STYLE (keep-literal, don't import), not the current live number; map's
own limit is 2000 now, not 512. A guard test pins the 50_000 literals together; re-verify with
grep -rn "50_000" src/tensor_grep/cli/inventory.py src/tensor_grep/cli/main.py.
DEFAULT_AGENT_REPO_MAP_LIMIT = 2000 (repo_map.py:157) budgets a full AST parse per file
for tg map/orient/context/edit-plan/session repo-map defaults — reusing it for
inventory would silently truncate any repo over ~2000 files and defeat the "whole-repo
manifest" purpose (inventory.py:36-39 states this explicitly in a code comment).
inventory is walk-only (stat() + an 8KB read for binary-sniffing per file), orders of
magnitude cheaper than an AST parse, so a much higher cap (50_000) is still safe even after the
AST-side raise.
CALLER_SCAN_FILE_CEILING was ALSO raised, 512→2000 (repo_map.py:167-177; backlog #57,
2026-07-09) — the "DIFFERENT constant that stays at 512" framing this file previously used is
itself now stale. It remains logically separate from DEFAULT_AGENT_REPO_MAP_LIMIT (they just
now happen to share a value) — the raise was safe only because #478 had already threaded a
--deadline hard-bound through the caller-scan loop, closing the task #52 100s-hang risk
("100s on a 1941-file repo at the old 512 cap", repo_map.py:163-164) that originally kept this
ceiling frozen below the map default (repo_map.py:1638-1642). It still backstops the flag-less
(--deadline omitted) default path and a --max-repo-files-raised mega-repo; raising past 2000
needs fresh cost data (repo_map.py:167-177). If you see the bare number 512 anywhere in this
subsystem going forward, it is describing HISTORY — check which constant before assuming either
reading is still live.
- Truncation is never silent, and it now has two distinguishable causes
(
scan_limit.truncation_cause in the JSON payload): "project-files" when --max-repo-files'
count cap was hit, or "deadline" when --deadline (below) fired first; the key is always
present, null when the scan completed. Text output mirrors the split (inventory.py:348-359): [!] truncated at max_files=... (cause=project-files) vs. [!] stopped after the time budget (cause=deadline) —
both ASCII-only (fixed from a U+26A0 emoji that crashed typer.echo on Windows cp1252 consoles —
#346, commit 6b7b518; ASCII-only is now the rule for all tg CLI output, not just
inventory). Either cause trips the same shared exit-2 gate, _scan_incomplete
(main.py:10817, checked at main.py:8329-8447) — it only looks at possibly_truncated, not
which cause fired.
- Fails closed: a nonexistent
path raises FileNotFoundError -> CLI exits 1
(inventory.py:201-202) — a missing path must never read as a valid empty repo.
--deadline SECONDS: the wall-clock twin of --max-repo-files (registered main.py:8297-8423)
Threads a deadline_seconds float (inventory.py:187) into build_inventory() so a huge/slow
tree returns a partial, honestly-labeled manifest instead of hanging. inventory's own
--deadline predates and is unrelated to #585 (2026-07-14), which extended --deadline to
source/docs-coverage/blast-radius-plan instead (a disjoint set of commands) — inventory's
flag shipped earlier (#395, issue #53), was hardened into a true wall-clock bound by #478
(issue #52), then had a zero-count bug fixed (next bullet) by #516.
- The walk and the per-file loop split the budget, they do not share it
(
inventory.py:204-223; _WALK_PHASE_DEADLINE_FRACTION = 0.7, inventory.py:48) — the
in-source comment calls this the "#130(a) fix" (shipped as #516 per commit history). The walk
phase (_iter_repo_files) gets only the first 70% of deadline_seconds; the remainder is
reserved for the per-file stat()/binary-sniff loop. Before this split, a slow walk could
consume the entire deadline, so the per-file loop's very first deadline check fired
immediately and totals.files read 0 despite the walk having discovered real files — the
in-source comment names the repro outright (inventory.py:204-208: "the 76s dogfood gap on
tg inventory --deadline 30").
- Either phase running out of budget sets
truncation_cause = "deadline" (inventory.py:297-306)
— including when the walk was ALSO count-capped by --max-repo-files; "deadline" wins the label
because a longer --deadline would help where a higher --max-repo-files would not. An earlier
version of this same guard mislabeled a real 20s deadline hit on C:/dev/projects as a file-cap
truncation (inventory.py:303-304, dogfood 2026-07-05) — exactly the bug this two-cause split
exists to prevent from recurring.
- Known narrow gap (low-priority, not load-bearing): the walk's initial listing of the SCANNED
ROOT itself is not deadline-interruptible.
_iter_repo_files (grep -n "^def _iter_repo_files" src/tensor_grep/cli/repo_map.py -- :1183 as of 2026-08-14, was :1143), on the
max_files is not None branch inventory always takes (it always calls with
max_files=max_files + 1, inventory.py:231-236), does one eager, unconditional
entries = list(os.scandir(normalized_root)) (repo_map.py:1009-1010) before any deadline
check exists in that branch — every subsequent bucket pull IS deadline-checked
(repo_map.py:1052-1057), just not this one root-level call. On a root whose own immediate
directory listing is itself pathologically large/slow, --deadline cannot preempt it. Dogfooded
against a 300k+-file workspace-union tree: tg inventory --deadline holds up fine per-project
and on most roots — this edge needs a pathologically huge flat fan-out sitting directly at the
scanned path to trigger, and is not worth a load-bearing lazy-scandir rewrite on its own; see
tensor-grep-large-repo-scale-campaign for the broader deadline-scale work this sits alongside.
Registration follows the standard 4-site table (KNOWN_COMMANDS in commands.py, native Rust
Commands::Inventory in rust_core/src/main.rs, PUBLIC_TOP_LEVEL_COMMANDS in
tests/e2e/test_routing_parity.py, the @app.command() in main.py) — see
tensor-grep-architecture-contract for why each site exists.
# Re-verify the two caps and why they differ
grep -n 'DEFAULT_MAX_INVENTORY_FILES\|max_repo_files' src/tensor_grep/cli/inventory.py src/tensor_grep/cli/main.py
grep -n 'DEFAULT_AGENT_REPO_MAP_LIMIT = \|CALLER_SCAN_FILE_CEILING = ' src/tensor_grep/cli/repo_map.py
# Re-verify --deadline registration + the walk/per-file budget split
grep -n '"--deadline"' src/tensor_grep/cli/main.py
grep -n 'deadline_seconds\|_WALK_PHASE_DEADLINE_FRACTION' src/tensor_grep/cli/inventory.py
# Smoke-test the command against the real binary (not CliRunner)
tg inventory . --json | python -m json.tool | head -30
Checklist: adding a flag or command
This is the single highest-value thing to get right in this repo — miss a registration site and the
new flag/command misroutes silently, passing CliRunner tests while breaking the real published
binary. See tensor-grep-architecture-contract for the full 4-site command / 2-site search-flag
registration table and the rationale for why each site exists, and tensor-grep-change-control for
the PR/merge gate around it (AGENTS.md:178-196) — this skill does not restate that table.
Worked non-registration example — tg prepare --out FILE (v1.93.0/#705, A12(d)). Not every new
flag triggers all 3 registration concerns this skill and its siblings track — a useful example of
"none of the above applied" to calibrate against: adding --out to the already-registered tg prepare
command needed (1) no new 4-site command registration (the command already existed), (2) no 2-site
search-flag registration (--out is not a search flag — it never reaches bootstrap's rg-passthrough
front door), and (3) no SearchConfig field-coverage classification (prepare doesn't build a
SearchConfig at all). It was a same-command, same-registration-footprint addition — a new typer.Option
on an existing @app.command, nothing else. Recognizing when a change genuinely needs none of the 3
checklist items (vs. assuming every new flag does) saves a wasted registration audit.
The third checklist item: native-delegation field coverage (SearchConfig)
Adding a new field to SearchConfig (src/tensor_grep/core/config.py) is a third registration
concern, separate from the 4-site command table and the 2-site search-flag table above — and it is
easy to miss because it fails silently, not loudly.
Why it exists: _can_delegate_to_native_tg_search (grep -n "^def _can_delegate_to_native_tg_search" src/tensor_grep/cli/main.py -- :4095 as of 2026-08-14, was :3709) hands an entire search
off to the native tg subprocess, which sys.exit()s before the Python-side BM25 rerank and
the in-backend file sort ever run. Any SearchConfig field that is output-affecting but neither
forwarded into the native argv (_build_native_tg_search_command -- grep -n "^def _build_native_tg_search_command" src/tensor_grep/cli/main.py, :4117 as of 2026-08-14, was :3731) nor listed in the refuse-tuple
_NATIVE_TG_DELEGATION_DEFAULT_REQUIRED_FIELDS (grep -n "_NATIVE_TG_DELEGATION_DEFAULT_REQUIRED_FIELDS = " src/tensor_grep/cli/main.py -- :1966 as of 2026-08-14, was :1894 onward) gets silently dropped —
the search still runs and returns a result, just the wrong one (unranked/unsorted), which is worse
than a crash because suppression reads as absence. This is the same bug class as the -u/-uu
no-op fixed in #336; the receipt this time was #342 (commit 5e6f780, v1.18.6->v1.19.0 range):
rank_bm25 and sort_files were parsed but forwarded nowhere, so tg search --rank --cpu /
--sort-files --cpu silently returned unranked/unsorted output on the delegated fast path.
The gate is now a governance ratchet, not a convention — tests/unit/test_native_delegation_field_coverage.py
AST-derives the forwarded-field set directly from _build_native_tg_search_command's source (via
ast.walk over `config.<
…(truncated)
1---2name: tensor-grep-config-and-flags3description: Use when adding, changing, or auditing a tg environment variable, CLI flag, or provider mode (native/lsp/hybrid); when a search flag silently leaks to ripgrep or a command misroutes; when deciding whether a config axis (GPU, LSP, classify, semantic) is production or EXPERIMENTAL default-OFF; when adding a new `SearchConfig` field and needing to know whether it must be forwarded/refused/KNOWN_GAP'd for native delegation; or before registering a new `tg search --flag` or `tg COMMAND` (including `tg inventory`'s `--max-repo-files`/`--deadline`). Catalogs the load-bearing TG_*/TENSOR_GREP_* env vars (routing, timeouts, GPU, classify, session, MCP security, LSP) with their default and guard, the 2-front-door / 4-site registration checklist, and the native-delegation field-coverage ratchet.4---56# tensor-grep config and flags78A ground-truthed catalog of every tg config axis — env vars, CLI flags, provider modes — plus the9registration checklist for adding a new one. Verified against source as of 2026-07-23, **v1.95.0**10(`pyproject.toml`). Re-verify commands are in [Provenance and maintenance](#provenance-and-maintenance)11because these drift with every release.1213## When to use this skill1415- You are adding, renaming, or removing an env var or CLI flag.16- A search flag is reaching ripgrep raw (`rg: unrecognized flag` at runtime) or a command 404s.17- You need to know whether a knob (`--gpu-device-ids`, `--provider lsp`, `TENSOR_GREP_CLASSIFY_PROVIDER=cybert`)18 is production-safe to recommend to a user, or still experimental/default-off.19- You need the authoritative default value or guard condition for a `TG_*` / `TENSOR_GREP_*` variable20 before writing docs, a benchmark harness, or an agent prompt that references it.2122## When NOT to use this skill (use the sibling instead)2324| If you need... | Use instead |25|---|---|26| The *why* behind the front-door/routing architecture, not just the flag list | `tensor-grep-architecture-contract` |27| The process gates for *shipping* a flag/command change (PR, CI, one-merge-per-tick) | `tensor-grep-change-control` |28| Reproducing/debugging a routing bug once you already know which flag is involved | `tensor-grep-debugging-playbook` |29| `tg doctor` output fields, dogfood harness, benchmark scripts | `tensor-grep-diagnostics-and-tooling` / `tensor-grep-benchmark-and-proof-toolkit` |30| How to actually *use* `tg` commands day to day (not configure them) | `.claude/skills/tensor-grep/SKILL.md` |31| Build/toolchain setup (uv, maturin, cargo) | `tensor-grep-build-and-env` |32| Release mechanics / positioning claims | `tensor-grep-release-and-positioning` |3334## The two front doors, and why config is split across them3536tg has **two CLI entry points that both parse flags**, and a config change that only lands in one37is a silent bug, not a crash:38391. **Python bootstrap** (`src/tensor_grep/cli/bootstrap.py`) — `tensor_grep.cli.bootstrap:main_entry`40 intercepts plain-text searches *before* the Typer app loads and forwards them straight to `rg`.412. **Rust native front door** (`rust_core/src/main.rs`) — the standalone `tg` binary, which re-implements42 flag parsing with `clap`.4344The canonical, always-current documentation of every env var and the full `search` flag surface lives45in two places that are meant to stay in sync — read these first when you need ground truth fast:4647- `tg --help` epilog: `src/tensor_grep/cli/main.py:187-200` (the `app = typer.Typer(help="""...""")` block).48- Native `tg --help` epilog: `ENVIRONMENT_OVERRIDES_HELP` const, `rust_core/src/main.rs:67`.4950If those two drift from each other or from this file, trust the source, not this document — see51[Provenance and maintenance](#provenance-and-maintenance).5253## Environment variable catalog5455Boolean env vars in tg follow one convention almost everywhere (`env_flag_enabled`,56`grep -n "def env_flag_enabled" src/tensor_grep/cli/runtime_paths.py` — was `:13-15`, now `:20-22`):57the raw value is lower-cased and stripped, and it is "on" only if it is exactly `1`, `true`, `yes`,58or `on` — anything else (including unset) is "off". **Two STRICT exceptions compare the raw value59against the literal `"1"` only, so `true`/`yes`/`on` do NOT enable them:** `TG_DOCTOR_OFFLINE` is60`== "1"` on the Python side (`grep -n "TG_DOCTOR_OFFLINE" src/tensor_grep/cli/main.py` — `:492`),61and `TG_RESIDENT_AST` is `!= "1"` on the Rust side (`grep -n "TG_RESIDENT_AST"62rust_core/src/main.rs` — `:7581`).6364### Routing / launcher6566| Var | Default | Effect | Source |67|---|---|---|---|68| `TG_SIDECAR_PYTHON` | `sys.executable` | Python executable used for sidecar-backed commands (classify, GPU sidecar). | `main.py:188`, `main.py:533` |69| `TG_NATIVE_TG_BINARY` (alias `TG_MCP_TG_BINARY`) | auto-resolved | Path to the native `tg` binary front door used by Python-backed commands. Priority 1 override; stale in-tree dev builds (`rust_core/target/{debug,release}/tg.exe`) are otherwise skipped unless pinned here. | `main.py:189`, `runtime_paths.py:238-248` |70| `TENSOR_GREP_NATIVE_FRONTDOOR_FLAVOR` (alias `TG_NATIVE_FRONTDOOR_REQUESTED_FLAVOR`) | `cpu` | `nvidia`/`cuda` prefers the NVIDIA release-native front-door asset (`tg-*-nvidia.exe`), with CPU fallback; anything else normalizes to `cpu`. | `main.py` (`grep -n "def _normalize_native_frontdoor_flavor" src/tensor_grep/cli/main.py` -- `:550` as of 2026-08-14; the prior `:7276`/`:454-473` cites were stale) |71| `TG_RG_PATH` | auto-resolved | Path to the `rg` executable used for text-search passthrough. | `main.py:191`, `runtime_paths.py:281` |72| `TG_FORCE_CPU` | off | Force CPU routing for search commands (boolean convention). | `main.py:192`, `main.py:2772` |73| `TG_RUST_FIRST_SEARCH` | off | Opt-in: prefer the Rust native front door before Python bootstrap logic for search dispatch. | `bootstrap.py:242` |74| `TG_RUST_EARLY_RG`, `TG_RUST_EARLY_POSITIONAL_RG` | off | Internal early-dispatch toggles surfaced by `tg doctor --json`; not documented in the public `--help` epilogs. | `main.rs:53-54`, `main.py:2775-2762` |75| `TG_RESIDENT_AST` | off | Enables the resident AST worker path (see `docs/runbooks/resident-worker.md`); reported by `tg doctor --json`. | `main.py:2773`, `main.rs` (search `TG_RESIDENT_AST`) |76| `TG_DISABLE_NATIVE_TG` | off | Kill-switch: forces `resolve_native_tg_binary()` to return `None`, fully bypassing the native `tg` binary front door (Python-backed commands fall back to pure-Python routing even if a compatible native binary is resolvable). | `runtime_paths.py:234` |77| `TG_DISABLE_RG` | off | Kill-switch: forces the native binary's ripgrep resolver to return `None`, so the native front door treats `rg` as unavailable regardless of `TG_RG_PATH`/PATH. | `rust_core/src/rg_passthrough.rs:13,477` |78| `TG_DOCTOR_OFFLINE` | off | Disables `tg doctor`'s PyPI latest-version probe: `pypi_latest` becomes `None` and `installation_health` reports `unknown_pypi` instead of a network result. Test/offline escape hatch for the v1.110.14 doctor schema-3 freshness fields; deliberately disclosed (never a silent clean). | `main.py:489-492` |7980### Timeouts8182| Var | Default | Effect | Source |83|---|---|---|---|84| `TG_RG_TIMEOUT_SECONDS` | **60.0s** (lowered from 600s in #288) | Ripgrep-passthrough search timeout. Fails fast with a stderr hint to scope the search or raise the timeout, instead of hanging. Overridden by `TG_SIDECAR_TIMEOUT_MS` when that is set to a positive value. | `grep -n "TG_RG_TIMEOUT_SECONDS" src/tensor_grep/cli/subprocess_policy.py` (was `:32-44`, now `:75`) |85| `TG_SIDECAR_TIMEOUT_MS` | unset | Milliseconds; if set and > 0, **takes precedence over `TG_RG_TIMEOUT_SECONDS`** for the ripgrep-passthrough timeout (`ms / 1000.0`). Also documented as the general sidecar-command timeout. | `subprocess_policy.py:32-40`, `main.py:193` |86| `TG_SUBPROCESS_TIMEOUT_SECONDS` | 600.0s | Default timeout for the generic `run_subprocess()` helper (git ops, MCP validation commands, etc.) unless a call site overrides `timeout_env_var`. | `subprocess_policy.py:82-87` |87| `TG_GIT_TIMEOUT_SECONDS` | 120.0s | Timeout for git subprocess calls (checkpoint/session git operations). | `subprocess_policy.py:28-29` |88| `TENSOR_GREP_TRITON_TIMEOUT_SECONDS` | 5.0s | Timeout for Triton-backed NLP (CyBERT) probes. | `cybert_backend.py:18-19`, `main.py:196` |89| `TENSOR_GREP_LSP_OPERATION_BUDGET_SECONDS` | 2.0s | Total per-command budget for optional external LSP provider requests before falling back to native evidence. | `grep -n "TENSOR_GREP_LSP_OPERATION_BUDGET_SECONDS" src/tensor_grep/cli/repo_map.py` (was `:95-96`, now `:193-194`), `main.py:198` |90| `TENSOR_GREP_LSP_REQUEST_TIMEOUT_SECONDS`, `TENSOR_GREP_LSP_INITIALIZE_TIMEOUT_SECONDS` | implementation defaults | Per-request / per-initialize LSP timeouts; reported (not overridden) by `tg doctor --json`. | `main.py:2777-2764` |9192Every non-positive or unparseable value for the float-typed timeout vars above silently falls back to93the compiled-in default (`_configured_positive_float`, `subprocess_policy.py:9-17`) — a bad value does94not crash, it just gets ignored. Do not assume "I set it, therefore it changed" without checking95`tg doctor --json`'s `env` block (see [Discovering effective config](#discovering-effective-config-tg-doctor---json)).9697### GPU9899| Var / flag | Default | Effect | Source |100|---|---|---|---|101| `--gpu-device-ids IDS` (CLI flag, e.g. `tg search --gpu-device-ids 0,1`) | unset (no GPU routing) | **Explicit, user-intent GPU pin** for search / `tg agent` / benchmark evidence probes. Comma-separated non-negative ints; parse errors raise `typer.BadParameter` immediately (`main.py:4061-4074`). | `main.py:5725-5762`, `main.py:6890-6969` |102| `TENSOR_GREP_DEVICE_IDS` | unset (all detected devices visible) | Lower-level env allow-list of GPU IDs available to tensor-grep at all (like `CUDA_VISIBLE_DEVICES`), consulted by device detection/memory-manager code, not just the CLI flag. | `device_detect.py:30-55`, `main.py:194` |103| `--gpu-timeout-s` (flag, `tg agent` only) | 5.0s | Max seconds for each opt-in agent GPU evidence subcommand. | `grep -n "gpu_timeout_s: float = typer.Option" src/tensor_grep/cli/main.py` (was `:6970-6975`, now `:9987-9991`) |104105**Fail-loud contract**: an explicit `--gpu-device-ids` request that cannot be honored raises106`ConfigurationError` (a `RuntimeError` subclass, `pipeline.py:20-21`) — it never silently falls back to107CPU. Example message shape (`pipeline.py:26-39`):108109```110GPU acceleration is experimental. Explicit GPU device selection [0, 1] could not initialize a111GPU backend: fixed-string (-F) search has no GPU backend112```113114This is deliberate: `-F`/`--fixed-strings` GPU search has no kernel yet, so pairing it with an explicit115`--gpu-device-ids` must error, not silently drop to CPU and report a clean result (see the Backend116Fail-Closed Contract in `AGENTS.md:214-224`, and `tensor-grep-architecture-contract` for the general117principle). By contrast, the **heuristic** (non-explicit) auto-GPU path degrades to CPU with a visible118`warnings.warn(...)` + `fallback_reason`, not an exception — only *explicit* user intent gets fail-loud119treatment (`pipeline.py:174-176`, `_should_honor_explicit_gpu_ids`).120121GPU remains **EXPERIMENTAL** end-to-end. GPU Phase-0 SHIPPED (v1.75.0-v1.75.4, PRs #593-#597):122NVIDIA native assets are built and locally correctness-proven (RTX 4070 `sm_89` / RTX 5070 `sm_120` --123`docs/gpu_crossover.md`), but gated OFF the public release by the CI Actions var124`TENSOR_GREP_RELEASE_NATIVE_ASSET_PROFILE` (default `native-frontdoor`, CPU-only; GPU asset125publishing needs the non-default `native-frontdoor-gpu`) -- Phase 1 is now a reversible flag-flip, not126a multi-week rebuild. That flip publishes assets only: no speed crossover is proven vs `rg`/`tg_cpu`,127GPU auto-recommendation stays `false`, and the reviewer-gated `public-gpu-proof.yml` speed-crossover128gate remains unmet (`docs/CONTRACTS.md:80-82`). Do not market or default-enable it.129130### Classify provider131132| Var | Default | Effect | Source |133|---|---|---|---|134| `TENSOR_GREP_CLASSIFY_PROVIDER` | `heuristic` (local, deterministic) | Set to `cybert` or `triton` to opt into the CyBERT/Triton NLP classifier for `tg classify FILE`. Any other value (including unset) uses the local regex-heuristic classifier. | `sidecar.py:16-17`, `sidecar.py:119-125` |135136`tg classify` always reports provenance in its JSON output (`classification_backend` /137`provider_requested` / `provider_used` / `provider_status` / `fallback_reason` / `cache`,138`sidecar.py:57-71`) so a caller can distinguish "asked for cybert, got heuristic because it failed"139from "asked for heuristic". Never read a classify result as model-backed without checking140`provider_used`.141142### Session / daemon143144| Var | Default | Effect | Source |145|---|---|---|---|146| `TG_SESSION_MAX` | 64 | Max on-disk cached sessions retained per root; oldest are pruned past this. | `grep -n "_SESSION_MAX_ENV\|_configured_session_max" src/tensor_grep/cli/session_store.py` (was `:49-51`, `:120-121`, now `:88-89` (env+default), `:159` (usage)) |147| `TG_SESSION_NEARBY_LOOKUP` | off | By default session discovery is confined to the explicit root; set to opt into parent/sibling-directory session discovery. | `grep -n "_SESSION_NEARBY_LOOKUP_ENV" src/tensor_grep/cli/session_store.py` (was `:52-54`, `:124-130`, now `:90-92`) |148| `TG_SESSION_DAEMON_IDLE_SECONDS` | 900.0s | Idle stretch (no requests) after which the warm `tg session daemon` self-shuts-down. Non-positive disables the idle limit. | `grep -n "_DAEMON_IDLE_SHUTDOWN_SECONDS_ENV\|_DEFAULT_DAEMON_IDLE_SHUTDOWN_SECONDS" src/tensor_grep/cli/session_daemon.py` (was `:67-72`, now env-name `:93`, default `:95`) |149| `TG_SESSION_DAEMON_MAX_UPTIME_SECONDS` | 86400.0s (24h) | Hard max daemon lifetime regardless of activity. Non-positive disables the uptime limit. | `grep -n "_DAEMON_MAX_UPTIME_SECONDS_ENV\|_DEFAULT_DAEMON_MAX_UPTIME_SECONDS" src/tensor_grep/cli/session_daemon.py` (was `:67-73`, now env-name `:94`, default `:96`) |150| `TG_SESSION_DAEMON_RESPONSE_TIMEOUT_SECONDS` | 60.0s | Client-side socket read timeout for a daemon response (#390, moat P0-6 step 5). Env-configurable so a large repo whose warm-daemon graph query legitimately needs >60s isn't killed by a hard cap that returns a bare "timed out"/exit 1/zero JSON. Does **not** by itself bound the daemon's own traversal — the served graph commands run on a cached map and are not covered by the scan-side `--deadline`; see the #390 daemon-path gap in `tensor-grep-large-repo-scale-campaign`. | `grep -n "_DAEMON_RESPONSE_TIMEOUT_ENV\|_DAEMON_RESPONSE_TIMEOUT_SECONDS =" src/tensor_grep/cli/session_daemon.py` (was `:52,:58`, now env-name `:68`, default `:58`) |151| `TENSOR_GREP_SESSION_RESPONSE_CACHE_MAX_BYTES` | 8 MiB (`8 * 1024 * 1024`) | Byte cap on the in-process session response cache. | `grep -n "_SESSION_SERVE_RESPONSE_CACHE_MAX_BYTES_ENV" src/tensor_grep/cli/session_store.py` (was `:44-45`, now `:57-58`), `main.py:200` |152153The daemon binds to `127.0.0.1` only (`session_daemon.py:46`) — it is not exposed off-host. Operational154detail (starting/stopping the daemon, `tg session daemon start|status|stop`) lives in155`.claude/skills/tensor-grep/REFERENCE.md`, not here.156157### Agent capsule158159| Var | Default | Effect | Source |160|---|---|---|---|161| `TG_CAPSULE_INLINE_CALLERS` | off (`env_flag_enabled`-style on-values: `1`/`true`/`yes`/`on`) | When on, `tg agent`/`tg prepare` prepend `# tg: callers=N (top: a, b)` to the PRIMARY snippet's source, reusing already-collected blast-radius evidence (no new scan). Off by default for a stronger reason than most flags here: it **mutates** `snippets[i].source`/`line_map`/`token_estimate` on the primary snippet (an inserted line shifts both the displayed source and its line-number mapping, and raises the token estimate ~+2.8%), rather than only adding a new field — a consumer that diffs/re-parses `source` byte-for-byte will see it change. An additive `snippets[i].inline_structural_annotation` field is also added. py/js/ts/rs comment syntax only; fails closed (no annotation) for any other language. `callers=N` is only ever emitted on a verified count; token-budget truncation is fail-closed (never silently drops the annotation without accounting for its cost). | `agent_capsule_constants.py` (find it: `grep -n "_CAPSULE_INLINE_CALLER_ANNOTATION_ENV = " src/tensor_grep/cli/agent_capsule_constants.py`) (`_CAPSULE_INLINE_CALLER_ANNOTATION_ENV`) |162163### MCP security gate (default-OFF)164165| Var | Default | Effect | Source |166|---|---|---|---|167| `TG_MCP_ALLOW_VALIDATION_COMMANDS` | off | Gates whether the `tg mcp` server's `tg_rewrite_apply` tool may accept and shell-execute `lint_cmd` / `test_cmd` (from either the direct call arguments **or** a loaded apply-policy JSON file — both paths are gated, not just the direct one). Off by default because these commands can be steered by untrusted repo content / prompt injection; the agent-safe edit loop does not require them. Rejected requests return `code="unsupported_option"`. | `mcp_server.py:249-254`, `apply_policy.py:41-45,226-230` |168169This is an Enablement Discipline case: default-OFF, opt-in only, and it is the kind of knob you should170**never** flip on in a shared/CI MCP server config without an explicit operator decision — see171`AGENTS.md` "Enablement Discipline (autonomous behaviors)" (referenced from the workspace root172`CLAUDE.md`) and `tensor-grep-change-control` for the graduation gate (council-verify → dry-run →173conscious flag-flip).174175### Evidence signing (`tg evidence emit --sign` / `tg evidence verify`)176177| Var | Default | Effect | Source |178|---|---|---|---|179| `TG_EVIDENCE_SIGNING_KEY` | unset → ambient default key | Ed25519 private key path for `--sign`. Precedence: `--signing-key` flag > this env var > the ambient per-USER default key `~/.tensor-grep/keys/evidence_ed25519.key`. **Consequence (A70): clearing/unsetting the env var does NOT disable signing when the ambient default key exists** — resolution falls through to the default path and `--sign` still signs. To force a true no-key fail-closed arm, isolate `HOME`/`USERPROFILE` (or remove the default key). With no resolvable key file, `--sign` fails closed: non-zero exit, no receipt written — never a silent unsigned fallback. | `grep -n "def resolve_signing_key_path" src/tensor_grep/cli/evidence_signing.py` — `:133-140` (precedence), `_default_signing_key_path` `:127-130`, `_DEFAULT_KEY_FILENAME = "evidence_ed25519.key"` `:60` |180| `TG_EVIDENCE_TRUSTED_KEYS` | unset | The trust pin: comma-separated base64 Ed25519 public keys, merged with repeatable `--trusted-key` flag values. `verify` always reports the signer's fingerprint recomputed from the actual key bytes, but only upgrades `key_trusted` to `true` against this out-of-band pinned set (`hmac.compare_digest`); `--require-trusted` fails `valid` closed on an unpinned key. An embedded public key proves internal consistency, never authenticity. | `grep -n "_TRUSTED_KEYS_ENV\|def resolve_trusted_public_keys" src/tensor_grep/cli/evidence_signing.py` — `:57`, `:143-152` |181182### Ledger (`tg ledger` claims / findings)183184| Var | Default | Effect | Source |185|---|---|---|---|186| `TG_LEDGER_CLAIM_TTL_SECONDS` | `900` | Claim TTL in seconds; expired claims are pruned. | `grep -n "_TTL_ENV\|_DEFAULT_TTL_SECONDS" src/tensor_grep/cli/ledger_store.py` — `:124-125` |187| `TG_LEDGER_AGENT_ID` | `anonymous` sentinel (after fallback) | Agent identity for claim/release, recorded verbatim (never inferred from process/user identity; do not put secrets in it — it lands in a plaintext, multi-agent-readable per-repo JSON). Precedence: `--agent-id` flag > this env > `TG_EVIDENCE_AGENT_ID` > the `anonymous` sentinel. The sentinel is DELIBERATE: two zero-config agents must both file as `anonymous` so `_find_overlaps` shows them each other's overlaps (#845) — do not auto-derive a per-checkout id. | `grep -n "_AGENT_ID_ENV\|_FALLBACK_AGENT_ID_ENV\|_DEFAULT_AGENT_ID\|def resolve_agent_id" src/tensor_grep/cli/ledger_store.py` — `:127-129`, `:302` |188| `TG_LEDGER_FINDING_TTL_SECONDS` | `86400` (24h) | Wall-clock backstop TTL for findings; revision match, not this TTL, is the primary freshness signal. | `grep -n "_FINDING_TTL_ENV\|_DEFAULT_FINDING_TTL_SECONDS" src/tensor_grep/cli/ledger_store.py` — `:168-169` |189| `TG_LEDGER_MAX_BLOB_BYTES` | 256 MiB | Total on-disk bytes across all DISTINCT (content-addressed, dedup'd) finding blobs for one root — independent of the live-findings count cap, so a flood of small findings cannot accumulate unbounded disk under the count cap. | `grep -n "_MAX_BLOB_BYTES_ENV\|_DEFAULT_MAX_TOTAL_BLOB_BYTES" src/tensor_grep/cli/ledger_store.py` — `:178-179` |190191### In-process caches (bound long-lived agent-loop state)192193These exist so a long-lived `tg session daemon` / MCP server process doesn't grow unbounded caches.194All are documented together in `main.py:199-200`; defaults are implementation-internal (read the195cited module if you need the exact number) — this skill's job is to tell you *that* they exist and196*where*, not to duplicate the numeric defaults, which drift independently of flags/commands.197198- `TENSOR_GREP_CPU_LITERAL_INDEX_CACHE_MAX_ENTRIES`199- `TENSOR_GREP_STRING_INDEX_CACHE_MAX_ENTRIES`200- `TENSOR_GREP_AST_QUERY_CACHE_MAX_ENTRIES`201- `TENSOR_GREP_AST_NODE_INDEX_CACHE_MAX_ENTRIES`202- `TENSOR_GREP_REPO_CONTEXT_CACHE_MAX_ROOTS`203- `TENSOR_GREP_LSP_PROVIDER_CLIENT_CACHE_MAX_ENTRIES`204- `TENSOR_GREP_LSP_PROVIDER_OPEN_DOCUMENT_MAX_ENTRIES`205206### Internal constants (not env-configurable)207208Not every load-bearing bound in `tg` is an environment variable — some are deliberately hardcoded209constants, single-sourced so multiple call sites cannot drift apart. Distinguish these from the210env-configurable knobs above before assuming a behavior can be tuned at runtime:211212- **`IMPLICIT_SEARCH_WALK_FILE_CEILING = 1500`** (DEFINED at `src/tensor_grep/io/scan_limits.py:106`;213 `io/directory_scanner.py` only RE-EXPORTS it. An earlier revision cited directory_scanner, which214 would send someone changing the ceiling to edit a re-export and wonder why nothing moved —215 matches the sibling `tensor-grep-architecture-contract` A9 wording) — the216 fast-refuse ceiling for an unscoped/defaulted-path search or `tg find` walk (A9, v1.92.3/#702). It is217 imported by both `src/tensor_grep/cli/main.py`'s `_LARGE_ROOT_SCAN_FILE_CEILING` and218 `src/tensor_grep/cli/bootstrap.py`'s `_search_paths_include_oversized_implicit_root` — one constant,219 two Python call sites, so a future change to the ceiling cannot silently desync the Typer-app path220 from the flag-less bootstrap-passthrough path. The Rust `rust_core/src/rg_passthrough.rs` keeps its221 own copy of the same numeral, synced by convention (not a shared build-time constant across the222 Python/Rust boundary) — if you ever change the Python value, grep `rg_passthrough.rs` for the223 matching literal and update it in the same PR, or the two front doors will silently disagree on224 where the ceiling sits.225- This is a distinct axis from **`TG_DIR_SCAN_MAX_ENTRIES`** (env-configurable, a different directory-226 scan bound) — do not conflate the two when reading a scan-refusal report; check which constant/env227 var actually produced the observed refusal before describing the mechanism.228229### LSP provider230231| Var | Default | Effect | Source |232|---|---|---|---|233| `TG_LSP_PROVIDER` | `native` | Overrides the LSP semantic-provider mode for editor/MCP clients; same value space as `--provider` (`native`/`lsp`/`hybrid`). Set by `tg lsp --provider ...` before calling `run_lsp()`. | `main.py:9649-9799`, `main.rs:51` |234| `TG_ALLOW_UNVERIFIED_TOOLCHAIN` | off | Security opt-out: skips checksum verification of downloaded LSP-toolchain archives/binaries (rust-analyzer, etc.) for air-gapped/offline installs — same default-secure/opt-out-to-weaken pattern as `TG_MCP_ALLOW_VALIDATION_COMMANDS` below. Off by default; fails closed (refuses the unverified binary) unless set. | `lsp_provider_setup.py:229-265,465-480` |235236## Provider modes: `native` / `lsp` / `hybrid`237238The `--provider` flag appears on every symbol/navigation command (`defs`, `refs`, `source`, `impact`,239`callers`, `blast-radius*`, `context-render`, `edit-plan`, `agent`, `lsp`) with the **same three-way240contract everywhere**, default `native`:241242```243tg defs REPO_PATH SYMBOL --provider lsp244tg blast-radius REPO_PATH SYMBOL --provider hybrid245tg lsp --provider hybrid246```247248- `native` — tg's own tree-sitter/AST-derived symbol graph. Production default.249- `lsp` — routes through an external language server (`ExternalLSPProviderManager`). **EXPERIMENTAL.**250- `hybrid` — combines native with LSP evidence when available.251252`tg lsp` validates the value explicitly and exits 2 on anything else253(`{"native", "lsp", "hybrid"}` check, `main.py:9656-9785`):254255```256Unsupported LSP provider mode; expected one of: native, lsp, hybrid257```258259**LSP-availability is not LSP-proof** — this is a load-bearing distinction from `AGENTS.md:163`:260"Treat `tg lsp-setup` / `tg doctor --with-lsp` availability as install evidence only; provider-backed261navigation must report `health_status`, `health_check`, `lsp_proof`, `lsp_evidence_status`, and262`not_lsp_proof_reason` when it falls back to native evidence. A navigation row counts as LSP proof only263when it carries `lsp_provider_response = true` from a completed provider request." Do not tell a user264"LSP is working" because `tg doctor --with-lsp` found a binary on PATH.265266## Production vs EXPERIMENTAL (default-OFF), and the guard that keeps it off267268| Axis | Status | Guard | Why |269|---|---|---|---|270| Native CPU/rg search, AST search (`tg run`), symbol nav (`native` provider) | **Production** | none — default path | Backbone of the tool. |271| `tg agent` / Actionable Context Capsule | **Production, opt-in by design** | explicit `tg agent` invocation | Not a default search mode; it's a distinct command surface, but it is a shipped, supported feature. |272| `classify` local heuristic | **Production** | default | Deterministic, no model download. |273| `--gpu-device-ids` / GPU backends | **EXPERIMENTAL** | must be explicitly requested; heuristic auto-GPU only fires when `rg` is unavailable | Slower than CPU today; no promotion-ready path (`AGENTS.md:226-234`). |274| `--provider lsp` / `--provider hybrid`, `TG_LSP_PROVIDER` | **EXPERIMENTAL** | explicit `--provider` value or `TG_LSP_PROVIDER` env | Availability ≠ working navigation; see LSP-proof contract above. |275| `TENSOR_GREP_CLASSIFY_PROVIDER=cybert`/`triton` | **EXPERIMENTAL** | explicit env opt-in | Requires a Triton/CyBERT model deployment; falls back before expensive model load if unavailable. |276| `TG_MCP_ALLOW_VALIDATION_COMMANDS=1` | **Off by design (security), not "not ready yet"** | explicit env opt-in on the MCP server process | Shell-executes `lint_cmd`/`test_cmd`, a prompt-injection surface. |277| Local hybrid semantic search (BM25 + CPU dense embeddings + RRF) | **SHIPPED, EXPERIMENTAL default-OFF** — `tg search --semantic` (`grep -n '"--semantic"' src/tensor_grep/cli/main.py` — was `:6619`, now `:7403`; `core/retrieval_dense.py` + `core/retrieval_fusion.py`) | explicit `--semantic` flag; requires the `semantic` extra (`model2vec`, `pyproject.toml:627`), fails closed with a `rank_fallback_reason` when unavailable | No API key, no GPU, pure local CPU dense leg fused with BM25 via RRF -- see `tensor-grep-semantic-search-campaign` for build history and promotion gates. A 2nd consumer of the same dense/fusion core is `tg find` (below). |278| `TG_FIND_DENSE_WEIGHT` (`tg find` only) | unset → **adaptive** (flip SHIPPED, #191/#634, first released v1.79.0): `5.0` for genuinely multi-word queries, `1.0` for single-token queries | Query-adaptive `dense_weight` for `tg find`'s `rank_chunks` calls ONLY -- gates ONLY `tg find`, never `--semantic`. Resolution (`_find_dense_weight`): unset/empty/malformed/non-finite env → `_FIND_DENSE_WEIGHT_ADAPTIVE_DEFAULT` (`5.0`, the ledger-swept 1:5 bm25:dense ratio) for a genuinely multi-word query (`len(query.split()) > 1`, the whitespace word-count gate, #191/#630); a single whitespace-free token (a literal identifier/symbol lookup) ALWAYS stays pinned at `_FIND_DENSE_WEIGHT_DEFAULT` (`1.0`) regardless of the env value; a malformed/non-finite value (`nan`/`inf`/garbage) falls to the adaptive `5.0` — treated exactly like unset, NOT clamped to `1.0` — so a typo cannot silently opt an operator out of the improved default (#634 must-fix; a non-finite value still never reaches `reciprocal_rank_fusion`'s sort). Explicit `TG_FIND_DENSE_WEIGHT=1.0` is the opt-out back to the old equal-weight fusion; any other finite value (e.g. `=3.0`) is honored verbatim. | `grep -n "_find_dense_weight\|_FIND_DENSE_WEIGHT_ADAPTIVE_DEFAULT" src/tensor_grep/cli/main.py` (env/default consts were `:4007-4008`, then `:4271-4272`, now `:4593-4594`; adaptive const now `:4600`; reader `_find_dense_weight` was `:4176-4227`, now `:4613-4684`). The old "still **default-OFF**; the flip to a non-1.0 default is a separate CEO checkpoint" framing is SUPERSEDED by the shipped flip — see `tensor-grep-semantic-search-campaign` STATUS UPDATE 4. |279280## `tg inventory`: walk-only repo manifest (v1.19.0, #343)281282`tg inventory PATH [--json] [--max-repo-files N] [--deadline SECONDS]` (`src/tensor_grep/cli/inventory.py`,283registered `main.py:8292-8404`) emits a single-pass file/byte/language/category manifest by284reusing the same gitignore-aware walker (`repo_map._iter_repo_files`) that `orient`/`callers`/285`blast-radius` trust — so counts stay truth-consistent with every other `tg` command and inherit286its `.tensor-grep`/`.git`/vendor exclusions for free.287288**`--max-repo-files` defaults to `50_000`, still well above the AST map limit** — this is a289deliberate, documented divergence, not an oversight. **The AST-side number changed underneath this290divergence** (backlog #1, 2026-07-06): `DEFAULT_AGENT_REPO_MAP_LIMIT` was raised from `512` to291**`2000`** (`repo_map.py:157`), and the CLI-side mirror `_DEFAULT_AGENT_REPO_SCAN_LIMIT` (`main.py:82`)292was raised to match — do not describe the AST cap as `512` anymore.293294- `DEFAULT_MAX_INVENTORY_FILES = 50_000` (`inventory.py:40`), passed to the CLI option as a295 literal `50_000` (`grep -n "50_000" src/tensor_grep/cli/main.py` -- **no line range**: the old `:8406-8413` pin sat INSIDE a `--deadline` option block that the 2026-08-23 de-duplication deleted outright, so it has no successor line to re-stamp to) rather than importing the constant, so the (heavy)296 `repo_map` import stays lazy. A nearby code comment still says "matching `map`'s 512 pattern" —297 that comment is about the STYLE (keep-literal, don't import), not the current live number; `map`'s298 own limit is 2000 now, not 512. A guard test pins the `50_000` literals together; re-verify with299 `grep -rn "50_000" src/tensor_grep/cli/inventory.py src/tensor_grep/cli/main.py`.300- `DEFAULT_AGENT_REPO_MAP_LIMIT = 2000` (`repo_map.py:157`) budgets a **full AST parse per file**301 for `tg map`/`orient`/`context`/`edit-plan`/session repo-map defaults — reusing it for302 `inventory` would silently truncate any repo over ~2000 files and defeat the "whole-repo303 manifest" purpose (`inventory.py:36-39` states this explicitly in a code comment).304 `inventory` is walk-only (`stat()` + an 8KB read for binary-sniffing per file), orders of305 magnitude cheaper than an AST parse, so a much higher cap (`50_000`) is still safe even after the306 AST-side raise.307- **`CALLER_SCAN_FILE_CEILING` was ALSO raised, `512`→`2000` (`repo_map.py:167-177`; backlog #57,308 2026-07-09) — the "DIFFERENT constant that stays at 512" framing this file previously used is309 itself now stale.** It remains logically separate from `DEFAULT_AGENT_REPO_MAP_LIMIT` (they just310 now happen to share a value) — the raise was safe only because `#478` had already threaded a311 `--deadline` hard-bound through the caller-scan loop, closing the task #52 ~100s-hang risk312 ("~100s on a 1941-file repo at the old 512 cap", `repo_map.py:163-164`) that originally kept this313 ceiling frozen below the map default (`repo_map.py:1638-1642`). It still backstops the flag-less314 (`--deadline` omitted) default path and a `--max-repo-files`-raised mega-repo; raising past 2000315 needs fresh cost data (`repo_map.py:167-177`). If you see the bare number `512` anywhere in this316 subsystem going forward, it is describing HISTORY — check which constant before assuming either317 reading is still live.318- Truncation is **never silent**, and it now has two distinguishable causes319 (`scan_limit.truncation_cause` in the JSON payload): `"project-files"` when `--max-repo-files`'320 count cap was hit, or `"deadline"` when `--deadline` (below) fired first; the key is always321 present, `null` when the scan completed. Text output mirrors the split (`inventory.py:348-359`): `[!] truncated at322 max_files=... (cause=project-files)` vs. `[!] stopped after the time budget (cause=deadline)` —323 both ASCII-only (fixed from a U+26A0 emoji that crashed `typer.echo` on Windows cp1252 consoles —324 `#346`, commit `6b7b518`; ASCII-only is now the rule for all `tg` CLI output, not just325 `inventory`). Either cause trips the same shared exit-2 gate, `_scan_incomplete`326 (`main.py:10817`, checked at `main.py:8329-8447`) — it only looks at `possibly_truncated`, not327 which cause fired.328- Fails closed: a nonexistent `path` raises `FileNotFoundError` -> CLI exits 1329 (`inventory.py:201-202`) — a missing path must never read as a valid empty repo.330331### `--deadline SECONDS`: the wall-clock twin of `--max-repo-files` (registered `main.py:8297-8423`)332333Threads a `deadline_seconds` float (`inventory.py:187`) into `build_inventory()` so a huge/slow334tree returns a partial, honestly-labeled manifest instead of hanging. `inventory`'s own335`--deadline` predates and is unrelated to `#585` (2026-07-14), which extended `--deadline` to336`source`/`docs-coverage`/`blast-radius-plan` instead (a disjoint set of commands) — `inventory`'s337flag shipped earlier (`#395`, issue #53), was hardened into a true wall-clock bound by `#478`338(issue #52), then had a zero-count bug fixed (next bullet) by `#516`.339340- **The walk and the per-file loop split the budget, they do not share it**341 (`inventory.py:204-223`; `_WALK_PHASE_DEADLINE_FRACTION = 0.7`, `inventory.py:48`) — the342 in-source comment calls this the "#130(a) fix" (shipped as `#516` per commit history). The walk343 phase (`_iter_repo_files`) gets only the first 70% of `deadline_seconds`; the remainder is344 reserved for the per-file `stat()`/binary-sniff loop. Before this split, a slow walk could345 consume the *entire* deadline, so the per-file loop's very first deadline check fired346 immediately and `totals.files` read `0` despite the walk having discovered real files — the347 in-source comment names the repro outright (`inventory.py:204-208`: "the 76s dogfood gap on348 `tg inventory --deadline 30`").349- Either phase running out of budget sets `truncation_cause = "deadline"` (`inventory.py:297-306`)350 — including when the walk was ALSO count-capped by `--max-repo-files`; "deadline" wins the label351 because a longer `--deadline` would help where a higher `--max-repo-files` would not. An earlier352 version of this same guard mislabeled a real 20s deadline hit on `C:/dev/projects` as a file-cap353 truncation (`inventory.py:303-304`, dogfood 2026-07-05) — exactly the bug this two-cause split354 exists to prevent from recurring.355- **Known narrow gap (low-priority, not load-bearing): the walk's initial listing of the SCANNED356 ROOT itself is not deadline-interruptible.** `_iter_repo_files` (`grep -n "^def _iter_repo_files" src/tensor_grep/cli/repo_map.py` -- `:1183` as of 2026-08-14, was `:1143`), on the357 `max_files is not None` branch `inventory` always takes (it always calls with358 `max_files=max_files + 1`, `inventory.py:231-236`), does one eager, unconditional359 `entries = list(os.scandir(normalized_root))` (`repo_map.py:1009-1010`) before any deadline360 check exists in that branch — every subsequent bucket pull IS deadline-checked361 (`repo_map.py:1052-1057`), just not this one root-level call. On a root whose own immediate362 directory listing is itself pathologically large/slow, `--deadline` cannot preempt it. Dogfooded363 against a 300k+-file workspace-union tree: `tg inventory --deadline` holds up fine per-project364 and on most roots — this edge needs a pathologically huge flat fan-out sitting directly at the365 scanned path to trigger, and is not worth a load-bearing lazy-scandir rewrite on its own; see366 `tensor-grep-large-repo-scale-campaign` for the broader deadline-scale work this sits alongside.367368Registration follows the standard 4-site table (`KNOWN_COMMANDS` in `commands.py`, native Rust369`Commands::Inventory` in `rust_core/src/main.rs`, `PUBLIC_TOP_LEVEL_COMMANDS` in370`tests/e2e/test_routing_parity.py`, the `@app.command()` in `main.py`) — see371`tensor-grep-architecture-contract` for why each site exists.372373```bash374# Re-verify the two caps and why they differ375grep -n 'DEFAULT_MAX_INVENTORY_FILES\|max_repo_files' src/tensor_grep/cli/inventory.py src/tensor_grep/cli/main.py376grep -n 'DEFAULT_AGENT_REPO_MAP_LIMIT = \|CALLER_SCAN_FILE_CEILING = ' src/tensor_grep/cli/repo_map.py377378# Re-verify --deadline registration + the walk/per-file budget split379grep -n '"--deadline"' src/tensor_grep/cli/main.py380grep -n 'deadline_seconds\|_WALK_PHASE_DEADLINE_FRACTION' src/tensor_grep/cli/inventory.py381382# Smoke-test the command against the real binary (not CliRunner)383tg inventory . --json | python -m json.tool | head -30384```385386## Checklist: adding a flag or command387388This is the single highest-value thing to get right in this repo — miss a registration site and the389new flag/command **misroutes silently**, passing CliRunner tests while breaking the real published390binary. **See `tensor-grep-architecture-contract` for the full 4-site command / 2-site search-flag391registration table and the rationale for why each site exists**, and `tensor-grep-change-control` for392the PR/merge gate around it (`AGENTS.md:178-196`) — this skill does not restate that table.393394**Worked non-registration example — `tg prepare --out FILE` (v1.93.0/#705, A12(d)).** Not every new395flag triggers all 3 registration concerns this skill and its siblings track — a useful example of396"none of the above applied" to calibrate against: adding `--out` to the already-registered `tg prepare`397command needed (1) no new 4-site command registration (the command already existed), (2) no 2-site398search-flag registration (`--out` is not a search flag — it never reaches `bootstrap`'s rg-passthrough399front door), and (3) no `SearchConfig` field-coverage classification (`prepare` doesn't build a400`SearchConfig` at all). It was a same-command, same-registration-footprint addition — a new `typer.Option`401on an existing `@app.command`, nothing else. Recognizing when a change genuinely needs none of the 3402checklist items (vs. assuming every new flag does) saves a wasted registration audit.403404### The third checklist item: native-delegation field coverage (`SearchConfig`)405406Adding a new field to `SearchConfig` (`src/tensor_grep/core/config.py`) is a **third** registration407concern, separate from the 4-site command table and the 2-site search-flag table above — and it is408easy to miss because it fails silently, not loudly.409410**Why it exists**: `_can_delegate_to_native_tg_search` (`grep -n "^def _can_delegate_to_native_tg_search" src/tensor_grep/cli/main.py` -- `:4095` as of 2026-08-14, was `:3709`) hands an entire search411off to the native `tg` subprocess, which `sys.exit()`s **before** the Python-side BM25 rerank and412the in-backend file sort ever run. Any `SearchConfig` field that is output-affecting but neither413forwarded into the native argv (`_build_native_tg_search_command` -- `grep -n "^def _build_native_tg_search_command" src/tensor_grep/cli/main.py`, `:4117` as of 2026-08-14, was `:3731`) nor listed in the refuse-tuple414`_NATIVE_TG_DELEGATION_DEFAULT_REQUIRED_FIELDS` (`grep -n "_NATIVE_TG_DELEGATION_DEFAULT_REQUIRED_FIELDS = " src/tensor_grep/cli/main.py` -- `:1966` as of 2026-08-14, was `:1894` onward) gets **silently dropped** —415the search still runs and returns a result, just the wrong one (unranked/unsorted), which is worse416than a crash because suppression reads as absence. This is the same bug class as the `-u`/`-uu`417no-op fixed in `#336`; the receipt this time was `#342` (commit `5e6f780`, v1.18.6->v1.19.0 range):418`rank_bm25` and `sort_files` were parsed but forwarded nowhere, so `tg search --rank --cpu` /419`--sort-files --cpu` silently returned unranked/unsorted output on the delegated fast path.420421**The gate is now a governance ratchet, not a convention** — `tests/unit/test_native_delegation_field_coverage.py`422AST-derives the forwarded-field set directly from `_build_native_tg_search_command`'s source (via423`ast.walk` over `config.<424425…(truncated)