Rotated out of CLAUDE.md on 2026-08-21 under Maintenance Practice 5, verbatim.
These entries are CLOSED history: every one names a date, and the tracker state
in them expired the moment it was written.
⚠⚠ Never quote an open-issue or open-PR count from this file. Run the query.
The block below already contained one internally contradictory count when it was
rotated (a ZERO open issues line dated 2026-07-28 sitting above a #375 REOPENED
entry dated 2026-07-26). That is the failure this warning exists for.
The standing lessons drawn from these entries live in CLAUDE.md under
Standing lessons; each names a date you can grep for here.
1.108.307 — "A phase boundary drawn at the wrong place" (2026-08-29). Rotated out of CLAUDE.md's Current State on 2026-08-30 under Maintenance Practice 5, verbatim. Lessons it earned are in CLAUDE.md: derive a skip list from its authority, a cache invalidated on every write is not a cache, and a phase boundary drawn at the wrong place names the wrong subsystem confidently.
- Prior (1.108.307): A phase boundary drawn at the wrong place. #557 (@Ticki84) closed by the reporter, who cloned the repo and instrumented his own long-running
watch-allafter three of our hypotheses died on the thread. ⚠⚠_walk_tsconfigsdescends into Rust'starget/on EVERY watcher event: 13.58s of a 13.75s reindex, against 0.27s once excluded — and it fires even when the watcher reportsno indexable changes, which rules out parsing and persistence by itself. ⚠⚠_TSCONFIG_SKIP_DIRSWAS THE FOURTH COPY OF A SKIP LIST IN THIS TREE AND THE ONLY ONE DERIVING FROM NOTHING, whilesecurity._SKIP_DIRECTORY_NAMESalready containedtarget(rules atimports.py/security.py). Adding"target"was the REPORTED fix and would have been our own "fix the call site, leave the mechanism" error. ⚠⚠ Second half:index_folderevicted the alias-map cache UNCONDITIONALLY, so every watcher-driven single-file re-index re-paid the walk that_load_tsconfig_aliases' module-level cache exists to make once. A cache invalidated on every write is not a cache (index_folder.py): a targeted run keeps the map unless a tsconfig was touched; a run that cannot know still evicts. ⚠⚠ THE RELEASE'S REAL LESSON IS ABOUT OUR OWN INSTRUMENT: .304's phase breakdown blamedsave=9.906sand we believed it.saveincludes rebuilding the in-memoryCodeIndexafter the SQLite transaction, and THAT reconstruction triggers the walk — so a phase boundary drawn at the wrong place names the wrong subsystem CONFIDENTLY, which is worse than no breakdown at all: it sent us hunting lock contention that was never there, andprocess_locks.waited_seconds(shipped in .305 for this) correctly reported nothing. ⚠ The build-tree regression asserts BEHAVIOUR, not timing: a poisonedtsconfig.jsoninsidetarget/whose aliases must never appear in the map, which holds on any machine at any speed. A separate test fails if the skip set is re-hardcoded — the derivation is the fix;targetbeing present is only its first visible consequence. ⚠ Measured here 0.617s -> 0.003s on a synthetic 9,200-entrytarget/, and that is a LOWER BOUND on a synthetic tree, not his number. [[grep-a-persisted-field-for-its-readers]] [[close-on-criteria-not-on-judgment]]
1.108.306 — "A count taken after the page, and a field nobody read" (2026-08-28). Rotated out of CLAUDE.md's Current State on 2026-08-29 under Maintenance Practice 5, verbatim. Standing lessons it earned are in CLAUDE.md: a count taken after the page describes the page, a refusal is not a zero, and grep a persisted field for its readers.
- Prior (1.108.306): A count taken after the page, and a field nobody read. Four issues from @lilubot, two sharing a root cause and a third falling out of the first. ⚠⚠
get_untested_symbolscomputeduntested_count = len(symbols)AFTER themax_resultsslice and derivedreached_pctfrom it, andget_repo_healthasks formax_results=1because it "only needs the count" — so the PUBLISHED health/radar test axis read ~100% reach on every repository with untested code (4,893 of 6,352, 23.0%, published as 100). It reached the grade and the observatory. ⚠⚠ The sweep for other instances found NONE, and that is the finding —find_importers/find_references/get_dead_code_v2all count before slicing. Invisible to any single call;test_counts_survive_truncation.pyholds the property. ⚠⚠detect_frameworkpersistsentry_point_patternsintocontext_metadataon every index, and a tree-wide search found that key WRITTEN IN ONE PLACE AND READ IN NONE. Three tools each reproduced their own answer to "is this a root?" and every one was Python (_ENTRY_POINT_FILENAMESis eleven.pynames plusMakefile), so a Next.js repo detected ZERO entry points: v2 returneddead_symbols: [], and 203 of 366 "unstable" files wereroute.tshandlers whose Ca is 0 BY CONSTRUCTION. ⚠⚠ Consuming that field naively would have been FAR WORSE than the defect: Flask and FastAPI shipped"*.py"in it, and under fnmatch a*crosses/, so the first reader would have declared every Python file in a Flask repo a live root — dead-code detection off across an ecosystem, silently, with those repos' coupling denominators emptied. Removed at the source AND refused by_is_catch_all, gated over every profile; directory SCOPE is what saves a pattern (routes/*.phpfine,**/*.phpnot). ⚠⚠ Coupling excludes entry points from the DENOMINATOR as well as the numerator — numerator-only shrinks a count without shrinking what it is a fraction of, the 84.0 B -> 88.8 B sign error of .305 pointing the other way. So an entry point with a realCeproblem is graded by nothing; the count and profile are disclosed. ⚠ Only the DETECTED profile excludes (seeget_repo_health.py). ⚠⚠ A REFUSAL IS NOT A ZERO — v2 returning[]WITH asignal_warningbecamedead_code_pct: 0.0and an axis of 100, the strongest claim assembled from an admission that nothing was established; it withholds composite and grade through .305's ownunmeasurable_axesnow. ⚠ Toolchain manifests leave the dead-code population by NAME (nothing imports a lockfile by design;package.jsonwas reported dead by the same run that READS it to find entry points) — never by extension, so an orphaneddata/fixtures.jsonis still a finding. ⚠ #560 was VERIFIED, not fixed: all four type-only import spellings resolve. Tested anyway, because the claim rested on nothing. ⚠⚠ Found on the way: THREE consumers reading keys their producers never emit, two in one renderer — the post-task untested diagnostic andassemble_task_context's audit stage were dark for their whole lives. The test guarding the first was the reason nobody noticed: its mock returned the INVENTED key, and a fabricated producer makes an absent-key defect structurally invisible to a test written about that exact code path. ⚠⚠ The unmocked replacement PASSED against the reintroduced defect on its first version — it asserted the name appeared in the message, and the name appears in three sections. An assertion that does not name which producer put the string there proves nothing about that producer. [[a-mock-can-supply-a-contract-the-producer-lacks]] [[grep-a-persisted-field-for-its-readers]]
1.108.305 — "Only the reader was never fixed" (2026-08-28). Rotated out
of CLAUDE.md's Current State on 2026-08-29 under Maintenance Practice 5,
verbatim. Standing lessons it earned are in CLAUDE.md: the shallow-clone
reader, NOT APPLICABLE vs COULD NOT MEASURE, .get(k, default) is not a None
guard, the host timezone selecting a test's input format, and the sdist
allowlist.
- Prior (1.108.305): Only the reader was never fixed. Nine tools run
git log --since=N days; a shallow clone answers every churn question with a small number and exit 0, sochurn_surfaceranked nothing but complexity and the grade came out FLATTERING. ⚠⚠ We had fixed this TWICE — Practice 6's Actionfetch --depth=1, and the observatory's cloner (81.3 B vs 75.6 C at ONE identical commit) — and both times we made OUR clones deep.actions/checkoutdefaults tofetch-depth: 1, so every user running the Action orjcodemunch-mcp healthin their own CI kept the defect on their own pull requests. Third instance of the standing lesson. ⚠tools/_git_history.pyasks COVERAGE, not shallowness, tri-state. ⚠⚠ THE FIRST FIX MADE THE NUMBER WORSE, which is the release. Omittingchurn_surfacethe wayruntime_coverageis omitted took the tree 84.0 B -> 88.8 B against a truth of 77.3 C: dropping a LOW-scoring axis RAISES a mean. NOT APPLICABLE and COULD NOT MEASURE are different states and only the first may be dropped silently.compute_radartakesunmeasurable_axesand withholds composite AND grade; measured axes stand. Default path byte-for-byte unchanged. ⚠⚠ TwoNonesites the tests then found, both user-facing:diff_radar's.get("composite", 0.0)— the default NEVER fires for a present key holding None — and_verdict, which would have printed "no meaningful change" on a contributor's PR on the one occasion nothing was measured. ⚠⚠ CI caught a 3.10 break I could not reproduce on ANY local version: git renders a UTC offset asZ, unparseable byfromisoformatbefore 3.11 — and git only emitsZon a UTC host. Runners are UTC, this box is CDT and got-05:00. The host's timezone selected the input format, so the version matrix was not the axis; the guard is a UNIT test over all four spellings. The tri-state held under the fault (complete: None, not a wrong verdict). ⚠⚠ Also:relnotes.md, a scratch copy of the release notes, SHIPPED INSIDE THE PUBLISHED 1.108.304 SDIST viagit add -A. The canary tests prove NAMED bad paths are absent and could never have caught it — a denylist catches the instance, an allowlist catches the class — andALLOWED_ROOT_FILESfound a SECOND instance minutes later:suite.log, from this session's own gate runs. ⚠⚠ @Ticki84 ran the new breakdown on the first build that had it and it answered at once:save=9.906sof a10.000stotal, everything else summing to 0.094s — the cost is entirelyincremental_save. That lock is taken BEFORE the write, so from the caller's timer a CONTENDED LOCK and a SLOW WRITE are the same number;process_locksnow reportswaited_secondsand NAMES the holder past 1s. ⚠ The round10.000ssays contention, but that is a HYPOTHESIS and shipping the instrument beats asking the reporter to test it — three earlier ones on this issue were each measured dead by him. [[a-module-that-imports-clean-has-been-tested-for-nothing]] [[a-one-directional-check-certifies-its-blind-side]]
1.108.304 — "Three hypotheses, each measured, each wrong" (2026-08-28).
Rotated out of CLAUDE.md's Current State on 2026-08-29 under Maintenance
Practice 5, verbatim. ⚠⚠ Read it beside 1.108.307: the phase breakdown
this release shipped is the instrument that then blamed save=, and #557's
real cause turned out to be inside that phase boundary rather than the
subsystem it named.
- Prior (1.108.304): Three hypotheses, each measured, each wrong. #557 (@Ticki84, Windows, ~10s to reindex one file where
index_filetook ~0.2s) drew an old version, the watcher's hash-cache reload, theJCODEMUNCH_INDEX_CACHE_TTLcliff and context providers. He answered all of it: 1.108.303, TTL unset, providers confirmed off FROM THE LOG'S OWN SILENCE, and the DEBUG line's(10.31s)isindex_folder's OWN duration — so the time is inside indexing and every hypothesis we offered is dead. ⚠⚠ He answered SIX HOURS BEFORE our next comment, which re-asked all three. ⚠⚠ What the hunt found instead: the fast path OPENED withstore.load_index(owner, repo_name) # always load base for branch check— a full symbol hydration on every watcher event, INSIDE the block whose entire purpose is to skip loading the index and THREE LINES ABOVE theuse_memory_hash_cacheflag that exists to make the store's hashes unnecessary. The saving that flag names was never realised on a cold read, because this ran first regardless. Every question that path asks of it is metadata (branch,git_head,file_hashes,has_source_file, the two re-parse stamps), so it is aSelectiveIndexViewnow: zero symbol rows, 0.172s -> under 1ms cold on 13,906 symbols. ⚠⚠parser_generationandracket_config_digestHAD TO JOINEXACT_FIELDSor the fix moves the cost instead of removing it — absent from that tuple they fall through__getattr__, which promotes, so the per-event upgrade check would still load every symbol to read one integer. If it lives in ametarow it belongs inEXACT_FIELDS. ⚠⚠ The test assertspromoted is False, NOT thatopen_selectivewas called — the mechanism check stays green while a newly addedexisting_index.symbolshydrates the corpus behind it, which is the only regression worth catching (Practice 9's shape, caught at authoring time for once). 4 of 5 new tests fail pre-fix; the 5th guards a future regression and is honestly vacuous today. ⚠ NOT his 10s and the thread says so — his index is 6,352 symbols, where that load costs under a tenth of a second here. Shipped because it is wrong. ⚠⚠ So the release's real deliverable is the INSTRUMENT: the watcher's re-index line now splits its own duration ([base_index= classify= read_hash= parse= git_head= save=], alsophase_secondson the result). Three rounds of guessing spent a reporter's patience; one line of one log now names the subsystem. ⚠ A full walk emits NO breakdown rather than a zeroed one — an empty bracket would read as "the fast path ran and cost nothing", the opposite of what happened, so absence means the fast path was not taken, which is the first thing worth knowing. ⚠ Practice 10's first real case, and it held: touched files 122 tests in 10s, suite once as the gate at 10:18. ⚠⚠ A background-task banner reported "exit code 0" for a suite that NEVER RAN (--timeoutplugin absent, pytest exited on argparse). Only the log's ownEXIT=line is evidence; the banner is not. [[a-trailing-command-hides-pytests-exit-code]] [[a-module-that-imports-clean-has-been-tested-for-nothing]]
1.108.303 — "The measurement was the defect" (2026-08-27). Rotated out of CLAUDE.md's Current State on 2026-08-28 under Maintenance Practice 5, verbatim. The standing lessons it earned are in CLAUDE.md under Standing lessons ("a set cannot count", "a competitor's fix list is a free defect probe").
- Prior (1.108.303): The measurement was the defect. Five instruments reported a good number about something they could not observe, and four of them were ours. ⚠⚠ THE RUST FIDELITY HARNESS, SIX DAYS OLD, GRADED A 37.9% NAME-COLLISION RATE AS A PERFECT RUN — it keyed bare names in a SET, and a set cannot COUNT. Proven by deleting the second symbol of every duplicated name in the fixtures:
extraandmissingdid not move, so extracting ONE ofdefs.rs's 108is_switchscored like extracting all 108. What it hid:impl Foo { fn new }andimpl Bar { fn new }both emitted a barenew, kindfunction, parent None, separated only by a~1/~2id suffix — the trait's own declaration qualified fine (T.go), so traits had an owner and impls did not.impl_itemsat insymbol_node_typesas"class"for the extractor's whole life and never produced ONE symbol (noname_fieldsentry could name it), and a container becomes a parent only if it EMITTED one — so it is a virtual scope now, emitting nothing, which is whatsynsays an impl block is too. ripgrep @3fce3b5b: 1,331/3,514 (37.9%) across 44/110 files -> 55 (1.6%); 2,199 symbols movefunction->method. ⚠undercountandqual_mismatchgate at 0 besideextra/wrong_span; the oracle emitsqualand tracks its scopes. The owner isself_ty, NEVER the trait —impl Display for FooputsfmtonFoo, and keying onDisplayis the same collision one level up. ⚠ Two more gaps fell out of the new buckets, invisible to everything that shipped: aconstinside animplcame out bare (35 in ripgrep) because_constant_symbolhardcodesqualified_name = name, andassociated_type(a trait'stype Carried;) was absent fromRUST_SPEC—.302'sfunction_signature_itemagain. ⚠⚠tests/test_rust_fidelity.pylisted its three fixture names as a LITERAL in everyparametrize— a SECOND roster beside the frozen artifact, where only the artifact had a test keeping it honest; the newqualification.rswas ungated on arrival. Read off disk now. ⚠ Practice 9 fired again:test_rust_fn_in_implassertedkind == "function"under the docstring "Without the impl parent being extracted, 'new' appears as a top-level function" — the defect written down as intended behaviour, passing only while it existed.PARSER_GENERATION6 -> 7 (qualified_name/kind/parenton unchanged content). ⚠⚠ #556 by @otherjoel is THIRTEEN of the eighteen entries — twelve findings, one per commit, each measured against Racket's EXPANDER, Racket's READER, or five real package layouts on disk rather than against our own output.#langis read before the grammar runs (#lang punctwas indexing Markdown code samples;conscriptlost 61% of definitions and FABRICATED ~100 that error recovery re-parented to module level); collection paths resolve throughinfo.rkt(splitflap 0/70 -> 13 edges, congame 304 -> 624); thedefine-genericsexemption was REMOVED rather than widened, becauseextra: 0was carrying a fabrication. ⚠ He declined thePARSER_GENERATIONbump ON PURPOSE and substituted a per-projectracket_config_digeststamp — an absent key is detectable forever, a stamp equal to the constant is not, our own.302lesson used against us correctly. We bumped to 7 anyway for the Rust fix, so those indexes are reached twice; the stamp is NOT redundant — it fires when a PROJECT editsracket_langs, which no global counter can see. ⚠ Also:.next/.nuxt/.output/.svelte-kit/.angular/.turbo/.parcel-cache/.dart_toolwere indexed as source (.next/server/**is a TRANSPILED copy of the user's own pages — the_builddefect a FOURTH time); the observatory scored eleven public repos on ONE COMMIT sochurn_surfaceread churn 1 everywhere and ranked nothing but complexity (jcm 81.3 B -> 75.6 C, i.e. we were FLATTERING ourselves);max_nestingcounted brackets, which in Python measures the deepest EXPRESSION (index_folder: 3 reported, AST truth 6); and the codex cache-hit-rate cut cannot separate its arms BY CONSTRUCTION — it is a ratio, so the arm with the least schema scores highest. ⚠ Three of the five were found by reading a competitor's fix titles against our tree. [[a-competitors-fix-list-is-a-free-defect-probe]] [[a-guard-covered-only-by-positive-tests-can-be-deleted]]
2026-07: Codex tool-surface benchmark forensics. Rotated out of CLAUDE.md on 2026-08-28 under Maintenance Practice 5. The STANDING warnings stay there; this is the full original section, verbatim.
⚠ Shipped in 1.108.271. Kept here rather than in the rotation because it is a STANDING warning about a measurement, not a release note that ages out.
⚠⚠ Do not quote the arm numbers; the honesty gate fired. Four arms x three
repeats on FastAPI at a pinned commit, answering an
r/codex benchmark that put
jCodeMunch at +28.45% on Codex and -3.34% on OpenCode. Largest arm
difference 568,617 tokens against a baseline varying against ITSELF by
1,143,229. Directions were incoherent too (full, carrying 24,007 tokens of
schema, came out CHEAPER than baseline). The hypothesis is untested, not
disproven — the instrument cannot resolve an effect that size.
⚠⚠ The finding that outlived the arms, and it corrects a claim this project
made: 86% of baseline input is CACHED. The schema block is stable across
requests, so it is paid at full rate roughly ONCE and at cache-read rates after.
Any framing of "24,007 tokens in every request" is wrong, and that framing was
used here before measuring. The fixed-cost story is a WEAKER explanation for
the r/codex result than the raw number suggests, not a stronger one.
--surface-only still measures the schema exactly (90 tools / 24,007 tokens at
default full, 6 / 1,030 at counter) and needs no API credits; what it does
not measure is what that costs in practice.
⚠ Those two numbers are a 2026-07 snapshot from THIS harness and are not the
canonical figures. benchmarks/schema_baseline.json is, written by
benchmarks/harness/capture_schema_baseline.py and guarded by
tests/test_schema_budget.py; it counts a different payload shape, so the two
sets will never agree digit for digit and neither is wrong. Quote the baseline
file. ⚠⚠ Reconciled 2026-08-14: the Counter avoids 95.9%, not the ~98% that
run_route_recall.py asserted for two months — that literal is now computed
from the baseline at runtime, with a test that fails if any schema-saving
percentage returns to that file. The gap existed because the budget guardrail
only walked tool_profile, which does not apply to the front door at all, so
the single largest lever in the project had no test under it.
⚠⚠ The same run killed tool_profile: "standard" as a token lever: it drops 9
of 91 tools and 5.7% of the payload. Anyone selecting it as the safe middle
setting gets nothing measurable. core (74.0%) and counter (95.9%) are the
only two settings that move the number; there is no gradient between them, and
the config surface currently implies there is. ⚠ Where the rest sits, from
--breakdown: under full, tool DESCRIPTIONS are 36% of the payload and
compact_schemas rewrites input schemas only, never descriptions. Schema
compaction is near its floor; descriptions are untouched ground.
⚠ Design flaw recorded so nobody repeats it: summing per-invocation input across a RESUMED conversation counts accumulated context on every step, so the total is dominated by how much the agent read early on, which compounds.
2026-08-24: #536 closed MANUALLY against the PUBLISHED artifact, and the first
probe was wrong. Rotated out of CLAUDE.md's server.py Key Files entry on
2026-08-28 under Maintenance Practice 5; the rule it earned stays there.
A real stdio handshake to jcodemunch-mcp==1.108.293 in a clean venv returns
serverInfo {"name":"jcodemunch-mcp","version":"1.108.293"} and a non-empty
instructions. This had to be done by hand because __version__ is "unknown"
under PYTHONPATH=src — so a green test does not prove the wire carries a real
number, and CI cannot close that gap either, because it runs from source too.
⚠⚠ The first probe was WRONG and the reason generalises: uvx jcodemunch-mcp
served a CACHED 1.108.275, which predates both fixes, so the wire showed the
SDK's own version and no instructions — i.e. exactly the pre-fix symptoms,
from a stale cache rather than a defect. Pin the version and build a fresh venv
(uv venv + uv pip install "jcodemunch-mcp==X.Y.Z"); never probe through bare
uvx and never read its output as evidence about what we ship.
2026-08-25: refresh.py's coverage check asked only whether the corpus GREW,
and for its whole life could not see the opposite failure. Rotated out of
CLAUDE.md's Key Files entry on 2026-08-28 under Maintenance Practice 5; the RULE
it earned stays there, this is the incident.
A source root that has moved, been unmounted, or been cleaned makes discovery
return [], so current and known are both empty, nothing drifts, nothing
errors, and the campaign stamps the target parser_generation having re-parsed
ZERO files. UNREPAIRABLE — a stamp equal to the constant is indistinguishable
from a genuine one, so the tool built to prevent the exempt bucket was putting
indexes INTO it. It now refuses on corpus_unreadable (discovery empty, index
not) and index_unreadable (_index_files returned None — UNKNOWN blocks, the
same rule as has_any()).
⚠ EMPTY-vs-NON-EMPTY deliberately, NOT a shrink threshold: a repo may
legitimately lose most of its files, so partial shortfall is DISCLOSED as
indexed_files_not_reparsed rather than guessed at.
⚠ Found by running the documented command on the three pinned benchmark
corpora — bare .git dirs, 8,220 stale symbols, all three stamped in under a
second. The lesson is in Standing lessons as
"a one-directional check certifies its blind side".
2026-08-18: #488 DECIDED BY JJG — OPTION A, "explicit config outranks the
zero-config ONNX default", with disclosure. NOT YET IMPLEMENTED; queued behind
#495. _detect_provider will check embed_model / JCODEMUNCH_EMBED_MODEL and
the cloud key pairs BEFORE returning local_onnx at priority 0, the result will
name the active provider and why, and the config.jsonc comment gets corrected.
⚠⚠ This was only safe to decide because #500 shipped first. Making explicit
config win makes provider changes MORE frequent, and before #500 each one left
the store holding two vector widths with the newer half silently excluded from
search. The migration hazard that looked like a cost of option A was a
pre-existing defect option A would merely have made more likely to fire.
⚠ Option C (local-only per #302) was REJECTED on a factual error in the
report: branches 1-3 are not vestigial, only SHADOWED, and only when
[local-embed] is ALSO installed. [semantic] without [local-embed] uses
branch 1 today and it works. Say that to the reporter — their largest
suggestion rests on it.
⚠ Disclosure is not optional in A: a caller whose provider changes needs to see
model_changed_from / rebuild_reason (#500's fields) rather than discover a
re-embed by watching the clock.
2026-08-20: #447 (@elfrost) IMPLEMENTED BY US via PR #519 at timebox expiry;
#443 CLOSED with credit. install-pack's pre-scan rejected a leading separator
and .., which is necessary and not sufficient: C:/Windows/Temp/evil.txt
carries neither, and base / relative DISCARDS base when relative is
absolute. mkdir(parents=True) ran BEFORE the write, so a hostile member created
directories outside the install root before any content existed. Unreleased.
⚠⚠ THE PROVENANCE IS THE FIRST THING TO SAY, EVERY TIME. elfrost found it,
analysed it and wrote a correct fix. We shipped our own pre-existing
_safe_content_path pattern applied to the call site that lacked it — an
INDEPENDENT path, not a clean-room copy — and said exactly that on both threads.
⚠ Confinement by RESOLUTION, never by pattern. A string test cannot finish
enumerating separator and drive spellings; resolving and comparing does not have
to. The pre-scan stays as an EARLY ABORT with the per-member check as the
authority — two checks, one authoritative, recorded at the call site.
⚠⚠ The rule had THREE spellings already (security.validate_path + a private
copy on IndexStore + another on SQLiteIndexStore) and the new call site
would have been a fourth. security.resolve_within() is the one definition now;
SQLiteIndexStore keeps its resolved-base cache by PASSING IT IN, so the hot path
survives without duplicating the rule to preserve it. A ratchet fails on a
commonpath anywhere else in src/.
⚠⚠ THE FIRST REGRESSION TEST PASSED AGAINST THE UNFIXED SOURCE, AND THE
NON-VACUITY PASS WROTE A REAL FILE INTO A REAL WINDOWS SYSTEM DIRECTORY. It
named the reported path verbatim, so the escape went OUTSIDE the directory the
assertion searched — invisible to tmp_path.rglob. A test for an
ARBITRARY-WRITE defect EXECUTES that defect every time you prove it is not
vacuous, so the target must be somewhere the test OWNS. Rebuilt against a
tmp_path sentinel; the artifact was deleted.
⚠ The refusal is deliberately NOT platform-pinned. C:/... is absolute on
Windows and an ordinary relative name on POSIX, where resolving it under the base
is CORRECT. Assert confinement; asserting that a string is refused writes platform
trivia into a security test.
⚠ A second test of mine asserted an OS ACCIDENT — that an embedded NUL fails
to resolve — and passed serially while failing under xdist, where the longer
worker temp path takes the other branch. The rule is that a RAISING resolve
refuses; which inputs happen to raise is the OS's business. Same tell as
Maintenance Practice 9: it stated a mechanism instead of an outcome.
⚠ Suite: 8083 passed, 17 skipped, 0 failed + ruff clean, all 12 CI checks
green. 3 red at the call site, 1 red on the one-definition guard.
⚠⚠ #443 cost EIGHT DAYS and SEVEN of our own conflicts and bought nothing.
See policy 3a, now absolute at 24 hours.
2026-08-20: the licence identifier is MAJOR-ONLY, and jdoc/jdata are synced.
@marcelruhf is a PLATFORM CUSTOMER operating an allowlist against this
identifier; jjg's standing instruction is top-tier consideration, bounded by no
harm to the rest of the user base. Unreleased (jcm PR #521).
⚠⚠ His cheapest-looking option was the one worst FOR HIM and that is the
reusable half. Dropping the version entirely means a substantive re-licence is
INVISIBLE to every allowlist — the identifier keeps matching while the terms
change. It buys us zero churn by moving risk onto the licensee. Keeping
-1.1 churns him for a typo. Major-only says the thing he needs: minor is
editorial, major means read it again.
⚠⚠ WE HAD ALREADY BROKEN THAT PROMISE ONCE, and checking is what found it.
f3c925c (2026-07-10) ADDED a redistribution and attribution obligation to
LICENSE condition 2 while the header stayed at Version 1.1. Nothing failed,
because a version line is a CONVENTION and conventions do not fail builds. So
the terms text is pinned by DIGEST: any edit fails, and clearing it forces the
substantive-or-editorial choice AT the edit rather than downstream. The test
cannot make that judgement and does not try — it makes the judgement happen.
⚠ Do it NOW was part of the answer, not a separate question. .288 is the ONLY
release that ever carried an identifier, and PyPI metadata is immutable per
version, so every later release widens the transition. Deferring a metadata
decision for discussion is not free when the cost grows monotonically.
⚠⚠ The digest was RED on all four Ubuntu legs and GREEN on all four Windows
legs: it hashed RAW BYTES, and git rewrites line endings on checkout, so it
pinned a property of the CHECKOUT rather than of the terms. A licence says the
same thing in either encoding. Normalise before hashing. Second
platform-shaped self-inflicted test defect in two days (the other was xdist).
⚠ jdoc #122 / jdata #4 (same reporter) MERGED, and BOTH had held CI. Only
license/cla was reported — the matrices had runs sitting action_required and
had NEVER run: four on jdoc, two on jdata. fork-pr-contributor-approval was
first_time_contributors on both; relaxed to match jcm, which fixed it
2026-08-13. A setting fixed in one repo of a suite is fixed in one repo.
⚠ He ported #518's ratchet into both siblings unasked and his version is
BETTER: mine asserted a version suffix EXISTS, his makes a Version line and a
suffix imply each other BOTH WAYS — those LICENSE files state no version, where
mine would have demanded one. Adopted back into jcm as #520. Ours was right
only about this repo's accident.
⚠ Policy 3a/3b now present in jdoc and jdata CLAUDE.md; they carried policy 3 as
a single line and had neither the 24-hour ceiling nor the held-run diagnosis.
2026-08-20: #517 (@marcelruhf) MERGED; #518 finished it. PyPI published the
entire LICENSE text as info.license because license = { file = "LICENSE" },
so a commercial user could not allowlist us BY IDENTIFIER — there was no
identifier to allowlist. PEP 639 now:
license = "LicenseRef-jCodeMunch-Dual-Use-1.1" + license-files, classifier
dropped. Unreleased.
⚠ Verified on BUILT ARTIFACTS, not on the diff: License-Expression +
License-File at Metadata-Version: 2.5, LICENSE still at
dist-info/licenses/LICENSE, twine check green on both.
⚠⚠ He could see ONE surface and we declare the licence on THREE.
.claude-plugin/plugin.json and the mcpb manifest both said LicenseRef-Dual-Use
— no product prefix, no version — so an allowlist keyed on the identifier still
needed two entries. That is the reported defect one surface over, the same
shape as #515 the day before. mcpb now DERIVES it from pyproject.toml.
⚠ The version suffix is load-bearing. LICENSE 1.2 must produce a NEW
identifier, or an allowlist that approved 1.1's terms keeps matching terms nobody
read. test_license_identifier_agreement.py pins the suffix to the file's own
Version line. Raised the recurring-cost trade-off with the reporter rather
than deciding it for him — he is the one operating an allowlist.
⚠ PyPI metadata is IMMUTABLE per version, so none of this reaches PyPI until
the next release and 1.108.287 keeps the full text. Said so on the thread; a
contributor who fixes packaging metadata needs to know when it takes effect.
2026-08-19: #515 (@rknighton) FIXED BY US via PR #516 — the reference table
gave the wrong default. CONFIGURATION.md's Tools row read [] while
DEFAULTS["disabled_tools"] ships ["test_summarizer"], so a reader expected
91 canonical tools in the schema and found 90. Unreleased.
⚠⚠ FOUR SURFACES DESCRIBE THIS DEFAULT AND THE THREE THAT AGREE ARE THE
POINT. The generated config template, the config --init comment and
test_guide_respects_disabled_tools.py's pin all state it correctly; only the
reference page disagreed, and it is the page a user opens when a tool they
expected is missing from the schema. A value pinned by a test can still be
mis-documented — the pin guards the value, not every claim about it.
⚠ tool_tier_bundles was wrong the same way and he scoped it out: documented
{}, ships populated. He was right that nothing observable changes (set-identical
to the _TOOL_TIER_CORE / _TOOL_TIER_STANDARD fallback the row already
described, verified). Fixed anyway — a cell that is accidentally harmless is still
a cell that will be read, and leaving it would have forced the ratchet to carry it
as an unexplained exception.
⚠⚠ tests/test_configuration_md_defaults.py is the deliverable, written over
the TABLE rather than the two reported rows: it parses every | Key | Type | Default | Description | block and compares each cell against config.DEFAULTS,
so the next key someone documents is covered on the commit that documents it.
3 red pre-fix, 63 green after. The cross-check over all 60 documented keys found
exactly the two he named — his "every other documented default matches" held.
⚠ The exemption is load-bearing, not a hole. tool_tier_bundles cannot be
inlined so its cell is prose, and the test asserts its repr is genuinely too long
to fit — a small wrong value cannot hide behind the same escape hatch — plus it
asserts the claim the prose makes rather than trusting it. An exemption that
does not police itself is how the ratchet becomes the next defect's cover.
⚠ Suite: 8067 passed, 17 skipped, 0 failed + ruff clean, all 12 CI checks
green on the merged SHA. Same-tree collect 8084 with the new file / 8021 without
= exactly its 63.
⚠⚠ #443 conflicted for the SEVENTH time, on a DOCUMENTATION merge. Policy 3b
governs order and was unavailable — #443 is CLA-BLOCKED, so it cannot go first —
so we shipped and owned the resolution. license/cla SURVIVED this push
(pending, count=1 on the new head), which is a genuinely unsigned state and not
an erasure. Read the status; the tally is 3 erased / 3 returned now.
⚠ gh pr checkout sets the branch's upstream to the FORK but git push origin
still means OUR repo — pushing the resolution with origin created a stray
branch in jgravelle/jcodemunch-mcp instead of updating theirs, and the PR stayed
CONFLICTING. Push to the FORK REMOTE by name (git push elfrost HEAD:<branch>);
the tell is the PR not changing state after an apparently successful push.
2026-08-19: #506/#507/#508/#509 (@rknighton) FIXED BY US via PRs #510/#511/#512.
All four were filed at 00:24-00:25 and every one probes a surface ADJACENT to
something we shipped the day before. Unreleased.
⚠⚠ THE SAME SHAPE THREE TIMES IN THREE DAYS, and it is the reusable finding:
we keep fixing the reported call site and leaving the mechanism. #495 was a
second GENERATOR carrying its own copy of the filter; #509 a second CALL SITE
with its own containment check; #507 a second DERIVATION of the tool set. In
each the fix is one sentence — ask the authority instead of reproducing its
logic — and in each we had applied it only where it was reported.
#506 — v1.108.286 filtered ### All tools and left ### Quick start as six
fixed strings no filter reached, so the guide could still instruct a caller to
run a disabled tool. ⚠⚠ The previous fix scoped to the reported SECTION, and
so did its test: _advertised() split on ### All tools and inspected only
what followed, so it could not observe this section and would not have observed
the next. Now scans the whole document. Steps are DATA now, dropped whole and
RENUMBERED, with the shared index_folder/index_repo continuation filtered
per-tool.
#509 — index_file picked the deepest containing source_root with NO
identity check, so a file from a nested independent clone was WRITTEN into the
parent's index. ⚠ The check is imported from resolve_repo, not copied —
which is the lesson AND which inherited #492's submodule boundary for free, so a
submodule path still resolves to the parent (his Case 3, untouched). ⚠ The
refusal NAMES the repository; falling through to "no indexed folder contains
this path" was wrong on the facts and pointed at the wrong remedy.
#508 — index_file passes repo= to three config reads and nothing on that
path ever called load_project_config, so the overlay was empty and all three
resolved to GLOBAL config. ⚠⚠ v1.108.286 threaded that keyword through six
sites (#491) without checking anything loads what it reads. A parameter that is
present and does nothing is indistinguishable from the defect it was added to
fix. ⚠ Fixed at the ENTRY POINT, not by lazy-loading inside config.get() —
load_project_config does not cache a MISS, so a lazy load re-stats on every
read for any repo without a project file, on the hottest function in the tree.
#507 — _get_active_tools rebuilt the active set from tool_profile + the
baked _PROFILE_TIERS, missing three inputs tools/list reads: the SESSION tier
override, tool_tier_bundles, and the languages gate on search_columns.
Measured 70 / 15 / 1 unmounted names. ⚠⚠ The session-override case needs NO
configuration — announce_model writes the session tier via
resolve_model_to_tier, and jcodemunch_guide is in _ALWAYS_PRESENT_TOOLS so
it stays reachable at every tier. ⚠ Filtering is a SUBTRACTION, so an empty
or failed build returns None = do not filter: a policy naming a few
unavailable tools beats a policy with no workflow left in it.
⚠⚠ tests/test_path_entry_point_invariants.py IS THE DELIVERABLE of that
batch. Written over the ENTRY POINTS rather than the two reported functions,
with resolve_repo and index_folder as the PASSING CONTROLS in each pair —
which is what proves an invariant achievable rather than aspirational. It read
2 failed / 2 passed against the pre-fix tree. Write the ratchet before
concluding the reported list is the list (#489 found 5 sites for a 3-site
report the same way).
⚠ Two of my own guards matched PROSE, not code: #507's first version matched
the literal _PROFILE_TIERS and failed on the COMMENT explaining why the helper
must not use it. Walk the AST — it cannot see comments. Same fix the src.
twin-import guard needed.
⚠ Suite: 7999 passed, 17 skipped, 0 failed on main + ruff clean.
2026-08-18: #488 (@pnm-jgb) FIXED BY US via PR #505 — an explicit local model
now outranks the zero-config default, and the NARROWING is the entry.
Unreleased.
⚠⚠ JJG DECIDED "OPTION A"; WHAT SHIPPED IS A IN ONE BRANCH ONLY, AND HE
APPROVED THE NARROWING AFTER IT WAS SURFACED. Full A turned
tests/test_paid_embeddings_optin.py RED, and that file is not incidental — it
exists because jdocmunch's resolver auto-selected OpenAI from an ambient
OPENAI_API_KEY and began billing a remote account and shipping the indexed
corpus off the machine. jcm's second line of defence IS that ONNX wins before
any cloud branch is reached. A developer with [local-embed] and an exported
OPENAI_APIKEY+OPENAI_EMBED_MODEL would have silently started paying per call
and sending their source off the box.
⚠⚠ THE ASYMMETRY THE ISSUE NEVER ADDRESSED, and the reusable half:
embed_model is FREE and ON-MACHINE; Gemini and OpenAI are PAID and REMOTE.
Promoting the first costs a re-embed. Promoting the others costs money and
exfiltrates the corpus. A principle stated over a set ("explicit beats default")
can be right for part of the set and wrong for the rest — check what each member
costs before applying it uniformly.
⚠ A RED TEST IS SOMETIMES THE SPEC. The instinct on 33 reds and one
money-safety red is to fix the tests. Here one of them was the design document
and the other 33 were reporting a real regression. Read the docstring of a
failing test before assuming it is stale.
⚠⚠ The usability probe was WRONG on its first pass and 33 tests caught it.
Probing sentence_transformers importability UNCONDITIONALLY meant that on any
machine without the package JCODEMUNCH_EMBED_MODEL selected nothing, so the
caller got a bare None instead of the actionable pip install 'jcodemunch-mcp[semantic]' error. The probe now decides PRECEDENCE, never
SELECTION: an uninstalled backend does not displace a WORKING ONNX install,
but with no ONNX the setting is selected as before.
⚠ provider_reason + provider_skipped added to embed_repo's result: an
explicit setting we cannot honour is DISCLOSED, never dropped. Silently ignoring
it is the reported defect; silently failing on it at embed time is that defect
with a louder symptom.
⚠ Option (4) from the report (remove branches 1-3 + the [semantic] extra +
~5 GB of torch) was DECLINED ON A FACTUAL ERROR IN THE REPORT — those branches
are not vestigial, only SHADOWED, and only when [local-embed] is ALSO
installed. [semantic] without [local-embed] uses branch 1 today and it works.
Said so on the thread; his largest suggestion rested on it.
⚠ This change was only possible because #500 shipped in .285. Making explicit
config win makes provider changes more frequent, and before .285 each one split
the store silently. The migration hazard was a pre-existing defect the change
would merely have made more likely to fire.
⚠ tests/test_explicit_embed_model_wins.py (12), 6 red pre-fix but only TWO
behavioural — the other four fail because _detect_provider_detailed does not
exist there, which is a signature fact and not evidence. Report that split;
"6 red" alone overstates it. The 6 passing both sides are the money-safety
class and the wrapper-shape controls.
⚠ Suite: 7976 passed, 17 skipped, 0 failed + ruff clean; +12 over .285's
7981-after-#495.
2026-08-18: #504 (@lsg1103275794) VERIFIED, TIMEBOXED TO 2026-08-19, NOT YET
FIXED. Repeat index_folder on a GIT ROOT never reaches the incremental
no-change path: the v1.96 collision guard at index_folder.py:2224 is
if _existing_source_root == _git_root: with NO walk_prefix test, so a
full-root re-walk assigns _merge_with_existing and the incremental branch at
:2402 (gated on _merge_with_existing is None) is unreachable. Every
scheduled freshness check rebuilt the whole corpus. Reproduced at .285.
⚠ He offered to PR and sign the CLA; we said yes and posted the window with
the default (we implement + credit at expiry). First-time contributor.
⚠⚠ NOT a one-line fix and he said so BEFORE writing it — and walk_prefix
alone breaks test_full_root_walk_after_subdir_replaces_everything, because a
full-corpus incremental diff cannot be layered onto a source_roots marker that
is still partial. His account: one full rebuild establishes source_roots == [""], after which repeat root walks take the no-change path. That is a
DISCLOSED MIGRATION and must reach the CHANGELOG, not be found by a user whose
first post-upgrade index is unexpectedly slow.
⚠ It makes _refresh_git_head_if_advanced fire MORE OFTEN (no-change runs
finally happen), which is #493's ground from .285. Correct in index_folder
precisely because that path walks the whole corpus — verify in review, do not
assume.
⚠ Measured by him: 5.0-5.7s -> 1.58s on 1,132 files / 9,926 symbols. His
machine, his number; do not transcribe as canonical.
⚠ Droppable from the release if it needs care. It is a PERFORMANCE fix — the
index produced is correct, just rebuilt needlessly — and #447's SECURITY fix must
not wait behind it.
2026-08-18: #495 (@rknighton) FIXED BY US via PR #503 — the guide advertised a
tool the same process refuses to run. Unreleased.
⚠⚠ AT SHIPPED DEFAULTS, no config file and no env overrides.
disabled_tools ships ["test_summarizer"] and
_generate_claude_md_snippet walked a static constant, so the guide named it,
tools/list omitted it, and call_tool rejected it before the handler ran.
Reachable out of the box is what makes this worth a release rather than a
note.
⚠⚠ THE FILTERING ALREADY EXISTED AND A SECOND GENERATOR WALKED AROUND IT —
e086e9a added it to cli/init.py for #242, and server.py's generator never
got it. Reused _get_active_tools; a third copy is how the first two
drifted. Same shape as #491 (the guard existed, the call sites bypassed it) and
the src.jcodemunch_mcp twin sweep.
⚠ Widened past the reporter's scope DELIBERATELY and said so on the PR: they
scoped to disabled_tools correctly (a profile-hidden tool stays dispatchable,
so it costs context not failure), but the tool's own description promises to
match "surface, tier and disabled_tools", and tier IS the profile. Filtering
one and not the other leaves the description making a claim the code does not
keep.
⚠⚠ tests/test_config.py::test_generate_full_snippet ASSERTED THAT EVERY
CANONICAL TOOL NAME APPEARS, so it could only pass WHILE THE BUG EXISTED.
test_summarizer is canonical and disabled by default. Third test this release
found asserting the behaviour it should have prevented (after
test_embed_drift.py's literal wording and my own two in #489). When a fix
turns an old test red, read whether the test was encoding the defect before
"fixing" the code back.
⚠ tests/test_guide_respects_disabled_tools.py (9), 5 red pre-fix; the four
constraints include a PIN ON DEFAULTS["disabled_tools"] so the issue's premise
cannot silently change out from under the case.
⚠ Suite: 7964 passed, 17 skipped, 0 failed + ruff clean; +9 over .285's 7972.
2026-08-18: #489 (@pnm-jgb) FIXED BY US via PR #502 — the tool schema
advertised three key-requiring providers and hid the free one. Unreleased.
⚠⚠ The semantic PARAMETER DESCRIPTION is the expensive site and the harm is
invisible from outside. It is not documentation a human browses — it is the
tool schema, and the ONLY information an agent has when deciding whether to set
semantic: true. An agent reading "requires one of three env vars" against an
environment with none set correctly concludes semantic search is unavailable and
never tries it, on a machine where it works for free. No error, no warning,
no degraded result: the inverse of a false positive, where the tool
under-reports its own function.
⚠⚠ THE REPORT NAMED THREE SITES; THE RATCHET FOUND FIVE. The two extras were
only visible once a test asserted the PROPERTY instead of the instances: the
embed_repo TOOL DESCRIPTION in server.py (equally agent-facing, same
omission) and retrieval/embed_drift.py, whose own copy named the bundled
encoder LAST, behind the two that bill per call. Write the ratchet before
concluding the reported list is the list.
⚠ All five now derive from embeddings/advice.py; _LOCAL_FIRST leads both
strings, mirroring _detect_provider's priority so advice and resolver cannot
disagree about which wins. Option (3) from the report — a schema stating the
RUNTIME fact rather than setup instructions — is NOT shipped; noted on the PR
rather than dropped.
⚠⚠ MY BUDGET WARNING WAS WRONG AND MEASURING IS WHAT CAUGHT IT. I told jjg to
watch the hard 4,000-token core_compact ceiling (10 tokens of headroom) and was
ready to trim a description. semantic is in _COMPACT_STRIP_PARAMS and never
reaches the compact schema at all — live core_compact is 3,990 before and
after. A test pins that, so if semantic ever stops being stripped the budget
question returns visibly. Measure the constraint before paying for it.
⚠⚠ tests/test_embed_drift.py PINNED THE LITERAL OLD WORDING, which is HOW
that site kept a stale copy — a test keyed to one spelling of a sentence guards
the spelling, not the behaviour. My own ratchet had the identical defect on its
first pass (matched "No embedding provider is configured" WITH the is, and
caught embed_drift only by luck via a different clause), and my site-2 test
asserted on the CONSTANT rather than on search_symbols — true the moment the
constant exists, so it checked the fix instead of the site and passed against a
tree where that site was still stale. Corrected; it is now among the pre-fix
reds, and was not before. Three instances of one mistake in one change.
⚠ tests/test_embedding_provider_advice.py (10), 4 red against the pre-fix
CONSUMERS with advice.py present — stashing the module too only proves it is
new. Keep the new module and revert the call sites; that is the pass that
means something.
⚠ Suite: 7955 passed, 17 skipped, 0 failed + ruff clean; +10 over .285's 7962.
2026-08-18: #443 resolved a THIRD time, still ours. v1.108.285 plus #489 both
touched the [Unreleased] block. Same resolution, suite 7961/17/0, +6 =
exactly elfrost's tests, all 11 real CI checks green on the merge ref;
license/cla PENDING is the only blocker.
⚠⚠ CLA erase-on-push tally, measured across five resolutions of #443: ERASED 3 times; RETURNED on its own twice; on 2026-08-19 it did NOT return within 5 minutes and was still absent when we stopped watching. So the "our push provokes it back" note is a TENDENCY, not a remedy. ⚠ It does not block the CONTRIBUTOR — signing posts a fresh status against the current head — but the PR then shows eleven green checks and NO cla row, which reads as "done, waiting on them" and is the opposite. Say so on the thread every time; do not close+reopen to chase it (1 success / 2 failures, and it notifies them for nothing).
⚠ A comment was posted BEFORE CI confirmed it ("everything green except
license/cla"). It held, but it was a prediction at the time. Post the claim after
the run, or say it is expected rather than observed.
2026-08-18: #491 (@rknighton) FIXED BY US via PR #499 — the two exclusion
opt-outs never read the project config that documents them. security.py read
exclude_skip_directories / exclude_secret_patterns without repo=, so the
project overlay was skipped and the documented per-project opt-out did nothing.
Unreleased; see CHANGELOG [Unreleased].
⚠⚠ The COMMENTS are what make it a defect rather than a missing feature. The
note above the skip list says these are ordinary English words that can name a
real package, "which is why exclude_skip_directories exists"; is_secret_file's
docstring claims it applies the project overrides ONE LINE above the global-only
read. Both described an intent the code did not implement — same shape as
#500's promise-without-detection, found the same day.
⚠ Nothing surfaces it: discovery_skip_counts gives skip_dir: 2 with no
directory or rule name, and the pruned path goes to logger.debug.
⚠ FOURTH report of one shape after #300 / #187 / #304, and #301 audited
~40 call sites for exactly this, listed get_extra_ignore_patterns as fixed and
named neither of these; v1.108.197 then fixed the three max_* resolvers and
left them too. A fifth audit finds a fifth instance; the ratchet finds it on
the commit that introduces one.
⚠ Signature-only would have been a FALSE GREEN — adding the parameter and
leaving callers bare changes nothing observable, so the call-site check walks the
AST, not the signature.
⚠ index_repo is exempt BY NAME, not by omission: a project config is found
by walking up from a LOCAL path and a GitHub tree has no checkout, so passing the
owner/repo id would imply a lookup that cannot succeed. This is a stated
DEVIATION from the reporter's acceptance criterion 5, said so on the PR.
⚠ tests/test_security_exclusions_are_project_overridable.py (13), all red at
b85ef61 — but three are constraints, red only because repo= is not a
parameter there, so each also asserts the no-argument form. Do not report
"all red" without that distinction; it overstates the evidence.
2026-08-18: #500 FILED AND FIXED BY US via PR #501 — a promise in a comment
with no code behind it, found while checking whether #488 was safe to ship.
embed_repo's # Detect dimension mismatch — if the stored model differs, force a rebuild implemented NO detection: stored_dim only seeded dim, nothing
compared stored model to active, and set_dimension fired only when
dim is None (first-ever embed). A model change wrote new-width vectors beside
the old under a meta row naming the first.
⚠⚠ THE CONSEQUENCE COMPOUNDS AND IS SILENT. EmbeddingMatrix infers width
from the FIRST row and drops the rest, and the inferred width follows the
majority of PRE-EXISTING rows — so every symbol embedded after the change is
excluded from semantic search, forever, and the gap grows with every new file.
Measured {384: 6, 768: 1} with meta reporting 384. A recall failure that reads
as a finding.
⚠⚠ THE READ PATH IS NOT THE DEFECT AND WAS LEFT ALONE. _build's exclusion
is a faithful port of what _cosine_similarity did before the matrix existed and
its comment says so. Fixing the consumer would have HIDDEN the producer —
the fix must go where the mixed store is CREATED. Same lesson as #493's write:
find what was proven, not what was written.
⚠ Unknown is not a change: a store with no persisted model name must NOT
force a rebuild, or every existing user is billed a full re-embed for a model
that may be identical.
⚠ stored_dim is cleared inside the force branch, which REPAIRS A SECOND
BUG nobody reported: the pre-existing task_type force path cleared the store
and left dim seeded, so the dim is None gate never re-fired and the meta kept
advertising the old dimension against fresh vectors.
⚠ skipped_dim_mismatch was computed, stored on the object and read NOWHERE
(grep found only its three defining lines). A count that exists and is
discarded is the same defect as not counting. Now surfaced as
_meta.semantic_partial + channels.semantic: "partial", because the producer
fix does not heal stores already mixed.
⚠ evidence/capability.py has called get_model() since v1.108.221 behind a
type: ignore and a bare except, so the capability certificate reported
model: "unknown" for EVERY repo. Found by adding the method, not by reading
the call site — a bare except around a type: ignore is a permanent silent
failure by construction.
⚠⚠ THIS IS THE BLOCKER ON #488 AND THAT IS WHY IT WAS FILED SEPARATELY.
Making explicit config outrank the ONNX default makes provider changes MORE
frequent, and until now each one silently degraded the index. The "migration
hazard" that looked like a cost of #488's option A was a pre-existing defect
option A would merely have made more likely to fire. Check whether a hazard is
introduced or merely exposed before pricing it against a design choice.
⚠ tests/test_embedding_model_change.py (9), 8 red at pre-fix (2 of those are
signature-only reds); the same-model no-rebuild control passes both sides.
⚠⚠ PROCESS, MEASURED THIS SESSION: a push is the RELIABLE way to re-provoke a
missing license/cla; close+reopen is NOT. #499 opened with zero statuses
on its head (the #479 shape — the bot never fired, which reads identically to
our-push-erased-it). Close+reopen left count=0; git commit --amend --no-edit
- force-push restored it
successwithin a minute. That is now 2 failures and 1 success for close+reopen and 2 successes for a push. ⚠ It also blocks the merge for real now thatlicense/clais required (3d), so an unfired bot on OUR OWN PR presents asBLOCKEDwith 11 green checks. ⚠ Batching worked: #490/#491/#492/#493/#500 were all merged before touching #443, and it was resolved ONCE instead of five times. That is the lever policy 3b leaves when the contributor PR is BLOCKED and cannot go first.license/claSURVIVED this push — the opposite of the previous one, so read the status, do not predict it.
2026-08-18: #493 + #492 (@rknighton) FIXED BY US via PRs #496 / #498.
Unreleased; see CHANGELOG [Unreleased].
#493 — index_file advanced the repo git_head after proving one file.
repo_is_stale is "index SHA differs from live HEAD", so refreshing one file out
of a two-file commit CLEARED staleness for the file never refreshed, and
get_file_content served commit-A content reading channels.index: fresh
against a clean tree.
⚠⚠ THE WRITE IS NOT THE DEFECT; WHAT HAS BEEN PROVEN BEFORE IT IS.
index_folder._refresh_git_head_if_advanced makes the IDENTICAL write on a
no-change run (#330) and is CORRECT there, because that run walked the corpus.
Two calls, one write, opposite correctness. The reporter drew that
distinction himself and the fix is built on it — a diff of the two functions
would have shown nothing.
⚠ Fix is one git diff --name-only --relative against the stored head; advance
only if every other moved path is one the index neither carries nor would index.
--relative is load-bearing (a monorepo subtree must not be held back by a
sibling commit). An ADDED source file blocks too — not in the corpus, so not
"a file we carry that moved", but advancing would certify a complete index over
a corpus missing a file.
⚠ _paths_changed_between returns None for "could not ask", NEVER an empty
set, or a failed git call reads as a clean diff. Unknown → do not advance,
same asymmetry as .209.
⚠ Branch-delta path deliberately UNCHANGED (writes branch_meta, own
base_head); the reporter made no claim about it. Recorded, not swept.
⚠ tests/test_index_file_head_advance.py (10): 5 red at b85ef61, and the
other 5 pass on BOTH sides BY DESIGN — they are the constraint tests (#330
must not regress, a single-file commit must still clear staleness). A guard
that never advanced would satisfy every assertion about the bug and leave every
repo reading stale forever. Say so, or a reviewer reads them as vacuous.
#492 — resolve_repo answered a repository question with a filesystem fact.
Fast path 1 matched source_root containment alone, so a path inside an
independent nested clone returned the PARENT index as indexed: true.
⚠⚠ Whether it LOOKS wrong depends on something irrelevant to the defect.
Gitignored nested repo → read fails, absent, indistinguishable from a normal
empty result. Absorbed into the parent walk → same wrong repo, read SUCCEEDS,
state: ok. Two symptoms, one mis-resolution — and only the second case
proves it without involving absence semantics at all.
⚠ Guard is a .git stat, never a subprocess: fast path 1 exists to avoid
the resolve_index_identity walk that can HANG (#303), so a correctness guard
that spawned a process would trade the reported bug for the one the fast path
was built to prevent. Asserted by monkeypatching subprocess.run to raise.
⚠⚠ Classify by where .git POINTS, not by file-vs-directory.
.git/worktrees/ vs .git/modules/ is #372's distinction; submodules still
resolve to the parent because their content IS indexed into it. A
--separate-git-dir clone leaves a .git FILE pointing at neither, and a
file/directory test reads it as a submodule — tested by name.
⚠ A file outside the parent's corpus (gitignored/oversize/skipped) still
resolves to the parent: being outside the corpus and belonging to another
repository are different conditions.
⚠ tests/test_resolve_repo_nested_repo_boundary.py (11): 7 red at b85ef61,
4 boundary tests pass both sides by design. Submodules and linked worktrees
tested against REAL git layouts (git submodule add -c protocol.file.allow=always,
git worktree add), not fabricated .git markers.
⚠ Suite: 7923 passed, 17 skipped, 0 failed + ruff clean; +21 over #490's
7919 decomposes as 10 + 11.
⚠⚠ PROCESS TRAP, NEW AND CHEAP TO REPEAT: gh pr merge --delete-branch on a
PR that is the BASE of a stacked PR CLOSES the stacked PR. GitHub normally
retargets a stacked PR when its base merges; deleting the base branch in the
same operation closes it instead. A closed PR's base cannot be changed and it
cannot be reopened while the base is gone — gh pr edit --base returns
"Cannot change the base branch of a closed pull request", gh pr reopen returns
"Could not open the pull request". #497 died this way and was recovered as #498
from the same intact head branch. Merge a stacked base WITHOUT
--delete-branch, or retarget the child first.
⚠⚠ A PR stacked on a branch base GETS NO TEST MATRIX AND LOOKS CLEAN.
test.yml is pull_request: branches: [main], so #497 showed 3 green checks
(radar / retrieval gate / CLA) and mergeStateStatus: CLEAN with the matrix
never run — the fork-PR "only license/cla ran" hazard wearing a different
costume, and CLEAN is the part that sells it. Remedy is the workflow's own
escape hatch: gh workflow run test.yml --ref <branch> (all 9 jobs green,
run 32092744385). Count the checks; a green rollup is not a run matrix.
2026-08-18: #443's conflict was OURS for the SIXTH time, resolved on their
branch. Three of our merges (#490, #492, #493) landed in the same
[Unreleased] block, and a CONFLICTING fork PR has no refs/pull/N/merge and
therefore NO CI. Merged main in, resolved to one ## [Unreleased] with
elfrost's #447 section first, pushed to their fork. Suite on the merged tree
7929 / 17 / 0, +6 = exactly their tests; all 11 CI checks green on the merge
ref; license/cla PENDING is the only blocker.
⚠⚠ Six is not six incidents, it is one wrong merge order repeated — and
this round it was avoidable in a way the earlier ones were not: all three of
our merges happened while their PR sat blocked, and we batched none of them.
Policy 3b governs ORDER when we have a choice; when the contributor PR is
BLOCKED and we ship anyway, the remaining lever is how many separate
[Unreleased] merges we make before resolving once. Resolve after the LAST
one, not after each.
⚠ The CLA status was erased by our push and came back within ~2 minutes as
pending — both halves of the documented hazard fired in one push (erases an
existing status, provokes a missing one). count=0 was observed and is NOT
"cleared". Said so on the thread so eleven green checks are not read as done.
2026-08-17: #490 (@rknighton) FIXED BY US via PR #494 — a cache that
announced readiness one key early. The BM25 corpus cache publishes FOUR keys
behind a check-then-build guarded on idf alone, and
cache["idf"], cache["avgdl"], cache["inverted"] = _compute_bm25(...) is THREE
__setitem__ calls, with centrality a fourth statement after a whole pass
over the corpus. A second caller passed the readiness check and raised
KeyError: 'centrality' — through the dispatcher, Internal error processing search_symbols. Unreleased; see CHANGELOG [Unreleased].
⚠⚠ The window is the entire runtime of _compute_centrality, so it WIDENS
with corpus size — the installs most likely to hit it are the ones where the
rebuild is most expensive. Do not file this shape as "a narrow race".
⚠ The lock was real and correctly held; the build WAS single-flight as #370
intended. What leaked is the readiness SIGNAL, which is read outside the lock by
design and therefore must not become true early. Diagnose which of the two the
defect is before reaching for the lock.
⚠⚠ THREE modules carried the identical block (search_symbols,
get_ranked_context, plan_turn), so fixing the reported one leaves two —
[[feedback_guard_every_path_that_shares_the_hazard]] again. One
ensure_bm25_cache() helper now serves all three; the fast path checks ALL FOUR
keys, not the sentinel, so a future reorder costs a lock acquisition instead of
a KeyError.
⚠ pagerank and name_map were CHECKED and deliberately LEFT — each writes
the one key it also checks, atomic by construction. Their
getattr(index, "_bm25_lock", None) or threading.Lock() fallback is a separate,
milder weakness (a fresh lock per caller guards NOTHING, so a lockless index
would duplicate work rather than crash); unreachable today because both
CodeIndex and SelectiveIndexView carry the lock. Recorded rather than
swept, same treatment as #473's module-level perf_db_path().
⚠⚠ THE FIRST SHIPPED-PATH TEST PASSED AGAINST THE BROKEN SOURCE. Signalling
from inside the build and letting the second thread race is not enough on a
two-file corpus: the builder finishes before the racer arrives. Only when the
build is held open until the second caller is demonstrably INSIDE its call does
it go red. A concurrency test that does not pin the interleaving is testing
its own machine's scheduler, and the tell is the non-vacuity pass: 7 of 8 red
first time, 8 of 8 after. [[a-concurrency-test-must-pin-the-interleaving]]
⚠ His Event framing said this in the issue body — "it does not create a
window" — and the first test ignored it. Read the reporter's note about their
own harness; it is usually load-bearing.
⚠ Suite: 7902 passed, 17 skipped, 0 failed + ruff clean. Total 7919 against
.284's 7911 = exactly the 8 new tests, so nothing else moved.
2026-08-17: #476 (@rknighton) FIXED BY US — one telemetry db spent another's
trim. _perf_rows_since_trim was one int on the _State process singleton
while the trim runs on conn, so with two stores alternating one tool_calls
was never trimmed. Now a dict. Unreleased; see CHANGELOG [Unreleased].
⚠ Low severity and the REPORT said so — opt-in, local-only, single-store
installs cannot reach it, cost is disk. He rates his own findings honestly; the
standing note that he understates still holds, but check each time.
⚠⚠ Keyed by the SAME str(path) the connection cache uses, and that IS the
fix. Keyed on the raw base_path instead, two spellings of one directory each
get their own budget toward a trim on one shared table — the same defect wearing
a new key. v1.108.280 resolved that spelling problem for the cache after #465;
this inherits it rather than re-opening it. [[feedback_guard_every_path_that_shares_the_hazard]]
⚠ Added _ensure_perf_db_locked_with_key rather than calling _perf_db_path
twice — re-deriving the key at the trim site repeats that helper's mkdir on
every write, and #442 exists because per-write cost on this exact path was the
whole problem. Two callers keep the old connection-only signature.
⚠ close_perf_dbs() clears the counters with the connections so a key cannot
outlive its store; the bounded cost is ~1000 rows of slack for a database whose
connection is dropped mid-cycle, against a cap that is already an
every-1000-writes approximation.
⚠ Nothing is backfilled: an already-oversized tool_calls trims on its own next
cycle.
⚠ tests/test_perf_trim_is_per_database.py (4) asserts on the COUNTER MAP, not
row counts after 1000 writes — 2000 rows across two databases would be slow and
would pin the trim interval. All 4 red against a restored single counter.
2026-08-17: #443's conflict was OURS and we resolved it on their branch.
elfrost's PR sat CONFLICTING/DIRTY since 2026-08-12 — and a conflicting fork
PR has no refs/pull/N/merge, so it gets NO CI AT ALL, which is why it read as
stalled contributor work when it was our CHANGELOG merges. Merged main in,
resolved to one ## [Unreleased] heading with their section first, pushed to
their fork, said on the thread that the conflict was ours. MERGEABLE again,
suite green (7856/17, +6 = their tests).
⚠ Checked before promising: elfrost is a User, not an Organization, so the
maintainerCanModify-lies trap did not apply and the push worked.
⚠ The CLA status SURVIVED this push — the documented erase-on-push hazard did
not fire here. Do not treat either outcome as the rule; read the status.
⚠⚠ #447 was NOT implemented, deliberately. Its window is posted publicly to
2026-08-20 and jjg reaffirmed it stands as posted. Resolving the conflict is
the move that respects the promise AND unblocks them — it removes the reason the
PR was dark without shortening anything.
Merged 2026-08-16: #479 (@mikemikimike) closes #475 — IndexStore /
SQLiteIndexStore keyed their init caches on the SPELLING of base_path, so a
relative storage_path skipped mkdir and schema setup for the second store
after a chdir. Two source lines. Unreleased; see CHANGELOG [Unreleased].
⚠ The mock cleanup is the larger half. patch("...Path.resolve", return_value=X) replaces resolve on the Path CLASS, so it answered for every
path in the process — including the storage path IndexStore resolves at
construction. Four test_tools.py tests then CREATED their index directory at
the faked location (a stray C:\work\project locally; mkdir death at
/workspaces/myrepo and \\server\share\ in CI). ⚠⚠ Nothing in the suite
could have reported it, because the writes landed where no assertion was
looking — same family as #439's blanket os.path.exists mock. _resolve_only
in tests/__init__.py is narrow, and needs autospec=True so self reaches the
side effect. All 19 patch sites converted; the other 15 were wrong too, just
inert. ⚠ The expanduser() half MOVES an existing case (a literal ~ in
storage_path built a directory named ~); disclosed, not migrated.
2026-08-16: the suite runs in PARALLEL, and that surfaced a test living on file
ordering. pytest-xdist at -n 4 --dist loadfile, wired into test.yml.
Measured on a 24-core box: 599s serial vs 183s parallel, same 7,859
collected; CI's exact command (with coverage) is 258s locally. Test-only + CI, no
version bump; rides the next release.
⚠ --dist loadfile is load-bearing. Whole file per worker preserves
within-file order; the default --dist load spreads individual tests and breaks
any file sharing module-level state.
⚠ Worker isolation is STRONGER, not weaker — everything conftest resets
(_GLOBAL_CONFIG, index cache, perf-DB handles) is process-global and each
worker is its own process. What parallelism removes is the accidental
cross-FILE ordering the serial run gave for free.
⚠⚠ The two failures it produced were NOT caused by parallelism — they
reproduce serially in isolation. test_css.py and test_json.py imported via
src.jcodemunch_mcp, a different module object from jcodemunch_mcp (is
→ False) carrying its own config._GLOBAL_CONFIG that conftest never resets.
The twin lazily read the developer's real ~/.code-index/config.jsonc,
is_language_enabled gated the language out of the languages allowlist, and
parse_file returned [] against a direct extractor's 10 symbols.
⚠ Which half failed is the proof of mechanism: test_css.py drives BOTH
css and scss through parse_file and only scss broke — css is in the
allowlist, scss is not. They passed serially only because test_config.py
(also src.-prefixed) overwrote the twin earlier in alphabetical order.
⚠ Maintenance Practice #8 in a spelling its guard cannot see —
test_config_isolation_guard.py knows nothing of the src. prefix. 14 files
still import through the twin, and test_al.py / test_blade.py are the same
defect UNFIRED, passing only because al and blade sit in this box's config.
Not fixed; the two live failures are. Next sweep starts there. DONE — see
the sweep entry immediately below.
2026-08-17: the package twin is RETIRED and the guard now sees the spelling.
All 140 src.jcodemunch_mcp references across 14 test modules converted, and
tests/test_config_isolation_guard.py gained the check. Test-only, no version
bump; rides the next release.
⚠⚠ The guard already existed and a different IMPORT PATH walked around it —
the same shape as the defect that file was written for, where the guard existed
and the CALL SITES walked around the reset. That is why the check went INTO that
file rather than a new one.
⚠ Two of the fourteen were live, twelve were unfired. test_al.py and
test_blade.py are the identical parse_file defect and passed only because
al and blade sit in this box's languages allowlist.
⚠⚠ The patch("src.jcodemunch_mcp...") form fails the OTHER way and is the
worse half: it patches the twin's attribute while the test drives the canonical
module, so the patch does nothing and the test passes without testing what it
names. Two existed (test_config.py:351, test_git_sha_verification.py:159).
Converting imports without converting these would have left a false green.
⚠ Detector matches a string only when it STARTS with the twin root (the shape of
a patch target) and skips docstrings, so prose naming the hazard is not a
violation — asserted by name. ⚠ _TWIN_ROOT is assembled from two literals so
the guard does not exempt ITSELF; as one string it flags its own source line,
and exempting the file or special-casing its name both stop it policing itself.
⚠ Non-vacuity proven against the REAL pre-fix tree, not just synthetic
fixtures: restoring tests/test_al.py from HEAD turns it red naming lines 6-7.
TWIN_EXEMPT is EMPTY and its parametrize-over-nothing SKIP is the ratchet at
rest.
⚠ Suite: Windows 7850 passed, 17 skipped, 0 failed, coverage 79.66%, ruff
clean. Delta from 7864 is EXACTLY +3 and decomposes as +2 passing guard tests
and +1 skip (the empty parametrize) — the skip count moving 16 → 17 is the
ratchet arriving, not a lost test.
⚠ CI pinned to -n 4, deliberately not -n auto — GitHub runners are
4-core so auto matches today and would jump silently on a resize, and extra
workers contend on the same ~/.code-index process-lock scopes that caused
.261's 47m outlier.
⚠⚠ The local-uv lock hazard fired in its THIRD direction on a change that only
added a test runner. Local uv 0.12.1 vs the CI pin 0.9.5 gave 76 insertions /
52 deletions, and beyond the known nvidia widening it stripped
python_full_version guards off the google-api deps and typing-extensions,
changing what installs on 3.10 vs 3.14. Re-locked with uvx --from uv==0.9.5 uv lock: 24 insertions, 0 deletions. Diff the lock after EVERY uv lock, not
just version bumps.
⚠⚠ It went RED on CI and the failure was a REAL production defect the serial
runner had never exercised — call_tool ate its caller's format argument.
arguments.pop("format") popped from the CALLER's dict, so a caller reusing one
args object got JSON first and server_output's default after. Fixed at the
dispatcher (arguments = dict(arguments)), not in the tests, because the Counter
front door re-dispatches through the same path. Over the wire it is unreachable —
every request arrives as a fresh dict — so only in-process callers are exposed.
⚠⚠ It presented as an environment quirk and that is the reusable part. The
second call falls back to auto, where the 15% encoding gate decides per
response, and the response carries timing_ms. Coverage instrumentation slows
the call, moves that number, moves the byte count, tips the gate. Red on ubuntu
3.10/3.11/3.12, GREEN on ubuntu 3.13, green on all four Windows legs, green
locally without --cov, red locally with it. Chasing the platform matrix would
have found nothing.
⚠ Reproduced on a WSL Ubuntu 3.12 copy, which is what made it cheap — Windows
cannot produce it at all, and a CI cycle is 4 minutes against WSL's 3. Docker
Desktop was not running; wsl -d Ubuntu with a tar-copied tree and its own uv
was enough. ⚠ WSL interop expands $PATH into the command string and the Windows
PATH contains parens, so bash -lc dies on a syntax error — use absolute paths
and no variables.
⚠ tests/test_dispatcher_arg_mutation.py (3) asserts on the ARGUMENT DICT, never
on the response encoding, so it does not inherit the gate's environment
sensitivity. Reverting turns 2 of 3 red; the third is the control.
⚠ Suite with the fix: WSL Linux 3.12 7833 passed, 0 failed (+9 sdist errors
that are an artifact of copying without .git); Windows see release line.
Delta decomposes as 7828 + 2 fixed + 3 new = 7833. Fold into the Tests: line
at release, not before.
2026-08-15: #428's remaining four languages IMPLEMENTED BY US (Rust, Go, Java,
PHP), closing it. Shipped as 1.108.281 via PR #478; see Current State.
⚠⚠ This is a REVERSAL of an open handoff, not a timebox expiring, and it was
jjg's call. The half was @mussonking's by an offer with no date on it — the
standing rule is that every handoff names a date AND the default that fires on
it, and this one named neither, which is exactly how it sat seven days. Credit
for the report and for the plural-helper design stays his in the CHANGELOG.
The process lesson is the open-ended offer, not the reversal.
[[feedback_never_hand_off_without_a_timebox]]
⚠⚠ Java needed more than a branch and the gate was the real defect. The
constant walk was parent_symbol is None, which keeps function locals out — and
a Java constant is a class member, so field_declaration sat in
constant_patterns unreachable by construction. The gate now also accepts a
CONTAINER parent for _CLASS_SCOPED_CONSTANT_LANGUAGES ({"java"}), never a
function parent. ⚠ Relaxing it for every language was DECLINED: Python class
bodies, JS class fields and PHP class constants would all start emitting
constants they never have, moving symbol counts in every index and every
published dead-code grade. One named set, one sample per member, asserted by
name in test_only_named_languages_reach_constants_through_a_container.
⚠ Scala looked like a counter-example and is not — its val_definition is in
symbol_node_types, so it never touches the constant gate at all. Checking that
before copying its shape is what kept the widening narrow.
⚠ The exclusions are the careful half: Rust static mut (a
mutable_specifier says the binding changes), Java bare final (per-instance)
and bare static (mutable shared state). A missing constant is a recall bug
the reporter could see; an ordinary field arriving as kind="constant" is a
precision bug nobody goes looking for.
⚠ Grammar shapes were DUMPED, not assumed — the TOML left-recursion defect
came from assuming. Go binds N names two ways at once (const ( ... ) groups
plus const A, B = 1, 2), which is what _extract_constants being plural is
for. ⚠ No case heuristic anywhere: const IS the declaration, and filtering on
case would silently drop Go's unexported lowercase constants.
⚠ tests/test_v1_108_281.py (10), 9 fail against d10490e; the 1 passing
both sides is the control that Java function locals are still not constants.
EXEMPT in test_constant_extraction_guard.py is now EMPTY — the ratchet
forced its own deletion, and its parametrize-over-nothing SKIP is the ratchet at
rest, not a lost test.
Merged 2026-08-14: #473 (@rknighton) closes #465 — the perf-db connection
cache keyed on the caller's SPELLING, not the resolved path, so a relative
storage_path wrote one store's telemetry rows into another's after a chdir.
Two source lines, three tests, unreleased; see CHANGELOG [Unreleased].
⚠⚠ The reusable half is where the fix went. _perf_db_path has one caller
and all three telemetry sinks reach the cache through it, so resolving where the
path is BUILT fixes every consumer including the one the issue left open. Fixing
it at the cache would have covered the sinks that were reported and missed that
one ([[feedback_guard_every_path_that_shares_the_hazard]]).
⚠ The module-level perf_db_path() helper is still unresolved ON PURPOSE,
checked rather than assumed: it never touches the cache, and an unresolved
spelling opens the same file on disk. Recorded here so a later sweep does not
"finish the job" and call it a fix.
⚠ Both exits needed the change and only one is covered by the row-level
tests — they all pass an explicit base_path, so the no-argument exit carries
its own assertion. Reverting either .resolve() alone turns the file red, which
is the non-vacuity pass done per-edit rather than per-file.
⚠ First timebox to expire in the contributor's favour under policy 3a: #465
was handed off with a 2026-08-21 default, and the PR arrived in a day. The
window decides whose commit it is, and here it was his.
⚠ His verification note is worth copying: he reported a DELTA, not totals.
Ten test modules importorskip at module level, so a missing optional dep costs
a whole module as ONE skip and no per-test trace — two correct runs of the same
commit can differ by a hundred. Same class as the --extra watch under-collect
in the Tests line above, found independently from the other side.
In flight 2026-08-13: #441 + #442 (@rknighton), ranking-ledger write path.
Same path .272 touched. Unreleased; see CHANGELOG [Unreleased].
⚠⚠ The reusable lesson is that measuring the SAFE fix first is what chose the
risky one. #442 has an obvious low-risk shape — remember the schema is ready,
skip the eight IF NOT EXISTS statements, keep the per-write open/close, no
connection lifetime to manage. Measured: it captures 2% of the available
saving. The other 98% is the open/close itself. Had that not been measured, the
cheap fix would have shipped, looked principled, and delivered nothing.
⚠ Shipped path is 3.455ms vs 16.615ms, a 79% cut, agreeing with his 82%
(absolutes are machine-local; the ratio transfers). ⚠ check_same_thread=False is
REQUIRED (searches dispatch via asyncio.to_thread, so a cached connection
outlives its opening thread) and SAFE only because every caller holds
_State._lock — recorded at the call site because a future edit could quietly
invalidate it.
⚠⚠ Caching a connection introduces TWO silent failure modes the report did not
name, and the benchmark found the first by crashing: a stray close() poisons
the cache so every later caller gets a dead handle (telemetry off, every write
still reporting success), and a DELETED db file gets written into an unlinked
inode forever (pre-caching, the next event just recreated it). A liveness probe
catches only the first; the exists() check is what catches the second. Together
0.344ms, 2.1% of the pre-fix write. ⚠ Windows cannot produce the orphan case
at all (it refuses to unlink a file with an open handle) — the end-to-end test
is POSIX-only and says so, with a portable unit test for the predicate. Do not
read that skip as cross-platform coverage.
⚠ Suite at this point: 7763 passed, 9 skipped, 0 failed + ruff check src/
clean. Reconciled by same-tree collect: 7772 total, 7753 with
test_v1_108_276.py ignored (= its 19), so nothing else moved. The 9th skip is
the POSIX-only orphan test. Fold this into the Tests: line at release, not
before — it is not a released count yet.
⚠ #441 pre-existing rows keep NULL = UNKNOWN and are NOT backfilled; inferring
count == len(returned_ids) is the defect itself. ⚠ He filed it against his own
earlier claim in Discussion #430 and caught it on re-verification. ⚠ Severity
checked and it is genuinely analysis-only: regret and ledger_trust read
returned_ids only for emptiness/>1, and truncation starts above 50.
Merged 2026-08-13: #439 (@JayceeB1) Windows drive-root child Git repos, closing #438 — plus #453 fixed on top, test-only. Both ride the next release; no version bump. ⚠⚠ The reusable lesson is one sentence and it cost most of a day: a mock broad enough to satisfy an assertion can be broad enough to bypass what the assertion is about. It fired three times in three different costumes.
⚠⚠ My own review advice on #439 was WRONG and nearly shipped a hole. I told
them _path_safety_part_count(path) == 2 subsumed not drive.startswith("\\\\").
It does not: the helper is a DEPTH rule and the UNC clause is a SCOPE rule.
A UNC share root has ONE real part and the helper adds one for the
\\server\share anchor, so it computes to exactly 2 — the same as C:\repo.
With the clause gone, \\server\share holding a .git is admitted, handing the
indexer a whole file server through the guard that exists to stop that (#321/#322).
⚠ len(path.parts) genuinely WAS a redundant depth notion — that half was
right. The error was concluding that a second condition mentioning the same
variable must therefore be redundant too. Check what a predicate is FOR, not what
it reads. Restored with both clauses, the reason recorded in the docstring, and
the bad advice corrected in the CHANGELOG rather than deleted
([[feedback_a_fix_comment_is_not_evidence_about_its_siblings]] — same reason).
⚠⚠ The regression test for it was ALSO wrong, in a way that PASSED. It patched
os.path.exists to a blanket True, which also answers _is_container()'s
/.dockerenv probe — that drops _MIN_PATH_PARTS from three to two, so 2 < 2
skips the guard entirely and _is_shallow_windows_git_root was never called
(proven by spying on it: zero invocations). ⚠ The tell was that it failed
IDENTICALLY with and without the fix. A test failing on both sides is as
uninformative as one passing on both sides, and it is the cheaper tell to notice
because you are already looking at a red. Run the non-vacuity pass even when the
test is currently failing.
⚠⚠ #453's tripwire had the same disease a third time: it could not fire.
_no_real_access_under raised AssertionError, and every read site it guards is
wrapped in a bare except Exception in production, so a deliberately re-broadened
mock passed cleanly with the guard installed. Now derives from
BaseException. A guard that cannot fire is worse than no guard, because it
reads as coverage. Always prove a new guard fires by breaking the thing it
watches.
⚠ #453's actual root cause was NOT the one I inferred, and the difference
mattered. I traced seven network read_text calls (detect_framework probing
manifests under a blanket Path.exists=True) and took them for the culprit; they
are real network I/O in a unit test but are swallowed by production's
except Exception and never failed anything. The failure was
resolve_index_identity → folder_path.is_file() (storage/git_root.py:160),
never patched. Only pulling the real CI traceback settled it — attempt 1 of a
rerun run, via gh api .../runs/<id>/attempts/1/jobs, because a rerun flips the
run's conclusion to success and hides the failure from gh run list.
Path.is_file() swallows ENOENT-class errors (a box with no such share) but
propagates WinError 64 (a runner with live-but-failing networking) — same test,
opposite outcomes, decided by whose network answered.
⚠ Process note: git checkout -- <file> destroyed uncommitted work TWICE
during the non-vacuity passes, because the falsification edit and the fix lived in
the same file. Copy the fixed file to the scratchpad first and restore from that.
Merged 2026-07-25: #379 (@oderwat) Gleam import extraction — Gleam was already
in LANGUAGE_REGISTRY, so symbols extracted but the import graph stayed EMPTY,
leaving find_importers/get_blast_radius/get_dependency_graph silently blind
on Gleam projects. Same shape as the week's verdict work: capability present,
wiring absent. Verified against a TRIAL MERGE onto current main (branch-green is
not merged-green), 210 neighbouring import/language tests green. Landed AFTER the
1.108.170 release commit, so it rides the NEXT release, not that one.
Merged 2026-07-25: #378 (@zuoYu-zzz) TOML symbol extraction — tables → type,
array tables → class, key-value pairs → constant. Merged rather than
review-round-tripped, then fixed on top in f0eda7b. ⚠ The defect worth
remembering: _extract_key scanned a dotted_key's DIRECT children for
bare_key/quoted_key, but tree-sitter-toml nests dotted_key
LEFT-RECURSIVELY ([tool.ruff.lint] = dotted_key(dotted_key(tool, ruff), lint)), so every segment but the last was dropped. Two-level paths worked,
which is exactly why it read as correct — the bug only shows at three-plus, and
on jcm's OWN pyproject.toml [tool.hatch.build.targets.wheel] came back as
wheel with signature [wheel], a header that appears nowhere in the file
(search_symbols would have handed an agent fabricated source text). Fix returns
path SEGMENTS and recurses; building from segments also fixed name/
qualified_name, which the PR set to the same value (now leaf / full dotted path,
matching every other extractor). New test asserts three- AND five-deep tables plus
a signature-occurs-in-source check, proven non-vacuous. The PR's own test used
only single-segment headers, so nothing in the suite could have caught it — the
general lesson for any new nested-grammar walker. Rides the next release with #379.
Closed 2026-07-25: #380 Atlas Cloud summarizer (@binyangzhu000-sudo). Closed on
DEMAND, not quality: CLA unsigned (hard blocker), and the capability is fully
reachable today via OPENAI_API_BASE + SUMMARIZER_PROVIDER=openai since Atlas
Cloud is OpenAI-compatible. Cost of merging was 8 permanent env-var spellings
(ATLASCLOUD_/ATLAS_CLOUD_ × API_KEY/API_BASE/BASE_URL/MODEL) plus 3
aliases, permanent under the 1.x no-removal contract. ⚠ Do NOT re-close a future
one of these "we don't take branded providers" — MiniMax/GLM/OpenRouter are
exactly this shape and already merged; the comment concedes that on the record.
The bar is a user asking, same as platform installers. It correctly added
atlascloud to _PAID_CLOUD_PROVIDERS, so the money-safety guard was respected.
Tracker state 2026-07-28: ZERO open issues, ZERO open PRs. Verified against
gh issue list --state open / gh pr list --state open, with an
--state all query alongside to prove the empty result was not a failed query
([[feedback_empty_cli_query_is_not_evidence]]).
⚠⚠ DO NOT quote a tracker count from this file — re-run the query. The line that used to live here said "Open issues: #375, #377. Open PRs: #387 and #388" while a paragraph twelve lines below it recorded #388's own close. It was internally contradictory and it was believed anyway, which is how a stale "#375 ONLY" got written into this file on 2026-07-28 for an issue closed the day before. A count is the one fact here with a guaranteed expiry date.
Closed 2026-08-05: #414 (@MotoMato85) byte offsets slicing a decoded str in
16 extractors — shipped as 1.108.244, see Current State. ⚠ The report is the
best-instrumented one this project has received: an AST audit finding exactly
35 source subscripts and classifying 34 as byte-offset plus the ONE correct
character-domain case, a per-symbol drift table proving the shift EQUALS the
extra UTF-8 byte count, a 118-function whole-repo measurement (105 wrong before,
1 after — and he diagnosed the survivor as an unrelated tree-sitter ERROR
node), and a fix proposal with an AST argument for why substituting source is
behaviour-preserving. Every claim I checked reproduced byte-identically.
⚠ His proposed helper had one flaw worth remembering: its
for cut in range(4) decode loop trims trailing bytes until a decode succeeds,
which for a slice containing an INVALID byte in the middle silently returns the
prefix and DROPS the rest. Shipped version only trims when the bad run reaches
the END of the chunk. A test I wrote for the degradation path caught it;
neither of us would have caught it by reading. ⚠⚠ The reusable lesson is that
fixing the producer left the DATA wrong and the obvious remedy did not work:
"re-index" is a no-op here, because the corrupt rows sit in files that never
changed ([[feedback_fixing_a_producer_does_not_fix_its_history]]). Hence
PARSER_GENERATION, and it must be checked BEFORE every early-returning fast
path. ⚠ A pure-ASCII fixture cannot fail on this class at all, which is why it
survived every existing parser test.
Closed 2026-08-05: #413 (@LuigiNicaPRO) a silent full rebuild replacing a
requested incremental — shipped as 1.108.243, see Current State. ⚠ The
reusable lesson is that the READ side and the WRITE side of one store drifted.
inspect_index was built in PR #291 specifically to discriminate the causes
load_index collapses into None, four read-path tools adopted it, and the two
INDEXING tools kept a hand-written branch that named one cause for seven. His
own grep is the diagnostic worth copying: loadab / load_error /
index_status / sqlite_corrupt / index_present returned zero hits in
index_folder.py while existing_index is None was present. ⚠ He measured
the harm honestly and DOWN: on his repos the substituted rebuild costs about a
second, and he said so unprompted rather than inflating it. The defect is the
undiagnosability, not the time. ⚠ We shipped his options (1) and (2) and NOT
(3) — an on_unloadable_index="error" parameter is permanent surface under the
1.x no-removal contract, and he stated (1)+(2) covers his case. Demand-driven, as
with platform installers.
Closed 2026-08-04: #412 (@rknighton) git_sha verification accepted a truncated
cache — shipped as 1.108.235, see Current State. ⚠ The reusable lesson is
about WHERE it was measured, not about the comparison. His repro exercised
_slice_matches directly, which is the layer his own #401 patch had edited. At
get_symbol_source's entry point the sibling content_verified already returned
False on those caches, so the served response was contradictory rather than
uniformly wrong. Neither view alone is the whole answer, and the tool response
is the one that describes what a caller experiences
([[feedback_verify_at_the_users_entry_point]]). ⚠ This is the first finding of
his that got LESS severe on inspection — the standing note says he understates,
and #411 was the first time verifying upward came back neutral. Two in a row now
land off that pattern: check every time, report whichever way it lands, and do
not reach for the expected direction.
Closed 2026-08-04: #411 (@rknighton) test config isolation — _run_config
in tests/test_v1_108_194.py scrubbed JCODEMUNCH_MAX_FILE_SIZE but left the
subprocess reading the developer's real ~/.code-index/config.jsonc, so both
max_file_size assertions failed on any box with the key set. Reported WITH a
git apply --check-verified patch; applied as written in 5a3ee39.
⚠ TemporaryDirectory, not the mkdtemp used elsewhere in that file — the
config reporter WRITES a config.jsonc into the directory it is pointed at, so
an uncleaned temp dir per call accumulates. That is the whole reason the patch
costs a reindent, and it is the part a later "simplification" would undo.
⚠ His severity framing was checked upward and HELD, which is the notable
part — the standing note on this reporter is that he understates, so both
larger readings were tested and came back NEGATIVE. (1) Not a production
precedence bug: config beats env for max_file_size and the resolver agrees,
but max_folder_files / max_index_files behave identically, so .193's key
follows its siblings. (2) Not wider than one file: test_surface_cli.py is the
only other CLI-shelling test without CODE_INDEX_PATH isolation and it PASSES
under a hostile config because its assertions are shape-based, not value-based
(latently exposed if anyone adds a value-based one); test_watch_all.py is
isolated by argument (watch_all.py:48 honours storage_path).
⚠ The observation worth keeping: the file's docstring says it guards the #375
failure mode, so the test protecting the large-file escape hatch was broken by
USING the large-file escape hatch. Reaching it at all required being exactly
the user it was written for, which is why it went unseen on every dev box that
had not capped out on a large repo. Same family as jdata's test_v1_15_0 /
test_v1_16_0 false-greens reading the real ~/.data-index. Test-only, no
version bump; rides the next release.
Closed this session: #390 (@lazy-geeek, its own repro already fixed by
.194), #391 (@amarakramali, rewritten as 1.108.197), #387 (@nyxst4ck,
rewritten wider), #377 (P3 remainder + P4 to ROADMAP.md with close
conditions and @mightydanp's credit, same treatment as #385/#386). #375 and
#388 were already closed 2026-07-27.
⚠⚠ PROCESS FAILURE WORTH NOT REPEATING: #388 fixed #384 and was opened
2026-07-27 06:51 UTC. We shipped our own .189 fix and closed #384 at 12:56 UTC
having NEVER LOOKED AT OPEN PRs — the cross-reference sat on #384's timeline the
whole time. CHECK gh pr list BEFORE WRITING CODE ON AN ISSUE. Their fix then
went CONFLICTING/DIRTY because .189 rewrote the same functions. Resolved by
PORTING the gap they covered and we missed (maybe_takeover) in v1.108.190
with credit in the CHANGELOG, release notes and close comment, rather than
asking a pre-empted first-time contributor to both rebase onto our version of
their fix AND sign a CLA. #388 closed 2026-07-27. Cleaned up in
v1.108.189 on a standing rule jjg set: an issue opens when work STARTS or when
a USER is BLOCKED — an issue is a problem to fix or a feature to build, not a
to-do list. #383 and #384 are FIXED (see Current State); #385/#386 (evidence
Phases 5 and 6) were CLOSED and moved to ROADMAP.md — accepted design with no
start date and an unmet dependency is a plan, not an issue. ⚠ Closing them is
NOT a rejection of @mightydanp's design and the close comments say so explicitly;
credit and close conditions moved verbatim. ⚠ The convention that GENERATED
the clutter was our own — "new scope gets its own close condition", cited in
#385's body. It is right for scope being WORKED and wrong for scope PARKED.
Remaining: #375 (needs a re-run from @dkiaulakis at >=1.108.182, not code) and
#377 (down to two concrete Phase 2 P3 edges @mightydanp pinned 2026-07-27:
an absence receipt still links a MUTABLE absent:<sha> key note_absence can
overwrite across snapshots, and validation vs rendering do two SEPARATE receipt
lookups instead of one atomic snapshot).
#375 (index_folder silent 1800s+ on Linux) — REOPENED 2026-07-26, and the
blocker is a RE-RUN, not code. Closed 2026-07-26 on our own measurement after
five releases; @dkiaulakis re-ran at 1.108.176 and the SAME tools/eidos subtree
took 268s SIGTERM'd vs a 240s baseline at .169 — no improvement, which is
exactly the condition the close comment said would reopen it. ⚠ No py-spy this
round: ptrace is restricted in his sandbox and his agent correctly declined to
grant itself CAP_SYS_PTRACE mid-task. ⚠ The 5400s full-repo number is a
CLIENT-side MCP timeout and does NOT prove the server job stopped — he flagged
that himself; he runs 10+ concurrent stdio servers and had no safe way to
identify his own process. What .176 DID deliver, in his words: "we can now see
the problem we could not previously see" — index_coverage read ABSENT before
and now reports a number plus index_stale: true (git_head_lag). The freshness
half stands; the stall is a separate axis. v1.108.182 shipped three bounds in
response (provider-discovery budget, walk pruning at iter_source_files,
per-file parse_file_budgeted), two of them his own twice-proposed suggestions.
⚠ STATED LIMIT, do not overclaim it: a watchdog stops the CALLER waiting, it
cannot stop the WORK — Python cannot preempt a thread and tree-sitter is C, so
an abandoned parse keeps burning CPU. It makes the index finish and the gap
visible; it does not cap CPU. Sub-problems: A -> #383, FIXED in .189. B closed
not-a-defect (default log_level is WARNING, so a healthy run emits nothing).
C fixed in .176 (a partial index no longer reports itself fresh; complete
is TRI-state and pre-.176 indexes report null, NEVER true — re-index or the
signal is not there to see). D near-ruled-out (every indexwrite acquire
passes wait_seconds=60.0 and RAISES naming the holder, so it cannot present as
unbounded silence). The double-index finding -> #384, FIXED in .189.
⚠ Next action is a PING, not a patch.
Closed 2026-07-26: #382 "Old tree sitter dependency?" (@kecsap) — asked why we
pin tree-sitter-language-pack>=0.7.0,<1.0.0 when "other code parser MCP tools
happily use >= 1.0.0". Tested 1.13.3 against the full suite before answering;
the pin STAYS, and the rationale now lives as a comment on the dep itself so this
is not re-derived. ⚠ The load-bearing reason: 1.x STOPPED BUNDLING GRAMMARS.
The wheel ships a single _native.pyd and an empty bindings dir; get_parser
downloads the grammar from a remote manifest into %LOCALAPPDATA%\tree-sitter- language-pack\v<ver>\libs on first use (proven by watching that cache go 0 -> 67
shared libs while walking our language list). That is runtime network access plus
executable-writes-to-disk in a tool that advertises itself as read-only and local,
and it breaks airgapped installs outright — i.e. exactly the class of undisclosed
persistent/network behavior that caused the PyPI quarantine, so it could never ride
a dependency-housekeeping commit anyway. Two smaller blockers: autohotkey,
ejs, verse do not exist in 1.x (DownloadError: not available for download),
so bumping silently drops three languages; and the nim grammar was swapped for a
different upstream (source_file/proc_declaration/identifier ->
module/stmt/routine/symbol/ident), so our nim extractor returns zero symbols.
Suite on 1.13.3: 5812 passed, 12 skipped, 1 failed (test_nim_parsing, and
only that). ⚠ There is NO API incompatibility to cite — we use exactly one symbol
from this package, get_parser — so do not argue the pin on API grounds; the
blockers are all behavioral. ⚠ Unrelated pre-existing pathology found while
testing, NOT a 1.x regression and NOT filed: get_parser("cobol").parse(b"x")
hangs indefinitely on BOTH 0.13.0 and 1.13.3. A 1-byte input, no timeout.
Unreachable today (we only feed it real .cbl files) but it invalidated the first
version of the compatibility harness, so any future per-language sweep must resolve
parsers WITHOUT parsing pathological input.
Closed 2026-08-15: #480 neuforge-pay metering pitch, at jjg's direction —
a mass-mailed vendor solicitation, not a feature request. The same text sits in
84 issues across GitHub, including Snailclimb/JavaGuide, a Java reading
list with no endpoints and no LLM calls, so nothing about this repo was read
before filing. ⚠ Three independent reasons, and the spam is the weakest one.
It decorates @app.get("/v1/query") and jcm is a local stdio server with no
endpoint, no per-call price and no hosted session, so there is nothing to attach
to. A payment SDK is third-party network egress plus a Merchant of Record
relationship inside a tool that advertises itself as read-only and local — the
#382 objection exactly, and the class that caused the PyPI quarantine. And it
fails #380's demand bar: branded providers get in when a USER asks
(MiniMax/GLM/OpenRouter did), and one vendor pitching its own SDK to 84 repos is
not a user asking. ⚠ neuforge-pay itself was deliberately NOT fetched or
inspected; the decision rests on none of it.
#381 (MCP Toplist badge) CLOSED by jjg — 120 identical drive-by PRs from that author; the badge renders "Top 1% of 81,432", not the rank the PR body promised, and it is live third-party-controlled content in a README that also renders on PyPI.
Rotated out of CLAUDE.md Current State under Maintenance Practice 5
(3 newest releases), verbatim. Newest first.
-
Prior (1.108.295): What the guard could not see. Four items, three of them a check that could not observe the thing it claimed to check. (1)
_build. We skippedbuildand.buildand NOT the underscore spelling — what Elixir/Mix, Sphinx and Dune use. ⚠⚠mixcopies dependency SOURCES into_build, so an Elixir project indexed EVERY dependency symbol twice and the copies competed with the originals in ranking: the v1.108.234 duplicate-source-tree defect wearing a third name. ⚠ Bounded by gitignore, listed anyway —build/is in that same gitignore. (2) The strict deny (#541)._bash_targets_outside_rootsreads path tokens out of the RAW command string, sogrep ~/x.mdwas ALLOWED andgrep $HOME/x.mdwas DENIED — same destination, opposite verdicts. A deny now requires a RESOLVABLE target, the caution already applied tofind, pipelines and../; it downgrades to a nudge, never to silence. ⚠⚠ The detector is deliberately NOT a bare\$— a trailing$is a regex end-anchor andgrep "foo$" src/is idiomatic, so suppressing on any$would silently weaken the enforcement a strict user opted into. ⚠⚠ A SECOND blindness surfaced FROM that test:_BASH_PATH_TOKEN_REmatched only POSIX roots, so/c/Users/j/x.mdwas seen andC:/Users/j/x.mdwas not — a strict deny on a path outside every root, on the platform most users are on, with the verdict depending on how the drive was spelled. Genuinely resolvable, so a real fix not a downgrade. (3) The cache hit-rate.analyze_perfpublishedhit_ratebare, where a hit is KEY-PRESENCE in the session LRU. arXiv:2608.20280 measured raw 51-60% falling to 1.1-2.2% once validity was checked. ⚠⚠ The system already knew the difference and the metric did not — #377 item 3 revalidates cached ABSENCES, #404 re-annotates row freshness. Raw rate KEPT withhit_rate_basis, three buckets beside it,hit_rate_revalidatedNone not 0.0 when nothing was validated. ⚠ Onlysearch_symbolsrevalidates, sohits_unvalidatedis non-empty BY CONSTRUCTION. (4) Docs: themcp_toolsetdefault_configdefer path, and the vendor's 30-50 tools degradation threshold against our 91. [[a-ratchet-can-pass-against-the-defect-it-names]] -
Prior (1.108.288): The reported surface was never the only one. Four fixes; in three of them the report named one site and the tree held several — which is .287's own finding ("we fix the reported call site and leave the mechanism") acted on BEFORE shipping rather than after. #447 (@elfrost) —
install-pack's pre-scan rejected a leading separator and.., necessary and NOT sufficient:C:/Windows/Temp/evil.txtcarries neither, andbase / relativeDISCARDSbasewhenrelativeis absolute, withmkdir(parents=True)running BEFORE the write. Confinement is by RESOLUTION now; the pre-scan stays as an early abort. ⚠⚠ The rule had THREE spellings already (security.validate_path+ a private copy on each index store) and the new call site would have been a fourth — one definition insecurity.resolve_within(), both stores delegating, ratchet on a straycommonpath. ⚠⚠ THE FIRST REGRESSION TEST PASSED AGAINST THE UNFIXED SOURCE AND ITS NON-VACUITY PASS WROTE A REAL FILE INTO A REAL WINDOWS SYSTEM DIRECTORY: it named the reported path verbatim, so the escape went OUTSIDE the directory the assertion searched. A test for an ARBITRARY-WRITE defect EXECUTES that defect every time you prove it is not vacuous — the target must be somewhere the test OWNS. ⚠ Refusal deliberately NOT platform-pinned:C:/...is absolute on Windows and an ordinary name on POSIX. ⚠ Implemented BY US at timebox expiry; elfrost found it, analysed it and wrote a correct fix #443 could not merge for CLA reasons — provenance stated on both threads. #517 (@marcelruhf) —license = { file = "LICENSE" }made PyPI publish the whole licence TEXT asinfo.license, so a commercial user had no identifier to allowlist. PEP 639 expression now. ⚠⚠ He could see ONE surface; we declared it on THREE — plugin.json and the mcpb manifest both saidLicenseRef-Dual-Use, so an allowlist still needed two entries. ⚠ The version suffix is load-bearing: LICENSE 1.2 must produce a NEW identifier or consent to 1.1's terms is inherited by terms nobody read; the ratchet pins the suffix to the file's ownVersionline. ⚠ PyPI metadata is IMMUTABLE per version, so it starts HERE and 1.108.287 keeps the full text. #515 (@rknighton) —CONFIGURATION.mddocumenteddisabled_toolsas[]against a shipped["test_summarizer"]. ⚠⚠ FOUR surfaces describe that default and the THREE that agree are the point — template,config --initcomment, and a TEST PINNING THE VALUE. A value pinned by a test can still be mis-documented; the pin guards the value, not every claim about it. Ratchet compares EVERYDefaultcell in the document. #504 (@lsg1103275794, his PR) — the v1.96 collision guard assigned_merge_with_existingon a matchinggit_rootwith NOwalk_prefixtest, so a full-root re-walk could never reach the incremental branch and every scheduled freshness check rebuilt the corpus. ⚠ DISCLOSED MIGRATION: one rebuild per index to establishsource_roots == [""]. [[push-to-the-fork-remote-by-name]]
Per-release suite counts for 1.108.286 and earlier, verbatim from CLAUDE.md.
The standing warnings drawn from these runs stayed in CLAUDE.md; what is here
is the per-release evidence behind them.
⚠ Prior (1.108.286): 7976 passed, 17 skipped, 0 failed; ⚠ Prior (1.108.285): 7945 passed, 17 skipped, 0 failed; ⚠ Prior (1.108.284): 7894 passed, 17 skipped, 0 failed + uv run ruff check src/ clean. ⚠ Reconciled by same-tree collect: 7911 with test_code_index_path_is_honoured.py, 7902 without = exactly its 9, and 7902 is .283's total, so nothing else moved. ⚠⚠ This release also measured the REAL STORE, which no pass count can show: a full run now CREATES nothing under ~/.code-index and emits no _watcher_*.signal, so the process-lock scopes are isolated. _savings.json and session_stats.json still move, because token_tracker was deliberately left on the home default (see Current State). Assert the side effect, not just the exit code. ⚠ Prior (1.108.283): 7883 passed, 17 skipped, 0 failed, and the 3.13 CI-env reproduce returned the SAME totals AND the same skip split — stronger than the usual same-total-different-split. ⚠⚠ TWO INDEPENDENT FALSE-GREEN MECHANISMS were found across these two releases and BOTH reported exit code 0. (1) PYTHONPATH=src python -m pytest tests/ -n 4 --dist loadfile — pytest-xdist lives in the dev group inside .venv and is INVISIBLE to a bare python -m pytest, so pytest rejected the flags, collected NOTHING, and exited 0 while the harness reported success. Use uv run pytest whenever xdist flags are passed. (2) A trailing | tail swallows pytest's exit status — a run with one real failure was reported as "exit code 0", because the pipeline's status is tail's. Write to a log and echo the exit code BEFORE any pipe; every number in this line was obtained that way. ⚠ Local suite is uv run pytest tests/ -n 4 --dist loadfile at ~200-300s against ~600s serial; CI pins -n 4, deliberately not -n auto. Prior (1.108.282): 7849 passed, 10 skipped, 0 failed + uv run ruff check src/ clean. Prior (1.108.281): 7848 passed, 10 skipped, 0 failed. ⚠⚠ Reconciled by a SAME-TREE COLLECT against origin/main, and arithmetic against the previous release line would have been wrong by 16 — main moved twice between .280 and this bump (#474's 14 tests, #477's 2), so the usual "delta from the last release" method had two unrelated merges inside it. Pick the method that matches how the work landed. Measured: 7858 collected on the branch vs 7847 on d10490e, +11. Decomposition: test_v1_108_281.py 10, plus a net +1 from the ratchet rearranging — four languages leave EXEMPT and join test_declared_constant_pattern_extracts_a_constant (+4) while the four test_exemptions_are_not_stale cases collapse into ONE empty-parametrize item (-3). ⚠ The 10th skip is that empty parametrize and is the ratchet AT REST, not a lost test; it re-arms the moment anyone adds an exemption (same shape as _JS_VARIANT_EXEMPT in .273). ⚠ The release commit adds no tests, so the post-bump run reproduces the pre-bump 7848/10 exactly. ⚠ 3.13 CI-env reproduce 7842 passed / 16 skipped, the SAME 7858 TOTAL, different skip split, via uv run --python 3.13 --group dev --extra watch python -m pytest tests/ -q. ⚠ Run SEQUENTIALLY after the local suite, never alongside it — two full runs share ~/.code-index process-lock scopes and contention is the documented cause of .261's 47m outlier (.280 records the reversal). Prior (1.108.280): 7822 passed, 9 skipped, 0 failed + uv run ruff check src/ clean. ⚠ Delta from .279's 7828 total is EXACTLY the 3 new test_perf_db_path_resolution.py tests, and that release carried no other code, so nothing else could have moved. ⚠⚠ The two suites were run in SEQUENCE, not in parallel, and that was a deliberate reversal mid-release. Both were started together, then the 3.13 arm was killed before it produced anything: two full runs on one box contend for the same ~/.code-index process-lock scopes, and contention is the documented cause of .261's 47m outlier. A false red costs a re-run and, worse, a few minutes of reading a real-looking failure. Sequence them; the wall-clock saving was never worth the ambiguity. ⚠ 3.13 CI-env reproduce 7816 passed / 15 skipped, the SAME 7831 TOTAL, different skip split, via uv run --python 3.13 --group dev --extra watch python -m pytest tests/ -q — without --extra watch it collects 105 fewer and reports a clean pass (see .278 below). Prior (1.108.279): 7819 passed, 9 skipped, 0 failed + uv run ruff check src/ clean. ⚠ Delta from .278's 7808 total is EXACTLY the 20 new test_schtasks_locale.py tests; the release's other half is docs-only, so nothing else could have moved. ⚠ 3.13 CI-env reproduce 7813 passed / 15 skipped, the SAME 7828 TOTAL, different skip split. Prior (1.108.278): 7799 passed, 9 skipped, 0 failed + uv run ruff check src/ clean. ⚠ Reconciled by DECOMPOSITION against .277's 7797 total: test_identity_normalized_tier.py 10 (#458) + test_schema_baseline_transcription.py 2 (#467) - 1 REMOVED (test_the_core_compact_schema_budget_is_unchanged) = +11, and 7797 + 11 = 7808 exactly. A removal is part of the delta and the usual add-only arithmetic hides it. ⚠⚠ THE DOCUMENTED 3.13 REPRODUCE COMMAND UNDER-COLLECTS BY 105 TESTS, and it reports a clean pass while doing it. uv run --python 3.13 python -m pytest tests/ -q collected 7703 against the local 7808. The missing 105 are ENTIRELY three watcher files (test_watcher_serve.py 49, test_watcher_lock.py 40, test_watcher_dynamic.py 16), each gated on pytest.importorskip("watchfiles") — and watchfiles is an OPTIONAL extra. CI installs it (uv sync --locked --group dev --extra watch, test.yml:84); the documented command does not. Use uv run --python 3.13 --group dev --extra watch python -m pytest tests/ -q — that run is 7793 passed / 15 skipped, the SAME 7808 TOTAL, different skip split. ⚠ The totals convention is what caught it: passed counts alone read as a plausible pass either way, and a whole subsystem being absent is invisible from N passed. ⚠ Do NOT read this as .277's number being wrong — 7782 + 15 = 7797 is internally consistent, so that run DID collect the watcher tests. uv run reuses an already-synced environment, so the same command can collect differently depending on what last synced it. The command is unreliable, not that record. Prior (1.108.277): 7788 passed, 9 skipped, 0 failed + uv run ruff check src/ clean. ⚠⚠ This release adds NO test file of its own and a flat delta would have been the RED flag on any other release — every prior one ships a test_v1_108_NNN.py, so "no new tests" normally means the bump outran the work. Here the work landed across the day in #459/#462/#463/#464 and the release commit is version metadata + changelog + rotation only. Reconciled by DECOMPOSITION rather than a same-tree collect, because the collect diff has nothing to subtract: test_html_file_class.py 4 (#459) + test_v1_108_277.py 6 (#462) + test_pid_reuse_identity.py 10 (#451 via #464) + test_claude_md_rotation.py 4→9 = +5 (#463) = 25, and .276's 7763 + 25 = 7788 exactly. ⚠ Pick the reconciliation method that matches how the work landed; applying the usual one here yields a zero and proves nothing. ⚠ 3.13 CI-env reproduce: 7782 passed / 15 skipped, same 7797 TOTAL, different skip split — compare totals across interpreters, never passed counts. Prior (1.108.276) + uv run ruff check src/ clean. ⚠ Reconciled by same-tree collect: 7772 total, 7753 with test_v1_108_276.py ignored (= its 19); the +5 over .275's 7748 is five new def test_ functions in test_tools.py from the #438/#439 drive-root work — COUNTED in git diff v1.108.275..HEAD, not inferred, because "nothing else moved" was not true this release and asserting it would have been the same shape of error the count notes below are about. ⚠⚠ The 3.13 CI-env reproduce totals the SAME 7772 but splits 7757 passed / 15 skipped — six tests that RUN on 3.10 SKIP there. A passed-count comparison ACROSS interpreters is meaningless; compare TOTALS. ⚠ The 9th skip is the POSIX-only orphaned-inode test for #442 — Windows refuses to unlink a file with an open handle, so this box CANNOT produce that case. Do not read that skip as cross-platform coverage; it is a real local gap covered only by the portable unit test for the predicate. Prior (1.108.275) + uv run ruff check src/ clean. ⚠ Reconciled by same-tree collect: 7748 total, 7734 with test_v1_108_275.py ignored (= its 14), and 7734 is exactly .274's total, so nothing else moved. Prior (1.108.274) + uv run ruff check src/ clean. ⚠ Reconciled by same-tree collect: 7734 total, 7728 with test_security_disclosure.py ignored (= its 6), and 7728 is exactly .273's total, so nothing else moved. ⚠⚠ This line was briefly written with a GUESSED number before the run finished, and the guess (7734) was the TOTAL rather than the passed count — it would have read as a plausible, wrong figure. Never pre-write a count; the run is the only source. Prior (1.108.273) + uv run ruff check src/ clean. ⚠ Reconciled by same-tree collect: 7728 total, 7717 with test_v1_108_273.py ignored (= its 11), and the +1 over .272's 7717 is next ENTERING the #435 sweep now that its exemption is gone. ⚠ The 8th skip is EXPECTED and is not a lost test: _JS_VARIANT_EXEMPT is empty, so the ratchet parametrizes over an empty set and pytest skips it ("got empty parameter set"). That is the end state of a ratchet that did its job; it re-arms the moment anyone adds an exemption. ⚠⚠ A version bump MID-RUN voids the run — the rotation gate compares CLAUDE.md to pyproject.toml, so a suite spanning the bump is not evidence. Bump and rotate FIRST, then run once. (Done wrong on .273 and the run was discarded.) Prior (1.108.272) + uv run ruff check src/ clean. ⚠ Delta is EXACTLY the 9 new test_v1_108_272.py tests, reconciled by COLLECTING the same tree twice (7716 with the file, 7707 with it --ignored) rather than by arithmetic against this line. ⚠⚠ That method was forced, because this line was STALE by ~239 for two releases — it read "7470 (1.108.269)" while .270's 31 and .271's 124 were never folded in, so the documented baseline was unusable as one. A count that is only ever appended to during a release rots the moment a release skips it; prefer a same-tree collect diff, which cannot go stale, and treat this number as a report rather than a baseline. ⚠⚠ The count was mis-reported once during this release and the ARITHMETIC caught it, not the reading — an intermediate run was quoted as "7469 passed, 0 failed", a combination that never happened: it was 7469 passed WITH 1 failed, totalling 7470. Always reconcile passed+failed against the prior release's total plus the new test count; eyeballing N passed at the end of a 17-minute run is how a red run gets read as green. ⚠ The failure was the CLAUDE.md rotation gate correctly refusing a Current State naming 1.108.269 while pyproject.toml still read .268 — the gate fires BEFORE the version bump lands, so a red rotation test mid-release is expected and must not be waved through as "just the gate"; it clears only when every pin site agrees. Prior (1.108.268): 7436 passed, 7 skipped, 0 failed + uv run ruff check src/ clean. ⚠ Delta from .267's 7428 is EXACTLY the 8 new test_stdio_guard.py tests; nothing else moved. ⚠⚠ The CLAUDE.md rotation gate caught a real mistake this release — a 4th entry was added without demoting .267 or moving the Older releases boundary, and the gate failed the build rather than letting the history drift. Prior (1.108.267): 7428 passed, 7 skipped, 0 failed + uv run ruff check src/ clean. ⚠ Delta from .266's 7404 is EXACTLY the 24 new test_constant_extraction_guard.py tests; nothing else moved. Prior (1.108.266): 7404 passed, 7 skipped, 0 failed (isolated worktree run) + uv run ruff check src/ clean + CI all 9 jobs green on the pushed SHA. ⚠ The delta from .265's 7394 is EXACTLY the 10 new test_format.py cases; nothing else moved. ⚠⚠ Nothing moving is itself the finding — not one existing test pinned a fusion or semantic confidence value, which is precisely why a ~5x mis-scaling shipped and survived. ⚠ +17 after .264 shipped: the file-IO scanner needed TWO MORE iterations (see below), test-only, no bump. ⚠⚠ A green suite is NOT a green build — lint was RED for four releases while this line said 0 failed. Quote ALL THREE (suite, ruff, CI) from now on. ⚠⚠ A green suite is NOT a green build — lint was RED for four releases while this line said 0 failed. Quote BOTH numbers here from now on, and read the CI run for the pushed SHA. ⚠ .261's run took 47m45s against ~16-17m before and after it on the same tree — same counts, same result, so it was machine contention and NOT a signal. Do not treat a wall-clock outlier as a regression. ⚠⚠ A config change is the one edit whose blast radius is the whole suite - 128 test files touch _GLOBAL_CONFIG directly, so a "small" resolver change is never a small run. ⚠ The "KNOWN 12 local-ONNX test_semantic_search env failures" are GONE — .207's autouse no_local_onnx fixture fixed them, so a local run is now fully green and any red is a real signal. Do not carry that 12-failure allowance forward; it papered over a real failure once already (.197 had one hiding inside it). ⚠ Still do not eyeball the COUNT — diff the FAILED names against the same tree with your changes stashed; for .199 and .205 that diff was empty, and for .209 the failure set was empty outright, which is the one case that needs no baseline. ⚠ Stashing is the wrong tool when the change is already committed and pushed — for .205 the comparison ran in a throwaway git worktree add --detach <pre-release-sha>, which also survives a concurrent writer in the main tree.
- Prior (1.108.298): A campaign that saw nothing must not certify everything.
refresh's pre-stamp discovery asked only whether the corpus had GROWN (current - known) and for its whole life could not see the opposite failure: a source root that has MOVED, been UNMOUNTED, or been CLEANED makes discovery return[], socurrentandknownare both empty, nothing drifts, nothing errors, and the campaign stamps the target generation having re-parsed ZERO files. ⚠⚠ UNREPAIRABLE, which is what lifts it above a wrong number — a stamp EQUAL to the constant is indistinguishable from a genuine one, so the tool built to drain the exempt bucket was filling it, and the way in was running the documented command. Found on the three pinned benchmark corpora: bare.gitdirs, 8,220 pre-.246symbols, all three stamped2in under a second each. Now refuses oncorpus_unreadable; ⚠_index_filesreturningNonerefuses too (index_unreadable) — UNKNOWN blocks, same rule ashas_any(). ⚠ EMPTY-vs-NON-EMPTY deliberately, NOT a shrink threshold: a repo may legitimately lose most of its files, so the partial case is DISCLOSED asindexed_files_not_reparsedrather than guessed at. Also re-measured the benchmark reference, stale 22 days: 27.9x -> 27.4x vs grep-top-3, 237.3x -> 233.4x vs read-all — our side moving AGAINST us, which is the failure Practice 4 exists for. ⚠ gin is the clean parser signal (+81 symbols from#428, identical corpus); express/fastapi each gained 4 files at the SAME commit, which is COVERAGE not parsing, and fastapi's symbol count did not move at all. ⚠⚠ EIGHT artifacts mirror one run, not four — both sync tests passed with FIVE still on August-3 figures, including README's line-3 tagline and a table whose grand total and per-repo rows were 22 days apart. Also ships Racket (.rkt/.rktl/.rktd), #548 by @otherjoel — a custom head-symbol walker, because the tree-sitter grammar is fully HOMOICONIC (no nameddefine/structnodes;(...)and[...]share thelisttype), same shape as the three Lisps already here. ⚠⚠ The PR's own point is the MEASUREMENT, not the feature:benchmarks/racket_fidelity/scores the extractor against Racket's own expander over 211 files / 3,526 definitions, withextraandwrong_spanBOTH 0 and gated in CI off frozen oracle data so the check runs with no Racket installed.syntax-original?separates human-typed names from macro-introduced ones — without it the gap looks several times worse than it is. ⚠missing(485, 86.2% coverage, 152 of 211 files completely clean) andcallable_unknowable(212) are REPORTED not gated, because neither is reachable by parsing more carefully. ⚠ NoPARSER_GENERATIONbump and the reasoning is theirs, verified independently: that counter re-parses files ALREADY in an index;.rktwaswrong_extensioneverywhere, so Racket arrives through DISCOVERY. Coverage, not extraction. ⚠⚠ Known and pre-existing, disclosed by them, affects EVERY language ever added: an explicitlanguageslist — whichjcodemunch-mcp initWRITES — never picks up a new language, andconfig --upgradeonly injects missing KEYS so it cannot repair a list. ⚠.scrbldeliberately unsupported ON A MEASUREMENT: it parses withhas_error: Falseand yields garbage, and a green parse with an empty result is worse than no support. [[a-one-directional-check-certifies-its-blind-side]] [[a-sync-ratchet-that-checks-the-total-misses-the-rows]]
Section: 'Open threads — verify, do not quote'. Rotated because it named two issues whose state it could not vouch for and pointed at the live surfaces anyway, which is the rule it was restating.
#375 (Linux stall, needs a re-run not a patch) and #377 (Phase 2 P3 edges)
were the last two carried here. Both may have moved. The catalog moratorium is
tracked in Current State and ROADMAP.md, which are the live surfaces.
- Prior (1.108.299): A name the file never spells. Racket
structforms now contribute the bindings the macro generates —posn?,posn-x,posn-y, setters under#:mutable,make-posnfor thedefine-structfamily — names that occur NOWHERE in the file text, so they exist in an index only if synthesised. They share the struct form's byte range, soget_symbol_source("posn-x")returns the form that generates it. #549 by @otherjoel, a follow-up to his own #548. 3,060 -> 3,438 symbols on the 211-file corpus; a real Racket project 1,754 -> 1,935. ⚠⚠ The PR's own headline is the FABRICATION its harness caught, not the feature: emittingmake-<name>for everydefine-structinventedmake-base-object/cfor(define-struct base-object/c (...) #:constructor-name NEVER_CALL_THIS)—#:constructor-nameREPLACES the default constructor where#:extra-constructor-nameADDS one. One fabricated name in 211 files, caught by theextrabucket that must stay at zero, on the author of the harness. ⚠ Own fields only —(struct derived base (c))bindsderived-c, notderived-a; the supertype occupies the slot before the field list, so the fields are the FIRST list child after the name, never a fixed index and never the last list. That one rule is also what stops#:guard (lambda (a b n) ...)and#:property prop:procedure (lambda (s) 1)being read as fields, and what resolves(serializable-struct/versions posn 1 (x y) ())to(x y). ⚠serializable-structandserializable-struct/versionswere absent from the form table entirely and produced no symbol at all, not even the struct name. ⚠struct:<name>deliberately not emitted (ranking noise);#:name/#:extra-nameemitted astype, because a struct-type transformer is not callable. AlsoPARSER_GENERATION2 -> 3. ⚠⚠ #548 shipped Racket WITHOUT a bump and was RIGHT; this one needs one, and the pair is the clearest statement of that line we will get:.rktwaswrong_extensioneverywhere, so the LANGUAGE arrived through DISCOVERY — a file nobody parsed cannot hold a stale parse. But .297 and .298 both parse Racket, so an index built by either holds.rktat generation 2 with the old symbol set and will never re-read it. Coverage does not need a bump; extraction does. ⚠ Scope NARROW — three extensions, window opened 2026-08-24 — and bumped anyway because the decision is cheap today and IMPOSSIBLE next week. Also the Racket coverage figure had THREE mirrors and the PR regenerated one:results.jsonwent 86.2 -> 86.5 /missing485 -> 475 whileLANGUAGE_SUPPORT.mdand the harness README kept the older run, green either side.tests/test_racket_fidelity_artifacts.pyderives six figures from the artifact and checks the ROWS, not the headline;clean_filesand the 10-worst total are derived fromper_filebecause no summary field carries them. ⚠ Five assertions fail against the stale tree, three pass, and the three that pass are the figures the change genuinely did not move. [[a-sync-ratchet-that-checks-the-total-misses-the-rows]] [[a-manual-stamp-drifts-from-what-it-names]]
⚠⚠ The RULES stayed in CLAUDE.md; this is the evidence behind them. Each policy there carries its operative statement, its one-line reason, and any command a human runs. What moved here is the incident record — the measurements, the dates, and the reasoning that produced each rule.
⚠ Read this before proposing a change to any of those policies. Several were written after we broke them ourselves, and 2e in particular is recorded in the first person precisely because the wrong call sounded reasonable at the time. A policy argued against without its forensics is a policy argued against blind.
Verbatim copy of the section as it stood at 1.108.304+dev:
1. One issue, one verdict. A multi-finding report gets SPLIT at triage into one issue per finding, cross-linked, credit on each. Nothing is dropped and no detail is discouraged. The reason is closure mechanics: a 4-finding issue closes only when the last one settles, so three finished fixes sit behind one unfinished conversation and the tracker cannot say which is which.
⚠ This is the correction to a mistake we made deliberately. On 2026-07-27 we CONSOLIDATED five jdoc issues (#80/#89/#90/#93) into one gate, #95. It cut the open count from 5 to 1 and manufactured a single artifact with the power to block a release. Tracker-tidiness and granularity pull in opposite directions; do not optimize the count.
2. A release is NEVER blocked on an open issue, including a verification we asked for. Done + tested + green ships on schedule, carrying a plain-language verification-status line (the #95 disclosure sentence is the template; it is deliberately weaker than a sign-off and the changelog must never blur the two). Late re-verification counts IN FULL and is announced retroactively. Nothing expires. Every timebox names its default action ("verification by X, or Y ships with disclosure Z"); a date with no stated consequence is a wish.
⚠ The point is that a reviewer's thoroughness must never become a veto. If being careful can stall a release, careful review becomes expensive to accept, which is backwards.
2e. NEVER BATCH OUR RELEASE BEHIND SOMEONE ELSE'S CLOCK (jjg, 2026-08-18, after it happened). Policy 2 says a release is never blocked on an open issue. The way that rule gets broken is not by someone overruling it — it is by an apparently sensible batching argument that never mentions it.
⚠⚠ The exact failure, recorded because it was MINE and it sounded reasonable. On 2026-08-18, five fixes were merged and green (#488/#489/#490's siblings, #495). I recommended holding the release until 08-19/08-20 so it could include #504 and #447, on the grounds that each of our releases re-conflicts elfrost's CLA-blocked #443 and batching means resolving once instead of three times. jjg accepted it. That recommendation coupled our shipping schedule to a contributor's CLA signature and a first-time reporter's availability, which is the precise outcome policy 2 exists to prevent.
⚠⚠ It was also wrong ON THE MERITS, which is the part that generalises. Batching reduces the NUMBER of conflict resolutions, not whether they happen — #443 conflicts on whatever release comes next, whenever that is. Each resolution is a scripted three-way merge plus one suite run, measured at minutes. The trade was "finished, tested, user-facing fixes sit unreleased for two days" in exchange for "we do a cheap chore once instead of three times." Weigh the cost of the chore against the cost of the delay before proposing a batch; here it was not close.
⚠ The timeboxes are NOT the problem and must not be "fixed". Every one names a default that ships the work regardless (policy 3a). A posted window decides whose commit it is, never whether the fix ships or whether we can release. If a window ever appears to block a release, the batching decision is what is blocking it, not the window.
⚠ The test, before proposing to hold a release: name the thing being waited for, and whether it is OURS. If it is anyone else's action — a signature, a PR, a reply, a re-run — the answer is ship now and let them ride the next one. Contributor work is never worse off for this: their default still fires, their credit is unchanged, and their PR merges into a smaller diff.
⚠ Corollary: "reduce OUR churn" is not a release criterion. Conflict resolution, re-runs and re-merges are our costs to absorb. The moment avoiding them starts shaping WHEN users get fixes, the optimisation has inverted — we are spending their latency to buy our convenience. [[never-batch-a-release-behind-someone-elses-clock]]
2f. THE ONE CASE WHERE NOT CUTTING A RELEASE IS LEGAL — and it is narrow (jjg, 2026-08-20). 2e forbids holding a release behind someone else's clock. This is not that, and the difference has to be stated precisely or 2f becomes the loophole that kills 2e.
⚠⚠ THE DISCRIMINATOR IS WHETHER A USER IS WAITING FOR ANYTHING IN THE BLOCK.
In #443 we held a SECURITY FIX behind a contributor's CLA: real users, real
exposure, eight days. In 1.108.289 the entire [Unreleased] block is licence
metadata whose ONLY beneficiary is the customer we would be waiting on.
Shipping it gets no user anything. So the timing question is not "do we make
users wait" — it is "what serves the one party this release is for", which is a
different question with a different answer.
⚠ The asymmetry that decides it: released metadata is PERMANENT per version,
unreleased metadata is FREE. Cutting .289 before the customer confirms the
identifier form risks a THIRD spelling (.288 -1.1, .289 -1, .290 whatever),
and their allowlist fans out across all three. This is the same immutability
argument that justified deciding FAST, pointing the other way at the release
step. Deciding early is cheap; publishing early is not.
⚠⚠ THE TEST, and it must be applied every time before invoking 2f: name what is in the block and who is waiting for it. If ANY entry is a fix, a feature or a correctness change, 2f does not apply and 2e governs — cut it now. A block that is entirely metadata for one named recipient is the only shape this covers.
⚠ Both triggers fire independently and neither can be deferred: the next content update ships it regardless of any reply, and a reply ships it regardless of what else is ready. A held release with no trigger is a forgotten release, which is why the hold is recorded in Current State where it is read every session, not only here.
⚠ "We never wait for a reply to ship a fix; we can decline to CUT a release whose only content is a thing the recipient has not confirmed." Those are different acts. Only the first is what 2 and 2e protect against.
3. A contributor's PR is never the only path. Timebox it and keep our own path warm (#388 taught this the expensive way).
3a. NO TIMEBOX WE OFFER RUNS LONGER THAN 24 HOURS (jjg, 2026-08-14, widened the same day from the CLA-only version). It covers every shape: signing the CLA, opening a PR already written, and taking an issue to implement. The CLA case is the easiest to justify — CLA Assistant prompts the moment a PR opens and signing takes about 30 seconds, so a longer window parks a finished, green, reviewed fix behind a form — but the rule is not limited to it.
⚠⚠ The window is only fair BECAUSE the default action preserves credit. At expiry we implement the fix ourselves and credit them in the CHANGELOG, the release notes and the close comment. So the 24 hours decide whose COMMIT it is, never whether they are credited and never whether the fix ships. Quote the default in the same comment as the deadline — a 24-hour clock with an unstated consequence reads as a threat, and it is not one.
⚠ An extension the contributor ASKS FOR is not the same as a default we hand out, and CONTRIBUTING.md invites the ask by name. Hold it when they ask; the clock exists to stop work going quiet, not to catch anyone out.
⚠ Stated consequence, not hidden: on an IMPLEMENTATION handoff, 24 hours means in practice that we implement it and they are credited, because nobody lands an additive-schema-plus-dispatcher change around a job in a day. That is a change in what a handoff IS, not only in how long it lasts. It is the intended trade — our throughput over their commit — and it should be made in the open rather than discovered at expiry.
3d. license/cla IS A REQUIRED STATUS CHECK ON THE DEFAULT BRANCH OF ALL
THREE REPOS (jjg, 2026-08-17 for jcm; extended suite-wide 2026-08-21).
Enabled because it was NOT one: until this date the repo had no branch
protection, no rulesets and no required checks, so the CLA was read but never
enforced and one distracted click could have merged unsigned code. Open PRs now
read MERGEABLE/BLOCKED rather than MERGEABLE/UNSTABLE.
⚠⚠ For four days this was fixed in ONE repo of three, which is the recurring
shape. jdoc was protected but required NOTHING; jdata had no protection at
all. Measured 2026-08-21 on jdata PR #5: the CLA was genuinely unsigned, the PR
read MERGEABLE/UNSTABLE, and nothing would have stopped the merge. A setting
fixed in one repo of a suite is fixed in one repo — the same sentence jdata's
own brief already carried about fork-pr-contributor-approval, written after a
contributor hit it first. All three now read identically: contexts ["license/cla"], strict false, enforce_admins false, force-push and deletion
off.
# All three; the default branch is `main` for jcm and `master` for jdoc/jdata.
for r in jcodemunch-mcp:main jdocmunch-mcp:master jdatamunch-mcp:master; do
GITHUB_TOKEN="" gh api "repos/jgravelle/${r%%:*}/branches/${r##*:}/protection" --jq '{contexts:.required_status_checks.contexts, strict:.required_status_checks.strict, enforce_admins:.enforce_admins.enabled}'
done⚠ enforce_admins: false and strict: false are both deliberate. The admin
override is what lets jjg land a merge pushed to a contributor's fork; strict
would force every PR to be up-to-date with main before merging, i.e. a rebase
after every release — the exact churn that kept #443 dark for five days.
⚠ Enabling protection also turned OFF force-push and deletion on main.
⚠⚠ This composes with the missing-status hazard and now FAILS CLOSED. ANY
new head — our push to a fork, or the contributor's own merge of main —
arrives with license/cla absent, and with the check required that reads as
BLOCKED until the bot posts on that SHA. Correct, and it will look like a new
problem the first time.
⚠⚠ NOTHING IS ERASED AND THE WORD MATTERS — corrected 2026-08-24 on #535.
This entry said a push "wipes" the status, which frames a per-SHA reporting
model as an act of destruction and sends you chasing the bot or the
contributor's signature. Measured: license/cla is a legacy commit
status, not a check run — every other check on the PR is a check-run from
the github-actions app, which GitHub re-runs per head automatically; a legacy
status is a record posted to ONE SHA by an external service, and a new commit
starts life with zero of them. On #535 the old head 0db4478 still read
license/cla success while the new head ea12029 read count=0. The old
status was untouched, and the signature — stored per ACCOUNT at
cla-assistant.io — was never in question.
⚠ The consequence is the useful part: the gate cannot tell "not signed" from
"not reported", so it fails closed to the same BLOCKED for both. Absent
still means DO NOT MERGE. But the remedy is to get a status posted on the
current head, never to re-verify an agreement that did not change.
⚠ "Usually under a minute" was optimistic. On #535 the bot had not posted
30+ minutes after the new head, and it has never commented on that PR at all —
it posted the status directly, which is what it does when the author signed on
an earlier PR.
⚠⚠ THE RE-TRIGGER, AND IT IS OURS — no contributor action, ~25 seconds
(established 2026-08-24, #535). license/cla comes from a REPO WEBHOOK to
https://cla-assistant.io/github/webhook/<repo> on pull_request, not from
any in-repo workflow. The event is not lost and the bot is not down — the
deliveries list showed his synchronize arriving and answering 200 OK, with
no status posted. Redelivering that same event makes it post.
HID=$(GITHUB_TOKEN="" gh api repos/jgravelle/<repo>/hooks --jq '.[0].id')
# ⚠ jq mangles delivery ids (past float precision) — read them with python.
GITHUB_TOKEN="" gh api "repos/jgravelle/<repo>/hooks/$HID/deliveries?per_page=15" > dv.json
python -c "import json;[print(x['delivered_at'][5:16],x['event']+'/'+str(x.get('action')),x['status_code'],x['id']) for x in json.load(open('dv.json'))[:8]]"
GITHUB_TOKEN="" gh api --method POST "repos/jgravelle/<repo>/hooks/$HID/deliveries/<exact-id>/attempts"⚠ Diagnose before reaching for it. Both #535 deliveries were synchronize
on a draft: true PR and only the later one stayed silent, so draft is NOT
the discriminator — the distinguishing feature was a MERGE COMMIT pulling in
commits authored by us. Check the deliveries list first; a delivery that never
arrived is a different problem from one that arrived and did nothing.
⚠⚠ NEVER POST THE STATUS OURSELVES. We hold admin and the Status API would
clear the gate in one call. license/cla is a legal assertion about an
agreement, and a maintainer-authored success is a forged one — it would also
be indistinguishable from the genuine article afterwards. Redelivering makes
CLA Assistant reach its OWN verdict, which is the whole difference. If a
redeliver does not produce a status, the answer is a recheck comment or a
contributor push, never a hand-written status.
⚠⚠ It does NOT solve vendor time-wasting and must not be sold as if it does.
Signing costs a campaign nothing — #485's author has 748 merged PRs across
GitHub, so they clear CLAs routinely. The only contributor this gate blocked in
its first hour was elfrost, who found a real security defect. Legal
exposure and spam are different problems; this closes the first, 3c closes the
second.
3c. PROFILE THE AUTHOR BEFORE REVIEWING A VENDOR-SHAPED PR (jjg, 2026-08-17). Any PR adding a named third-party provider, gateway, SDK or endpoint gets three queries FIRST, before a line of the diff is read:
GITHUB_TOKEN="" gh api users/<login> --jq '"created=\(.created_at[0:10]) repos=\(.public_repos) company=\(.company) bio=\(.bio)"'
GITHUB_TOKEN="" gh api "search/issues?q=is:pr+author:<login>&per_page=1" --jq .total_count
GITHUB_TOKEN="" gh api "search/issues?q=is:pr+author:<login>+<vendor>+in:title&per_page=1" --jq .total_count⚠⚠ The discriminator is the RATIO, not the volume. A prolific contributor
is fine. #485's author had 3,089 PRs, 2,242 with "minimax" in the title
alone (73%), ~19/day since March, and a profile reading
company: Independent Developer. #487's had 87 forks, 86 PRs, all OrcaRouter,
on a 7-day-old account. Both were found in under a minute; #485 was reviewed
in depth twice before anyone looked. That is the cost this rule removes.
⚠ Also check whether we have a DEMAND signal, which is the actual #380 bar
and is one query:
gh api "search/issues?q=repo:jgravelle/jcodemunch-mcp+<vendor>". MiniMax
cleared it honestly as a summarizer (#184, a user asking); MiniMax TTS did not,
and the only tracker mention was the PR itself.
⚠⚠ Quality is NOT the discriminator and must not be used as one. #485's
diff was better than most human PRs — a real output_format-versus-container
finding, a three-point review addressed in hours, a self-corrected test count.
Good work aimed at something nobody asked for is still something nobody asked
for. Close on demand, credit the finding, and say plainly that quality was not
the reason.
⚠ Do not assert employment you cannot prove. State the numbers, ask the affiliation question on the thread, and let the ratio speak. #487's author volunteered their affiliation unprompted and it cost them nothing — that is the contrast worth drawing, not an accusation.
⚠⚠ A posted timebox's default can be RETRACTED IN THE OPEN when the facts change, but never silently. #485's clock promised that at expiry "we implement the same change ourselves" and that the window "never decides whether the feature ships." The authorship-and-credit half was honoured; the feature half was withdrawn ON THE THREAD, with the reason, because it was written before the campaign was known. Letting a promise lapse quietly is the failure mode; retracting it out loud is not.
3b. A MERGEABLE contributor PR merges BEFORE any changelog-touching work of
our own (jjg, 2026-08-14). Not a courtesy and not a preference — a measured
cost. Every entry we add lands in the same [Unreleased] block a contributor's
entry occupies, so each of our merges puts their PR into conflict, and a
CONFLICTING fork PR has no refs/pull/N/merge and therefore gets no CI at
all. Their branch goes dark for a reason that has nothing to do with their
change.
⚠⚠ Measured 2026-08-14: #443 conflicted FIVE TIMES IN ONE DAY — twice from our own PR merges, twice from releases, once from the docs work — and every one was resolved by us pushing to their fork. Five is not five incidents, it is one wrong merge order repeated.
⚠ The boundary, or the rule fails on its first real case. A BLOCKED contributor PR cannot go first: #443 was unsigned-CLA the whole time, so "contributor first" was never available. When it is blocked we ship anyway (policy 2 — a release is never blocked on an open issue) and we own the resolution: push the merge to their branch, resolve it ourselves, and say on the thread that the conflict was ours. This rule is about ORDER when we have a choice, never about holding our work behind someone else's form.
⚠⚠ RE-READ THE THREAD FOR THE OPERATIVE DATE; DO NOT QUOTE ONE FROM HERE OR FROM MEMORY. Measured 2026-08-17 on #443: 2026-08-26 was posted 08-12 18:19, elfrost accepted it on 08-13 13:05 quoting that date, and 08-20 was posted 85 minutes later at 14:30 and reaffirmed thirteen times since. The contributor never acknowledged the change and may still be planning around the older date — which is the concrete harm, not the six days. A thread can carry two dates; only the query settles which is in force:
GITHUB_TOKEN="" gh api repos/jgravelle/<repo>/issues/<n>/comments \
--jq '.[] | select(.user.login=="jgravelle") | "\(.created_at[0:16])"' # then grep bodies for dates⚠ The 08-12 wording also promised "your authorship on the commit" where the 08-20 wording promises "credit" — a second, quieter downgrade in the same swap. When restating a default, restate the STRONGER posted version or say explicitly that it changed. jjg's call on 2026-08-17 was that 08-20 stands; the lesson recorded here is the silent substitution, not the date.
⚠ Do not shorten a timebox already posted. State the new window on new PRs. A public promise to a contributor outlives the policy that produced it, and retracting one to save six days costs more than the six days. ⚠⚠ Reaffirmed by jjg when this rule widened: #447 (2026-08-20), #465 (2026-08-21) and #456 (2026-08-27) stand AS POSTED. The new ceiling applies to timeboxes offered after that date, and to nothing already promised.
⚠⚠ CLOSED OUT 2026-08-20, and 3a is now ABSOLUTE: 24 hours, no exceptions, never again. jjg, on reading the 08-12 comment on #443 offering 2026-08-26: "Not again. 24 hour. Tops. Ever." Every grandfathered window above has since expired or closed and there are no live long timeboxes; the grandfathering clause is spent and must not be revived as precedent for a new one. The next posted window that exceeds 24 hours is a mistake regardless of what produced it.
⚠⚠ The failure mode has a NAME now and naming it is the point: a CLA hostage negotiation. #443 went eight days — a real security fix, reviewed and green, held behind a 30-second form, while SEVEN of our own merges conflicted its branch. Not one of those days bought anything. The window never decides whether the fix ships (the default action ships it) and never decides credit (the default preserves it), so a window longer than 24 hours purchases exactly one thing: the chance the contributor's commit is theirs — and it pays for that chance in the user's exposure to an unfixed defect. Twenty-four hours is already generous for that trade; eight days is not a trade at all.
⚠⚠ Do NOT answer "an issue is stuck" with aggregate stats. Measured 2026-07-28: jcm median 0 days to close (80 issues, 70 within a day, 2 ever past a week); jdoc median 1 day. Those numbers are TRUE and they are NOT a response. jjg: a fraction of an eyelash commands full attention and impairs binocularity; "it is a small fraction of your body" helps nobody. The cost of a blocked issue is CONCENTRATED, not distributed. Design the fix at the OUTLIER (policy 2), never at the median. See [[feedback_dont_answer_pain_with_aggregates]].
Surfaces: CONTRIBUTING.md ("One issue, one verdict" + "A release is never
blocked on an open issue") and .github/ISSUE_TEMPLATE/ (bug_report,
multi_finding_report, config.yml pointing parked design at ROADMAP.md).
⚠ CONTRIBUTING.md is now IDENTICAL suite-wide (jcm/jdoc/jdata differ only by
product name, repo slug, and jcm's extra quality-gates section). Two pre-existing
bugs fell out of normalizing it: the documented install command
pip install -e ".[test]" was WRONG IN ALL THREE REPOS — no repo declares a
test extra; dev deps live in a PEP 735 [dependency-groups] block, so the
FIRST command a new contributor ran failed. And jcm's
README.md#license-dual-use anchor pointed at a heading that does not exist
(## License). ⚠ CI installs with uv sync and never runs the command the
docs give a human, which is why this survived: the thing we test is not the
thing they do.
- Prior (1.108.308): Ownership and freshness are different properties. No user-facing fix: this release is about the fact that we ran 1.108.293 against a 1.108.307 tree for six days and fourteen releases while developing jcodemunch with jcodemunch. ⚠⚠ The release checklist's eight steps are complete with respect to USERS and silent with respect to US — none of them touch the dev box, and the package was a regular (copied) install, so nothing ever refreshed it. ⚠⚠
verify_package_integrity()cannot see this and is not meant to: it asks whether the running module belongs to the OFFICIAL distribution, a supply-chain question, and would certify a fourteen-release-old install without complaint. Having a startup check that inspects the distribution made it feel covered. ⚠⚠ The version gap was not the real tell — the VERIFICATION PATH ROUTED AROUND THE PRODUCT. Every fix that week was checked withPYTHONPATH=srcrather than through the server; each choice was right alone and the pattern was the finding. ⚠install-statusreportssource_drift, tri-state, anddrifted: None(COULD NOT ESTABLISH) is neverFalse— the .305/.306 defect shape, guarded by tests that fail 4-of-10 when UNKNOWN is collapsed. ⚠ All five suite packages are EDITABLE now, so tree-vs-install drift is impossible; only the restart remains, because a running server serves what it loaded.scripts/repair-munch-installs.ps1repairs an interpreter and REFUSES while any server runs — uninstalling one mid-session removes the.pthand dist-info, then dies on the locked.exe, leaving it unimportable. ⚠⚠ My own two renderer tests reproduced #559: a hand-written report stub that did not match the producer, dying on a keyinstall_status()actually emits. They build on a real report now. [[a-mock-can-supply-a-contract-the-producer-lacks]]
- Prior (1.108.309): A mean hides the tail, and a default invents a comparison. Two instruments reported a confident number about a distribution they could not see, both found by reading an external audit against our own tree (Revenium, 2026-08: top 1% of 14,680 agent runs carried 46% of spend, top 5% carried 77%, one unattended session at 4,819 calls). ⚠⚠
analyze_perf's_diff_baselinereadfloat(b.get("p50_ms", 0.0)), and the ONLY baseline that ships carriestokens_savedand no latency keys at all — so the zero stood in for a measurement nobody took and the subtraction was published asp50_delta_ms, a name asserting a comparison happened. Measured againstv1.108.163.json: a tool at p95 900 ms reportedp95_delta_ms: 900.0, read by any human as a 900 ms regression against a release that never timed it. ⚠⚠ Its test could not see it because the FIXTURE WAS RICHER THAN THE ARTIFACT — the synthetic baseline carriedcalls/p50_ms/p95_ms, keys the real file has for no tool, so the fabricated path was structurally invisible to the test written about that exact code. The guard reads every baseline OFF DISK now. ⚠ Absent →None+not_comparablenaming the SIDE (absent_in_baseline/absent_in_current/absent_in_both); calls and tokens keep a meaningful zero on the CURRENT side, latency has none, and inventing one is the same defect from the other end. ⚠⚠ Second half:slowest_by_p95ranks how slow ONE call is and was the ONLY ranking. Where the time went is count x latency, and the orderings disagree whenever a fast tool is called often — 4,000 calls at p95 900 ms is 100x three calls at 12,000 ms, and the report put the second first.heaviest_by_total_ms/totalsanswer the other question; a share over a zero total REFUSES rather than dividing. ⚠⚠ A ring-capped tool's share is a LOWER BOUND and the cap bites hardest on the busiest tool — the one the ranking exists to surface — soring_capped_toolsnames them. ⚠ The per-tool shape has ONE producer now (token_tracker.latency_bucket);analyze_perfheld the second copy and the two AGREED DIGIT FOR DIGIT, which is what makes a later divergence invisible. Its local_percentileis deleted, not wrapped — an unused copy is what regrows.p95_is_maxis MEASURED, not derived from n, and fires for every n <= 20: two published fields carried one sample. ⚠⚠ Same shape one module over:analyze_regret'sratiois a MEAN, so one need burning 400 calls inside a corpus of 1,000 reports 1.4x and the digest one-liner quotes exactly that.concentration(basisexcess_calls— every need costs one call, so a share over CALLS is diluted by the floor) reportstop_need_share+head_share; the digest now distinguishes two ledgers the ratio cannot. [[a-fixture-authored-from-the-schema-tests-nothing]] [[a-module-that-imports-clean-has-been-tested-for-nothing]]
- Prior (1.108.314): A rate written for a FUTURE date is wrong for every day before it.
receipt's_MODEL_PRICES_USD_PER_MTOK["sonnet"]read3.0from 2026-06-24 commented "Sonnet 5 / 4.6" — Sonnet 5 has NEVER been $3: it launched at $2 introductory with the $3 rise SCHEDULED for 2026-09-01, cancelled the day before it would have applied. The entry recorded a FUTURE price as the current one and was wrong all 69 days it stood, and nothing distinguishes that from a stale value — both read as a plausible number beside a plausible date, and the date makes it look checked. ⚠⚠ THE PIN AGREED WITH IT:_EXPECTED_RATESrestated3.0, and a third assertion was a DERIVED"$0.09"in rendered text that no search for the rate's NAME can see — so the suite was green against a rate the vendor never charged for the model named beside it. Re-read the SOURCE when touching a pinned table, never the other copy. ⚠ Four copies suite-wide and this fixes ONE; ours is right only instorage/token_tracker.py, whose key isclaude_sonnet_4_6— a key naming a FAMILY inherits whichever member's price someone last looked at. jmunch/jdoc/jdata still carry it, and jmunch also hasclaude_opusat the RETIRED $15 (3x) on the block stamped onto LIVE responses — a self-flattering error, the one direction a savings metric must not drift; work orders written, not fixable from here. ⚠⚠ Also: the publishedcountersurface is BYTE-PINNED (tests/test_counter_surface_stability.py; 6 tools, 4,184 B; name+order, per-tool sha, total, whitelist membership,tool_profileindependence; non-vacuity 5/5) — it sits in the CACHED PREFIX, so a reworded description is a full-rate cache write for every user, and it pins the catalog-can-GROW property arXiv:2608.22708 is built around, which the Counter had by construction and nothing asserted. ⚠ AndCLI Subcommands+Env Vars(16.6% of the budget) split toCLI-AND-ENV.md: 69 rows moved, 27 stayed, the ⚠ marker under-selected AGAIN (9 of 27 keepers). [[a-ratchet-can-pass-against-the-defect-it-names]] [[a-budget-that-names-one-section-licenses-the-rest]]
⚠⚠ The MCP registry API nests each row as {server: {...}, _meta: {...}}
(schema 2025-12-11). name, version and packages[] sit under server;
isLatest and publishedAt sit under
_meta["io.modelcontextprotocol.registry/official"]. A flat row["name"]
read returns ZERO rows on a publish that completely succeeded — measured
minutes after mcp-publisher confirmed 1.108.301, where the flat parse found
0 of 45 rows and the nested parse found all 45 with isLatest: 1.108.301.
⚠⚠ This is a SECOND false negative on top of the known paging trap, and
unlike that one it SURVIVES &limit=100 — so the documented remedy does not
help and the symptom is indistinguishable from a failed publish. Never
re-publish on a zero-row read; fix the parse. Also confirm
server.packages[].version advanced, not only server.version — an entry can
move one and not the other.
Verbatim; the rules each entry states live on in Key Files and Standing lessons.
-
Prior (1.108.316): A display preference edited the data it was displaying. The SHARED result cache stored the caller's dict and handed that same dict back (#572, @rknighton), so the dispatcher's metadata step —
meta_fields, a per-user DISPLAY setting — reached into the session cache and changed what every later caller was served. ⚠⚠meta_fields: []is the SHIPPED DEFAULT, so out of the box the secondfind_references/get_blast_radiuscall came backKeyError: '_meta'— and the crash was the LOUD case.suppress_metais a per-CALL argument, so on an ORDINARY config one call passing it emptied the shared entry and the next caller, who had asked for metadata, was served an empty_meta; a partialmeta_fieldsdoes the same by replacement. ⚠⚠ THE WINDOW IS THE MISS PATH: both tools rebuild_metafromdict(cached)on a hit, so a repeat call survives and it is the call that FILLS the cache that hands the dispatcher the stored object — which is why a two-call reproduction shows the crash and NEITHER quiet case, and why reproducing the report's own second claim needed the suppressing call moved to position one. ⚠⚠ FIXED IN THE CACHE, NOT AT THE TWO CALL SITES, and that was the reporter's argument:search_symbolskeeps its own cache and had already paid for this TWICE (#377 item 3 for_meta.verdict, then #404, also theirs, for the rows) and neither fix reached the shared one — a third per-consumer patch clears both tools today and arms the trap for the tool written next. ⚠_isolateclones CONTAINERS ONLY at unbounded depth: leaves are JSON-serialisable immutables by then, and container-only measured 4.15 ms againstcopy.deepcopy's 16.58 ms on an 800 KB response; a rule shaped to today's two callers would be a guard written against a spelling. ⚠ Identity was never the contract — sevenisassertions intests/test_result_cache.pywere the defect written down and are==now (Practice 9). ⚠ #570'scached.get("_meta", {})guard is merged and KEPT; it covers one tool, sees neither quiet case, and is why one non-vacuity arm stays green. [[a-cache-that-returns-its-stored-object]] [[a-guard-written-against-a-spelling]] -
Prior (1.108.315): A fix for a false positive can install a false negative, and suppression has no symptom.
find_dead_code'sconfidence: 1.0is documented as PROVABLY UNREACHABLE — a claim about the TREE, computed from the INDEX with nothing in between. ⚠⚠encoding/schemas/is enumerated bypkgutil.iter_modules(__path__)at import time, an edge no static graph can see, so TWELVE live encoders published at 1.0 — and which three of fifteen escaped depended only on whether a test happened to import the module directly, i.e. test-authoring habit, published as proof. ⚠⚠ THE FIRST WORKING DRAFT INSTALLED A FALSE NEGATIVE AND EVERY ASSERTION STAYED GREEN: matching any__path__madepkgutil.iter_modules(schemas_pkg.__path__)in a TEST file read as the test directory self-enumerating — 502 files went live, suppressing every real finding undertests/. Only a BARE__path__/__file__, or a local alias bound to one, names the loader's own package; the alias IS the motivating case (from . import __path__ as pkg_pathone line above the call), so reading only the call argument resolves nothing. ⚠_corpus_adequacy.pyreads the disclosures the index already carries and caps at 0.6, below the 0.8 default, so the default call REFUSES —search_texthandled the identical situation correctly on the identical index in the same session. UNKNOWN caps; NOT APPLICABLE (no_source_root,complete is None) does not. ⚠⚠ THE THIRD SURFACE IS THE DESTRUCTIVE ONE:check_delete_safereachessafe_to_deletefrom a no-refs-at-all fallback REGARDLESS of the dead-code confidence it just consulted, then FLOORS it at 0.85 — so capping the report left the delete certified, and the twelve encoders each graded safe.corpus_inadequateis classified in_stop_rule._BOUNDEDand never terminal; only ABSENCE verdicts are replaced. ⚠json_passthrough.pysurvives as the one likely TRUE positive of the original thirteen — reported, not deleted. [[a-fix-for-a-false-positive-can-install-a-false-negative]] [[a-guard-covered-only-by-positive-tests-can-be-deleted]]
Verbatim. The 1.108.317 entry is condensed to a stub in CLAUDE.md; this is the full text. The 1.108.315 stub left the rotation entirely (its forensics were rotated here 2026-09-04).
-
Prior (1.108.317): — CI runs the harness on every change; publishing is a dispatched workflow. Eight workflows are five (
pr-gate.yml,main.yml,nightly.yml,security.yml,release.yml); every PR-gate job is a REQUIRED check onmainby name;enforce_adminsandstrictare ON; a release isrelease.ymldispatched with a version, Test PyPI first, trusted publishing, post-publish smoke on both OSes. ⚠⚠ The gate caught its own author four times before it merged (unformatted scripts, a subprocess withoutencoding=, invalid YAML inrelease.ymlfound by CodeQL, a venv path uv cannot resolve on windows) and the release pre-flight was wrong twice about a MAIN commit (C-13, C-14: the PR gate's jobs live on the PR's merge ref; the witnesses on main aremain.yml's). ⚠ Windows runners are 3x this box on the full tier: a platform-scoped Floorsuite.full_seconds_ci_windows, not a loosening. ⚠ Also in this release: tiedsearch_symbolsscores rank by symbol id (harness F-13), the token reference is captured on CI,types.error_maxanddeps.vuln_maxFloors,SECURITY.mdreporting policy. Forensics:docs/cicd/. [[the-cicd-pipeline-lives-in-docs-cicd]] -
Prior (1.108.315): A fix for a false positive can install a false negative (#569, #566):
encoding/schemas/is enumerated at import time, so twelve live encoders published as dead at confidence 1.0, and the first draft of the fix revived 502 files undertests/with every assertion green;check_delete_safecertified the deletes regardless. Rules: Key Filestools/_runtime_discovery.py,tools/_corpus_adequacy.py,tools/check_delete_safe.py; forensics:ISSUE-HISTORY.md(rotated 2026-09-04).
⚠ Prior (1.108.316): 9161 passed, 13 skipped, 0 failed (9174 total + uv run ruff check src/ clean, measured on the settled tree after the bump and the rotation. ⚠ 9174 TOTAL, +26 over the .315 line's 9148, and it reconciles EXACTLY: 11 from this release's tests/test_result_cache_isolation.py, 2 from @rknighton's merged #570, 7 from #571's test_kind_enum_is_derived.py, 5 from test_savings_usd_basis.py and 1 from the holdout-artifact gate — four of those five shipped between the two measurements. ⚠ A delta is only readable when both ends name the same tree; three commits sat between these two.
Moved verbatim under Maintenance Practice 5 to make room for the --diagnostics invariants. The rule survives in CLAUDE.md twice already — Key Files tier_switch_cost.py and Standing lesson 08-30 — so this is the duplicate, not the rule.
⚠⚠ A mid-session tier switch is priced, and one of the three tiers is a
LOSING destination. full -> standard needs 174 requests to repay the
cache it invalidates (864 with 100k of history); full -> core needs
4. Regenerate with price_tier_switch.py; weights are read live from
_build_tools_list, so nothing here is hand-typed. tier_switch_cost.classify
refuses the non-paying narrowing at both switch sites and never refuses a
widening.
⚠ This EXTENDS the codex_surface finding below, it does not repeat it. That
one says standard is not a lever (6.7% of the payload). The addition is that
as a TRANSITION it is negative, for longer than any session lasts -- and that
the "fewer tokens is better" intuition is correct uncached and wrong cached,
which is the whole reason it shipped.
The one-line lesson stays in CLAUDE.md; this is its forensic paragraph.
- A frozen version string cannot say whether a running process serves current
code. 08-31 (rotated out of Current State with 1.108.313):
__version__isimportlib.metadata, fixed at install time and never read from the tree, so the source-drift verdict false-alarmed forever on an editable install (the module IS the tree) and was blind to the copied install, which was the actual incident. Every process on a source install reports the same number, so the answer comes fromstarted_atvs source mtime instead — it caught that session's own server on the first run. ⚠ Ownership and freshness are different properties:verify_package_integrity()asks which distribution the running module came from and would certify a fourteen-release-old install. [[grep-a-persisted-field-for-its-readers]]
The twelve newest Standing lessons carried their full incident prose in CLAUDE.md until the file reached 24 characters of headroom against its 140,000 Floor. Each rule, its date, every warning-marked imperative and every memory link stayed in CLAUDE.md; the measurements and narrative are below, verbatim, so nothing is lost -- grep by date or issue number.
-
A guard written against a SPELLING is fixed for that spelling only. 09-01 (#566): #550 taught that
from . import receiptsdepends onreceipts.py, then gated the fix onset(specifier) == {"."}.from ..retrieval import embed_driftis the same dependency with the package named, and it kept resolving to__init__.pyfor the whole life of the "fix" — 21 edges over 12 modules on our ownsrc/, every one of them published byfind_dead_codeat confidence 1.0. ⚠ The reported case and the property are different sizes; #550's own comment argued the property and the code implemented the example. ⚠⚠ And the same error twice more in the fix: the code comment's first count (134) and the first repo-level ratchet (built > 90) both identified a synthesised edge by its SHAPE — "the last segment appears innames" — which also matches the hand-writtenfrom .tools.index_repo import index_repo. This repo has 113 of those, already resolving, so the ratchet passed against the reintroduced defect and the number was 6x high. Compare against the import statements actually WRITTEN in the file. [[a-ratchet-can-pass-against-the-defect-it-names]] -
A denylist catches the instance; an allowlist catches the class. 08-28:
relnotes.md, a scratch copy of the release notes, was swept up bygit add -Aand shipped inside the published sdist. The sdist canary tests prove NAMED bad paths are absent and could never have seen it — a scratch file has no name to plant a canary under. ⚠ Build release notes outside the repository; a.gitignoreentry protects only the spelling someone remembered. ⚠ Assert the allowlist in BOTH directions — an entry naming a file that no longer ships makes the list stop describing the artifact. ⚠⚠ It found a SECOND instance minutes later and it was mine:suite.log, the pytest redirect used for every gate run that day, sitting in the repo root. A release cut while one existed ships it, and a pytest log carries absolute paths and usernames. Redirect gate runs to the scratchpad, never the repo. -
A command that does not CREATE its environment is testing whatever was left there. 08-28: the release checklist's CI-env reproduce ran
uv run --python 3.13 python -m pytestwith no sync and no--extra watch, so it inherited.venv. It looked right for months because a previous sync's packages survived; switching interpreters cleared them and 105 tests silently stopped executing while the run reported exit 0 and the totals reconciled exactly. ⚠ Read the SKIP count, not just the exit code and the total. ⚠⚠ The fix is in a GITIGNORED skill file, so the durable copy and its ratchet live in the repo —tests/test_ci_env_reproduce_command.pybinds CLAUDE.md's command topr-gate.yml's install line (it wastest.ymluntil 2026-09-04). [[pipes-and-missing-xdist-both-report-exit-zero]] -
A field written by nobody's reader is a defect with no symptom. 08-28 (#561/#562):
detect_frameworkpersistedentry_point_patternsintocontext_metadataat index time, and a tree-wide search found that key written in ONE place and read in NONE. Three tools each reproduced their own "is this a root?" answer and every one was Python, so a Next.js repo detected zero entry points. ⚠⚠ An unconsumed field also rots unnoticed: Flask and FastAPI carried"*.py"there, which under fnmatch declares the whole tree — the first naive reader would have switched dead-code detection off across an ecosystem. Grep a persisted field for its readers before trusting it, and before adding one. [[a-module-that-imports-clean-has-been-tested-for-nothing]] -
An optimisation has a SWITCHING cost, and the intuition about it inverts once the thing is cached. 08-30:
set_tool_tier("standard")narrowed the tool block by 6.7% and cost a full-rate rewrite of the whole cached prefix -- 174 requests to break even, 864 with 100k of history, against 4 forcore. ⚠⚠ Uncached the same switch pays back immediately, which is why a surface built to save tokens shipped a control that spends them: "fewer tokens is better" is true right up to the point the block is stable. Ask what a saving costs to START, not only what it saves per unit. ⚠ A widening is never refused — it buys a capability, and only a narrowing claims to save. [[a-one-directional-check-certifies-its-blind-side]] -
A constant written for a FUTURE date is wrong for the whole interval before it, and looks identical to a stale one. 09-01: the receipt priced
sonnetat $3 from 2026-06-24, the increase SCHEDULED for 2026-09-01 — cancelled the day before. Sonnet 5 was never $3; the entry was wrong all 69 days, and its dated comment made it look checked. ⚠⚠ The pin agreed with it (two literal3.0s plus a DERIVED"$0.09"a name-search cannot see), so green meant nothing — re-read the SOURCE when touching a pinned table, never the other copy. ⚠ Four copies suite-wide; ours was right only intoken_tracker.py, whose key isclaude_sonnet_4_6: a key naming a FAMILY inherits whichever member's price someone last looked at. -
A fix for a false positive can install a false negative, and only the non-vacuity pass sees it. 09-01 (#569): the runtime-discovery scanner asked whether
__path__appeared in the enumeration call.pkgutil.iter_modules(schemas_pkg.__path__)in a TEST file is ANOTHER package's search path, so the test directory read as self-enumerating and 502 files went live, suppressing every real finding undertests/— with every assertion in the new test file still green. ⚠ Suppression has no symptom: the false positives it was written to remove were gone, which is what success looks like. Ask what the fix makes INVISIBLE, and write that test before the one that proves it works. [[a-set-cannot-count]] -
Dropping an unmeasurable axis is not the same as omitting an inapplicable one, and the difference has a SIGN. 08-28: gating
churn_surfaceon a shallow clone by omitting it — the conventionruntime_coveragealready uses — took the same tree from 84.0 B to 88.8 B while full-clone truth was 77.3 C. Removing a low-scoring axis RAISES a mean, so the fix moved the published grade further from reality than the defect had. NOT APPLICABLE may be dropped silently; COULD NOT MEASURE must withhold the composite and the grade. ⚠ Ask which direction a "safe" omission moves the number before shipping it. [[a-one-directional-check-certifies-its-blind-side]] -
A number that reproduces on one box is reproducible on one box. 09-03 (harness F-13): the token benchmark was deterministic on this machine AND on CI and disagreed by 2.5% between them, for three causes at once — a CRLF checkout, ranking ties broken by
os.walkorder (NTFS vs ext4), and a_metacounter read from HOME. Capture a published reference where the gate runs, and diff per-row, never per-total — the total hid one cause behind another. ⚠ Anduv run --python XREBUILDS.venvwithout the extras; the fast tier ran 1055/112-skipped at exit 0 minutes later. The fast tier has a skip ceiling now. [[pipes-and-missing-xdist-both-report-exit-zero]] -
A gate's exit status is never the left side of a pipe. 09-04 (inbound item 6):
python gate.py ... | tee out; rc=$?records tee's status under Actions' defaultbash -e, so every decline the pre-flight computed was ignored and the model would have run. The reviewer named one site; the ratchet (test_no_pipe_hides_a_gate_exit_status) found four more across the stack. ⚠ Its first draft matched per PHYSICAL line and stayed green with the pipe back, because the invocation and| teesat on different\-continued lines; normalise the text the way the shell does before scanning it. [[a-trailing-command-hides-pytests-exit-code]] -
We fix the reported call site and leave the mechanism. Three times in three days (08-19, #506/#507/#508/#509): a second generator, a second call site, a second derivation. The one-sentence fix each time is ask the authority instead of reproducing its logic. ⚠⚠ 09-02 (#572) is the same shape in a cache, and the contributor made the argument for us:
search_symbolshad fixed return-the-stored-object twice inside its OWN cache (#377 item 3, then #404) and neither fix reached the SHARED one, so a display preference kept editing cached data. Ask whether the fix belongs one layer down, where the tool written next inherits it. -
The host's timezone can select a test's INPUT FORMAT. 08-28: git renders a UTC offset as
Z, whichdatetime.fromisoformatcould not parse before 3.11 — so a boundary date was unreadable on 3.10, in CI only. Every runner is UTC; this box is CDT and got-05:00, which parses everywhere. ⚠⚠ No local run on any version could reproduce it,uv run --python 3.13included — the version matrix was not the axis that mattered. Pin format-sensitive parsing with a UNIT test over every spelling; an integration test can only observe the one its host emits. [[a-module-that-imports-clean-has-been-tested-for-nothing]]
The 1.108.316 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.316): A display preference edited the data it was displaying (#572, @rknighton): the shared result cache handed back its stored dict, so
meta_fields(the SHIPPED default[]) and per-callsuppress_metarewrote what every later caller was served; fixed in the cache, not at the two call sites. Rules: Key Filesstorage/token_tracker.py; forensics:ISSUE-HISTORY.md(rotated 2026-09-04).
The 1.108.318 entry's product half, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.318 — The process is code that cannot skip a step, and the field is measured from result files. Three layers shipped in this block: the workflows layer (
/feature·/fix-issue·/release·/review·/triage-issue·/benchmark-compare·/competitive-compare, with hooks that refuse a commit without the fast tier, a PR without a full-tier stamp on THIS tree, and every irreversible verb), the inbound layer (nine headless jobs, OFF untilINBOUND_ENABLED, the model never holding a write token) and the competitive tier (the nulls, jCodeMunch and nine adapters over pinned corpora in a sandbox, every FINDINGS number script-written from a result file). Product: "where is this name used" reachescheck_referenceson every surface that steers it (CF-63); a failed embedding batch names its cause (CF-66); the watcher registers once per root, a moved subtree, a renamed root and an edit under.github/reach the index, and an empty full incremental scan reconciles deletions and says when it removed everything (#641, #629, @marcelruhf); an install on tree-sitter-language-pack 1.x says so and what it costs (#608, @kecsap); the CodeQL triage's fixes (#628, C-16); the sdist no longer carries.github/. ⚠⚠/releasestep 4 REFUSED for the second release running, on the same six README per-repo benchmark rows (workflows W-16, REPORTED): they mirrorbenchmarks/rag_baseline_results.jsonwhile the grand total mirrorsjcm_reference.json, andtests/test_provenance.pycovers only the total. Both releases shipped over it by a human's call; regenerate the rows from one artifact or widen the ratchet, or the refusal is ceremony. Forensics:docs/workflows/FINDINGS.md,docs/inbound/FINDINGS.md,docs/competitive/FINDINGS.md.
The Tests: line's 1.108.316, 1.108.315 and 1.108.314 counts, verbatim:
- 1.108.316: 9161 passed, 13 skipped, 0 failed (9174 total).
- 1.108.315: 9135 passed, 13 skipped, 0 failed (9148 total).
- 1.108.314: 9108 total.
Maintenance Practice 5 as it stood in CLAUDE.md before the 2026-09-26 haircut, verbatim. The loaded file keeps every rule; the measurements and incident dates behind them are here.
- Rotate, never delete — and the budget is the WHOLE FILE, not one section.
Current Statekeeps the 3 newest releases and theTests:line keeps the same three; closed dated entries go toISSUE-HISTORY.md, which no session loads.tests/test_claude_md_size.pyis the gate. ⚠⚠ The prose version of this rule was followed and the file broke anyway. On 2026-08-21 CLAUDE.md hit 200,543 chars and the harness refused to load it, whileCurrent State— the only section this practice named — was 14% of it. The growth was in dated issue history (82k) and aTests:line carrying per-release counts back to 1.108.268 (16k). A rule that names one section licenses every other section to grow. ⚠ When an entry rotates out, ask what LESSON it earned and put that one line in Standing lessons with its date. An entry with no reusable lesson needs no line; an entry whose lesson is already there needs no second one. ⚠⚠ MEASURE THE SECTIONS BEFORE CHOOSING WHAT TO ROTATE — the answer has twice been a section nobody suspected. On 2026-08-28 at 139,184/140,000 I proposed rotating Standing lessons and was wrong: it was 6.4% of the file, where Key Files was 40.1% (55,643 chars) and the issue/release policy 15.4%. Rotating what I proposed would have recovered almost nothing. Split by heading and sort by size first; it is one script and it settles it. ⚠⚠ REVERSED 2026-09-16 on a re-measurement, and the reason is arithmetic: Standing lessons was 6.4% when that call was made and is 13.0% now, while Key Files has not moved. The 2026-09-16 pass rotated the dated INCIDENT PROSE out of its twelve newest entries and kept every rule, date and marker -- 9,312 chars to 7,193. The section is still not a split candidate; nothing derives a standing lesson, which is why it stays in the loaded file. ⚠ Key Files at 40% is the NEXT rotation target and the hardest, because it is also the most load-bearing — the per-file ⚠⚠ warnings are what stop a defect recurring. Rotate its dated INCIDENT prose, never its rules. ⚠⚠ SPLIT 2026-08-29, and the axis is the reusable part: WHAT IS DERIVABLE LEAVES, WHAT IS NOT STAYS. Key Files was 61,593 chars (44.4%) and the file was at 139,531/140,000 with 469 characters of room. The descriptive half -- what each module IS -- moved toKEY-FILES.md, which no session loads, because jcodemunch answers it live (get_file_outline,get_repo_outline). Nothing answers "this cache is evicted on every write, so it is not a cache", so every invariant stayed. 76 entries moved, 44 stayed, 120,344 chars (86.0%). ⚠⚠ The⚠marker is a PROXY for load-bearing and it over-cut by 15.producers.py,receipts.py,runtime/confidence.pyand twelve others carry rationale with no marker on it -- a prohibition, a constraint whose violation causes a defect, a "because". They are named inRATIONALE_ENTRIESintests/test_key_files_split.py, and adding a name there to buy budget is the thing the split exists to stop. ⚠⚠@pathimports DO NOT WORK for this -- they are expanded at launch, so a split into imports recovers exactly nothing. Verified against the docs before choosing, and it is the obvious wrong answer. NestedCLAUDE.mdand.claude/rules/both load ON READ, and this project routes exploration through MCP tools andsed/cat, neither of which triggers it -- so the mechanism that looks purpose-built would have loaded nothing here. ⚠⚠ SPLIT AGAIN 2026-08-31, same axis, and the marker under-selected AGAIN.CLI Subcommands(8,367) +Env Vars(13,097) were 16.6% of the budget and went toCLI-AND-ENV.md: 69 rows moved, 27 stayed, 129,052 -> 121,580 chars on the SETTLED tree (headroom 10,948 -> 18,420; the rows are -8,718 and documenting the split cost 1,160 back).--helpandjcodemunch-mcp configderive the moved half live. The ⚠ marker found 9 of the 27 keepers; the other 18 were read by hand and carry a prohibition (JCODEMUNCH_RUNTIME_REDACT: never on production traces), a belief-correcting constraint (JCODEMUNCH_PERF_TELEMETRY: the ring is ALWAYS tracked) or a rationale with no marker on it. They are named inCLI_RATIONALE/ENV_RATIONALEintests/test_cli_env_split.py; adding a name there to buy budget is the thing the split exists to stop. ⚠ Its "in neither" direction is DELIBERATELY one-sided — 37JCODEMUNCH_*names and 12add_parsernames are legitimately in neither table, so it asserts only that a DOCUMENTED row still resolves insrc/. ⚠CONFIGURATION.mdalready documents 18 of these variables in prose; that overlap predates the split and is NOT resolved by it. ⚠ The ratchet asserts each entry lives in EXACTLY ONE file. Its first run caught its own defect: keying entries by BASENAME collapsedruntime/redact.pywithredact.pyandruntime/confidence.pywithretrieval/confidence.py, reporting a duplication that did not exist. A name is not an identity, the Rust-fidelity lesson, reproduced inside the guard written to prevent drift. ⚠⚠ THE SPLIT TARGET MUST BE TRACKED, ANDdocs/IS NOT --.gitignore:83isdocs/*. The first version of this wrotedocs/KEY-FILES.md, which would have made 76 entries MACHINE-LOCAL: not in git, not in CI, gone on a fresh checkout, exactly the gitignored-skill trap this file already warns about. It surfaced only becausegit statusdid not list the new file. Checkgit check-ignoreon any path a rotation writes to; creating the file proves nothing. It lives at the repo root beside the other shipped docs, andALLOWED_ROOT_FILESnames it in both directions. ⚠⚠ MEASURED 2026-08-28: Key Files has almost NO rotatable narrative left. A scan of its 119 entries found FOUR provenance clauses (1,713 chars), three of them rules; Standing lessons and Current State each duplicate NOTHING from it. It is 42% of the file because it is 119 modules of non-redundant invariants, not because it is padded — so documenting one release under Practice 1 cost more than a full rotation pass recovered. The next lever is a SPLIT and it is jjg's call. Do not raiseBUDGET(the gate says its 10k buffer is the last one) and do not buy room by deleting ⚠⚠ rules. ⚠ The 2026-08-28 pass took the issue/release policy from 21,448 to 12,391 by keeping every rule, every operational command and every prohibition, and moving only the forensics — verified by asserting all nine policy numbers, six commands and seven prohibitions still resolve. Write that check as a script; a rotation reviewed by eye is how a command goes missing.
The 1.108.317 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.317): CI runs the harness on every change; publishing is a dispatched workflow (five workflows; every PR-gate job a REQUIRED check on
mainby name;enforce_adminsandstrictON;release.ymldispatched with a version, Test PyPI first, trusted publishing). ⚠⚠ The gate caught its own author four times before it merged and the pre-flight was wrong twice about a MAIN commit (C-13, C-14: the PR gate's jobs live on the PR's merge ref; main's witnesses aremain.yml's). ⚠ Windows runners are 3x this box on the full tier:suite.full_seconds_ci_windows, a platform-scoped Floor, not a loosening. Rules: the CI/CD section,docs/cicd/; forensics:ISSUE-HISTORY.md(rotated 2026-09-11).
The 1.108.319 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.319 — The numbers a competitor published about us were right, and so was the refusal we had shipped over twice. An external benchmark (
amritessh/scalpel-fse2027-artifact, reproducible from its own raw data) named two real defects in us. TypeScript and TSX never indexed anabstract classat all — the grammar spells itabstract_class_declarationand neither spec listed it, so every abstract class was absent and its methods lost their owner (#698). Andsearch_symbolscut its page on score alone, so an exact-name match could be evicted OUTRIGHT — not ranked lower, absent — by same-named locals declared inside function bodies (#699); all three cut sites read one(declaration rank, score)key now, and locals are demoted, never filtered. ⚠⚠ That rank needs BOTH a kind and an owner condition, and the first draft shipped the owner probe alone, passed its own suite, and left the reported case byte-for-byte unchanged — the fixture could not express the other crowding shape, so re-run the REPORTED corpus through the product before calling a fix done. ⚠⚠ W-16 is FIXED after two releases shipped over its refusal: every per-repo cell inREADME.mdandbenchmarks/README.mdderives frombenchmarks/jcm_reference.json, andtests/test_benchmark_tables_mirror_the_reference.pygates the cells a reader quotes —test_provenance.pygated only the grand total, which is the figure everyone remembers to update. Also in the block, in full inCHANGELOG.md:get_tectonic_mappartitions by Louvain instead of one plate holding two-thirds of the tree (#668) and discloses an untrustworthy co-churn signal (#667); changed paths resolve when the index is rooted below the git top level (#685); an unproducible triage result is escalated, not retried forever (#670); committing a tree no longer invalidates the full-tier stamp taken on it (#675); the inbound jobs that bill a model have their own switch and it is OFF. Forensics:docs/workflows/FINDINGS.md.
The Tests: line's 1.108.317 count, verbatim:
- ⚠ Prior (1.108.317): 9241 passed, 19 skipped, 0 failed (9260 total; the skip count is 19 under
uv runand 13 underPYTHONPATH=src python -m pytest, harness F-05).
The 1.108.318 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.318): The process is code that cannot skip a step, and the field is measured from result files. Three layers: the workflows layer (
/feature·/fix-issue·/release·/review·/triage-issue·/benchmark-compare·/competitive-compare, with hooks that refuse a commit without the fast tier, a PR without a full-tier stamp on THIS tree, and every irreversible verb), the inbound layer (nine headless jobs, OFF untilINBOUND_ENABLED, the model never holding a write token) and the competitive tier (the nulls, jCodeMunch and nine adapters over pinned corpora in a sandbox, every FINDINGS number script-written from a result file). Forensics:docs/workflows/FINDINGS.md,docs/inbound/FINDINGS.md,docs/competitive/FINDINGS.md.
The 1.108.320 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.320 — A member the index never saw and a caller the search never found both read as nothing there. Class members and class state are indexed and owned in the languages that dropped them (Java, Kotlin, Go, Rust, Dart, Ruby, PHP, Python, JS/TS, C/C++, Pascal, F#, Nim, Zig, Swift; Haskell extracted nothing at all; #713-#858). The delete and edit preflights stopped certifying an absence their search could not see: a dynamic import that can load the file (#876, L-70, L-73, L-75;
tools/_dynamic_boundary.pyis the one reach rule), a name no call site writes (#714, L-80, L-81; a_stop_rule._UNSETTLEDverdict is never terminal), a call in another Unicode spelling (L-84:check_referencesfolds line and import matches through NFKC), a differently-cased sibling (L-88), and a page of test mentions hiding the caller (L-89:check_delete_safereads every file). ⚠⚠ The definition-span exclusion is NEVER folded and matches the exact declared spelling — an exclusion removes matches, and both a folded and a case-insensitive one certified a used methodsafe_to_deleteat 1.0. ⚠⚠ It shipped 14 days after 1.108.319, with 124 PRs merged in between: nothing in the fix loop asked whether it was time to ship (Standing lesson 09-30). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.318 count, verbatim:
- ⚠ Prior (1.108.318): 10185 passed, 24 skipped, 0 failed (10209 total; the skip count is 24 under the full tier, harness F-05).
The 1.108.319 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.319): The numbers a competitor published about us were right, and so was the refusal we had shipped over twice. TypeScript/TSX never indexed an
abstract class(#698), andsearch_symbolscould evict an exact-name match outright on score alone; all three cut sites read one(declaration rank, score)key (#699). ⚠⚠ That rank needs BOTH a kind and an owner condition — the owner probe alone passed its suite and left the reported case unchanged. ⚠⚠ W-16: every per-repo cell inREADME.mdandbenchmarks/README.mdderives frombenchmarks/jcm_reference.json, gated bytests/test_benchmark_tables_mirror_the_reference.py. Full text:CHANGELOG.md; the product half verbatim inISSUE-HISTORY.md(2026-09-30).
The 1.108.321 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.321 — A call written with an escape is still a call. A Java, C# or JavaScript call can spell a name with a Unicode escape, and the compiler reads the plain name; the reference search compared raw text and the delete preflight graded the called function
safe_to_deleteat 1.0 (L-86).check_references._fold, the one comparison every reference match goes through, decodes escapes before it compares. ⚠⚠ A literal escape typed into a tool input reaches disk DECODED: build escapes fromchr(92)and assert the backslash reached the fixture (two drafts tested the plain spelling). Also the first release under the cadence gate (STANDARD N8,release.unreleased_max_hours), whose own tests failed onmainfor three merges because they inherited the runner's CI event (Standing lesson 10-01). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.319 count, verbatim:
- ⚠ Prior (1.108.319): 10521 passed, 24 skipped, 0 failed (10545 total).
The 1.108.320 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.320): A member the index never saw and a caller the search never found both read as nothing there. Class members and state indexed in the languages that dropped them (#713-#858); the delete and edit preflights stopped certifying an absence their search could not see (a dynamic import, an unsearchable name, another Unicode spelling, a differently-cased sibling, a page of test mentions). ⚠⚠ The definition-span exclusion is NEVER folded and matches the exact declared spelling. Shipped 14 days after .319 (Standing lesson 09-30). Full text:
CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-01).
The 1.108.322 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.322 — The deletion investigator reads the match the search made.
check_referencesmatches import names through_fold; the investigator filtered those rows again with a rawtarget_name in names, so an import in another Unicode form or written with an escape was dropped andexport_not_importedread SATISFIED over a live importer (L-90). It readsmatch_typeand compares no name of its own. ⚠⚠ Never re-compare whatcheck_referencesmatched;find_referencesstill does, at two sites (L-95). First release cut inside the 2-hour cadence (N8). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.320 count, verbatim:
- ⚠ Prior (1.108.320): 13905 passed, 25 skipped, 0 failed (13930 total).
The 1.108.321 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.321): A call written with an escape is still a call.
check_references._folddecodes Unicode escapes before it compares, so an escaped Java, C# or JavaScript call is a reference and the delete preflight no longer grades the called functionsafe_to_delete(L-86). ⚠⚠ A literal escape typed into a tool input reaches disk DECODED: build escapes fromchr(92)and assert the backslash reached the fixture. First release under the cadence gate (STANDARD N8; Standing lesson 10-01). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-01, release 1.108.322).
The 1.108.323 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.323 — An entry point has no importer, and that does not make it dead. The deletion investigator called an importer dead when nothing imported it, so a name imported only by
main.py,package.json'smain, a package__init__.pyor a test readexport_not_importedSATISFIED (L-94)._split_importers_by_livenessasksfind_dead_codeand keeps no answer of its own; only a file reported at the default confidence is dead. ⚠⚠ It inherits that tool's gaps with its roots: an undeclared non-Python entry point (L-102) still reads dead; fix it THERE. The root-leveltests/gap (L-101) is fixed in[Unreleased]:tools/_test_paths.is_test_fileis the ONE test-file rule. Also:registry_verify.pyretries, reads every page inside a time budget and exitsUNREADABLEfor a read that got no answer (L-96, L-98, L-99; RUNBOOK 3a). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.321 count, verbatim:
- ⚠ Prior (1.108.321): 13991 passed, 25 skipped, 0 failed (14016 total).
The 1.108.322 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.322): The deletion investigator reads the match the search made. It re-filtered
check_references' import rows with a raw name comparison and dropped an import spelled in another Unicode form or with an escape (L-90); it readsmatch_typenow. ⚠⚠ Never re-compare whatcheck_referencesmatched;find_referencesstill does, at two sites (L-95). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-01, release 1.108.323).
The 1.108.324 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.324 — One rule says whether a path is a test file. Six rules under
src/answered it and no two agreed; the dead-code pair needed a slash beforetests/, so a root-level test file read dead and the deletion investigator called a name it imports unimported (L-101).tools/_test_paths.is_test_fileis the one rule (Key Files). ⚠⚠check_delete_safeandcheck_edit_saferead it to DOWNGRADE a blocking verdict:*_test.*,tests.pyand*_tests/are kept, so a production consumer named that way readstest_coverage_only/safe_to_edit(L-104, OPEN). Forensics:docs/workflows/LEDGER.md; harness F-41 (the local full tier sits a few seconds under its wall-clock Floor).
The Tests: line's 1.108.322 count, verbatim:
- ⚠ Prior (1.108.322): 13994 passed, 25 skipped, 0 failed (14019 total).
The 1.108.323 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.323): An entry point has no importer, and that does not make it dead. The deletion investigator's
_split_importers_by_livenessasksfind_dead_codeand keeps no answer of its own (L-94). ⚠⚠ It inherits that tool's gaps with its roots: an undeclared non-Python entry point (L-102) still reads dead; fix it THERE. Alsoregistry_verify.pyretries, pages inside a budget and exitsUNREADABLEfor an unanswered read (L-96, L-98, L-99; RUNBOOK 3a). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-02, release 1.108.324).
The 1.108.325 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.325 — A lookup that found nothing is remembered.
config._resolve_repo_keydocumented a negative cache and never wrote one, so arepo=matching no index listed every index (opening every.db) on every call, and discovery asks once per candidate FILE (#960, @ebataeva). Both answers are cached;forget_repo_resolutions()runs onIndexStore.save_index/delete_index, and a miss expires after_REPO_MISS_TTL_SECONDSfor a save made by another process. ⚠⚠ A lookup writes NOTHING if a forget overtook its listing (_REPO_CACHE_GENERATION); without it a miss written after a save hid the new index's project config. ⚠ The identifier is compared raw and many root PATHS past ~256 indexes are not all held (L-105). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.323 count, verbatim:
- ⚠ Prior (1.108.323): 14049 passed, 25 skipped, 0 failed (14074 total).
The 1.108.324 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.324): One rule says whether a path is a test file.
tools/_test_paths.is_test_filereplaced six disagreeing rules; a root-leveltests/no longer reads dead (L-101). ⚠⚠ The delete and edit preflights read it to DOWNGRADE a blocking verdict:*_test.*,tests.pyand*_tests/are kept, so a production consumer named that way readstest_coverage_only/safe_to_edit(L-104, OPEN; Key Files). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-02, release 1.108.325).
The 1.108.326 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.326 — The
semanticextra requires asentence-transformersthat does not run a local model's code. Before 5.6.0 a LOCAL model directory ran its own Python withtrust_remote_codeoff (GHSA-jhr6-gm9c-rqjv), andembed_repohandsembed_modelto it. Thesemantic/allextras floor is>=5.6.0; the lock holds 6.1.0 andurllib32.8.0.tests/test_semantic_extra_floor.pyreadspyproject.toml, tracked pin files and the lock. ⚠⚠ The floor is install metadata: an upgrade without the extra named keeps an old release and the call site does not check (L-108, OPEN). ⚠ The RAG baseline is not re-run under the new pin (L-106); three sandbox.pinshold an oldurllib3(L-107). ⚠ The PR gate's audit reads the RUNTIME set only. Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.324 count, verbatim:
- ⚠ Prior (1.108.324): 14125 passed, 25 skipped, 0 failed (14150 total).
The 1.108.325 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.325): A lookup that found nothing is remembered.
config._resolve_repo_keycaches hits and misses;forget_repo_resolutions()runs onIndexStore.save_index/delete_index(#960, @ebataeva). ⚠⚠ A lookup writes NOTHING if a forget overtook its listing (_REPO_CACHE_GENERATION). ⚠ The identifier is compared raw (L-105, OPEN). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-02, release 1.108.326).
The 1.108.327 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.327 — A local model path is refused when the installed
sentence-transformerswould run its code.tools/embed_repo._refuse_local_model_on_an_old_releaseraises beforeSentenceTransformer(...)whenembed_modelis an existing path and the imported release is older than 5.6.0, a pre-release of it, or unreadable (L-108; GHSA-jhr6-gm9c-rqjv). A Hub name is not refused. ⚠⚠ The message leads with the cause and the remedy:FailureLedgerkeeps 300 characters and a path can fill them. ⚠⚠search_symbols(fusion=true)names ANY failure of its similarity channel as the body keysemantic_channel_error(declared in the compact encoder's_JSON_BLOBS), labels the channelunavailable, and does NOT cache that answer; a zero-row one staysabsent(the v1.108.185 ruling). ⚠ jdoc and jdata have no such check (L-109). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.325 count, verbatim:
- ⚠ Prior (1.108.325): 14142 passed, 25 skipped, 0 failed (14167 total).
The 1.108.326 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.326): The
semanticextra requires asentence-transformersthat does not run a local model's code. Thesemantic/allextras floor is>=5.6.0; the lock holds 6.1.0 andurllib32.8.0;tests/test_semantic_extra_floor.pyreadspyproject.toml, tracked pin files and the lock. ⚠ The RAG baseline is not re-run under the new pin (L-106); three sandbox.pinshold an oldurllib3(L-107). ⚠ The PR gate's audit reads the RUNTIME set only. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-02, release 1.108.327).
The 1.108.328 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.328 — A file a
package.jsonscript runs is an entry point (L-102).tools/_entry_points.package_json_entriesis the one manifest reader of both dead-code tools and readsscriptsas an ALLOWLIST:[NAME=value] [npx|cross-env] RUNNER [flags its table knows] PATH, twelve runners, every per-runner fact run against the installed binary; anything else roots NOTHING (rules: Key Files_entry_points.py; residue L-111). Also a repeatindex <subdir>inside a git root is incremental (#961, @ebataeva). Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.326 count, verbatim:
- ⚠ Prior (1.108.326): 14177 passed, 25 skipped, 0 failed (14202 total).
The 1.108.327 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.327): A local model path is refused when the installed
sentence-transformerswould run its code (L-108; GHSA-jhr6-gm9c-rqjv). ⚠⚠search_symbols(fusion=true)names any similarity-channel failure assemantic_channel_error, labels the channelunavailableand does not cache it; a zero-row one staysabsent. ⚠ jdoc and jdata have no such check (L-109). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-04, release 1.108.328).
The 1.108.329 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.329 — The parse budget stops a slow parse (L-114).
JCODEMUNCH_PARSE_BUDGET_SECONDSis enforced insideparse_file(parser/parse_budget.py): one deadline per file, bound to every parser bygrammar_pack.get_parser, ended by tree-sitter's own timeout; an over-budget file is skipped and named inwarningson every route, at any size. ⚠ Python-side time after the parse is NOT covered (L-116), and a Vue/Svelte plain<script>is slow there (L-115). Rules: the Env Vars row. Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.327 count, verbatim:
- ⚠ Prior (1.108.327): 14213 passed, 25 skipped, 0 failed (14238 total).
The 1.108.328 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.328): A file a
package.jsonscript runs is an entry point (L-102):tools/_entry_points.package_json_entriesreadsscriptsas an ALLOWLIST and anything else roots NOTHING (rules: Key Files_entry_points.py; residue L-111). Also a repeatindex <subdir>inside a git root is incremental (#961, @ebataeva). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-05, release 1.108.329).
The 1.108.330 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.330 — The parse budget bounds a file's Python-side time (L-116). No second thread waits on a parse:
parse_budget.checkpoint()raises insideparse_fileonce the file's deadline (the parsing thread's own time) has passed, on every route, at any size. ⚠ One slow call (a whole-file regex, PyYAML) is not cut short (L-118), and loops are still slow BELOW the budget (L-115, L-117). Also a bare Python module name resolves to the script directory beside the importer (#972, @whakomatic; refusals and residue: L-120). Rules: the Env Vars row. Forensics:docs/workflows/LEDGER.md.
The Tests: line's 1.108.328 count, verbatim:
- ⚠ Prior (1.108.328): 14684 passed, 25 skipped, 0 failed (14709 total).
The 1.108.329 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.329): The parse budget stops a slow tree-sitter parse (L-114): the deadline lives inside
parse_file(parser/parse_budget.py), bound to every parser bygrammar_pack.get_parserand ended by tree-sitter's own timeout (rules: the Env Vars row). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-06, release 1.108.330).
The 1.108.331 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.331 — A full re-index keeps the embedding metadata beside its vectors (#522, @lsg1103275794).
save_indexdeletes the index's ownmetakeys and leaves those starting withembedding_store.META_KEY_PREFIX. ⚠embed_repoafter a full re-index no longer re-embeds everything;force=truedoes. ⚠ The semantic top-up insearch_symbolsstill checks no stored model (L-121), and stale or orphan vectors survive (L-122). Alsoembed_repo's task-type gate has THREE states (#523):Noneis never recorded and is not a change,""is a recorded value, and vectors with no embedding metadata at all are still rebuilt (jjg, 2026-10-06), named byrebuild_reason. Also a Vue or Svelte<script>'s comment lookup is one table per parent node, linear where it was quadratic (L-115). Forensics:CHANGELOG.md,docs/workflows/LEDGER.md.
The Tests: line's 1.108.329 count, verbatim:
- ⚠ Prior (1.108.329): 14726 passed, 25 skipped, 0 failed (14751 total).
The 1.108.330 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.330): The parse budget bounds a file's Python-side time (L-116):
parse_budget.checkpoint()raises insideparse_fileonce the file's deadline has passed, and every loop on the parse path starts with one (rules: the Env Vars row; residue L-117, L-118). Also a bare Python module name resolves to the script directory beside the importer (#972, @whakomatic; L-120). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-06, release 1.108.331).
The 1.108.332 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.332 — A macro's docstring is the comment directly above it (L-126):
_PrecedingComments(parser/sql_preprocessor.py) gives a dbt or Jinja/Twig directive the{# #}that ENDS where the text before it ends, less one whitespace-control mark per side. ⚠PARSER_GENERATIONis 10: a stored docstring moved on unchanged content. ⚠ Residue, each owing its own generation: L-129, L-130, L-131. Also three loops on the parse path are linear in the file (L-117; residue L-127, L-128). Forensics:CHANGELOG.md,docs/workflows/LEDGER.md.
The Tests: line's 1.108.330 count, verbatim:
- ⚠ Prior (1.108.330): 14783 passed, 25 skipped, 0 failed (14808 total).
The 1.108.331 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.331): A full re-index keeps the embedding metadata beside its vectors (#522, @lsg1103275794):
save_indexleaves themetakeys underembedding_store.META_KEY_PREFIX. ⚠embed_repoafter a full re-index re-embeds everything only onforce=true(residue L-121, L-122). Also its task-type gate has THREE states (#523):Noneis never recorded and is not a change,""is a recorded value, and vectors with no embedding metadata are still rebuilt, named byrebuild_reason. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-07, release 1.108.332).
The 1.108.333 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.333 — The parse budget stops a slow parse again on a fresh install (reported by Dave):
pyproject.tomldeclarestree-sitter>=0.25,<0.26. ⚠⚠ The range stops at the minoruv.lockpins, andtests/test_tree_sitter_range_is_the_tested_range.pyfails when it admits a later one: tree-sitter 0.26.0 removedParser.timeout_micros, CI installs from the lock, and a user installs from the range, so the cancel guard never ran on what users got. Moving to a later tree-sitter means moving the lock first (0.26 is unsupported, L-133); the lock'srequires-distline is edited by hand with it. Alsoinit's index line reads the keysindex_folderreturns (cli/init.py_index_summary; it printed? files, ? symbols; residue L-134), and three more inputs gave a macro the wrong docstring (L-129, L-130, L-131; residue L-132). ⚠PARSER_GENERATIONis 11. Forensics:CHANGELOG.md,docs/workflows/LEDGER.md.
The Tests: line's 1.108.331 count, verbatim:
- ⚠ Prior (1.108.331): 14800 passed, 25 skipped, 0 failed (14825 total).
The 1.108.332 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.332): A macro's docstring is the comment directly above it (L-126):
_PrecedingComments(parser/sql_preprocessor.py) gives a dbt or Jinja/Twig directive the{# #}that ENDS where the text before it ends, less one whitespace-control mark per side (PARSER_GENERATION10). Also three loops on the parse path are linear in the file (L-117; residue L-127, L-128). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-07, release 1.108.333).
The 1.108.334 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.334 — A semantic search writes no vector of a second model (L-121):
stale_reason(storage/embedding_store.py) is THE rule for "do the stored vectors belong with the active model", andembed_repoandsearch_symbols' semantic top-up both ask it; aset_manycaller insrc/that does not failstests/test_semantic_topup_checks_the_stored_model.py. ⚠⚠ On a reason the search does NOT rebuild (two clients on one store would re-embed it on every search): it writes nothing and returnssemantic_store_mismatchin the body;embed_reporebuilds. ⚠ A stamp never outlives its vectors:clear()removes the dimension/model/task-type rows, a writer drops a stamp beside no vectors withdrop_orphan_stamp()(neverclear()), and each writer decides fromread_meta(for_writer=True). Residue L-135, L-136, L-137. Forensics:CHANGELOG.md,docs/workflows/LEDGER.md.
The Tests: line's 1.108.332 count, verbatim:
- ⚠ Prior (1.108.332): 14861 passed, 25 skipped, 0 failed (14886 total).
The 1.108.333 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.333): The parse budget stops a slow parse again on a fresh install (reported by Dave):
pyproject.tomldeclarestree-sitter>=0.25,<0.26. ⚠⚠ The range stops at the minoruv.lockpins (tests/test_tree_sitter_range_is_the_tested_range.py): 0.26.0 removedParser.timeout_microsand is unsupported (L-133); move the lock first, and edit itsrequires-distline by hand. Alsoinit's index line reads the keysindex_folderreturns. ⚠PARSER_GENERATIONis 11. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-08, release 1.108.334).
The 1.108.335 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.335 — A read-only open reads the file the path names (L-137):
_uri(storage/generation.py) escapes%,#and?in the path, andreadonly_uriand the immutable fallback both build through it. ⚠⚠ SQLite reads the string as a URI: under a directory namedc#projthe open was WRITABLE on a stray filecandhas_any()answered False over stored vectors. ⚠ Escape those three only,%first; a test database intmp_pathcannot express this (tests/test_readonly_uri_quotes_the_path.py). Forensics:CHANGELOG.md.
The Tests: line's 1.108.333 count, verbatim:
- ⚠ Prior (1.108.333): 14948 passed, 25 skipped, 0 failed (14973 total).
The 1.108.334 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.334): A semantic search writes no vector of a second model (L-121):
stale_reason(storage/embedding_store.py) is THE rule, andembed_repoandsearch_symbols' semantic top-up both ask it (tests/test_semantic_topup_checks_the_stored_model.py). ⚠⚠ On a reason the search does NOT rebuild: it writes nothing and returnssemantic_store_mismatch;embed_reporebuilds. ⚠ A stamp never outlives its vectors (drop_orphan_stamp(), neverclear()). Residue L-135, L-136. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-08, release 1.108.335).
The 1.108.336 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.336 — A fusion search scores no query against another model's vectors (L-135):
_search_symbols_fusionasksstale_reasonbefore it calls the provider and returnssemantic_store_mismatch, the semantic exit's body (_stale_store_mismatch,_width_store_mismatch). ⚠⚠ A function that callsscore_alland neverstale_reasonfailstests/test_semantic_topup_checks_the_stored_model.py; the L-121 scan was keyed onset_many, and this exit reads. ⚠_embed_task_types(tools/embed_repo.py) is the ONE task-type rule for the writer that stamps and the exits that compare. Residue L-139, L-140, L-141. Forensics:CHANGELOG.md.
The Tests: line's 1.108.334 count, verbatim:
- ⚠ Prior (1.108.334): 14997 passed, 25 skipped, 0 failed (15022 total).
The 1.108.335 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.335): A read-only open reads the file the path names (L-137):
_uri(storage/generation.py) escapes%,#and?,%first, and every read-only open builds through it. ⚠⚠ SQLite reads the string as a URI: underc#projthe open was WRITABLE on a stray filec. A test database intmp_pathcannot express this (tests/test_readonly_uri_quotes_the_path.py). Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-08, release 1.108.336).
The 1.108.337 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.337 — A shutdown checkpoints a database without migrating it (L-143):
checkpoint_db(storage/sqlite_store.py) opens a PLAIN connection, checkpoints, and logs asqlite3.Errorat debug. ⚠⚠ Never route it through_connect: that connection creates the index schema or migrates, so a shutdown raisedno such tableon a database still being created and put index tables intelemetry.db. ⚠ It never raises; it runs fromfinallyblocks (tests/test_store_close_only_checkpoints.py). Residue L-144. Forensics:CHANGELOG.md.
The Tests: line's 1.108.335 count, verbatim:
- ⚠ Prior (1.108.335): 15038 passed, 25 skipped, 0 failed (15063 total).
The 1.108.336 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.336): A fusion search scores no query against another model's vectors (L-135):
_search_symbols_fusionasksstale_reasonbefore it calls the provider and returnssemantic_store_mismatch. ⚠⚠ A function that callsscore_alland neverstale_reasonfailstests/test_semantic_topup_checks_the_stored_model.py. ⚠_embed_task_types(tools/embed_repo.py) is the ONE task-type rule. Residue L-139, L-140, L-141. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-09, release 1.108.337).
The 1.108.338 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.338 — A .NET build tree and a NuGet restore tree stay out of the index (#1001, @outoftheblue9):
obj//bin/are pruned only beside a.csproj/.vbproj/.fsproj,packages/only when a child holds its own.nupkg,.vs/always (security.py). ⚠⚠ A solution file is NOT a marker: a root.slnpruned a hand-writtenbin/deploy.pyandsearch_textcertified the absence. ⚠ Both rules prune on evidence the writer left, never on the name.index_fileapplies neither andindex_repoonly.vs/(L-142). Forensics:CHANGELOG.md.
The Tests: line's 1.108.336 count, verbatim:
- ⚠ Prior (1.108.336): 15056 passed, 25 skipped, 0 failed (15081 total).
The nightly of 2026-10-09 failed index.cold_self_seconds at 5.207 against a Floor of 5.2. Triage compared readings BETWEEN jobs, saw
about 2x between runners, closed #1005 as runner variance and drafted a loosening of the Floor, twice. The second review of that draft
divided each job's cold-index reading by another timing of the same job; over the 24 recorded CI runs
(harness/results/self_latency_ci_runs_*.json) the ratio steps 1.29x to 1.36x between the nightlies of 2026-09-29 and 2026-09-30 with
no overlap. #1005 was reopened with a correction and the loosening dropped (jjg). The cause was the dynamic-import scan of #876
walking a file's syntax tree per name and per pass; fixed in #1011, released as 1.108.339. A local timing of the suspect commit on a fixed
older copy of src/ had shown too little to see: the cost was the new code on the new files. The Standing lesson dated 10-09 is the
rule this earned.
The 1.108.337 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.337): A shutdown checkpoints a database without migrating it (L-143):
checkpoint_db(storage/sqlite_store.py) opens a PLAIN connection and never raises. ⚠⚠ Never route it through_connect, which creates the index schema or migrates. Residue L-144. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-09, release 1.108.338).
The 1.108.339 entry, verbatim, dropped when it was compressed to a Prior line:
- Version: 1.108.339 — The dynamic-import scan walks a Python file once (L-147, #1005):
_python_dynamic_imports(parser/imports.py) reads ONE node list with parent links. ⚠⚠ Never answer a per-name question there withast.walk(tree)or a pass overall_nodes;tests/test_dynamic_import_scan_walks_the_tree_once.pycounts visits and calls per node. ⚠ Output unchanged, noPARSER_GENERATIONbump. Not recovered: a tenth of a cold index against pre-#876. Residue L-148, L-149. Forensics:CHANGELOG.md.
The Tests: line's 1.108.337 count, verbatim:
- ⚠ Prior (1.108.337): 15063 passed, 25 skipped, 0 failed (15088 total).
The 1.108.338 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.338): A .NET build tree and a NuGet restore tree stay out of the index (#1001, @outoftheblue9):
obj//bin/pruned only beside a project file,packages/only on its own.nupkg(security.py). ⚠⚠ A solution file is NOT a marker. Residue L-142. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-09, release 1.108.339).
The Tests: line's 1.108.338 count, verbatim:
- ⚠ Prior (1.108.338): 15138 passed, 25 skipped, 0 failed (15163 total).
The 1.108.339 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.339): The dynamic-import scan walks a Python file once (L-147, #1005). ⚠⚠ Never answer a per-name question in
_python_dynamic_importswithast.walk(tree)or a pass overall_nodes. Residue L-148, L-149. Full text:CHANGELOG.md; verbatim inISSUE-HISTORY.md(2026-10-10, release 1.108.340).
The Tests: line's 1.108.339 count, verbatim:
- ⚠ Prior (1.108.339): 15173 passed, 25 skipped, 0 failed (15198 total).
The 1.108.340 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.340): ASP.NET Web Forms is indexed, and a handler wired in markup is referenced (#1012, @outoftheblue9): language
aspx(never arazoralias), ten XML-shaped .NET extensions asxml. ⚠⚠.config/.settingsstay UNMAPPED until connection strings are redacted. ⚠ A markup regex is one C call no parse budget interrupts: anchor every name (tests/test_aspx_review_guards.py). Full text:CHANGELOG.md.
The Tests: line's 1.108.340 count, verbatim:
- ⚠ Prior (1.108.340): 15386 passed, 25 skipped, 0 failed (15411 total).
The 1.108.341 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.341): The
indexCLI takes--identity-mode, and an existing directory is a local target however it is spelled (#1017, @sKlat-beep):_index_target_is_local(server.py) is the one local-or-GitHub rule. ⚠⚠ A URL-shaped target skips only the directory PROBE, on an ANCHORED match;://anywhere in the string once movedC://dirto GitHub. Full text:CHANGELOG.md.
The Tests: line's 1.108.341 count, verbatim:
- ⚠ Prior (1.108.341): 15415 passed, 25 skipped, 0 failed (15440 total).
The 1.108.342 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.342):
get_dead_code_v2's text fallback reads a file once (L-150):FileWords(tools/_call_graph.py) answers which lines name a word. ⚠⚠ Never answer a per-symbol question there with a pass over a file's text. Residue L-152. Full text:CHANGELOG.md.
The Tests: line's 1.108.342 count, verbatim:
- ⚠ Prior (1.108.342): 15614 passed, 25 skipped, 0 failed (15639 total).
The 1.108.343 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.343):
get_dead_code_v2reads a file as text wherever its language records no call (L-155, L-152):_files_read_as_textasks the INDEX per file. ⚠⚠ Never gate caller evidence on a repository-wide test, and never on_CALL_NODE_TYPES(it names PHP and Dart, which record none). Residue L-154, L-157, L-159, L-160. Full text:CHANGELOG.md.
The Tests: line's 1.108.343 count, verbatim:
- ⚠ Prior (1.108.343): 15775 passed, 25 skipped, 0 failed (15800 total).
The 1.108.344 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.344):
get_dead_code_v2asks every other text-read code file for a caller, import edge or not (L-157):_CallersElsewhere. ⚠⚠ The answer is kept per NAME; per function it cost files times functions. ⚠ Weakest evidence in the tool, toward not reporting: limits in L-161. Full text:CHANGELOG.md.
The Tests: line's 1.108.344 count, verbatim:
- ⚠ Prior (1.108.344): 15809 passed, 25 skipped, 0 failed (15834 total).
The 1.108.345 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.345): A name that starts or ends in a mark is matched as a token (L-154):
word_rule.py(Key Files). ⚠ Not a superset of the old rule; limits in L-163. Full text:CHANGELOG.md.
The Tests: line's 1.108.345 count, verbatim:
- ⚠ Prior (1.108.345): 16024 passed, 25 skipped, 0 failed (16049 total).
The 1.108.346 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.346): A call through a receiver records the METHOD, never the receiver (L-159):
_extract_call_namereads the grammar's callee fields. ⚠⚠ Never fall back to an identifier inside the callee.PARSER_GENERATION12. Residue L-158, L-164. Full text:CHANGELOG.md.
The Tests: line's 1.108.346 count, verbatim:
- ⚠ Prior (1.108.346): 16062 passed, 25 skipped, 0 failed (16087 total).
The 1.108.347 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.347): A call inside a Rust macro invocation or
macro_rules!body is recorded (L-164):_record_macro_calls. ⚠⚠ Each token is read ONCE, from the macro node down; never ask a node for its parent (two versions were quadratic).PARSER_GENERATION13. Residue L-165. Full text:CHANGELOG.md.
The Tests: line's 1.108.347 count, verbatim:
- ⚠ Prior (1.108.347): 16111 passed, 25 skipped, 0 failed (16136 total).
The 1.108.348 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.348):
get_dead_code_v2counts a call from another file where the language needs no import (L-165):LANGUAGES_CALLED_ACROSS_FILES_WITHOUT_AN_IMPORT. ⚠⚠ Python, JS and TS stay out. ⚠ By name: hides an uncalled function behind any call of its name. Residue L-166. Full text:CHANGELOG.md.
The Tests: line's 1.108.348 count, verbatim:
- ⚠ Prior (1.108.348): 16128 passed, 25 skipped, 0 failed (16153 total).
The 1.108.349 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.349): A Java
super(...)records the superclass's constructor (L-166):_record_super_constructor_callsreads the class node DOWN.PARSER_GENERATION14. ⚠__init__,constructorandinitializestill readno_callers: L-167. Full text:CHANGELOG.md.
The Tests: line's 1.108.349 count, verbatim:
- ⚠ Prior (1.108.349): 16157 passed, 25 skipped, 0 failed (16182 total).
The 1.108.350 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.350): The watcher follows a HEAD move that changed no file (#1040):
_reconcile_head_moveruns the ordinary incremental index. ⚠⚠ Never a bare SHA rewrite: it marks unread filesfresh. ⚠ Reads under.gitare bounded and inSECURITY.md. Residue L-168. Full text:CHANGELOG.md.
The Tests: line's 1.108.350 count, verbatim:
- ⚠ Prior (1.108.350): 16204 passed, 25 skipped, 0 failed (16229 total).
The 1.108.351 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.351): Creating a class counts as a call of its constructor (L-167):
constructors_as_their_classeshandsget_dead_code_v2each__init__/constructoras its class. ⚠⚠ A subclass joins its base only from the base's file or an importer (by name across the index was quadratic). ⚠ Ruby not covered: L-172. Full text:CHANGELOG.md.
The Tests: line's 1.108.351 count, verbatim:
- ⚠ Prior (1.108.351): 16243 passed, 25 skipped, 0 failed (16268 total).
The 1.108.352 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.352): An initialiser outside every function is read for callers (L-169):
_residue_textkeeps the lines ofSTATE_KINDSsymbols. ⚠⚠ A line a function or method holds stays out (zod loses 265 rows, not 158, without it). ⚠ Text, not calls: L-173. Full text:CHANGELOG.md.
The Tests: line's 1.108.352 count, verbatim:
- ⚠ Prior (1.108.352): 16285 passed, 25 skipped, 0 failed (16310 total).
The 1.108.353 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.353): A C#
new Made()records the class it creates (L-170):object_creation_expressionis a C# call node.PARSER_GENERATION15. ⚠ A target-typednew(),: base(...)and an attribute record nothing: L-174. Full text:CHANGELOG.md.
The Tests: line's 1.108.353 count, verbatim:
- ⚠ Prior (1.108.353): 16321 passed, 25 skipped, 0 failed (16346 total).
The 1.108.354 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.354): A Go call the grammar reads as a conversion is recorded (L-171):
type_conversion_expressionis a Go call node (Generic[int](1),fs[i](1)).PARSER_GENERATION16. ⚠ A real conversion to a generic type records the type's name: L-174. Full text:CHANGELOG.md.
The Tests: line's 1.108.354 count, verbatim:
- ⚠ Prior (1.108.354): 16376 passed, 25 skipped, 0 failed (16401 total).
The 1.108.355 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.355): A bare Ruby method call is recorded (L-172):
_record_bare_ruby_callsreads each method in source order; a bare name is a call unless a local of that name exists by then.PARSER_GENERATION17. ⚠ Code outside every method is not read: L-174. Full text:CHANGELOG.md.
The Tests: line's 1.108.355 count, verbatim:
- ⚠ Prior (1.108.355): 16457 passed, 25 skipped, 0 failed (16482 total).
The 1.108.356 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.356): A JavaScript call through a string key records the method (L-173):
_js_string_key_namereadst["m"]()asmin JS, TS and TSX.PARSER_GENERATION18. ⚠ Any other key, and every other language's subscript, records the container: L-174. Full text:CHANGELOG.md.
The Tests: line's 1.108.356 count, verbatim:
- ⚠ Prior (1.108.356): 16607 passed, 25 skipped, 0 failed (16632 total).
The 1.108.357 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.357): The generated agent policy no longer orders the index for everything (L-175):
cli/policy.pysays where each route is cheaper;resolve_repoandplan_turnare conditional. ⚠ An installed policy is never rewritten; the start-up instructions and hooks still steer to the index: L-175. Full text:CHANGELOG.md.
The Tests: line's 1.108.357 count, verbatim:
- ⚠ Prior (1.108.357): 16711 passed, 25 skipped, 0 failed (16736 total).
The 1.108.358 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.358): A multi-line Python import records its names (L-176):
_py_from_clausereads the wholefrom x import (...).PARSER_GENERATION19. The surface offer reaches the user once in the firstresolve_repoanswer (surface_notice, SECURITY.md). ⚠ Other languages' grouped imports: L-177. Full text:CHANGELOG.md.
The Tests: line's 1.108.358 count, verbatim:
- ⚠ Prior (1.108.358): 16772 passed, 25 skipped, 0 failed (16797 total).
The 1.108.359 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.359):
diagnosewrites one local report (diagnose.py): a fixed list of keys AND values, no path or repo name. Two local per-tool counters (storage/tool_counters.py,_tool_counters.json, SECURITY.md). ⚠ Residue: L-179, L-181. Full text:CHANGELOG.md.
The Tests: line's 1.108.359 count, verbatim:
- ⚠ Prior (1.108.359): 16844 passed, 25 skipped, 0 failed (16869 total).
The 1.108.360 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.360): A Web Forms attribute is read in every value form (#1067, @outoftheblue9): one quote-aware pass per tag in both readers.
.xsd/.wsdlstay capped everywhere, asbuild_consumedor the neutralschema_consumer.PARSER_GENERATION20. ⚠ Tags are still split quote-blind: L-182. Full text:CHANGELOG.md.
The Tests: line's 1.108.360 count, verbatim:
- ⚠ Prior (1.108.360): 16926 passed, 25 skipped, 0 failed (16951 total).
The 1.108.361 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.361): An installed policy is refreshed only when asked (
cli/policy_refresh.py):claude-md --refreshshows the diff, asks, keeps a backup and replaces only the block (policy_block_span, shared withinitand the drift check).policy_noticeonce per policy text (SECURITY.md). ⚠uninstallkeeps its own boundary: L-186.///summaries unwrapped (#1075, @outoftheblue9),PARSER_GENERATION21. Full text:CHANGELOG.md.
The Tests: line's 1.108.361 count, verbatim:
- ⚠ Prior (1.108.361): 16978 passed, 25 skipped, 0 failed (17003 total).
The 1.108.362 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.362): A
migrationspackage is indexed (security.is_generated_migrations_directory): only a dir holding a generated-named entry is skipped (generated_migrations). Start-upinstructionsmatch the policy (#1079). Full text:CHANGELOG.md.
The Tests: line's 1.108.362 count, verbatim:
- ⚠ Prior (1.108.362): 17169 passed, 25 skipped, 0 failed (17194 total).
The 1.108.363 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.363): Hooks name the install that ran (
install_layout.own_executable): never PATH first, never a relative launch path, never uv's cache (initsays which). Full text:CHANGELOG.md.
The Tests: line's 1.108.363 count, verbatim:
- ⚠ Prior (1.108.363): 17197 passed, 25 skipped, 0 failed (17222 total).
The 1.108.364 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.364): SQL procedures, triggers and constraints are symbols; a script parses one batch at a time (#1080, #1081, @outoftheblue9). Ranking ignores git blame (
properties_are_search_terms),PARSER_GENERATION24. Full text:CHANGELOG.md.
The Tests: line's 1.108.364 count, verbatim:
- ⚠ Prior (1.108.364): 17423 passed, 25 skipped, 0 failed (17448 total).
The 1.108.365 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.365): git blame is off by default (L-189); with it on, a provider whose properties aren't vocabulary (
properties_are_search_terms) adds nothing to a file summary. SECURITY.md lists it. NoPARSER_GENERATIONbump. Full text:CHANGELOG.md.
The Tests: line's 1.108.365 count, verbatim:
- ⚠ Prior (1.108.365): 17438 passed, 25 skipped, 0 failed (17463 total).
The 1.108.366 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.366): env vars stay supported (L-193, #1098): the loader logs no deprecation and no doc promises a 2.0 removal (
tests/test_env_vars_stay_supported.py). Full text:CHANGELOG.md.
The Tests: line's 1.108.366 count, verbatim:
- ⚠ Prior (1.108.366): 17442 passed, 25 skipped, 0 failed (17467 total).
The 1.108.367 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.367): a session lists only its own workspace's indexes (
tools/_visibility.py,repo_visibility, ruling 5): an exact id still loads anything; home or a root lists all. Full text:CHANGELOG.md.
The Tests: line's 1.108.367 count, verbatim:
- ⚠ Prior (1.108.367): 17456 passed, 25 skipped, 0 failed (17481 total).
The 1.108.368 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.368): a context provider that runs out of time runs again on the next index (L-190): the cut is stored as
context_metadata.providers_cutand every delta save keeps it. Full text:CHANGELOG.md.
The Tests: line's 1.108.368 count, verbatim:
- ⚠ Prior (1.108.368): 17476 passed, 25 skipped, 0 failed (17501 total).
The 1.108.369 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.369): indexing a drive root keeps its files (L-197,
path_prefix.dir_prefix); LSP starts rust-analyzer with build scripts off (L-198); branch deltas never ran (L-196). Full text:CHANGELOG.md.
The Tests: line's 1.108.369 count, verbatim:
- ⚠ Prior (1.108.369): 17486 passed, 25 skipped, 0 failed (17511 total).
The 1.108.370 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.370): seven import forms record the names they import (L-177): PHP group use, Kotlin
as, Scala selectors, Haskell lists, Dartshow, destructuringrequire, Pythonimport \;PARSER_GENERATION25. Full text:CHANGELOG.md.
The Tests: line's 1.108.370 count, verbatim:
- ⚠ Prior (1.108.370): 17538 passed, 25 skipped, 0 failed (17563 total).
The 1.108.371 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.371): a schema's dead-code advice follows the right evidence (L-184): Razor and Blazor count as .NET, a bare
.fsdoes not (_entry_points.has_dotnet_evidence). Full text:CHANGELOG.md.
The Tests: line's 1.108.371 count, verbatim:
- ⚠ Prior (1.108.371): 17558 passed, 25 skipped, 0 failed (17583 total).
The 1.108.372 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.372): uninstall keeps your own section after the policy (L-186):
uninstallaskspolicy.policy_block_span, whose start is a real heading past a BOM and fences. Full text:CHANGELOG.md.
The Tests: line's 1.108.372 count, verbatim:
- ⚠ Prior (1.108.372): 17579 passed, 25 skipped, 0 failed (17604 total).
The 1.108.373 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.373): claude-md --refresh can write the policy your server uses (L-185): the notice marker records the server's policy SETTINGS, never text;
--match-serverrebuilds withpolicy.policy_for. Full text:CHANGELOG.md.
The Tests: line's 1.108.373 count, verbatim:
- ⚠ Prior (1.108.373): 17595 passed, 25 skipped, 0 failed (17620 total).
The 1.108.374 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.374): a Python virtualenv is skipped whatever its folder is called (L-206):
security.is_virtualenv_site_packages_directoryprunes onlysite-packagesunder a root whosepyvenv.cfghashome; also #1117 audit reliability (lilubot). Full text:CHANGELOG.md.
The Tests: line's 1.108.374 count, verbatim:
- ⚠ Prior (1.108.374): 17702 passed, 25 skipped, 0 failed (17727 total).
The 1.108.375 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.375): a typo can no longer switch off a protective setting (L-207):
config.BOOL_ENV_TYPO_KEEPS_DEFAULTkeeps six default-on switches on for an unrecognised env value; every other boolean reads as before (pinned). Full text:CHANGELOG.md.
The Tests: line's 1.108.375 count, verbatim:
- ⚠ Prior (1.108.375): 17815 passed, 25 skipped, 0 failed (17840 total).
The 1.108.376 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.376): install-pack honours every savings opt-out (L-212):
install_pack._install_telemetry_allowedposts only when the old check ANDconfig.get("share_savings")allow it; also a once-per-install licence line ininit/install. Full text:CHANGELOG.md.
The Tests: line's 1.108.376 count, verbatim:
- ⚠ Prior (1.108.376): 17886 passed, 25 skipped, 0 failed (17911 total).
The 1.108.377 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.377): a text search earns no savings credit (L-180):
search_text/check_referencesearn zero in the receipt and the meter; the receipt total reads Modeled gross savings; also L-142, L-144, L-149, L-209. Full text:CHANGELOG.md.
The Tests: line's 1.108.377 count, verbatim:
- ⚠ Prior (1.108.377): 17976 passed, 25 skipped, 0 failed (18001 total).
Verbatim as they stood in CLAUDE.md; the rule (read the skip count) and the command stay there.
⚠⚠ Caught mid-release, and the near-miss is the lesson: it returned EXIT 0
and the totals reconciled EXACTLY (8,740 + 18 new tests = 8,758) — the two
things "green" normally means here. Meanwhile passed fell 8,721 → 8,634 and
skipped rose 19 → 124: 105 tests silently did not execute, because
watchfiles (the [watch] extra CI installs BY NAME) was absent.
⚠ Third instance of one family, and CONTRIBUTING.md already carries the
sentence: CI installs with uv sync and never runs the command the docs give a
human, so the thing we test is not the thing they do. The first was
pip install -e ".[test]" (an extra no repo declares); the second was
-n 4 --dist loadfile under a bare python -m pytest, which collects nothing
and exits 0. [[pipes-and-missing-xdist-both-report-exit-zero]]
The 1.108.378 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.378): install-pack's licence lookup writes no config file (L-215):
resolve_effective_license_keyre-reads withload_config(create_missing=False). Full text:CHANGELOG.md.
The Tests: line's 1.108.378 count, verbatim:
- ⚠ Prior (1.108.378): 17985 passed, 25 skipped, 0 failed (18010 total).
The 1.108.379 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.379): a spawned agent no longer waits minutes for its jCodeMunch briefing (#1139):
cli/hooks/briefing.pyhydrates at most the deepest indexed root containing the cwd, only underHYDRATE_MAX_SYMBOLS; an unmatched cwd briefs no repo; PageRank stops atBUDGET_SECONDS. Full text:CHANGELOG.md.
The Tests: line's 1.108.379 count, verbatim:
- ⚠ Prior (1.108.379): 18056 passed, 25 skipped, 0 failed (18081 total).
The 1.108.380 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.380): compacting a session no longer waits on a large index's landmarks (L-219): the hook size rule and budget live in
cli/hooks/_common.py;_iter_loaded_repos(size_capped=, stop_at=)applies them before each load, and the PreCompact landmarks section names every index it skipped. Full text:CHANGELOG.md.
The Tests: line's 1.108.380 count, verbatim:
- ⚠ Prior (1.108.380): 18066 passed, 25 skipped, 0 failed (18091 total).
The 1.108.381 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.381): one repository's index can no longer replace or stand in for another's (GHSA-xxx4-frv5-chp2, @mich3333):
SQLiteIndexStore._guard_slot, called by_db_path/_content_dirand the facade's_slot, refuses a slot whose index records another repository; saves re-check underBEGIN IMMEDIATE. Full text:CHANGELOG.md.
The Tests: line's 1.108.381 count, verbatim:
- ⚠ Prior (1.108.381): 18082 passed, 25 skipped, 0 failed (18107 total).
The 1.108.382 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.382): a task capsule's risk entry carries the PR's real risk score (L-61):
assemble_task_context._stage_riskreadsget_pr_risk_profile'srisk_score/risk_level/signal_breakdown, keeps a withheld scoreNonewithunmeasurable_axes, and capsunresolvable_filesat 20 with its total. Full text:CHANGELOG.md.
The Tests: line's 1.108.382 count, verbatim:
- ⚠ Prior (1.108.382): 18112 passed, 25 skipped, 0 failed (18137 total).
The 1.108.383 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.383): refreshing summaries no longer erases complexity and the call graph (#1161, @SlimLemon):
SQLiteIndexStore.update_symbol_summariesis a one-column UPDATE, guarded bycontent_hash, that evicts the index cache;summarize_repowrites only changed summaries and reportsnot_written. Full text:CHANGELOG.md.
The Tests: line's 1.108.383 count, verbatim:
- ⚠ Prior (1.108.383): 18119 passed, 25 skipped, 0 failed (18144 total).
The 1.108.384 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.384): the watcher no longer polls trees nothing indexes (#1163, @yagiii1344):
index_folder._directory_prune_reasonis the one directory rule for discovery andwatcher._watch_directories; a gitignored or extra-ignored directory is pruned only when the matcher ignores two probe descendants and no!reaches it. Also L-226:update_symbol_summariesnotifiesstorage/write_listeners.py, andsearch_symbolsevicts the repo (in-process; cross-process is L-228). Full text:CHANGELOG.md.
The Tests: line's 1.108.384 count, verbatim:
- ⚠ Prior (1.108.384): 18139 passed, 25 skipped, 0 failed (18164 total).
The 1.108.385 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.385): a summary written by another process reaches this one's searches (L-228, L-229):
update_symbol_summariesadvances asummaries_generationmeta counter in its transaction,CodeIndex.summaries_generationcarries it (inselective.EXACT_FIELDS), and it is the last element ofsearch_symbols' cache key. Also L-224:save_indexpre-warms the cache with_symbol_to_dict, so a same-process read keepsecosystem_context; and #1172's README line 3 and registry description. Full text:CHANGELOG.md.
The Tests: line's 1.108.385 count, verbatim:
- ⚠ Prior (1.108.385): 18145 passed, 25 skipped, 0 failed (18170 total).
The 1.108.386 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.386): the AI summary prompt can carry each symbol's own source, opt-in (#1175, #1179, @skibadubskiybadubs):
summarizer_body_chars(default 0) adds up to that many characters of the symbol's INDEXED text plus its decorators to the prompt; a non-local provider gets them only withallow_remote_summarizer; redaction runs before the cut. Also #1177 (Flaskmethods=read bydecorator-routes) #1178 (Context:lines in first-seen order), and L-227 (#1182: the deferred summary writes one column). Full text:CHANGELOG.md.
The Tests: line's 1.108.386 count, verbatim:
- ⚠ Prior (1.108.386): 18265 passed, 25 skipped, 0 failed (18290 total).
The 1.108.387 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.387): a licensed install is not shown a sales line (2026-10-20: paid clients are never marketed to):
org.license.licence_key_configured(readsJCODEMUNCH_LICENSE_KEYorlicense_key, never validates, soinitmakes no network call) gatesinit's $79 line andinstall-pack's "Get a license";licenseandlicense --jsonshow the pricing link only when no key is set (license_status_json). Also certifi and Pillow lock updates. Full text:CHANGELOG.md.
The Tests: line's 1.108.387 count, verbatim:
- ⚠ Prior (1.108.387): 18280 passed, 25 skipped, 0 failed (18305 total).
The 1.108.388 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.388): the license names jMunch, LLC, and the product says "license": LICENSE 1.2, effective 2026-10-21, copyright jMunch, LLC, terms and SPDX identifier unchanged (#1191, digest pin moved); "licence" respelled outside identifiers, quotations and released history, and a rejected key gets a support pointer (#1192). Full text:
CHANGELOG.md.
The Tests: line's 1.108.388 count, verbatim:
- ⚠ Prior (1.108.388): 18284 passed, 25 skipped, 0 failed (18309 total).
The 1.108.389 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.389): the core tools load up front where clients defer MCP tools (#1197):
_apply_always_loadmarks_instruction_tool_names(surface)with_meta["anthropic/alwaysLoad"](one list for both); stdio startup_indexed_here(resolve_repo, 1.5 s, source_root must contain cwd) makes the note say "is indexed (N files)"; the counter hook namesorderand its load step (#1188);cli.policyresolves the surface like the server (#1189). Full text:CHANGELOG.md.
The Tests: line's 1.108.389 count, verbatim:
- ⚠ Prior (1.108.389): 18330 passed, 24 skipped, 0 failed (18354 total).
The 1.108.390 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.390): the local embedding model is pinned and checked before it is used (#1201):
local_encoder.download_modelfetchesMODEL_REVISIONand checks each file againstMODEL_SHA256(fails closed on a missing digest); a fetch goes to<name>.partandos.replaces only after it matches; a mismatching file on disk is removed and re-fetched. The search-time load does not re-hash; the sentence-transformers route is not pinned. Size now stated as about 90 MB. Full text:CHANGELOG.md.
The Tests: line's 1.108.390 count, verbatim:
- ⚠ Prior (1.108.390): 18339 passed, 24 skipped, 0 failed (18363 total).
The 1.108.391 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.391): a question about how code works ranks the code above its tests (#1207):
scoring.query_idfweights plain-word function words in a multi-word query atFUNCTION_WORD_WEIGHT;test_file_factor(TEST_SYMBOL_WEIGHT) applies only where a caller RANKS, verdicts read the raw score. Also #1204 (#1202):checkout_boundary.separate_checkout_between(worktrees=True)keeps a nested worktree out of the parent's index in resolve_repo and the hooks; #1205: CLI backups arecli/backup.backup_path; #1208: the ubuntu 3.10 CI leg has its own wall Floor. Full text:CHANGELOG.md.
The Tests: line's 1.108.391 count, verbatim:
- ⚠ Prior (1.108.391): 18417 passed, 24 skipped, 0 failed (18441 total).
The 1.108.392 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.392): a warning in a tool's answer survives the compact encoding (#1212):
schema_driven.UNIVERSAL_BODY_JSON/is_universal_body_key(negative_evidenceand any key containing "warning") ride every encoder as__json.<key>and are restored on decode;encode_responsedecodes and compares the contract keys and falls back to JSON if one is lost. Also #1210:scoring.rank_factorreturns(weight, reason),non_code_fileatNON_CODE_WEIGHTfor config and stylesheet symbols unless the query names them; #1211: call-hierarchy rows carryvia, depth is re-derived along it,direct_caller_count, encoderch2. Full text:CHANGELOG.md.
The Tests: line's 1.108.392 count, verbatim:
- ⚠ Prior (1.108.392): 18479 passed, 24 skipped, 0 failed (18503 total).
The 1.108.393 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.393): a default install adds nothing to the model's context (#1218):
_silent_hooks(PostToolUse, PreCompact, TaskCompleted) are the default;_guidance_hooksneedinit --guidance, policy pastes--policy; an install keeps what it has;init --remove-guidancestrips both with backups;config --checktreats their absence as opt-in. Also #1217:scoring.is_data_symbolfields and UPPER_CASE constants weighted atDATA_KIND_WEIGHTunless the query names values; benchmark reference re-captured with an approved rise (--accept-rise, reference-only; STANDARD Regression 6). Full text:CHANGELOG.md.
The Tests: line's 1.108.393 count, verbatim:
- ⚠ Prior (1.108.393): 18537 passed, 24 skipped, 0 failed (18561 total).
The 1.108.394 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.394): index_dependency validates package paths (#1220): npm/PyPI names and
top_level.txtentries are name-validated; every package dir and its metadata must resolve inside the repo and not be or contain a store root (_is_store,_metadata_in_repo); the snapshot is one dir directly underdeps/and is rebuilt;_copy_snapshotrunsshould_exclude_file+ binary + host-gitignore;index_folder(paths_honor_gitignore=True)applies the ancestor.gitignoretopaths=. Full text:CHANGELOG.md.
The Tests: line's 1.108.394 count, verbatim:
- ⚠ Prior (1.108.394): 18584 passed, 24 skipped, 0 failed (18608 total).
The 1.108.395 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.395): a text match says which function it is in (#1222):
tools/_enclosing.attach_enclosing_symbolsaddsin: {id, line, end_line}to search_text and check_references matches (innermost span; on a tie the later, deeper symbol; kinds fromSTATE_KINDS); search_text body carriestruncated,timed_outand an empty-scopenote; encodingst3. Full text:CHANGELOG.md.
The Tests: line's 1.108.395 count, verbatim:
- ⚠ Prior (1.108.395): 18593 passed, 25 skipped, 0 failed (18618 total).
The 1.108.396 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.396): the first search on a large repository resolves each import once (#1224):
parser/imports.resolve_specifierhands its importer-independent tail to_resolve_tail, memoized per (specifier, frozenset of files, alias map) for every importer that is not.gleam(the one tail step that reads the importer);_get_sql_stemskeys by the caller's frozenset;clear_resolution_caches()(called byinvalidate_cache) clears both. Full text:CHANGELOG.md.
The Tests: line's 1.108.396 count, verbatim:
- ⚠ Prior (1.108.396): 18625 passed, 24 skipped, 0 failed (18649 total).
The 1.108.397 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.397): a cut search names the files it left off, and file_pattern takes braces (#1226):
tools/search_text._file_patternsexpands brace groups (as-written pattern always kept; nesting, an unclosed brace or >MAX_BRACE_ALTERNATIVES(32) match as written); a truncated page addsmore_files(capMORE_FILES_CAP20) +more_files_total/_truncated/_complete, counted by_count_matcheswith the page's own line test (bytes prefilter only on ASCII files); encodingst4. Full text:CHANGELOG.md.
The Tests: line's 1.108.397 count, verbatim:
- ⚠ Prior (1.108.397): 18648 passed, 24 skipped, 0 failed (18672 total).
The 1.108.398 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.398): the semantic and all extras install a fixed filelock on every Python (#1229, #1228): the extras carry
filelock==3.20.3; python_version < '3.11'andfilelock>=4.2.1; python_version >= '3.11'(4.2.1+ needs 3.11, 3.20.3 is the only clean release 3.10 installs, so a floor alone admits 4.1.0 there);[tool.uv] constraint-dependencieskeeps only setuptools;tests/test_filelock_extra_floor.pyapplies markers per Python and the 3.10 install cap. Full text:CHANGELOG.md.
The Tests: line's 1.108.398 count, verbatim:
- ⚠ Prior (1.108.398): 18674 passed, 24 skipped, 0 failed (18698 total).
The 1.108.399 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.399): four core tool descriptions open with the question each tool answers (#1233, #1234): search_symbols/get_symbol_source/get_call_hierarchy/check_references, pinned by
tests/test_question_queries_find_core_tools.py; the counterinstructionsadd a named-code line whose route() phrasingstests/test_instructions_named_code.pychecks againstclassify_intent. No effect on use is claimed. Full text:CHANGELOG.md.
The Tests: line's 1.108.399 count, verbatim:
- ⚠ Prior (1.108.399): 18696 passed, 24 skipped, 0 failed (18720 total).
The 1.108.400 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.400): a text search returns the code it found, and a retired tool name says what replaced it (#1237, #1236):
search_textattachessourceswhen matches sit in at mostSOURCE_MAX_SYMBOLSsymbols withinSOURCE_MAX_LINES/SOURCE_MAX_BYTES, disclosingsources_omitted_reason; encodingst5.counter.RETIRED_TOOL_NAMESmapsget_symbol/get_symbolstoget_symbol_sourcefor the unknown-tool error andorder. Full text:CHANGELOG.md.
The Tests: line's 1.108.400 count, verbatim:
- ⚠ Prior (1.108.400): 18715 passed, 24 skipped, 0 failed (18739 total).
The 1.108.401 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.401): a redirected Grep can carry the index's answer, and a substring search opens only the files that can match (#1240):
cli/hooks/grep_answer.answer_enabledis an allowlist (1/true/yes/on) onJCODEMUNCH_HOOK_GREP_ANSWER, default OFF; once per session and Grep arguments, never over a stale index or a cut page.storage/text_filter.pykeeps a trigram bitmap per cached file in<repo>.textfilter, a sidecar so a repaired row never moves the.dbmtime that keys the index cache; a bitmap may say maybe, never no. Full text:CHANGELOG.md.
The Tests: line's 1.108.401 count, verbatim:
- ⚠ Prior (1.108.401): 18773 passed, 24 skipped, 0 failed (18797 total).
The 1.108.402 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.402): the server instructions name the tools in the first 512 characters, and tool names hold across 1.x (#1248, #1247, #1246): ToolSearch follows the tool bullets;
tests/fixtures/tool_names_1x.jsonfreezes the names, and a rename keeps an alias forcounter.ALIAS_MIN_DAYS; a modified folder no longer re-walks a watched repo (L-256);license_noticeshows the license terms once. Full text:CHANGELOG.md.
The Tests: line's 1.108.402 count, verbatim:
- ⚠ Prior (1.108.402): 18839 passed, 24 skipped, 0 failed (18863 total).
The 1.108.403 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.403): a file pattern means the same thing in every tool, and the rate_limit config key turns the limit on (#1255, #1254, #1252, #1251):
path_glob.pyis the one file_pattern rule for search_text, the index store and the scope checks;rate_limitreads through the config loader, so the limitjcodemunch-mcp configshows is the one enforced; Codex's Windows entry is the bare command name when PATH finds the same install. Full text:CHANGELOG.md.
The Tests: line's 1.108.403 count, verbatim:
- ⚠ Prior (1.108.403): 18949 passed, 24 skipped, 0 failed (18973 total).
The 1.108.404 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.404): a failed summarizer keeps the summaries it had, and tool descriptions open with the agent's question (#1290, #1256, #1257):
summarize_repo(force=true)keeps the stored summary of each symbol the provider failed on, and reportsprovider_fallbacks/previous_summaries_kept(L-274: an unbuildable client still resets); 17 descriptions open with the agent's question. The colon base-list fix shipped as .405 (the 2-hour release gate blocked it). Full text:CHANGELOG.md.
The Tests: line's 1.108.404 count, verbatim:
- ⚠ Prior (1.108.404): 19007 passed, 24 skipped, 0 failed (19031 total).
The 1.108.405 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.405):
get_class_hierarchyandfind_implementationsread a base list written after a colon (#1287, fixes #1284): the colon list is read for C#, Kotlin, Swift, C++ and Rust, plus Ruby<and Scala/Dartwith; an enum's underlying type is never a base; signatures already stored are read, so no re-index. Full text:CHANGELOG.md.
The 1.108.406 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.406): watch-all leaves temp and idle indexes unwatched, and agents sharing an index stop reloading it (bundle of 27 PRs, #1258-#1292): watch-all skips temp and idle indexes and
watchexits when it cannot watch; a reader no longer invalidates other processes' cache;init --git-hookre-indexes a branch switch; a key holder never sees a sales link. Full text:CHANGELOG.md.
The Tests: line's 1.108.406 count, verbatim:
- ⚠ Prior (1.108.406): 19550 passed, 24 skipped, 0 failed (19574 total).
The 1.108.407 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.407): a stdio server exits when the process that started it is gone, and storage reports how much disk the index uses (#1297-#1301):
serveon stdio watches its parent (PID plus start time, past Windows launchers) and exits when it is gone,JCODEMUNCH_PARENT_WATCH_SECONDSsets the interval and 0 turns it off;jcodemunch-mcp storagereports the store by index without opening a database; a query that names a tool ranks that tool first. Full text:CHANGELOG.md.
The Tests: line's 1.108.407 count, verbatim:
- ⚠ Prior (1.108.407): 19613 passed, 24 skipped, 0 failed (19637 total).
The 1.108.408 entry, verbatim as it stood in CLAUDE.md:
- Prior (1.108.408): a rare work-license reminder, never shown to a license holder (#1303): a machine is told work use needs a license 7 days after its first index is seen, then at most every 30 days, on stderr after a human-read command or in
license_notice; never with a key, on CI, afterconfig set license_reminder falseor withJCODEMUNCH_LICENSE_REMINDER=0; the console script enters throughserver.cli. Full text:CHANGELOG.md.
The Tests: line's 1.108.408 count, verbatim:
- ⚠ Prior (1.108.408): 19660 passed, 24 skipped, 0 failed (19684 total).