tensor-grep — Large-Repo Scale-Honesty Campaign
A decision-gated runbook for the project's hardest live problem: making tg
bounded and honest at customer scale. Two failure shapes, one contract:
- Never hang. Every scan path has a wall-clock bound; when the bound trips it
returns a partial result, never spins forever.
- Never silently lie. A truncated/partial result is flagged (
result_incomplete /
partial in JSON, an exit-2 signal for symbol commands, a stderr warning) so an
agent can tell "complete zero" from "gave up early." A quietly-empty result that
reads as "no matches / no callers / dead code" is the one bug a context tool cannot
ship.
This skill is the campaign map: what already shipped, what is still open, the exact
commands + expected numbers at each gate, the wrong paths that are fenced off, and how
promotion routes through change-control. You ship nothing user-visible from this
skill without beating a measurable gate and a conscious flag-flip (Phase 4).
Why this is the live frontier (receipts)
--deadline is now a hard end-to-end wall-clock bound on the graph commands
(task #52, CLOSED by PR #478 / 67f9779, shipped v1.54.3).
Receipt (2026-07-05, on a real 1884-file TypeScript repo, pre-#396):
tg callers QueryEngine --deadline 10 took **25 s** (not ~10 s), and a direct
deadline_seconds=8 call took >90 s. Root cause: build_symbol_callers_from_map
(the caller-scan) re-reads/re-parses candidate files in an any()-loop per
definition, so "each stage is bounded" did not make the pipeline bounded. #478
closed the residual gap a verify-plan-against-code pass found after #396/#440: four
loops still had no wall-clock bound -- (A) _iter_repo_files' file-tree walk (count-only
bound, no deadline), (B) _relevant_tests_for_symbol's two unguarded any() loops (the
dominant cause on a high-fan-out symbol like "main", confirmed a 3x-recurring P0), (C)
build_symbol_impact_from_map had no deadline parameter at all, and (D) the string_refs
second pass in build_symbol_refs_from_map. All four now fold into the existing
_DeadlineBreakFlag/partial/deadline_limit machinery, guarded if deadline_monotonic is not None so an undeadlined caller sees byte-identical behavior. See S2 for what this
does and does not close (the #390 daemon-path gap is explicitly separate).
- #396 (v1.39.1) shipped a caller-scan re-parse cache +
Path.resolve() memoization
— the code comment measures ~18 s of ~22 s wall time was redundant resolve()
churn (src/tensor_grep/cli/repo_map.py:76-80), claims 7.9x on central symbols.
So the pre-#396 receipt numbers are STALE — Phase 0 re-measures at HEAD.
- The unscoped-
tg search hang was real and is now fixed, not in-flight. AGENTS.md
(grep -n "hangs ~600 s then errors" AGENTS.md; was :378, now :480 — drifted +102 lines this
pass, same-size drift as the other AGENTS.md cites below, consistent with one insertion earlier
in that file)
still narrates the pre-fix symptom ("hangs ~600 s then errors" because tg's own index dirs +
a vendored tree were not auto-excluded) — that doc lags. The fix, #400, shipped in
v1.40.4 (bb14abe) and was hardened further by #413 (v1.42.0) and #428; see
§1 below. Do not present this as an open bug or an unmerged PR.
- Whole-repo GRAPH commands (agent / callers / blast-radius / orient) are SLOW AT SCALE, not
hung — scope to a package root. On a big tree the graph is O(files):
tg agent . / tg orient .
walk + parse the whole repo. Native Windows they COMPLETE (agent ~18s on an 872-file repo,
workspace orient . ~48s on 50k files) but they are not fast. The agent-honest usage on large
trees is a package root — tg agent REPO/src "task", tg callers REPO/src SYMBOL — 3-5x faster.
Two CPU latency wins shipped 2026-07-11 (v1.63.3 deweight #534 removed a per-file resolve() hot
loop from the shared hot path; v1.63.4 parse-cache #535 deduped the 2-3x Python parse, 36%
faster on a warm re-query); the remaining scan cost is inherent and is the warm-daemon's job (#94).
- A "whole-repo hang past 60-90s" reported from WSL over
/mnt/c is a 9p-filesystem ARTIFACT,
NOT a tg deadlock. WSL reading a Windows-mounted tree is ~3-5x slower than native NTFS at the
file-walk + stat()/realpath() these commands do, which tips scope-proportional work past a test
timeout. Reproduce a WSL latency report NATIVELY (Windows or native Linux, not /mnt/c) before
treating it as a tg bug — the 2026-07-11 v1.63.2 "10-timeout P0 regression" dogfood was exactly
this (native: every flagged command completed). Memory:
tensor-grep-wsl-mnt-c-latency-artifact-2026-07-11.
- Real-world scale dogfood (2026-07-23, c:/dev workspace, 300k+ files across dozens of
nested projects — not a synthetic reference repo) confirms the shipped mechanisms hold at
genuinely large scale, not just on the TypeScript reference repo.
tg orient bounds itself
via scan_limit(2000) + centrality scoring to a useful ~2910-token capsule in 4.9 s; an
unscoped tg search degrades gracefully instead of hanging — it returns a partial result with
an honest "exceeded timeout" incomplete_reason and exits 124 (the #400 native-walk
deadline doing its job, not a new mechanism); tg inventory --deadline bounds correctly
per-project. See §2 below for the one known-open edge this same dogfood surfaced.
0. When to use this skill — and when to use a sibling instead
Use this skill when the task is bounding/honesty at scale: a hang, a --deadline
overrun, task #52, the #390 daemon gap, the exit-code partial contract, or a
latency profile of a graph command on a big repo.
| If you actually need to… |
Use this sibling instead |
| A hang/slowness whose CAUSE is unknown — systematic bisection first |
tensor-grep-debugging-playbook (then return here) |
| Single-file / small-repo micro-latency, or make a number claim-quality |
tensor-grep-benchmark-and-proof-toolkit |
| The front door / routing / registration / backend contract |
tensor-grep-architecture-contract |
| Register a new flag/command (2 front doors, 4 sites) |
tensor-grep-config-and-flags |
| Merge/release/experimental-flag gates + the incidents behind them |
tensor-grep-change-control |
| Learn a settled battle (FFI reverts, dep caps, mock-vs-real, golden-sensitivity) |
tensor-grep-failure-archaeology |
| Build/run the toolchain (uv, maturin, cargo) |
tensor-grep-build-and-env |
| Update README/AGENTS/docs after shipping |
tensor-grep-docs-and-writing |
| Position externally (never "faster grep") |
tensor-grep-release-and-positioning |
This skill never routes around change-control. It produces the evidence; the
flip is a tensor-grep-change-control decision (Phase 4).
1. What already shipped (verify each before you build on it)
The P0-6 "moat" deadline program (#384-#401) + the now-shipped unscoped-hang fix (#400,
#413, #428, #702). Every anchor below is at HEAD on 2026-07-24 (v1.96.0); re-verify line numbers,
they drift — main.py is 17032 lines and repo_map.py is 18752 lines as of this pass, do not
trust an older total. Both files grew substantially (repo_map.py by ~1850 lines) across the
Java/C#/PHP language-support campaign (#719-#726) landed since the last full citation pass at
v1.93.2 — several anchors below drifted by 1000+ lines and have been re-pinned in this pass.
| Ship |
What it does |
Verify |
| #384-#388 deadline threading |
deadline_seconds -> _deadline_monotonic_from_seconds -> build_repo_map(deadline_monotonic=...); converted once to an absolute time.monotonic() stamp so the scan can self-bound and return partial. |
grep -n deadline_seconds src/tensor_grep/cli/repo_map.py (e.g. _deadline_monotonic_from_seconds at grep -n "^def _deadline_monotonic_from_seconds" src/tensor_grep/cli/repo_map.py (:768 as of 2026-08-14), was :752; build_repo_map's own deadline_monotonic param at grep -n "^def build_repo_map" src/tensor_grep/cli/repo_map.py (def :7669 as of 2026-08-14), was :7427 — no longer 1-line churn; re-grep rather than trust either number) |
#389/#393 graph-command CLI --deadline |
tg callers / refs / impact / blast-radius gained --deadline FLOAT; #393 bounds the caller-scan traversal itself. |
tg callers --help shows --deadline FLOAT RANGE; source: grep -n "^def callers|^def refs|^def impact|^def blast_radius" src/tensor_grep/cli/main.py → callers:12467, refs:12360, impact:12155, blast_radius:12853 (was main.py:11629/11645/11440/12083 — all four drifted +715/+715/+715/+770 lines; re-grep, do not trust these numbers either) |
#394 payload result_incomplete |
Truncation stamped at the payload layer so MCP/_json consumers see it, not just the CLI. |
grep -n result_incomplete src/tensor_grep/cli/repo_map.py |
#395 tg inventory --deadline |
inventory walk wall-clock bounded. |
tg inventory --help shows --deadline; source grep -n "^def inventory" src/tensor_grep/cli/main.py → :8823 (was :8414, drift +409), build_inventory(..., deadline_seconds=...) at cli/inventory.py:185 (was :183, drift +2) |
| #396 caller-scan cache |
_mtime_aware_cache (mtime+size in key) + _resolved_path_str lru_cache(8192) + _module_aliases_for_path lru_cache(16384)->frozenset (PR #345). 7.9x on central symbols. |
grep -n "_mtime_aware_cache|_resolved_path_str|_module_aliases_for_path" src/tensor_grep/cli/repo_map.py → defs at :108/:101/:8953 as of 2026-08-14 (was repo_map.py:100/93/8693 at the 2026-07-24 pass, was repo_map.py:99/92/8181 before that -- _module_aliases_for_path keeps drifting) |
| #398/#399/#401 exit semantics |
#398 exit 2 on ANY truncated partial; #399 walked it back to exit 2 only when the partial is also EMPTY (found-but-capped exited 0); #401 reverted #399 — a UNANIMOUS design council restored exit 2 on ANY truncated/partial result REGARDLESS of whether matches were found ("truncation trumps found": an agent must never trust a capped caller-set as exhaustive). This is the CURRENT, final contract — do not describe #399's found-exits-0 behavior as current. |
grep -n "^def _emit_symbol_command_result" src/tensor_grep/cli/main.py → :11762 (function body runs to :11832; was main.py:10931-11099, drift +708 lines); docs/CONTRACTS.md:157 (was :156, drift +1 — grep -n "Symbol-command exit codes are a three-state agent contract" docs/CONTRACTS.md) |
#400 unscoped-hang fix (e7f18b7) — shipped v1.40.4 (bb14abe), hardened by #413 (v1.42.0) and #428 |
(A) _SKIP_DIR_NAMES excludes _tg_refs / .tg_semantic_index / external_repos (grep -n "^_SKIP_DIR_NAMES" src/tensor_grep/cli/repo_map.py -- :203 as of 2026-08-14, was :195, was :194 before that); (B) native per-file search walk got a wall-clock bound — compute_native_walk_deadline / native_walk_deadline_exceeded (backends/cpu_backend.py), checked per file, breaks to a partial with result_incomplete+stderr warning (grep -n native_walk_deadline_exceeded src/tensor_grep/cli/main.py -- :8627-8634 as of 2026-08-14, was :8248-8270, was :7979-7992 before that); (C) _should_refuse_unbounded_vendored_root_scan (grep -n "^def _should_refuse_unbounded_vendored_root_scan" src/tensor_grep/cli/main.py -- :5527 as of 2026-08-14, was :5205; backed by the O(top-level-entries)-only probe _root_top_level_vendored_dir_names at grep -n "^def _root_top_level_vendored_dir_names" src/tensor_grep/cli/main.py -- :5508 as of 2026-08-14, was :5186; exit 2, <1s — never walks) refuses a root with node_modules/vendor/external_repos/third_party at top level, duplicated by design into bootstrap.py's _search_paths_include_vendored_root (grep -n "^def _search_paths_include_vendored_root" src/tensor_grep/cli/bootstrap.py -- :1082 as of 2026-08-14, was :998) because that front door fast-paths native/rg past main.py (the recurring "two front doors" class) — both guards import the same UNBOUNDED_VENDORED_ROOT_DIR_NAMES set, now DEFINED in io/scan_limits.py:60 (re-verified exact this pass) and merely re-exported from io/directory_scanner.py, as the single source of truth so they cannot drift apart. #413 added a bounded-scandir instant-refusal for a large single-project root (no vendored top-level dir but still huge); #428 ported the same walk-deadline/refusal into the MCP surface (tg_search/tg_ast_search had never inherited it). |
tg --version (expect >= 1.40.4); git log --oneline --all | grep -i '#400|#413|#428' |
#478 (67f9779, shipped v1.54.3) -- CLOSES #52, the 4 residual unbounded loops |
(A) _iter_repo_files' file-tree walk gains deadline_monotonic/deadline_hit params (was count-only bound); build_inventory now computes its deadline BEFORE the walk, not after. (B) _relevant_tests_for_symbol's two unguarded any() loops (the dominant cause on a high-fan-out symbol like "main") now break on a shared deadline. (C) build_symbol_impact_from_map (grep -n "^def build_symbol_impact_from_map" src/tensor_grep/cli/repo_map.py -- :16797 as of 2026-08-14, was :16458, was :16441 before that) gained a deadline_monotonic parameter it never had, threaded into its _preferred_definition_files/_relevant_tests_for_symbol calls, plus a new partial/deadline_limit payload block; build_symbol_impact/build_symbol_blast_radius_from_map updated to pass it through. (D) the string_refs second pass in build_symbol_refs_from_map folds into the existing refs_scan_deadline_hit local. All four guarded if deadline_monotonic is not None (byte-identical no-op otherwise). Scope note: the design doc explicitly keeps session_store.py (the daemon call sites) out of scope -- see #390 in S2, which #478 narrows but does not close. |
git show --stat 67f9779; `grep -n "deadline_monotonic" src/tensor_grep/cli/repo_map.py |
| tg find (v1.77.0, #189, main.py:4728-4837, was :4574-4676, drift +147 lines) — whole-repo hybrid NL search, a NEW command that reuses this campaign's bounding shape from day one rather than retrofitting it later | Takes --deadline/--max-repo-files plus an internal corpus-wide chunk cap (_FIND_CORPUS_CHUNK_CAP); a truncated scan sets result_incomplete=true and exits 2 (found-but-truncated prints results THEN exits 2 -- same "truncation trumps found" rule as #401 below, not the found-exits-0 shape #399 walked back). Unlike tg search, tg find does NOT get the instant vendored/workspace-root refusal (#400) -- it always attempts the bounded scan rather than refusing outright, because ranking the whole repo (not raw-text matching it) is the command's entire point. | tensor-grep-run-and-operate §11c has the full exit-contract prose; grep -n "^def find" src/tensor_grep/cli/main.py → :4721 |
| #702 (v1.92.3, A9) -- a 4th unscoped front-door fix alongside #400/#413/#428, closing a gap those three never covered | bootstrap._run_rg_passthrough (bootstrap.py:1337, was :1088, drift +249) -- the plain flag-less search front door that runs before main.py's Typer app is ever reached -- had NO walk ceiling at all until this shipped; the three existing main.py-side refusal guards (#400/#413) never ran for this path. Fixed by adding bootstrap._search_paths_include_oversized_implicit_root (bootstrap.py:1053, was :804, drift +249, the same-size shift as _run_rg_passthrough above -- consistent with one insertion earlier in the file), gated on paths_defaulted, sharing the SAME IMPLICIT_SEARCH_WALK_FILE_CEILING = 1500 constant (io/scan_limits.py:106, re-verified exact this pass, re-exported via io/directory_scanner.py) the other 3 doors already used -- natively reproduced, not a WSL artifact. An over-ceiling implicit root now refuses in ~1.7s instead of silently walking up to the 60s TG_RG_TIMEOUT_SECONDS backstop. | grep -n _search_paths_include_oversized_implicit_root src/tensor_grep/cli/bootstrap.py; grep -n IMPLICIT_SEARCH_WALK_FILE_CEILING src/tensor_grep/io/scan_limits.py |
Merge/release state to stamp every session: #400/#413/#428/#702 are all in the installed
binary as of v1.96.0 — this is not a source-only or in-flight fix. If a future session finds
a NEW in-flight PR referenced by this skill, do not describe it as shipped until
git log --oneline origin/main | head shows a chore(release) commit above it (see the
project's merge-gate guardrail: an open PR is guidance, not a receipt, until it lands on main).
2. Still open (the campaign's actual work)
- #52 — end-to-end deadline ineffective on a large TS repo — CLOSED by PR #478
(
67f9779, shipped v1.54.3). See the shipped-table row in S1 for the four loops it
bounded (A: file-tree walk, B: _relevant_tests_for_symbol, C: build_symbol_impact_from_map,
D: string_refs second pass). This supersedes the likely-closed framing this skill
previously carried after #396/#440/the CALLER_SCAN_FILE_CEILING chokepoint alone — those
three narrowed the gap but a verify-plan-against-code pass still found the four loops above
live and unbounded; #478 is the commit that actually closes it. Still do a one-time fresh
Phase-0 re-measure on a real large TS repo before citing a specific wall-clock number in a
benchmark claim or PR -- "the mechanism is closed" and "I have re-confirmed the number on my
reference repo" are different claims; S3 Phase 0 gives the exact command.
- #390 — daemon-path deadline gap — CLOSED (task #203, shipped #647/#652/#653, v1.81.2-.4).
This was framed OPEN in earlier passes of this skill — that framing is now STALE and risks a
future session re-implementing already-shipped code. Verify directly rather than trusting
either framing:
session_store.py (the daemon's request-dispatch module) now threads a real
deadline_monotonic value into its build_symbol_impact_from_map (session_store.py:1449, was
:1276, drift +173) and
build_symbol_callers_from_map (session_store.py:1490, was :1317, drift +173) call sites — the code comment at
session_store.py:1484-1488 (was :1311-1315, drift +173) states explicitly: "the exact #390 daemon-path shape this task
(#203) closes." The same threading was extended to refs/blast-radius/file_importers
(search session_store.py for WARM_DAEMON_DEFAULT_DEADLINE_SECONDS to see every site). A
daemon-served graph query on a cached session repo_map is now bounded the same way a fresh
CLI invocation is — the #478 design doc's explicit "session/daemon territory out of scope" carve-out
from Candidate (c) below has since been executed as its own follow-up, not left open.
Candidate (c) below is retained as the historical solution-menu entry that predicted this fix's
shape, not as an open task — do not re-propose closing #390 as new work.
- Default budget. The native-walk bound reuses
configured_ripgrep_timeout_seconds(),
which now defaults to 60 s (subprocess_policy.py:75, confirmed exact this pass, was 600 s).
AGENTS.md:480 (was :378, drift +102 -- grep -n "hangs ~600 s then errors" AGENTS.md)
still narrates the pre-#400 "600 s" symptom — that doc lags; the resolver is the source of
truth.
- NEW (surfaced by the 2026-07-23 c:/dev dogfood receipt above) — a pathological
workspace-union root can still blow an
inventory --deadline budget, even though
#478 bounded _iter_repo_files's walk. tg inventory always calls build_inventory
with an explicit integer max_files (default DEFAULT_MAX_INVENTORY_FILES = 50_000,
cli/inventory.py:42/188 — was :40/186, both +2 lines, never None), so _iter_repo_files (grep -n "^def _iter_repo_files" src/tensor_grep/cli/repo_map.py -- :1183 as of 2026-08-14, was :1144)
always takes its if max_files is not None: branch (grep -n "if max_files is not None:" src/tensor_grep/cli/repo_map.py -- :1209 as of 2026-08-14, was :1170), whose FIRST
step is a single blocking list(os.scandir(normalized_root)) (grep -n "list(os.scandir(normalized_root))" src/tensor_grep/cli/repo_map.py -- :1211 as of 2026-08-14, was :1172) with
no deadline check before or during that one call — the per-bucket
deadline_monotonic check (grep -n "loop A" src/tensor_grep/cli/repo_map.py -- :1256-1260 as of 2026-08-14, was :1220-1221) only runs AFTER this initial
top-level listing has already fully materialized. On an ordinary repo this is instant;
on a workspace-union root whose TOP LEVEL itself fans out to a huge number of entries
(e.g. a parent folder of many independently-cloned projects, not a normal nested tree),
that single scandir() call can burn the whole --deadline budget before any check
runs. Triage: rare, and NOT worth a load-bearing fix right now — the fix (a
lazy/incremental os.scandir iterator instead of list(...)) would touch a hot,
heavily-profiled walk path for an edge case that only bites an already-discouraged
usage (scanning a multi-project workspace ROOT instead of a package root; tg orient
/ tg agent already advise narrowing via suggested_scope/workspace_root_detected,
see docs/CONTRACTS.md). Do not silently re-open this as a new #52 — it is a
narrower, already-diagnosed gap in a single branch of one function, not an
end-to-end unbounded pipeline.
3. The phased runbook (decision-gated)
Run in order. Each gate states the expected observation and where to branch.
PowerShell is the dev-box shell; uv run is cross-platform. Use uv run --no-sync
so a bare uv run does not re-sync away the [dev] tree (tensor-grep-build-and-env).
Phase 0 — Reproduce the baseline on a REAL large repo (NEVER skip)
You cannot claim "bounded" without the unbounded number, and you cannot claim a fix
without the pre-fix number. Use a public large TS repo so this is reproducible
(never a private customer path).
# thousands of .ts files, deep import graph — a public customer-scale proxy
# (any large TS repo works; substitute one with >~1500 .ts files if you prefer)
git clone --depth 1 https://github.com/microsoft/TypeScript C:\tmp\ts-ref
$repo = "C:\tmp\ts-ref\src"
# Wall-clock the bounded command against its own budget (source or the installed >=v1.40.4
# binary both have #400/#440; either works, source keeps you at HEAD for a just-landed fix):
Measure-Command { uv run --no-sync python -m tensor_grep callers $repo Node --deadline 10 --json | Out-Null }
Expected + gate:
- GATE 0 (the #52 test): total elapsed should be <= deadline + ~10% (i.e.
~11 s for
--deadline 10). Read the JSON: partial/result_incomplete must be
true iff the scan was actually truncated, and a truncated result must carry a
non-empty incomplete_reason/caveat.
- If elapsed >> deadline (e.g. 25 s for a 10 s budget) -> a NEW regression, not #52
reopened, and not #390 either -- #52 is CLOSED at HEAD (#478/
67f9779, S1/§2), built
specifically to prevent exactly this shape across all four loops (A/B/C/D), and #390 (the
daemon-path gap) is ALSO CLOSED (task #203, S1/§2 above) -- a session-served query is bounded
the same way a fresh CLI one is now, so do not reach for "still-open #390" as the explanation.
A live overrun here means a new unbounded loop was introduced since #478/#203; if you can
reproduce it specifically on a session-served/daemon query and NOT on a fresh CLI one, that is a
fresh, narrower finding (a regression in the #203 threading), not a reopening of #390 itself.
Proceed to Phase 1 to find where the budget leaks.
- If elapsed is bounded AND truncation is honestly flagged -> matches the current
expectation (§2: #52 closed). Do NOT just declare victory: re-run on a second
large repo and a central symbol (highest fan-in), confirm the exit code matches
docs/CONTRACTS.md:157 (was :156, drift +1; exit 2 fires on ANY truncation, found or not — §5), then route
promotion of the "closed" claim through Phase 4 / change-control.
- If it HANGS (no return, no error) on an unscoped
tg search on a root with a
vendored dir -> this is a NEW bug, not the old #400 shape: #400's instant refusal
(_should_refuse_unbounded_vendored_root_scan, exit 2, <1s) plus the native-walk
deadline are shipped in the installed tg binary (>=v1.40.4) as well as source, so a
hang here on EITHER means the fix regressed -> tensor-grep-debugging-playbook.
Phase 1 — Phase-instrument the ACTUAL slow command (do NOT guess)
Profile the command Phase 0 flagged, on the same repo. cProfile is the oracle here
(the graph commands do not all expose --profile; only context-render and
blast-radius-render do — verified against tg --help). This invocation is verified
to run and print stats:
# tottime = internal (self) time -> finds the true hot function.
# tg's own output prints first; the profile table is APPENDED at the end -> tail it.
uv run --no-sync python -m cProfile -s tottime -m tensor_grep callers $repo Node --deadline 30 2>&1 | Select-Object -Last 30
Expected + gate (verified shape on a real repo): the top tottime rows are the
per-file parse — on Python targets {built-in method builtins.compile}, ast.walk,
and repo_map.py:2127(_python_imports_and_symbols) (was :1921, drift +206); on TS targets the analog is the
tree-sitter parse via repo_map.py:2271(_typescript_parser) (was :2065, drift +206) — invoked many times,
because build_symbol_callers_from_map re-parses candidate files in its any()-loop
(repo_map.py:3639 documents the "N definitions -> N re-reads/re-parses" hazard, was :3407-3409,
drift +231 — grep -n "N definitions means N re-reads" src/tensor_grep/cli/repo_map.py).
- GATE 1a — caller-scan re-parse dominates (many parse calls, high
ncalls on the
parse/resolve functions): the leak is the re-parse loop, not the one-shot map
build -> Solution menu candidate (a) or (b) in Phase 2.
- GATE 1b — a single
build_repo_map pass dominates (parse called ~once per file,
not per definition): the map build itself is the cost -> the fix is bounding/caching
build_repo_map, which #384-#388 already partly did via deadline_monotonic;
re-measure whether the deadline is honored INSIDE that pass -> different branch --
a NEW gap, since #390 (the daemon-path deadline gap) is CLOSED (S1/§2 above), not an
open "#390 gap" to attribute this to.
- GATE 1c —
resolve() / path work dominates despite #396: the cache is being
defeated (e.g. an uncached resolve() before the cache lookup — repo_map.py:2712
warns about exactly this, was :2506-2507, drift +206 — grep -n "this resolve() runs BEFORE the re_export_cache lookup" src/tensor_grep/cli/repo_map.py) -> candidate (b), fix the cache ordering.
Redundancy measurement before you design a cache (candidate b obligation):
monkeypatch the parse function with a collections.Counter keyed by path and count
how many times each file is re-parsed for one callers call. This is the technique
that overturned a code-review guess in PR #345 (see fenced paths). Numbers first,
then design the key.
Phase 2 — Solution menu (RANKED), each with a proof obligation
Pick top-down. Each candidate carries an obligation you must verify/measure, not
assume, before building.
Candidate (a) — bound the caller-scan re-parse loop (preferred)
Apply the same per-file deadline check #400 used for the native search walk to the
caller-scan any()-loop: check native_walk_deadline_exceeded (or an equivalent
deadline_monotonic) once per file, and on expiry break to a partial.
- Obligation: the partial must be fail-closed and flagged — set
result_incomplete = True + a concrete incomplete_reason, and let
_emit_symbol_command_result (grep -n "^def _emit_symbol_command_result" src/tensor_grep/cli/main.py -- :11722 as of 2026-08-14, was :11762 at the 2026-07-24 pass, was :11298 before that) apply the exit contract: per §5, ANY
truncated/partial result exits 2, regardless of whether it's empty or non-empty
(#401 — do not build toward the old #399 "found-but-capped exits 0" shape). Mirror
#400's shape (main.py:8137-8270, was :7979-7992): break, never return a clean empty.
- Why preferred: it directly closes #52 with a mechanism already proven in-tree; low
blast radius; deterministic. This is what #478 (
67f9779) actually shipped -- the same
per-file-deadline-check shape, applied to all four residual loops (S1/S2). This menu entry
stays useful as the template for the next #52-shaped finding, not just a historical proposal.
Candidate (b) — extend the #396 caching to the residual re-parse
If Phase 1 shows re-parses that #396's caches miss, widen coverage.
- Obligation — cache-key correctness: a repo-map cache MUST key on file
mtime+size (use the existing
_mtime_aware_cache (grep -n "^def _mtime_aware_cache" src/tensor_grep/cli/repo_map.py -- :108 as of 2026-08-14, was :100), not a plain
lru_cache keyed on path alone — a plain cache returns stale results in the
long-lived daemon (the code comments at repo_map.py:30-34 + the
_MTIME_CACHE_CLEAR_REGISTRY declaration (grep -n "^_MTIME_CACHE_CLEAR_REGISTRY" src/tensor_grep/cli/repo_map.py -- :64 as of 2026-08-14, was :55) document this trap
and the sweep). Prove the redundancy with the
Counter-monkeypatch (Phase 1) BEFORE adding the cache, and prove correctness with a
mutate-file-then-re-query test.
- Caching bounds the common case but does not bound a pathological single file —
ship it WITH candidate (a), not instead of it.
Candidate (c) — close the #390 daemon-path deadline gap — SHIPPED (task #203, #647/#652/#653, v1.81.2-.4); retained here as historical record, not an open candidate
This entry correctly predicted the shape of the eventual fix and is kept for that reason, but the
work itself is DONE — do not re-propose it as new. What actually shipped: session_store.py's
daemon call sites (build_symbol_impact_from_map at session_store.py:1449, was :1276, drift +173,
build_symbol_callers_from_map at session_store.py:1490, was :1317, drift +173, plus refs/blast-radius/
file_importers siblings) now pass a real deadline_monotonic value
(WARM_DAEMON_DEFAULT_DEADLINE_SECONDS-derived) into every builder call — exactly the call-site
wiring this candidate originally scoped as the remaining work after #478's builder-side prerequisite.
See S2 above for the closing citation (session_store.py:1484-1488's own code comment, was :1311-1315, names task
#203 as the closer). If a NEW daemon-served command is added in the future, use this shipped pattern
as the template — thread deadline_monotonic at the call site, mirroring the sibling commands in
the same file, rather than assuming the daemon path is unbounded by default going forward.
Candidate (d) — replace the regex/slow TS parse with tree-sitter (HIGHEST RISK — fenced)
Only if (a)+(b)+(c) leave the parse itself as the irreducible hotspot.
- Obligation (mandatory, before any swap): golden parity across the parser corpus.
A parser change alters symbol/import extraction for every downstream command; prove
byte-identical (or explicitly-diffed-and-accepted) output on the AST parity corpus
first (
benchmarks/run_ast_parity_check.py). Do this in a fenced branch; do not mix
it with a bounding fix.
Phase 3 — Build behind the fail-closed contract + measure the gate
- Write the failing test first (
tests/unit/test_repo_map_targets.py,
tests/unit/test_cli_modes.py are the patterns #400 used). TDD, then the smallest fix.
- Honor the Backend Fail-Closed / partial contract (§4): a bound that trips
produces a flagged partial, never a silent empty, never a raw crash.
- Re-run Phase 0 + Phase 1 on the reference repo after the fix.
- A6 — anti-hang test discipline (a hang-class regression test can itself hang): when the
red test is "this used to hang/overrun," wrap it so a still-broken fix fails FAST instead of
wedging the test run. (1) layer an OUTER shell timeout (
--kill-after=Ns) around an INNER
per-test timeout implemented via a thread/watchdog, not signal (signal-based timeouts are a
no-op on Windows and under the GIL for CPU-bound native calls — this repo runs CI and dev on
Windows). (2) either land the fix before the red test lands, or land the red test already
wrapped in both timeouts. (3) never leave an unbounded loop/subprocess-spawn/backtracking
regex in the TEST ITSELF without its own bound — the test must fail the same way the product
bug does (bounded, flagged), not hang the CI runner. (4) distinguish "slow but protected" from
"hung" by EXIT CODE (124/137 = killed) not elapsed wall-clock, which is noisy on a shared
runner. (5) apply the same two-layer timeout in CI as locally — a hang that only reproduces in
CI because the outer timeout was dev-box-only is the worst kind to debug.
Promotion gate (all must hold, measured on the reference repo, same symbol):
| Metric |
Requirement |
| Wall-clock vs budget |
elapsed <= --deadline + ~10% (the #52 bar) |
| Truncation honesty |
partial/result_incomplete set iff truncated; non-empty incomplete_reason |
| Exit code |
matches docs/CONTRACTS.md:157 (was :156, drift +1) (0 complete / 1 complete-not-found / 2 incomplete — regardless of whether anything was found, per #401) |
| No regression |
benchmark-regression CI gate green (benchmarks/check_regression.py); correctness identical on the AST parity corpus |
| Real binary |
dogfood via scripts/dogfood/ on the REAL artifact, not CliRunner |
- GATE 3: if the wall-clock still overruns the budget OR a partial isn't flagged,
do not ship — the honesty contract is the whole point. Iterate or record the
negative result.
Phase 4 — Promote through change-control (never here)
This skill produces evidence; tensor-grep-change-control owns the flip.
- One release-bearing PR per tick (respect the push-race / one-merge-per-tick rule,
tensor-grep-release-and-positioning).
- Attach Phase 3 evidence (before/after wall-clock table + exit-code proof + parity).
- Re-dogfood on the REAL large repo before declaring the contract done — this is
the #399 lesson: #398 shipped an exit-code rule that had to be walked back one release
later because the real-repo behavior (every large-repo query exiting 2) was wrong.
- Update
docs/CONTRACTS.md / AGENTS.md / SESSION_HANDOFF.md if the contract
changed (tensor-grep-docs-and-writing).
4. Fenced-off wrong paths (do NOT do these)
| Forbidden |
Why |
Do instead |
| Raise the default timeout to "fix" a hang |
Masks, doesn't bound. A bigger number still hangs on a bigger repo; it just moves the wall. |
Add a real per-file wall-clock bound that returns a flagged partial (#400 pattern). |
| Return a silent empty / clean 0-result on timeout |
Violates the Backend Fail-Closed Contract (AGENTS.md, grep -n "^## Backend Fail-Closed Contract" AGENTS.md → :2090; was :438-440, drift +1233 — a cross-file citation this far stale is worth a grep, not a re-stamped number). A partial that reads as "no matches / no callers / dead code" is the exact bug this campaign exists to kill. |
result_incomplete = True + incomplete_reason + stderr warning + the exit-2-regardless-of-found contract (§5). |
| Assume "each stage bounded => pipeline bounded" |
The #52 lesson: build_repo_map and caller-scan were each bounded in isolation, yet the end-to-end command overran because the caller-scan re-parses ~all files. |
Measure the WHOLE command wall-clock (Phase 0), then profile it (Phase 1). |
| Guess the hotspot from code review |
PR #345 receipt: a review council guessed a 3.6%-of-runtime path; the live profile found the real one (_module_aliases_for_path called ~1.4M times) -> lru_cache+frozenset cut the run 61.7 s -> 12.8 s (4.8x). |
Profile the ACTUAL slow command at scale (Phase 1); the profiler is the oracle. |
| "Fix" a golden to match your dev box |
#363 receipt: a "stale" golden was edited to include rg submatches, then reverted — the golden is backend-sensitive and CI has no rg. CI is the oracle. |
Read the failing CI job's -/+ diff first; force a deterministic backend in the test. tensor-grep-failure-archaeology. |
A plain lru_cache keyed on path in the daemon |
Returns stale results across a long-lived session when a file changes (repo_map.py:30-34, 55). |
_mtime_aware_cache (mtime+size in key) + register its cache_clear in the sweep registry. |
| Ship the parser swap (candidate d) with a bounding fix |
Conflates a correctness-risky change with a latency fix; a parity regression would hide behind the perf win. |
Fence the parser swap in its own branch, gated on AST parity FIRST. |
| Route around change-control / auto-merge |
Non-negotiable. Autonomy is draft-PR-only. |
Produce evidence here; let change-control gate the flip. |
5. The contract you are defending (exit codes + fail-closed)
Symbol-command exit codes are a 3-state agent contract (docs/CONTRACTS.md:157, was :156, drift +1,
enforced at main.py:11639-11832, was :11054-11099, drift +708 — grep -n "^def _emit_symbol_command_result" src/tensor_grep/cli/main.py). This is the current, FINAL shape — #401 reverted #399,
so exit 2 fires on ANY truncated/partial result, whether or not it found something:
| Exit |
Meaning |
Agent action |
0 |
complete result — the scan was NOT truncated |
trust the findings as exhaustive |
1 |
genuine not-found on a complete scan |
the symbol truly is absent |
2 |
INCOMPLETE — truncated by --deadline (partial: true) or a --max-repo-files/scan cap (result_incomplete: true) — REGARDLESS of whether anything was found |
do NOT treat as exhaustive even if it has results (a truncated caller-set is not a safe blast-radius); parse the JSON and retry with a larger budget or a narrower scope |
Do not describe a "found-but-scan-capped result exits 0" behavior anywhere — that was #399,
walked back by #401 after a unanimous design council concluded truncation must always trump
"found something," so a caller/blast-radius consumer can never mistake a capped result for a
complete one. The native-walk bound (#400) is the search-side analog: on expiry it sets
all_results.result_incomplete = True + incomplete_reason, writes a stderr warning,
and breaks (main.py:8137-8270, was :7979-7992) — a flagged partial, never a silent empty. Any new
bound you add MUST follow this shape (see tensor-grep-architecture-contract for the
full BackendExecutionError contract).
tg find (v1.77.0, #189) followed this shape from its first shipped version — a genuine
example of a new command adopting this campaign's contract instead of retrofitting it: any
--deadline/--max-repo-files/internal chunk-cap truncation sets result_incomplete=true and
exits 2, whether or not ranked matches were found. It is bounded but NOT refusal-gated the way
tg search is (#400's instant vendored/workspace-root refusal does not apply) — see the §1
shipped table.
6. When NOT to use this skill
- A hang/slowness whose cause you don't yet know — do the systematic bisection in
tensor-grep-debugging-playbook first; come back here once it's
…(truncated)
1---2name: tensor-grep-large-repo-scale-campaign3description: Use when tg hangs, stalls, or runs for minutes on a large/unscoped repo; when `--deadline` "seems ignored" and a symbol query still overruns its budget; when working task #52 (CLOSED, end-to-end deadline ineffective on a ~1800-file TS repo) or #390 (CLOSED, the daemon-path deadline gap) for historical context, caller-scan / build_repo_map latency, the unscoped-`tg search` hang, the generic >1500-file implicit-root fast-refuse (A9/#702), or the exit-2 partial-result semantics. The decision-gated campaign to finish agent-native SCALE HONESTY: tg must never hang and never silently lie (return an empty/partial result as if it were complete) on a customer-scale repo. Gives the reproduce -> phase-instrument (cProfile) -> ranked solution menu -> fail-closed build -> change-control promotion runbook with exact commands, expected numbers, and branch-on-mismatch forks. Verified against v1.110.14 (2026-08-11) on 2026-07-24 (#400/#413/#428/#702 fully shipped; #52 and #390 both CLOSED; the exit-code contract is exit-2-reg4---56# tensor-grep — Large-Repo Scale-Honesty Campaign78A decision-gated runbook for the project's hardest **live** problem: making tg9**bounded and honest at customer scale.** Two failure shapes, one contract:10111. **Never hang.** Every scan path has a wall-clock bound; when the bound trips it12 returns a **partial** result, never spins forever.132. **Never silently lie.** A truncated/partial result is **flagged** (`result_incomplete` /14 `partial` in JSON, an exit-2 signal for symbol commands, a stderr warning) so an15 agent can tell "complete zero" from "gave up early." A quietly-empty result that16 reads as "no matches / no callers / dead code" is the one bug a context tool cannot17 ship.1819This skill is the campaign map: what already shipped, what is still open, the exact20commands + expected numbers at each gate, the wrong paths that are fenced off, and how21promotion routes through change-control. **You ship nothing user-visible from this22skill without beating a measurable gate and a conscious flag-flip** (Phase 4).2324---2526## Why this is the live frontier (receipts)2728- **`--deadline` is now a hard end-to-end wall-clock bound on the graph commands29 (task #52, CLOSED by PR #478 / `67f9779`, shipped v1.54.3).**30 Receipt (2026-07-05, on a real ~1884-file TypeScript repo, **pre-#396**):31 `tg callers QueryEngine --deadline 10` took **~25 s** (not ~10 s), and a direct32 `deadline_seconds=8` call took **>90 s**. Root cause: `build_symbol_callers_from_map`33 (the caller-scan) **re-reads/re-parses** candidate files in an `any()`-loop per34 definition, so "each stage is bounded" did **not** make the pipeline bounded. #47835 closed the residual gap a verify-plan-against-code pass found after #396/#440: four36 loops still had no wall-clock bound -- (A) `_iter_repo_files`' file-tree walk (count-only37 bound, no deadline), (B) `_relevant_tests_for_symbol`'s two unguarded `any()` loops (the38 dominant cause on a high-fan-out symbol like `"main"`, confirmed a 3x-recurring P0), (C)39 `build_symbol_impact_from_map` had no deadline parameter at all, and (D) the `string_refs`40 second pass in `build_symbol_refs_from_map`. All four now fold into the existing41 `_DeadlineBreakFlag`/`partial`/`deadline_limit` machinery, guarded `if deadline_monotonic42 is not None` so an undeadlined caller sees byte-identical behavior. See S2 for what this43 does and does not close (the #390 daemon-path gap is explicitly separate).44- **#396 (v1.39.1) shipped a caller-scan re-parse cache + `Path.resolve()` memoization**45 — the code comment measures **~18 s of ~22 s** wall time was redundant `resolve()`46 churn (`src/tensor_grep/cli/repo_map.py:76-80`), claims **7.9x on central symbols.**47 So the pre-#396 receipt numbers are STALE — **Phase 0 re-measures at HEAD.**48- **The unscoped-`tg search` hang was real and is now fixed, not in-flight.** `AGENTS.md`49 (`grep -n "hangs ~600 s then errors" AGENTS.md`; was `:378`, now `:480` — drifted +102 lines this50 pass, same-size drift as the other `AGENTS.md` cites below, consistent with one insertion earlier51 in that file)52 still narrates the pre-fix symptom ("hangs ~600 s then errors" because tg's own index dirs +53 a vendored tree were not auto-excluded) — that doc lags. The fix, **#400**, shipped in54 **v1.40.4** (`bb14abe`) and was hardened further by **#413** (v1.42.0) and **#428**; see55 §1 below. Do not present this as an open bug or an unmerged PR.56- **Whole-repo GRAPH commands (agent / callers / blast-radius / orient) are SLOW AT SCALE, not57 hung — scope to a package root.** On a big tree the graph is O(files): `tg agent .` / `tg orient .`58 walk + parse the whole repo. Native Windows they COMPLETE (agent ~18s on an 872-file repo,59 workspace `orient .` ~48s on 50k files) but they are not fast. The agent-honest usage on large60 trees is a package root — `tg agent REPO/src "task"`, `tg callers REPO/src SYMBOL` — 3-5x faster.61 Two CPU latency wins shipped 2026-07-11 (v1.63.3 deweight #534 removed a per-file `resolve()` hot62 loop from the shared hot path; v1.63.4 parse-cache #535 deduped the 2-3x Python parse, **36%63 faster on a warm re-query**); the remaining scan cost is inherent and is the warm-daemon's job (#94).64- **A "whole-repo hang past 60-90s" reported from WSL over `/mnt/c` is a 9p-filesystem ARTIFACT,65 NOT a tg deadlock.** WSL reading a Windows-mounted tree is ~3-5x slower than native NTFS at the66 file-walk + `stat()`/`realpath()` these commands do, which tips scope-proportional work past a test67 timeout. **Reproduce a WSL latency report NATIVELY (Windows or native Linux, not `/mnt/c`) before68 treating it as a tg bug** — the 2026-07-11 v1.63.2 "10-timeout P0 regression" dogfood was exactly69 this (native: every flagged command completed). Memory:70 `tensor-grep-wsl-mnt-c-latency-artifact-2026-07-11`.71- **Real-world scale dogfood (2026-07-23, c:/dev workspace, 300k+ files across dozens of72 nested projects — not a synthetic reference repo) confirms the shipped mechanisms hold at73 genuinely large scale, not just on the TypeScript reference repo.** `tg orient` bounds itself74 via `scan_limit(2000)` + centrality scoring to a useful ~2910-token capsule in **4.9 s**; an75 unscoped `tg search` degrades gracefully instead of hanging — it returns a partial result with76 an honest "exceeded timeout" `incomplete_reason` and exits **124** (the #400 native-walk77 deadline doing its job, not a new mechanism); `tg inventory --deadline` bounds correctly78 **per-project**. See §2 below for the one known-open edge this same dogfood surfaced.7980---8182## 0. When to use this skill — and when to use a sibling instead8384Use this skill when the task is **bounding/honesty at scale**: a hang, a `--deadline`85overrun, task #52, the #390 daemon gap, the exit-code partial contract, or a86latency profile of a graph command on a big repo.8788| If you actually need to… | Use this sibling instead |89| --- | --- |90| A hang/slowness whose CAUSE is unknown — systematic bisection first | `tensor-grep-debugging-playbook` (then return here) |91| Single-file / small-repo micro-latency, or make a number claim-quality | `tensor-grep-benchmark-and-proof-toolkit` |92| The front door / routing / registration / backend contract | `tensor-grep-architecture-contract` |93| Register a new flag/command (2 front doors, 4 sites) | `tensor-grep-config-and-flags` |94| Merge/release/experimental-flag gates + the incidents behind them | `tensor-grep-change-control` |95| Learn a settled battle (FFI reverts, dep caps, mock-vs-real, golden-sensitivity) | `tensor-grep-failure-archaeology` |96| Build/run the toolchain (uv, maturin, cargo) | `tensor-grep-build-and-env` |97| Update README/AGENTS/docs after shipping | `tensor-grep-docs-and-writing` |98| Position externally (never "faster grep") | `tensor-grep-release-and-positioning` |99100**This skill never routes around change-control.** It produces the *evidence*; the101flip is a `tensor-grep-change-control` decision (Phase 4).102103---104105## 1. What already shipped (verify each before you build on it)106107The **P0-6 "moat" deadline program** (#384-#401) + the now-shipped unscoped-hang fix (#400,108#413, #428, #702). Every anchor below is at HEAD on 2026-07-24 (v1.96.0); re-verify line numbers,109they drift — `main.py` is 17032 lines and `repo_map.py` is 18752 lines as of this pass, do not110trust an older total. Both files grew substantially (repo_map.py by ~1850 lines) across the111Java/C#/PHP language-support campaign (#719-#726) landed since the last full citation pass at112v1.93.2 — several anchors below drifted by 1000+ lines and have been re-pinned in this pass.113114| Ship | What it does | Verify |115| --- | --- | --- |116| **#384-#388** deadline threading | `deadline_seconds` -> `_deadline_monotonic_from_seconds` -> `build_repo_map(deadline_monotonic=...)`; converted once to an absolute `time.monotonic()` stamp so the scan can self-bound and return partial. | `grep -n deadline_seconds src/tensor_grep/cli/repo_map.py` (e.g. `_deadline_monotonic_from_seconds` at `grep -n "^def _deadline_monotonic_from_seconds" src/tensor_grep/cli/repo_map.py` (`:768` as of 2026-08-14), was `:752`; `build_repo_map`'s own `deadline_monotonic` param at `grep -n "^def build_repo_map" src/tensor_grep/cli/repo_map.py` (def `:7669` as of 2026-08-14), was `:7427` — no longer 1-line churn; re-grep rather than trust either number) |117| **#389/#393** graph-command CLI `--deadline` | `tg callers / refs / impact / blast-radius` gained `--deadline FLOAT`; #393 bounds the **caller-scan traversal** itself. | `tg callers --help` shows `--deadline FLOAT RANGE`; source: `grep -n "^def callers\|^def refs\|^def impact\|^def blast_radius" src/tensor_grep/cli/main.py` → `callers:12467, refs:12360, impact:12155, blast_radius:12853` (was `main.py:11629/11645/11440/12083` — all four drifted +715/+715/+715/+770 lines; re-grep, do not trust these numbers either) |118| **#394** payload `result_incomplete` | Truncation stamped at the payload layer so MCP/`_json` consumers see it, not just the CLI. | `grep -n result_incomplete src/tensor_grep/cli/repo_map.py` |119| **#395** `tg inventory --deadline` | inventory walk wall-clock bounded. | `tg inventory --help` shows `--deadline`; source `grep -n "^def inventory" src/tensor_grep/cli/main.py` → `:8823` (was `:8414`, drift +409), `build_inventory(..., deadline_seconds=...)` at `cli/inventory.py:185` (was `:183`, drift +2) |120| **#396** caller-scan cache | `_mtime_aware_cache` (mtime+size in key) + `_resolved_path_str` `lru_cache(8192)` + `_module_aliases_for_path` `lru_cache(16384)`->`frozenset` (PR #345). **7.9x on central symbols.** | `grep -n "_mtime_aware_cache\|_resolved_path_str\|_module_aliases_for_path" src/tensor_grep/cli/repo_map.py` → defs at `:108/:101/:8953` as of 2026-08-14 (was `repo_map.py:100/93/8693` at the 2026-07-24 pass, was `repo_map.py:99/92/8181` before that -- `_module_aliases_for_path` keeps drifting) |121| **#398/#399/#401** exit semantics | #398 exit 2 on ANY truncated partial; #399 walked it back to exit 2 only when the partial is **also EMPTY** (found-but-capped exited 0); **#401 reverted #399** — a UNANIMOUS design council restored exit 2 on ANY truncated/partial result **REGARDLESS of whether matches were found** ("truncation trumps found": an agent must never trust a capped caller-set as exhaustive). This is the CURRENT, final contract — do not describe #399's found-exits-0 behavior as current. | `grep -n "^def _emit_symbol_command_result" src/tensor_grep/cli/main.py` → `:11762` (function body runs to `:11832`; was `main.py:10931-11099`, drift +708 lines); `docs/CONTRACTS.md:157` (was `:156`, drift +1 — `grep -n "Symbol-command exit codes are a three-state agent contract" docs/CONTRACTS.md`) |122| **#400** unscoped-hang fix (e7f18b7) — **shipped v1.40.4** (`bb14abe`), hardened by **#413** (v1.42.0) and **#428** | (A) `_SKIP_DIR_NAMES` excludes `_tg_refs` / `.tg_semantic_index` / `external_repos` (`grep -n "^_SKIP_DIR_NAMES" src/tensor_grep/cli/repo_map.py` -- `:203` as of 2026-08-14, was `:195`, was `:194` before that); (B) **native per-file search walk** got a wall-clock bound — `compute_native_walk_deadline` / `native_walk_deadline_exceeded` (`backends/cpu_backend.py`), checked per file, breaks to a partial with `result_incomplete`+stderr warning (`grep -n native_walk_deadline_exceeded src/tensor_grep/cli/main.py` -- `:8627-8634` as of 2026-08-14, was `:8248-8270`, was `:7979-7992` before that); (C) `_should_refuse_unbounded_vendored_root_scan` (`grep -n "^def _should_refuse_unbounded_vendored_root_scan" src/tensor_grep/cli/main.py` -- `:5527` as of 2026-08-14, was `:5205`; backed by the O(top-level-entries)-only probe `_root_top_level_vendored_dir_names` at `grep -n "^def _root_top_level_vendored_dir_names" src/tensor_grep/cli/main.py` -- `:5508` as of 2026-08-14, was `:5186`; exit 2, <1s — never walks) refuses a root with `node_modules`/`vendor`/`external_repos`/`third_party` at top level, **duplicated by design** into `bootstrap.py`'s `_search_paths_include_vendored_root` (`grep -n "^def _search_paths_include_vendored_root" src/tensor_grep/cli/bootstrap.py` -- `:1082` as of 2026-08-14, was `:998`) because that front door fast-paths native/rg past `main.py` (the recurring "two front doors" class) — both guards import the same `UNBOUNDED_VENDORED_ROOT_DIR_NAMES` set, now DEFINED in `io/scan_limits.py:60` (re-verified exact this pass) and merely re-exported from `io/directory_scanner.py`, as the single source of truth so they cannot drift apart. #413 added a bounded-`scandir` instant-refusal for a large *single-project* root (no vendored top-level dir but still huge); #428 ported the same walk-deadline/refusal into the MCP surface (`tg_search`/`tg_ast_search` had never inherited it). | `tg --version` (expect >= 1.40.4); `git log --oneline --all \| grep -i '#400\|#413\|#428'` |123| **#478** (`67f9779`, shipped v1.54.3) -- **CLOSES #52**, the 4 residual unbounded loops | (A) `_iter_repo_files`' file-tree walk gains `deadline_monotonic`/`deadline_hit` params (was count-only bound); `build_inventory` now computes its deadline BEFORE the walk, not after. (B) `_relevant_tests_for_symbol`'s two unguarded `any()` loops (the dominant cause on a high-fan-out symbol like `"main"`) now break on a shared deadline. (C) `build_symbol_impact_from_map` (`grep -n "^def build_symbol_impact_from_map" src/tensor_grep/cli/repo_map.py` -- `:16797` as of 2026-08-14, was `:16458`, was `:16441` before that) gained a `deadline_monotonic` parameter it never had, threaded into its `_preferred_definition_files`/`_relevant_tests_for_symbol` calls, plus a new `partial`/`deadline_limit` payload block; `build_symbol_impact`/`build_symbol_blast_radius_from_map` updated to pass it through. (D) the `string_refs` second pass in `build_symbol_refs_from_map` folds into the existing `refs_scan_deadline_hit` local. All four guarded `if deadline_monotonic is not None` (byte-identical no-op otherwise). **Scope note:** the design doc explicitly keeps `session_store.py` (the daemon call sites) out of scope -- see #390 in S2, which #478 narrows but does not close. | `git show --stat 67f9779`; `grep -n "deadline_monotonic" src/tensor_grep/cli/repo_map.py | grep -i impact` |124125| **`tg find`** (v1.77.0, #189, `main.py:4728-4837`, was `:4574-4676`, drift +147 lines) — whole-repo hybrid NL search, a NEW command that reuses this campaign's bounding shape from day one rather than retrofitting it later | Takes `--deadline`/`--max-repo-files` plus an internal corpus-wide chunk cap (`_FIND_CORPUS_CHUNK_CAP`); a truncated scan sets `result_incomplete=true` and exits 2 (found-but-truncated prints results THEN exits 2 -- same "truncation trumps found" rule as #401 below, not the found-exits-0 shape #399 walked back). **Unlike `tg search`, `tg find` does NOT get the instant vendored/workspace-root refusal (#400)** -- it always attempts the bounded scan rather than refusing outright, because ranking the whole repo (not raw-text matching it) is the command's entire point. | `tensor-grep-run-and-operate` §11c has the full exit-contract prose; `grep -n "^def find" src/tensor_grep/cli/main.py` → `:4721` |126| **#702** (v1.92.3, A9) -- a **4th** unscoped front-door fix alongside #400/#413/#428, closing a gap those three never covered | `bootstrap._run_rg_passthrough` (`bootstrap.py:1337`, was `:1088`, drift +249) -- the plain **flag-less** search front door that runs *before* `main.py`'s Typer app is ever reached -- had **NO walk ceiling at all** until this shipped; the three existing `main.py`-side refusal guards (#400/#413) never ran for this path. Fixed by adding `bootstrap._search_paths_include_oversized_implicit_root` (`bootstrap.py:1053`, was `:804`, drift +249, the same-size shift as `_run_rg_passthrough` above -- consistent with one insertion earlier in the file), gated on `paths_defaulted`, sharing the SAME `IMPLICIT_SEARCH_WALK_FILE_CEILING = 1500` constant (`io/scan_limits.py:106`, re-verified exact this pass, re-exported via `io/directory_scanner.py`) the other 3 doors already used -- natively reproduced, not a WSL artifact. An over-ceiling implicit root now refuses in ~1.7s instead of silently walking up to the 60s `TG_RG_TIMEOUT_SECONDS` backstop. | `grep -n _search_paths_include_oversized_implicit_root src/tensor_grep/cli/bootstrap.py`; `grep -n IMPLICIT_SEARCH_WALK_FILE_CEILING src/tensor_grep/io/scan_limits.py` |127128**Merge/release state to stamp every session:** #400/#413/#428/#702 are all in the **installed129binary** as of v1.96.0 — this is not a source-only or in-flight fix. If a future session finds130a NEW in-flight PR referenced by this skill, do not describe it as shipped until131`git log --oneline origin/main | head` shows a `chore(release)` commit above it (see the132project's merge-gate guardrail: an open PR is guidance, not a receipt, until it lands on main).133134---135136## 2. Still open (the campaign's actual work)137138- **#52 — end-to-end deadline ineffective on a large TS repo — CLOSED by PR #478139 (`67f9779`, shipped v1.54.3).** See the shipped-table row in S1 for the four loops it140 bounded (A: file-tree walk, B: `_relevant_tests_for_symbol`, C: `build_symbol_impact_from_map`,141 D: `string_refs` second pass). This supersedes the *likely-closed* framing this skill142 previously carried after #396/#440/the `CALLER_SCAN_FILE_CEILING` chokepoint alone — those143 three narrowed the gap but a verify-plan-against-code pass still found the four loops above144 live and unbounded; #478 is the commit that actually closes it. **Still do a one-time fresh145 Phase-0 re-measure** on a real large TS repo before citing a specific wall-clock number in a146 benchmark claim or PR -- "the mechanism is closed" and "I have re-confirmed the number on my147 reference repo" are different claims; S3 Phase 0 gives the exact command.148- **#390 — daemon-path deadline gap — CLOSED (task #203, shipped #647/#652/#653, v1.81.2-.4).**149 This was **framed OPEN in earlier passes of this skill — that framing is now STALE and risks a150 future session re-implementing already-shipped code.** Verify directly rather than trusting151 either framing: `session_store.py` (the daemon's request-dispatch module) now threads a real152 `deadline_monotonic` value into its `build_symbol_impact_from_map` (`session_store.py:1449`, was153 `:1276`, drift +173) and154 `build_symbol_callers_from_map` (`session_store.py:1490`, was `:1317`, drift +173) call sites — the code comment at155 `session_store.py:1484-1488` (was `:1311-1315`, drift +173) states explicitly: *"the exact #390 daemon-path shape this task156 (#203) closes."* The same threading was extended to `refs`/`blast-radius`/`file_importers`157 (search `session_store.py` for `WARM_DAEMON_DEFAULT_DEADLINE_SECONDS` to see every site). A158 daemon-served graph query on a cached session `repo_map` is now bounded the same way a fresh159 CLI invocation is — the #478 design doc's explicit "session/daemon territory out of scope" carve-out160 from **Candidate (c)** below has since been executed as its own follow-up, not left open.161 **Candidate (c) below is retained as the historical solution-menu entry that predicted this fix's162 shape, not as an open task — do not re-propose closing #390 as new work.**163- **Default budget.** The native-walk bound reuses `configured_ripgrep_timeout_seconds()`,164 which now defaults to **60 s** (`subprocess_policy.py:75`, confirmed exact this pass, was 600 s).165 `AGENTS.md:480` (was `:378`, drift +102 -- `grep -n "hangs ~600 s then errors" AGENTS.md`)166 still narrates the pre-#400 "600 s" symptom — that doc lags; the resolver is the source of167 truth.168- **NEW (surfaced by the 2026-07-23 c:/dev dogfood receipt above) — a pathological169 workspace-union root can still blow an `inventory --deadline` budget**, even though170 #478 bounded `_iter_repo_files`'s walk. `tg inventory` always calls `build_inventory`171 with an explicit integer `max_files` (default `DEFAULT_MAX_INVENTORY_FILES = 50_000`,172 `cli/inventory.py:42`/`188` — was `:40`/`186`, both +2 lines, never `None`), so `_iter_repo_files` (`grep -n "^def _iter_repo_files" src/tensor_grep/cli/repo_map.py` -- `:1183` as of 2026-08-14, was `:1144`)173 always takes its `if max_files is not None:` branch (`grep -n "if max_files is not None:" src/tensor_grep/cli/repo_map.py` -- `:1209` as of 2026-08-14, was `:1170`), whose FIRST174 step is a single blocking `list(os.scandir(normalized_root))` (`grep -n "list(os.scandir(normalized_root))" src/tensor_grep/cli/repo_map.py` -- `:1211` as of 2026-08-14, was `:1172`) with175 **no deadline check before or during that one call** — the per-bucket176 `deadline_monotonic` check (`grep -n "loop A" src/tensor_grep/cli/repo_map.py` -- `:1256-1260` as of 2026-08-14, was `:1220-1221`) only runs AFTER this initial177 top-level listing has already fully materialized. On an ordinary repo this is instant;178 on a workspace-union root whose TOP LEVEL itself fans out to a huge number of entries179 (e.g. a parent folder of many independently-cloned projects, not a normal nested tree),180 that single `scandir()` call can burn the whole `--deadline` budget before any check181 runs. **Triage: rare, and NOT worth a load-bearing fix right now** — the fix (a182 lazy/incremental `os.scandir` iterator instead of `list(...)`) would touch a hot,183 heavily-profiled walk path for an edge case that only bites an already-discouraged184 usage (scanning a multi-project workspace ROOT instead of a package root; `tg orient`185 / `tg agent` already advise narrowing via `suggested_scope`/`workspace_root_detected`,186 see `docs/CONTRACTS.md`). Do not silently re-open this as a new #52 — it is a187 narrower, already-diagnosed gap in a single branch of one function, not an188 end-to-end unbounded pipeline.189190---191192## 3. The phased runbook (decision-gated)193194Run in order. Each gate states the expected observation and where to **branch**.195PowerShell is the dev-box shell; `uv run` is cross-platform. Use `uv run --no-sync`196so a bare `uv run` does not re-sync away the `[dev]` tree (`tensor-grep-build-and-env`).197198### Phase 0 — Reproduce the baseline on a REAL large repo (NEVER skip)199200You cannot claim "bounded" without the unbounded number, and you cannot claim a fix201without the pre-fix number. Use a **public** large TS repo so this is reproducible202(never a private customer path).203204```powershell205# thousands of .ts files, deep import graph — a public customer-scale proxy206# (any large TS repo works; substitute one with >~1500 .ts files if you prefer)207git clone --depth 1 https://github.com/microsoft/TypeScript C:\tmp\ts-ref208$repo = "C:\tmp\ts-ref\src"209210# Wall-clock the bounded command against its own budget (source or the installed >=v1.40.4211# binary both have #400/#440; either works, source keeps you at HEAD for a just-landed fix):212Measure-Command { uv run --no-sync python -m tensor_grep callers $repo Node --deadline 10 --json | Out-Null }213```214215**Expected + gate:**216- **GATE 0 (the #52 test):** total elapsed should be **<= deadline + ~10%** (i.e.217 ~11 s for `--deadline 10`). Read the JSON: `partial`/`result_incomplete` must be218 `true` **iff** the scan was actually truncated, and a truncated result must carry a219 non-empty `incomplete_reason`/`caveat`.220- **If elapsed >> deadline (e.g. 25 s for a 10 s budget)** -> **a NEW regression, not #52221 reopened, and not #390 either** -- #52 is CLOSED at HEAD (#478/`67f9779`, S1/§2), built222 specifically to prevent exactly this shape across all four loops (A/B/C/D), and #390 (the223 daemon-path gap) is ALSO CLOSED (task #203, S1/§2 above) -- a *session-served* query is bounded224 the same way a fresh CLI one is now, so do not reach for "still-open #390" as the explanation.225 A live overrun here means a new unbounded loop was introduced since #478/#203; if you can226 reproduce it specifically on a session-served/daemon query and NOT on a fresh CLI one, that is a227 fresh, narrower finding (a regression in the #203 threading), not a reopening of #390 itself.228 Proceed to Phase 1 to find where the budget leaks.229- **If elapsed is bounded AND truncation is honestly flagged** -> matches the current230 expectation (§2: #52 closed). Do NOT just declare victory: re-run on a *second*231 large repo and a *central* symbol (highest fan-in), confirm the exit code matches232 `docs/CONTRACTS.md:157` (was `:156`, drift +1; exit 2 fires on ANY truncation, found or not — §5), then route233 promotion of the "closed" claim through Phase 4 / change-control.234- **If it HANGS (no return, no error) on an unscoped `tg search`** on a root with a235 vendored dir -> this is a NEW bug, not the old #400 shape: #400's instant refusal236 (`_should_refuse_unbounded_vendored_root_scan`, exit 2, <1s) plus the native-walk237 deadline are shipped in the installed `tg` binary (>=v1.40.4) as well as source, so a238 hang here on EITHER means the fix regressed -> `tensor-grep-debugging-playbook`.239240### Phase 1 — Phase-instrument the ACTUAL slow command (do NOT guess)241242Profile the command Phase 0 flagged, on the same repo. cProfile is the oracle here243(the graph commands do **not** all expose `--profile`; only `context-render` and244`blast-radius-render` do — verified against `tg --help`). This invocation is verified245to run and print stats:246247```powershell248# tottime = internal (self) time -> finds the true hot function.249# tg's own output prints first; the profile table is APPENDED at the end -> tail it.250uv run --no-sync python -m cProfile -s tottime -m tensor_grep callers $repo Node --deadline 30 2>&1 | Select-Object -Last 30251```252253**Expected + gate (verified shape on a real repo):** the top `tottime` rows are the254**per-file parse** — on Python targets `{built-in method builtins.compile}`, `ast.walk`,255and `repo_map.py:2127(_python_imports_and_symbols)` (was `:1921`, drift +206); on TS targets the analog is the256tree-sitter parse via `repo_map.py:2271(_typescript_parser)` (was `:2065`, drift +206) — invoked **many times**,257because `build_symbol_callers_from_map` re-parses candidate files in its `any()`-loop258(`repo_map.py:3639` documents the "N definitions -> N re-reads/re-parses" hazard, was `:3407-3409`,259drift +231 — `grep -n "N definitions means N re-reads" src/tensor_grep/cli/repo_map.py`).260261- **GATE 1a — caller-scan re-parse dominates** (many parse calls, high `ncalls` on the262 parse/`resolve` functions): the leak is the **re-parse loop**, not the one-shot map263 build -> **Solution menu candidate (a) or (b)** in Phase 2.264- **GATE 1b — a single `build_repo_map` pass dominates** (parse called ~once per file,265 not per definition): the map build itself is the cost -> the fix is bounding/caching266 `build_repo_map`, which #384-#388 already partly did via `deadline_monotonic`;267 re-measure whether the deadline is honored INSIDE that pass -> different branch --268 a NEW gap, since #390 (the daemon-path deadline gap) is CLOSED (S1/§2 above), not an269 open "#390 gap" to attribute this to.270- **GATE 1c — `resolve()` / path work dominates** despite #396: the cache is being271 defeated (e.g. an uncached `resolve()` before the cache lookup — `repo_map.py:2712`272 warns about exactly this, was `:2506-2507`, drift +206 — `grep -n "this resolve() runs BEFORE the re_export_cache lookup" src/tensor_grep/cli/repo_map.py`) -> candidate (b), fix the cache ordering.273274> **Redundancy measurement before you design a cache (candidate b obligation):**275> monkeypatch the parse function with a `collections.Counter` keyed by path and count276> how many times each file is re-parsed for one `callers` call. This is the technique277> that overturned a code-review guess in PR #345 (see fenced paths). Numbers first,278> then design the key.279280### Phase 2 — Solution menu (RANKED), each with a proof obligation281282Pick top-down. Each candidate carries an obligation you must **verify/measure**, not283assume, before building.284285#### Candidate (a) — bound the caller-scan re-parse loop (preferred)286Apply the **same per-file deadline check** #400 used for the native search walk to the287caller-scan `any()`-loop: check `native_walk_deadline_exceeded` (or an equivalent288`deadline_monotonic`) once per file, and on expiry break to a partial.289- **Obligation:** the partial must be **fail-closed and flagged** — set290 `result_incomplete = True` + a concrete `incomplete_reason`, and let291 `_emit_symbol_command_result` (`grep -n "^def _emit_symbol_command_result" src/tensor_grep/cli/main.py` -- `:11722` as of 2026-08-14, was `:11762` at the 2026-07-24 pass, was `:11298` before that) apply the exit contract: per §5, ANY292 truncated/partial result exits **2**, regardless of whether it's empty or non-empty293 (#401 — do not build toward the old #399 "found-but-capped exits 0" shape). Mirror294 #400's shape (`main.py:8137-8270`, was `:7979-7992`): break, never return a clean empty.295- **Why preferred:** it directly closes #52 with a mechanism already proven in-tree; low296 blast radius; deterministic. **This is what #478 (`67f9779`) actually shipped** -- the same297 per-file-deadline-check shape, applied to all four residual loops (S1/S2). This menu entry298 stays useful as the template for the next #52-shaped finding, not just a historical proposal.299300#### Candidate (b) — extend the #396 caching to the residual re-parse301If Phase 1 shows re-parses that #396's caches miss, widen coverage.302- **Obligation — cache-key correctness:** a repo-map cache MUST key on file303 **mtime+size** (use the existing `_mtime_aware_cache` (`grep -n "^def _mtime_aware_cache" src/tensor_grep/cli/repo_map.py` -- `:108` as of 2026-08-14, was `:100`), not a plain304 `lru_cache` keyed on path alone — a plain cache returns **stale** results in the305 long-lived daemon (the code comments at `repo_map.py:30-34` + the306 `_MTIME_CACHE_CLEAR_REGISTRY` declaration (`grep -n "^_MTIME_CACHE_CLEAR_REGISTRY" src/tensor_grep/cli/repo_map.py` -- `:64` as of 2026-08-14, was `:55`) document this trap307 and the sweep). Prove the redundancy with the308 Counter-monkeypatch (Phase 1) BEFORE adding the cache, and prove correctness with a309 mutate-file-then-re-query test.310- Caching bounds the *common* case but does **not** bound a pathological single file —311 ship it WITH candidate (a), not instead of it.312313#### Candidate (c) — close the #390 daemon-path deadline gap — SHIPPED (task #203, #647/#652/#653, v1.81.2-.4); retained here as historical record, not an open candidate314This entry correctly predicted the shape of the eventual fix and is kept for that reason, but the315work itself is DONE — do not re-propose it as new. What actually shipped: `session_store.py`'s316daemon call sites (`build_symbol_impact_from_map` at `session_store.py:1449`, was `:1276`, drift +173,317`build_symbol_callers_from_map` at `session_store.py:1490`, was `:1317`, drift +173, plus `refs`/`blast-radius`/318`file_importers` siblings) now pass a real `deadline_monotonic` value319(`WARM_DAEMON_DEFAULT_DEADLINE_SECONDS`-derived) into every builder call — exactly the call-site320wiring this candidate originally scoped as the remaining work after #478's builder-side prerequisite.321See S2 above for the closing citation (`session_store.py:1484-1488`'s own code comment, was `:1311-1315`, names task322#203 as the closer). If a NEW daemon-served command is added in the future, use this shipped pattern323as the template — thread `deadline_monotonic` at the call site, mirroring the sibling commands in324the same file, rather than assuming the daemon path is unbounded by default going forward.325326#### Candidate (d) — replace the regex/slow TS parse with tree-sitter (HIGHEST RISK — fenced)327Only if (a)+(b)+(c) leave the parse itself as the irreducible hotspot.328- **Obligation (mandatory, before any swap):** **golden parity across the parser corpus.**329 A parser change alters symbol/import extraction for every downstream command; prove330 byte-identical (or explicitly-diffed-and-accepted) output on the AST parity corpus331 first (`benchmarks/run_ast_parity_check.py`). Do this in a fenced branch; do not mix332 it with a bounding fix.333334### Phase 3 — Build behind the fail-closed contract + measure the gate335336- Write the failing test first (`tests/unit/test_repo_map_targets.py`,337 `tests/unit/test_cli_modes.py` are the patterns #400 used). TDD, then the smallest fix.338- Honor the **Backend Fail-Closed / partial contract** (§4): a bound that trips339 produces a **flagged partial**, never a silent empty, never a raw crash.340- **Re-run Phase 0 + Phase 1** on the reference repo after the fix.341- **A6 — anti-hang test discipline (a hang-class regression test can itself hang):** when the342 red test is "this used to hang/overrun," wrap it so a still-broken fix fails FAST instead of343 wedging the test run. (1) layer an OUTER shell timeout (`--kill-after=Ns`) around an INNER344 per-test timeout implemented via a thread/watchdog, not `signal` (signal-based timeouts are a345 no-op on Windows and under the GIL for CPU-bound native calls — this repo runs CI and dev on346 Windows). (2) either land the fix before the red test lands, or land the red test already347 wrapped in both timeouts. (3) never leave an unbounded loop/subprocess-spawn/backtracking348 regex in the TEST ITSELF without its own bound — the test must fail the same way the product349 bug does (bounded, flagged), not hang the CI runner. (4) distinguish "slow but protected" from350 "hung" by EXIT CODE (124/137 = killed) not elapsed wall-clock, which is noisy on a shared351 runner. (5) apply the same two-layer timeout in CI as locally — a hang that only reproduces in352 CI because the outer timeout was dev-box-only is the worst kind to debug.353354**Promotion gate (all must hold, measured on the reference repo, same symbol):**355356| Metric | Requirement |357| --- | --- |358| Wall-clock vs budget | elapsed **<= `--deadline` + ~10%** (the #52 bar) |359| Truncation honesty | `partial`/`result_incomplete` set **iff** truncated; non-empty `incomplete_reason` |360| Exit code | matches `docs/CONTRACTS.md:157` (was `:156`, drift +1) (0 complete / 1 complete-not-found / 2 incomplete — **regardless of whether anything was found**, per #401) |361| No regression | benchmark-regression CI gate green (`benchmarks/check_regression.py`); correctness identical on the AST parity corpus |362| Real binary | dogfood via `scripts/dogfood/` on the REAL artifact, not CliRunner |363364- **GATE 3:** if the wall-clock still overruns the budget OR a partial isn't flagged,365 **do not ship** — the honesty contract is the whole point. Iterate or record the366 negative result.367368### Phase 4 — Promote through change-control (never here)369370This skill produces evidence; **`tensor-grep-change-control` owns the flip.**3711. One **release-bearing PR per tick** (respect the push-race / one-merge-per-tick rule,372 `tensor-grep-release-and-positioning`).3732. Attach Phase 3 evidence (before/after wall-clock table + exit-code proof + parity).3743. **Re-dogfood on the REAL large repo before declaring the contract done** — this is375 the #399 lesson: #398 shipped an exit-code rule that had to be walked back one release376 later because the real-repo behavior (every large-repo query exiting 2) was wrong.3774. Update `docs/CONTRACTS.md` / `AGENTS.md` / `SESSION_HANDOFF.md` if the contract378 changed (`tensor-grep-docs-and-writing`).379380---381382## 4. Fenced-off wrong paths (do NOT do these)383384| Forbidden | Why | Do instead |385| --- | --- | --- |386| **Raise the default timeout** to "fix" a hang | Masks, doesn't bound. A bigger number still hangs on a bigger repo; it just moves the wall. | Add a real per-file wall-clock bound that returns a flagged partial (#400 pattern). |387| **Return a silent empty / clean 0-result** on timeout | Violates the Backend Fail-Closed Contract (`AGENTS.md`, `grep -n "^## Backend Fail-Closed Contract" AGENTS.md` → `:2090`; was `:438-440`, drift +1233 — a cross-file citation this far stale is worth a grep, not a re-stamped number). A partial that reads as "no matches / no callers / dead code" is the exact bug this campaign exists to kill. | `result_incomplete = True` + `incomplete_reason` + stderr warning + the exit-2-regardless-of-found contract (§5). |388| **Assume "each stage bounded => pipeline bounded"** | The #52 lesson: `build_repo_map` and caller-scan were each bounded in isolation, yet the end-to-end command overran because the caller-scan re-parses ~all files. | Measure the WHOLE command wall-clock (Phase 0), then profile it (Phase 1). |389| **Guess the hotspot from code review** | PR #345 receipt: a review council guessed a 3.6%-of-runtime path; the live profile found the real one (`_module_aliases_for_path` called ~1.4M times) -> `lru_cache`+`frozenset` cut the run **61.7 s -> 12.8 s (4.8x)**. | Profile the ACTUAL slow command at scale (Phase 1); the profiler is the oracle. |390| **"Fix" a golden to match your dev box** | #363 receipt: a "stale" golden was edited to include rg submatches, then reverted — the golden is backend-sensitive and CI has no rg. CI is the oracle. | Read the failing CI job's -/+ diff first; force a deterministic backend in the test. `tensor-grep-failure-archaeology`. |391| **A plain `lru_cache` keyed on path in the daemon** | Returns stale results across a long-lived session when a file changes (`repo_map.py:30-34`, `55`). | `_mtime_aware_cache` (mtime+size in key) + register its `cache_clear` in the sweep registry. |392| **Ship the parser swap (candidate d) with a bounding fix** | Conflates a correctness-risky change with a latency fix; a parity regression would hide behind the perf win. | Fence the parser swap in its own branch, gated on AST parity FIRST. |393| **Route around change-control / auto-merge** | Non-negotiable. Autonomy is draft-PR-only. | Produce evidence here; let change-control gate the flip. |394395---396397## 5. The contract you are defending (exit codes + fail-closed)398399**Symbol-command exit codes are a 3-state agent contract** (`docs/CONTRACTS.md:157`, was `:156`, drift +1,400enforced at `main.py:11639-11832`, was `:11054-11099`, drift +708 — `grep -n "^def _emit_symbol_command_result" src/tensor_grep/cli/main.py`). **This is the current, FINAL shape — #401 reverted #399,**401so exit 2 fires on ANY truncated/partial result, whether or not it found something:402403| Exit | Meaning | Agent action |404| --- | --- | --- |405| `0` | **complete** result — the scan was NOT truncated | trust the findings as exhaustive |406| `1` | genuine not-found on a **complete** scan | the symbol truly is absent |407| `2` | **INCOMPLETE** — truncated by `--deadline` (`partial: true`) or a `--max-repo-files`/scan cap (`result_incomplete: true`) — **REGARDLESS of whether anything was found** | do NOT treat as exhaustive even if it has results (a truncated caller-set is not a safe blast-radius); parse the JSON and retry with a larger budget or a narrower scope |408409Do not describe a "found-but-scan-capped result exits 0" behavior anywhere — that was #399,410walked back by #401 after a unanimous design council concluded truncation must always trump411"found something," so a caller/blast-radius consumer can never mistake a capped result for a412complete one. The **native-walk bound** (#400) is the search-side analog: on expiry it sets413`all_results.result_incomplete = True` + `incomplete_reason`, writes a stderr warning,414and **breaks** (`main.py:8137-8270`, was `:7979-7992`) — a flagged partial, never a silent empty. Any new415bound you add MUST follow this shape (see `tensor-grep-architecture-contract` for the416full `BackendExecutionError` contract).417418**`tg find` (v1.77.0, #189) followed this shape from its first shipped version** — a genuine419example of a new command adopting this campaign's contract instead of retrofitting it: any420`--deadline`/`--max-repo-files`/internal chunk-cap truncation sets `result_incomplete=true` and421exits 2, whether or not ranked matches were found. It is bounded but NOT refusal-gated the way422`tg search` is (#400's instant vendored/workspace-root refusal does not apply) — see the §1423shipped table.424425---426427## 6. When NOT to use this skill428429- **A hang/slowness whose cause you don't yet know** — do the systematic bisection in430 `tensor-grep-debugging-playbook` first; come back here once it's 431432…(truncated)