Conversation
Four tools under scripts/ answered three good questions and all three are downstream of one nobody asked. This repository's own worst trap is the reason: .claude/settings.json registers enabledPlugins, so the hooks firing in a contributor's session are served from the plugin cache while JIT_BASE resolves against $CLAUDE_PROJECT_DIR. The entries are the checkout and the code reading them is not, nothing errors, and every observable signal reports health. jit-doctor.sh names the tree it is judging first, then answers which copy of the hooks would run in four states — the plugin cache, a hooks block in settings, BOTH, and `cannot tell`. The fourth is a first-class answer: the scan is textual because this plugin has no jq, and Claude Code merges settings from locations no script in a repository can enumerate. Run against this repo it reports the cache, four installed plugin versions, and that which one loads is not decidable from where it stands. Per layer it reports entries, whether the matcher loads that layer at all — measured through the same jit_scan_layers() the hooks call — whether the index is there, and whether an entry beside it is newer. It reports whether hooks.log exists and when it was last written, because "never ran" and "ran and matched nothing" have been indistinguishable until now. Exit 1 is reserved for the one thing it establishes exactly: a layer holding entries and no index, whose rules are inert. The mtime comparison is advisory rather than a defect, deliberately — a fresh clone writes 00-index.tsv before the .md files beside it, so mtime alone accuses a tree that is current. jit-dry-run.sh compares frontmatter against the row and keeps that verdict; doctor points at it and reimplements neither it nor the pattern lint, so a doctor exit code can never mean two different things. Every heuristic is ADVISORY and none moves the exit code (#47 has CI consuming these). Both thresholds default in the script and print where they came from, which is what makes JIT_CONTEXT_DOCTOR_MAX_BYTE — singular — visible: config.env accepts any JIT_CONTEXT_* key, so a typo reads as a setting that applied. #183 also asks for entries never fired "for N days" and only the first half is computable: hooks.log records carry a time and no date, and an entry mtime is rewritten by every clone. The advisory says "no record in the log" and builds no threshold on a number with nothing under it. jit_report_keyword() moves from rebuild-tsv.sh to common.sh. jit-doctor.sh was the second bash caller, which is where a second copy stops being a duplicate and starts being drift — the way jit_report_name()'s copy moved in #131. The awk half stays put; awk cannot source a bash file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…scan, a silent empty dimension (#183) Three findings from the self-review, all of them the same defect class this script exists to report, reproduced inside it. 1. The fire-count key was `layer:file.md` for all three dimensions. The tools half of pre-tool-hook.sh writes the literal `tool:file.md` and never the layer name (pre-tool-hook.sh:535, 556, 577, 595), so every tools entry came back with a count of zero and was reported `never fired` however often it had actually matched. The fixture had no tools/ layer at all, so nothing saw it. 2. The enabledPlugins scan matched the KEY and ignored the VALUE, so `"claude-jit-context@dpt-plugins": false` — a plugin explicitly turned off — printed `the plugin cache serves the hooks`. That is the confident wrong answer the section's own header says is worse than none, in the one check the whole tool exists for. It now requires the value `true` and requires the file to carry an enabledPlugins block, which also scopes it away from the plugin name appearing inside a hook command path. The residual is stated in the comment rather than papered over: everything it cannot establish still falls through to `cannot tell`. 3. A dimension directory that existed with no layer under it printed nothing at all — the glob stays literal without nullglob and the `[ -d ]` drops it — so the dimension simply did not appear, which is what a scan that died halfway also looks like. The missing-dimension case already had its own line; this one now does too. Each is pinned by a test that goes red when the fix is reverted, verified by mutation. mk_tree() now builds a tools/ layer, so that whole branch is covered. CLAUDE.md was stale in two places the diff should have touched: the trigger table still named four tooling scripts, and the layout table had no row for the fifth. The dogfood-trap paragraph now points at the tool written for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt
pushed a commit
that referenced
this pull request
Aug 18, 2026
…ching (#191) A `<file>.sh:NNN` pointer is exact the day it is written and wrong on the next PR that inserts a line above it. It rots silently: a rotted citation reads exactly like a live one, so the reader follows it and lands on a plausible comment rather than on an error. Counted on main at 98386f1, outside the assembled changelog: nine citations of that shape. Six pointed at the wrong thing, one had drifted off the block it named, two were still right. #191 names three of the six; the sweep added here found the other three. #185 added one of them three hours before #190 moved it. Every citation in an enforced file now points at something greppable -- a function name, a distinctive literal, or the issue number, which additionally says why rather than where. tests/test-line-citations.sh has three outcomes rather than two. It FAILS on a citation in a tracked scripts or tests shell file; it REPORTS WITHOUT FAILING over every other tracked file; and it names the assembled changelog as deliberately not swept, because that file is generated and a finding in it is unactionable. The advisory half is advisory rather than enforced for one stated reason: its only remaining finding lives in an entry held by PR #192, and reddening a file this change may not edit is how a check gets disabled in its first week. Widening it is one awk pattern. The false-positive surface was costed before the check was written: over all 96 tracked basenames and the whole tree, 10 hits and 0 false. A bash diagnostic, a shellcheck line, an awk error and a longer basename are driven as controls that must not match, alongside a planted citation that must. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt
pushed a commit
that referenced
this pull request
Aug 18, 2026
- CLAUDE.md read as attributing the move to #191 itself, because the issue citation sat immediately after "the PR that moved it". #185 added it and #190 moved it three hours later; the sentence now says so. - scripts/jit-dry-run.sh: "which #7 is the reason for" -> "which exists because of #7". Accurate either way, clunky one way. A third finding was argued down rather than applied. The reviewer recounted the audit as 3 right / 1 drifted / 5 wrong and read the tooling.md citation as correct, on the strength of that entry own sentence "The truncation is pre-tool-hook.sh:127-144 and it is correct" -- where "it" is the truncation BEHAVIOUR being deliberate, not the line range being accurate. The same range in scripts/jit-dry-run.sh was judged wrong in the same review, which is the contradiction: at 98386f1 those lines are the #182 subagent_type comment, and the truncation is at 162-185. The count stands at 6 wrong / 1 drifted / 2 right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fdaviddpt
added a commit
that referenced
this pull request
Aug 18, 2026
…ses new ones (#191) (#197) * Cross-file line-number citations rotted silently, and nothing was watching (#191) A `<file>.sh:NNN` pointer is exact the day it is written and wrong on the next PR that inserts a line above it. It rots silently: a rotted citation reads exactly like a live one, so the reader follows it and lands on a plausible comment rather than on an error. Counted on main at 98386f1, outside the assembled changelog: nine citations of that shape. Six pointed at the wrong thing, one had drifted off the block it named, two were still right. #191 names three of the six; the sweep added here found the other three. #185 added one of them three hours before #190 moved it. Every citation in an enforced file now points at something greppable -- a function name, a distinctive literal, or the issue number, which additionally says why rather than where. tests/test-line-citations.sh has three outcomes rather than two. It FAILS on a citation in a tracked scripts or tests shell file; it REPORTS WITHOUT FAILING over every other tracked file; and it names the assembled changelog as deliberately not swept, because that file is generated and a finding in it is unactionable. The advisory half is advisory rather than enforced for one stated reason: its only remaining finding lives in an entry held by PR #192, and reddening a file this change may not edit is how a check gets disabled in its first week. Widening it is one awk pattern. The false-positive surface was costed before the check was written: over all 96 tracked basenames and the whole tree, 10 hits and 0 false. A bash diagnostic, a shellcheck line, an awk error and a longer basename are driven as controls that must not match, alongside a planted citation that must. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address the audit: an advisory sweep that could report coverage it never had (#191) Four findings from the self-review, all in tests/test-line-citations.sh unless noted: - The advisory sweep had no floor guard on its file count, so a broken selector would print `Clean across 0 file(s)` and exit 0 -- byte-identical, to run-all.sh which reads only the exit code, to a genuinely clean tree. It now takes the same `< 10 -> exit 2` floor the enforced sweep already had. - The enforced selector was `[^/]*`, so a shell file landing in a subdirectory one day would fall silently into the report-only bucket and stay there. Now `.*`, which closes the shape rather than half of it. - A tracked path that is not a readable file was skipped in silence in both loops, while the header above claimed a count the sweep never reached. Both are counted now; an unreadable path in the enforced set fails the suite. - The needle accepts an optional directory prefix, and no enforced file exercises that branch -- every real citation was bare-basename -- so a dead prefix branch would have read as clean. It has its own positive control now. scripts/jit-dry-run.sh named `jit_log_name()` and `jit_row_id()` as being "in pre-tool-hook.sh". They are defined in common.sh and only called there, which is an anchor wrong in the same way the line numbers were. Reworded to what is true: pre-tool-hook.sh routes every refused row through them. The 96-basename and 10-hit figures are now pinned to 98386f1 in all three places that carry them, rather than stated in the present tense. They were already stale by two the moment this branch added a file, which is the exact failure mode the change is about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Two prose findings from the re-review (#191) - CLAUDE.md read as attributing the move to #191 itself, because the issue citation sat immediately after "the PR that moved it". #185 added it and #190 moved it three hours later; the sentence now says so. - scripts/jit-dry-run.sh: "which #7 is the reason for" -> "which exists because of #7". Accurate either way, clunky one way. A third finding was argued down rather than applied. The reviewer recounted the audit as 3 right / 1 drifted / 5 wrong and read the tooling.md citation as correct, on the strength of that entry own sentence "The truncation is pre-tool-hook.sh:127-144 and it is correct" -- where "it" is the truncation BEHAVIOUR being deliberate, not the line range being accurate. The same range in scripts/jit-dry-run.sh was judged wrong in the same review, which is the contradiction: at 98386f1 those lines are the #182 subagent_type comment, and the truncation is at 162-185. The count stands at 6 wrong / 1 drifted / 2 right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * README said "Two suites are the exception" and this change made it three (#191) The test section enumerates the suites that are about this repository rather than the hooks, and the enumeration was a count. Adding tests/test-line-citations.sh made the sentence wrong in the quietest possible way -- a reader counting two and finding three has no reason to think anything is missing. Named rather than counted where it can be: the third one is called out by name and by what it refuses, and the "assembler they are about" clause now says which two it means. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Rebase onto 5b46095: two citations #194 wrote while this branch was open (#191) CI caught these, and the check that caught them is the one this branch adds. Neither is a defect in this diff -- they are in tests/test-session-markers.sh, section J, which #194 (issue #177) added to main after this branch was cut. - `session-start-hook.sh:32 parses the payload with jit_json_fields + jit_session_key` -- the line number was ALREADY WRONG on the day it was written: line 32 is mid-sentence in the LC_ALL paragraph, and the parse claim is three lines earlier. The comment names its own functions, so the number was carrying nothing the prose did not already carry. Dropped. - `the comment at session-start-hook.sh:25-29` -- accurate today, and a pointer to a comment rather than to code, which is the shape most likely to move. This one wanted more than a rename: it is a claim about TWO hooks agreeing, so it now names the pair of functions the claim is about and says which half of the claim this leg proves. The counts pinned to 98386f1 are re-pinned to 5b46095 in all three places that carry them, and the second measurement is kept beside the first rather than replacing it. Nine citations at 98386f1, eleven at 5b46095; 96 basenames / 10 hits / 0 false, then 98 / 12 / 0. Pinning was already the right call and the rebase is what proved it: the pin went stale in under a day and SAID SO, where a present-tense number would have read true and been wrong. That interval is now the argument the suite header leads with. The citations this check first went red on in CI were not the ones it was written for, they were written by somebody else while it was in review, and one of the two was born broken. A cleanup would not have caught either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The advisory half said it was blocked on #192, which has landed (#191) A reason that has been spent reads exactly like a reason that still holds -- which is the defect class this whole branch is about, in the file that exists to refuse it. The suite printed "advisory only while #192 holds one of the files it would flag" on every run, and #192 merged at eb9cf71. Replaced with what is true: widening is a scope decision nobody has taken, not a blocked one. Deliberately NOT widened here. It is unrequested scope on a pull request already in review, and the question it settles -- whether every future doc, template, example and jit-context entry is bound by this rule, in every installed project's contributor path -- is a design decision this branch was not briefed to make. Filed for the maintainer with the exact cost instead: two awk alternations in the selectors, one line in the entry currently flagged, and zero measured false positives on that surface across two commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Florian DAVID <fdavid@digital-village.fr> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #183.
The gap it fills
Four tools under
scripts/answered three good questions — can this pattern be honoured, what does a match cost, what does the agent keep failing to find — and all three are downstream of one nobody asked: is any of this running at all, and against which tree?This repository's own worst trap is the reason.
.claude/settings.jsonregistersenabledPlugins, so the hooks firing in a contributor's session are served from the plugin cache whileJIT_BASEresolves against$CLAUDE_PROJECT_DIR. The entries are the checkout and the code reading them is not. Nothing errors.Run against this repository, doctor answers it and then some:
The four-versions line was not in the issue, and nothing else in the repository could have produced it.
The two judgment calls the issue left open
1. Doctor POINTS AT
jit-dry-run.shrather than calling it. The issue says no second linter and does not pick between the two designs. Calling it would fold two verdicts into one status: a doctor1would then mean either your rules are inert or one pattern is unhonourable, which is this repository's own collapsed-outcome defect arriving through the exit code. It would also make doctor inherit that tool's per-row untrusted-text surface and its runtime, to answer a question about whether anything runs at all. The subjects are different — is it live against are the rules any good — soREFUSEDandSTALEstay there, doctor prints neither word, and thenextsection hands the reader over with the tree already named.2. Which copy of the hooks runs is a TEXTUAL scan that says so, and
cannot tellis a first-class verdict. There is nojqin this plugin, and Claude Code merges settings from locations no script in a repository can enumerate. Four states: the plugin cache, a hooks block in settings,BOTH, andcannot tell. The cache side requires anenabledPluginsblock and the valuetrue, which also scopes it away from the plugin name appearing inside a hook command path. The residual is stated in the comment rather than papered over.3. Log freshness is whole days from
perl -M, notdate -d, which is GNU-only and on neither the macOS nor the Git Bash leg. perl is already a runtime dependency of every hook here. Coarse and right on three platforms beats precise and wrong on two.Where I disagreed with the issue, and built the weaker thing on purpose
The mtime staleness check is ADVISORY, not a defect. The issue calls it the inert-rule trap and it is — but mtime is a proxy, and a fresh clone writes
00-index.tsvbefore the.mdfiles beside it, so mtime alone accuses a tree that is perfectly current.jit-dry-run.shcompares the frontmatter against the row and owns that verdict. Exit1is reserved for the one thing doctor establishes exactly: a layer holding entries and no index, whose every rule is inert.Entries that have never fired "despite existing for N days" is not computable from what this repository keeps, so only the first half is claimed.
hooks.logrecords carry a time and no date, and an entry's mtime is rewritten by every clone and every checkout. Either input would give a threshold a confident number with nothing under it. The advisory saysno record in the logand stops there.The heuristics, costed on this repo's own tree rather than assumed
The brief flagged this as the part most likely to be worth less than it costs. Measured:
01-oss, not firing in a non-oss sessionA 38% flag rate reads as noise until the fire count is beside it, which is exactly what the issue asked for:
Only
release.md(4,153 bytes, 57 over the threshold, 3 fires) is arguable. I would not raise the default on this evidence — raising it to 8192 silencesentries.mdandtests.md, which are ~229 KB between them in one session.Thresholds and the typo case
JIT_CONTEXT_DOCTOR_MAX_BYTES(4096) andJIT_CONTEXT_DOCTOR_MIN_KEYWORD(3), both defaulting in the script so it works against a tree with noconfig.env. Doctor prints each effective value and where it came from, which is the whole point:config.envaccepts anyJIT_CONTEXT_*key without refusing it, soJIT_CONTEXT_DOCTOR_MAX_BYTE— singular — parses clean, is read by nothing, and would otherwise be indistinguishable from a setting that applied. It is named as unread beside a threshold still saying(default). A non-numeric value is refused, named, and the default stands.Every heuristic is ADVISORY and none moves the exit code (#47 has CI consuming these).
Rider, called out rather than slipped in
jit_report_keyword()andJIT_KEYWORD_WITHHELDmove fromrebuild-tsv.shtocommon.sh.jit-doctor.shprints keywords and is the second bash caller — the point at which a second copy stops being a duplicate and starts being drift, a term printed by one tool and withheld by the other. This is the movejit_report_name()'s copy made in #131, for the reason stated there. The awk half stays put; awk cannot source a bash file.Self-review found three real bugs, all fixed and pinned
Second commit. Each was mutation-verified: revert the fix, watch the named test go red.
tools/. The tools half ofpre-tool-hook.shwrites the literaltool:file.mdand never the layer name (lines 535, 556, 577, 595), so every tools entry came back with a count of zero and was reportednever firedhowever often it had matched — this tool's own defect class, inside the section written to end it. The fixture had notools/layer, so nothing saw it;mk_tree()now builds one.enabledPluginsscan matched the key and ignored the value, so a plugin explicitly turned off printedthe plugin cache serves the hooks— the confident wrong answer the section's own header says is worse than none.nullglob— so it was indistinguishable from a scan that died. The missing-dimension case already had its own line; this one now does too.Tests
tests/test-jit-doctor.sh, 85 assertions, written before the script existed and watched fail. Almost every assertion is paired: a fixture where doctor must reach a verdict, and one where it must decline. A doctor that always saidcannot tellsatisfies half of them; one that always saidthe plugin cachesatisfies the other half. Only the pairs bind.One assertion is pinned by mutation: the keyword length is measured under
local LC_ALL=C, because${#s}counts characters under a UTF-8 locale and bytes under C. Unpinned, a two-character three-byte keyword is flagged as short and is not. That section skips loudly, naming what went untested, where no UTF-8 locale exists.Three existing suites picked the new work up with no registration:
run-all.shglobstest-*.sh,test-assertion-helpers.shdrives the new suite's two declared helpers against its 1 MB SIGPIPE payload, andtest-dogfood-entries.shenumeratesgit ls-files -- scriptsand would have gone red hadtooling.md's alternation not been widened.bash tests/run-all.sh— 33 suites, all passed, no SKIPPED suites.shellcheck -S warning scripts/*.sh tests/*.shexits 0. Both observed on macOS only; Linux and Windows/Git Bash are reasoned from the workflow matrix, not run.Docs
README.mdgains an Is any of this running at all? section and the twoconfig.envkeys.CLAUDE.mdgains a layout row, a fifth script in the trigger table, and a pointer from the dogfood-trap paragraph to the tool now written for it..claude/jit-context/paths/00-manual/tooling.mdgains the fifth script in itsmatch, a row in the exit-code table, and its suite line — index rebuilt and committed alongside. Changelog fragment atchangelog.d/183.added.md.Verified by the maintainer
Red re-run, mine, against
mainat 3553f5f with the script absent:tests/test-jit-doctor.shexits 1 withexit 127 ... scripts/jit-doctor.sh: No such file or directory. That is the honest strength of this particular red and it is worth naming: for a brand-new script the suite can only prove the file is missing, not that any assertion is non-vacuous. What carries the weight instead is the paired structure the author describes — a doctor that always answeredcannot tellsatisfies half the assertions and a doctor that always answeredthe plugin cachesatisfies the other half — plus the three self-review bugs, each mutation-verified by reverting the fix and watching a named test go red.The rider is accepted. Moving
jit_report_keyword()andJIT_KEYWORD_WITHHELDintocommon.shat the second bash caller is the same movejit_report_name()made in #131, and the alternative — a second copy — is a term printed by one tool and withheld by the other, which is the drift this repository's own comments name as its defect class. It is called out in the body rather than slipped in, which is what makes it acceptable in this diff.On the heuristics question I put to the author: the answer came back with a measurement rather than a preference, and I accept it. A 38% flag rate on fat entries reads as noise until the fire count is beside it —
tooling.mdat 26.5 KB × 43 fires is roughly 1.14 MB injected in one session. The 4096 default stays.Two things this PR surfaced are filed rather than carried: #188 —
tests/test-arg-flag-values.shdrives its scripts from a hand-written list, so its header's promise is true of flags and false of scripts, and a new tool is untested there with every printed number still going up. #189 — the four installed plugin versions this diff's own output revealed, and the fact that which one loads is not decidable from a shell.