diff --git a/.claude/jit-context/paths/00-manual/00-README.md b/.claude/jit-context/paths/00-manual/00-README.md index 3a4521e0..ac2b74fb 100644 --- a/.claude/jit-context/paths/00-manual/00-README.md +++ b/.claude/jit-context/paths/00-manual/00-README.md @@ -32,3 +32,53 @@ ever indexed or injected. fragment itself says "not chased further" and "worth a look if a second issue shows the same shape." One incident, no known trigger pattern in this repo's own paths/tools/vocabulary to match on. Revisit if a second issue repeats the same `could-not-classify` shape. + +- **jit-misses-generic-words-v-escape** (2026-10-06, `trap.d/424.jit-misses-generic-words-v-escape.md`). + Checked against HEAD (bda0282): already fixed. `scripts/jit-misses.sh` now compares the + generic-word list by exact `FILENAME` membership in `isgenericfile[]`, built from + `ENVIRON["GENERIC_ARG_FILES_ENV"]`, not from a `-v`-decoded path -- the same ENVIRON technique + #437 used for `JIT_BASE`/`tools_base`/`vocab_base`, landed in `e0f8d39` (#437/#438). The + remaining `-v logfile=`/`-v genname=` sites are cosmetic display text only, exactly as the + fragment itself flagged. + +- **rebuild-tsv-v-escape** (2026-10-06, `trap.d/424.rebuild-tsv-v-escape.md`). Two defects, one + outcome each. The `GENERIC_WORDS_FILE`/`wf` site: already fixed, same commit (`e0f8d39`, + #437/#438) -- `scripts/rebuild-tsv.sh` now splits `ENVIRON["GENERIC_WORDS_FILES"]` rather than + reading a `-v`-decoded `wf`. The `layerdir`/`bytesof()` site (the vocabulary byte-count + report): still true -- `layerdir` still reaches `bytesof()` via `-v layerdir=...` at HEAD. + Filed Digital-Process-Tools/claude-jit-context#475. + +- **wrong-worktree-edits** (2026-10-06, `trap.d/437.wrong-worktree-edits.md`). A lesson about + forgetting a per-call `cd &&` prefix, not a code defect. Declined here rather than + promoted: the mitigation the fragment itself names -- the supertool write op's own + `[branch: ...]` receipt line -- already surfaces exactly this after the fact, on every write, + for free. A jit-context reminder would have to fire on every `edit`/`paste`/`batch` call in + the whole repository (no ERE can tell "has a same-call `cd` prefix" from "does not" without + lookbehind) to catch the rare miss, which is a worse trade than reading the receipt that is + already there. See the parallel `tools/00-manual/00-README.md` entry for the tools-dimension + side of this same decision. + +- **awk-pipe-failure-masked-by-or-true** (2026-10-06, `trap.d/459.awk-pipe-failure-masked-by-or-true.md`). + Checked against HEAD (bda0282): still true. `tests/test-no-heredocs-459.sh`'s + `detect_heredoc_lines()` still pipes `awk ... | grep -nE -- "$HEREDOC_PATTERN" || true`, so an + `awk` crash and a genuinely clean file still render identically. Non-blocking, narrow, one + test's own shape. Filed Digital-Process-Tools/claude-jit-context#478. + +- **release-check-crash-reads-as-pass** (2026-10-06, `trap.d/461.release-check-crash-reads-as-pass.md`). + Checked against HEAD (bda0282): still true in both `tests/test-release-branch-461.sh` and + `tests/test-release-branch-437.sh` -- a `check_release_tree.py` crash with no `FAIL ` line + still reads as a pass in both. CI is covered separately; this is a local-run-only gap. Filed + Digital-Process-Tools/claude-jit-context#476. + +- **release-equivalence-builds-head-not-source-ref** (2026-10-06, + `trap.d/461.release-equivalence-builds-head-not-source-ref.md`). Checked against HEAD + (bda0282): still true. `tests/test-release-branch-461.sh` still builds `--ref HEAD` while + `.github/workflows/release-branch.yml` publishes `$SOURCE_REF`, so a `workflow_dispatch` run + against an older ref still checks the wrong tree's equivalence. Filed + Digital-Process-Tools/claude-jit-context#477. + +- **privacy-page-claims-vs-code** (2026-10-06, `trap.d/466.privacy-page-claims-vs-code.md`). + Checked against HEAD (bda0282): already fixed. All four claims (F1 opt-in scope, F2 the + 120-byte field, F3 `bytes-shown-*` aging, F4 `jit_worktree_mismatch_line`'s own read) are + stated correctly in `PRIVACY.md` as it reads now, landed in `1db1603` (#466/#468) and + `7046d0a` (#466/#470). diff --git a/.claude/jit-context/paths/00-manual/hooks.md b/.claude/jit-context/paths/00-manual/hooks.md index a53f2aae..f248f497 100644 --- a/.claude/jit-context/paths/00-manual/hooks.md +++ b/.claude/jit-context/paths/00-manual/hooks.md @@ -38,6 +38,10 @@ Hooks read `.claude/jit-context///00-index.tsv` — never the One awk trap, measured on both `awk version 20200816` and `GNU Awk 5.4.1` because the two disagree: **`split()` on a one-character separator also splits on newlines** — one-true-awk does, gawk does not. Any walk over alternate fields (the single-quote split that lifts paths out of `supertool` arguments) then reads its arguments from the wrong side of the separator on one platform and not the other. Bracket the separator — `"[\047]"` — and awk compiles a regex, which splits on that character alone everywhere. +**A dynamic regex needs a doubled escape byte to put one literal backslash inside a bracket expression; a `/literal/` regex does not.** Building a bracket class at runtime (`qclass = "[" q1 q2 q3 "]?"` with `q3` built from one escape byte) and handing it to `match()`/`gsub()` as a STRING treats that lone escape byte as an introducer for whatever follows it — here, the class's own closing bracket — so the bracket never closes and the whole ERE fails to compile: `nonterminated character class` and exit 2, refusing every call for the rest of the session (one `common-awk.sh` extension cost 205 of 343 tests this way, #432). Build `q3` from two escape bytes instead of one; the escape-consuming pass then collapses them back to a single literal one inside the class, confirmed on one-true-awk under `LC_ALL=C` — not yet checked against gawk with the same construct. + +**`in` is an awk reserved word, and a parameter or array named `in` compiles silently on both engines — no syntax error, just garbage downstream.** `function f(lines, n, in, i)` shadows the keyword `for (key in array)` uses, and neither gawk nor one-true-awk complains at parse time; every caller of that function then breaks with no error pointing at the cause (#442: 374/374 passing tests went to 163/374 on this alone, and a rename with no other change restored all of them). Treat `in` — and anything else that reads as a legal identifier but is actually a keyword or builtin, such as `getline`, `print`, `printf` or `length` — as reserved in a parameter or local-array list, not only at statement position. + **Never match a regex against a single character.** `c ~ /[A-Z]/` inside a `for` over `substr(s, i, 1)` is a multibyte decode, and one character of a UTF-8 string is one *byte* to one-true-awk: a lone continuation byte raises `towc: multibyte conversion failure`, which aborts the whole `END` block — the hook prints nothing and still exits 0, so it reads as having had nothing to say. Measured: `détail de la facturation` matched no vocabulary entry under `awk version 20200816` and matched under gawk. Use `index("ABC…XYZ", c) > 0` instead; it is a byte search with no decode, and no byte of a multibyte sequence is ever an ASCII letter or digit, so the verdict is unchanged. Mind that `index(s, "")` returns 1 — an empty operand needs its own guard. **The hook awks run under `LC_ALL=C`, and that is load-bearing.** The rule above is about one character; this is its whole-record neighbour. A `tolower()` or `gsub()` over a record that is not valid UTF-8 aborts one-true-awk's `END` block with `illegal byte sequence` — nothing on stdout, not even `{}`, the diagnostic in the session, exit 0 — and makes gawk print `Invalid multibyte data detected` (#68). A lone `0xE9` from a Latin-1 paste, or a sequence cut at a copy boundary, is enough; on the tool hook it defeated a `block` rule. Under `C` both engines read bytes and have nothing to decode. The cost is that `tolower()` no longer folds non-ASCII, which is why the Latin-1 table in `JIT_AWK_FOLD` carries **both** cases: a fold table that only lists lowercase forms is dead under `C`. Check any new normalisation against that table rather than assuming, and keep the pin on the `awk` invocation — `rebuild-tsv.sh` has the opposite contract and its own locale. diff --git a/.claude/jit-context/tools/00-manual/00-README.md b/.claude/jit-context/tools/00-manual/00-README.md index 97e3e5ef..6ea53185 100644 --- a/.claude/jit-context/tools/00-manual/00-README.md +++ b/.claude/jit-context/tools/00-manual/00-README.md @@ -16,3 +16,15 @@ so nothing here is ever indexed or injected. upstream as `Digital-Process-Tools/claude-oss#1042`. Until it lands, follow every `--apply` with `CLAUDE_PROJECT_DIR="$PWD" bash scripts/rebuild-tsv.sh` and commit both files. + +- **wrong-worktree-edits** (2026-10-06, `trap.d/437.wrong-worktree-edits.md`). A `supertool + edit`/`paste`/`batch` call sent without its own `cd &&` prefix landed in the main + clone instead of the worktree a task was meant to edit -- caught only by a later fixture run + behaving as if the old code were still there. Declined rather than promoted: there is no ERE + that can distinguish "this call already has a same-call `cd` prefix" from "it does not" + without lookbehind, which awk's dialect does not have (per `paths/00-manual/entries.md`), so + the only matchable rule here would fire on every `edit`/`paste`/`batch` call in the whole + repository, correctly-prefixed ones included -- noise on every legitimate call to catch a rare + miss. The op's own `[branch: ...]` receipt line already names which branch a write landed on, + checkable after the fact on exactly the calls that need it. See the parallel + `paths/00-manual/00-README.md` entry for the same decision. diff --git a/trap.d/424.jit-misses-generic-words-v-escape.md b/trap.d/424.jit-misses-generic-words-v-escape.md deleted file mode 100644 index 5983b51a..00000000 --- a/trap.d/424.jit-misses-generic-words-v-escape.md +++ /dev/null @@ -1,30 +0,0 @@ -#424 fixed the -v backslash-escape defect (a CLAUDE_PROJECT_DIR-derived path passed to -awk via -v, whose escapes awk's -v silently decodes) in scripts/pre-tool-hook.sh's -tools_base/vocab_base. Explore's self-review pass over that PR found the identical -defect, still live, in scripts/jit-misses.sh: the generic-words-file path is passed to -awk BOTH as a real positional command-line argument (unescaped, seen verbatim as -FILENAME) and via `-v genfile=`/`-v genname=` (escape-processed). The only way the awk -program recognizes "this is the generic-word list, not the log" is -`genfile != "" && FILENAME == genfile` -- so a backslash-bearing path (a checkout, or a ---generic-words override, under a Windows-shaped directory) mangles `genfile` while -the real `FILENAME` stays unmangled, the comparison never matches, and the -generic-word list's lines get read as if they were hooks.log records instead of -populating `generic[]`. Generic-word filtering silently degrades to "off" with no -error, indistinguishable from an unconfigured list. `-v logfile="$LOG"` carries the -same defect but only for display text (the printed "log: ..." line), cosmetic only. - -Fix shape, when someone picks this up: route genfile/genname/logfile through ENVIRON -the same way #424 routed JIT_BASE, tools_base and vocab_base -- LOG/GENERIC_WORDS_FILE -are already ordinary bash variables built from $CLAUDE_PROJECT_DIR, so `export` them -(or read `ENVIRON["..."]` directly if they are already inherited unassigned) rather -than continuing to build a second, `-v`-decoded copy for comparison purposes. - -Not fixed as part of #424 itself: different file, different subsystem (a maintainer -tool jit-misses.sh reads and reports on, not a hook that decides a rule), and pinning -it needs its own fixture (a log padded so jit-misses.sh's generic-word pass actually -runs, plus a positional-vs--v divergence case) rather than reusing #424's own test -shape. - -Sources: Explore's self-review of PR fixing #423/#424, scripts/jit-misses.sh lines -around 216, 345, 594, 596 (grepped, not cited by number -- see this repo's own -line-citation rule). diff --git a/trap.d/424.rebuild-tsv-v-escape.md b/trap.d/424.rebuild-tsv-v-escape.md deleted file mode 100644 index 67ea104b..00000000 --- a/trap.d/424.rebuild-tsv-v-escape.md +++ /dev/null @@ -1,31 +0,0 @@ -Same defect class as #424 (a CLAUDE_PROJECT_DIR-derived path passed to awk via -v, -silently escape-decoded), found by Explore's self-review of that PR in -scripts/rebuild-tsv.sh, in two more places, neither touched by #424: - -1. The ambiguous-vocabulary-keyword collision report builds `layerdir` (a glob under - `VOCAB_BASE="$JIT_BASE/vocabulary"`, itself CLAUDE_PROJECT_DIR-relative) and hands - it to awk via `-v layerdir=...`. The awk function `bytesof()` then does - `getline line < (layerdir "/" $2)` using the -v-decoded (potentially mangled) path. - On a backslash-bearing CLAUDE_PROJECT_DIR, every open fails and every entry in that - layer is reported as "unmeasured" instead of its real byte count -- a reporting-only - break in a tool a maintainer runs by hand, not a hook that runs in a stranger's - session. - -2. The generic-word classifier pass builds `-v wf="$GENERIC_WORDS_FILE"` (also - CLAUDE_PROJECT_DIR-relative or override-derived) and reads it with - `getline w < wf` inside the awk BEGIN block. A mangled `wf` leaves the `seen[]` - lookup table empty, so every keyword silently classifies as non-generic -- - this still returns one verdict per keyword (so it passes rebuild-tsv.sh's own - count-mismatch guard undetected), just the wrong verdict for all of them. - -Fix shape: same as jit-misses.sh's own fragment (#424.jit-misses-generic-words-v-escape.md) --- route layerdir/wf through ENVIRON, matching the technique #424 already used for -JIT_BASE/tools_base/vocab_base. - -Not fixed as part of #424 itself: a third file, a maintainer-only tool (not a -per-session hook), and each needs its own fixture to pin (a real byte-count -divergence for #1, a keyword-classification divergence for #2). - -Source: Explore's self-review of PR fixing #423/#424, scripts/rebuild-tsv.sh (grepped -around VOCAB_BASE/layerdir/bytesof and GENERIC_WORDS_FILE/wf -- not cited by line -number, see this repo's own line-citation rule). diff --git a/trap.d/432.dynamic-regex-bracket-backslash.md b/trap.d/432.dynamic-regex-bracket-backslash.md deleted file mode 100644 index f02781e2..00000000 --- a/trap.d/432.dynamic-regex-bracket-backslash.md +++ /dev/null @@ -1,30 +0,0 @@ -Writing awk into these scripts.sh JIT_AWK_* string blocks: a dynamic regex (a STRING -passed to match()/gsub(), not a /literal/ regex) that needs to match a literal backslash -inside a bracket expression needs the runtime string to carry TWO backslash bytes, not -one. - -Observed: `q3 = sprintf("%c", 92)` (one backslash byte) then `qclass = "[" q1 q2 q3 -"]?"` used as a dynamic-regex bracket. Running the hook produced: `awk: nonterminated -character class [^<]<<-?[ \t]*['"\]?[` and exit 2 -- the whole rule engine refused every -call for the rest of the session (fails closed, per this hook's own #397 posture, so it -was loud rather than silent, but still cost a full test-suite run to notice: 205/343 -tests failed with "the rule engine could not evaluate this call"). - -Root cause, as best determined: this awk's dynamic-regex compiler treats a lone -backslash inside a bracket expression as an escape introducer for whatever follows it -(here, the class-closing bracket), consuming it -- so the bracket never closes and the -whole ERE fails to compile. This is DIFFERENT from a /literal/ regex, where q1/q2 -(apostrophe, double-quote, no backslash) worked fine with the same bracket-building -approach. - -Fix: `q3 = sprintf("%c%c", 92, 92)` -- two backslash bytes, so the escape-consuming pass -collapses them back to one literal backslash inside the class, and the real closing -bracket right after it closes the class. - -Confirmed by: reverting to one backslash reproduced the exact error above; the two-byte -form ran clean and the full suite (353/353) passed. - -Not sure whether this generalizes to every dynamic regex in this codebase or is specific -to one-true-awk's implementation (LC_ALL=C pin was active; not tested against gawk with -this exact construct) -- worth someone checking both engines explicitly if another -dynamic-regex-with-backslash is ever written here. diff --git a/trap.d/437.wrong-worktree-edits.md b/trap.d/437.wrong-worktree-edits.md deleted file mode 100644 index da0d77d2..00000000 --- a/trap.d/437.wrong-worktree-edits.md +++ /dev/null @@ -1,21 +0,0 @@ -Observed: three `supertool edit`/`paste` calls, issued without a `cd` prefix in their -own Bash call, landed in the MAIN CLONE (/Users/floriandavid/Documents/claude-jit-context) -instead of the worktree I was supposed to be editing -(/Users/floriandavid/Documents/jit-wt/437). This happened right after a call that DID -start with `cd .../jit-wt/437 && ...` -- the cwd reset between Bash calls (stated in the -environment system-reminder) is easy to forget mid-task once a few calls in a row have -all been correctly prefixed. - -Cost: two script edits (scripts/common.sh, scripts/rebuild-tsv.sh) plus one stray -directory write (data/generic-words/README.md) landed in the main clone. Caught only -because a fixture test run afterward produced output inconsistent with what the edits -should have changed -- `bash -x` tracing showed the OLD code path still running in the -worktree, which is what triggered the investigation. Fixed by `git status`/`git diff ---stat` in the main clone (confirmed only those three paths were touched), `git checkout ---` on the two tracked files, and `rm -rf` on the stray untracked directory. - -How to avoid: every `supertool edit:@-`/`paste:@-`/`batch:@-` call in a worktree-based -task needs its OWN `cd &&` prefix, with no exception for "I just cd'd two -calls ago." The op's own output has a `[branch: ...]` line at the end precisely so this -is checkable after the fact -- read it before trusting that a write landed where -intended, especially right after any call that did NOT start with `cd`. diff --git a/trap.d/442.awk-in-is-reserved.md b/trap.d/442.awk-in-is-reserved.md deleted file mode 100644 index 841b3fd0..00000000 --- a/trap.d/442.awk-in-is-reserved.md +++ /dev/null @@ -1,17 +0,0 @@ -While extending jit_heredoc_quote_states() in scripts/common.sh (issue #442, cross-line quote-state carry-over), the function was first written as: - - function jit_heredoc_quote_states(lines, n, in, i, state) { - state = 0 - for (i = 1; i <= n; i++) { - in[i] = state - state = jit_heredoc_line_exit_state(lines[i], state) - } - } - -Using `in` as a parameter/array name. `in` is an awk reserved word (it is the keyword used in `for (key in array)` and in the `(key in array)` membership test), and both gawk and the system `awk` on this machine accepted the script without a syntax error -- there was no parse-time complaint at all -- but the function silently produced garbage, and every caller downstream of it broke. - -Cost: running `bash tests/test-pre-tool-hook.sh` after introducing this went from 374/374 passing to 163/374 passing, 211 failed, with no single obviously-related failure message pointing at the cause -- the failures looked like a broad, unrelated regression across completely different test sections, not like a syntax error in one small new function. It took reading the diff again and recognizing `in` as a keyword (rather than re-running with a debugger or bisecting) to find it; renaming the parameter to `qin` immediately restored 374/374 (then 375/375, 378/378 as further tests were added). - -Confirmed by: the fix (rename only, no other change) made every failing assertion pass again, with no other code touched. - -Not sure how general this is -- it may be worth a rule for "awk reserved words that are legal-looking identifiers and compile without error" (also candidates, untested here: `getline`, `print`, `printf`, `length` is actually a builtin function name and might be a separate case). Logging it as observed rather than deciding whether/how it becomes a durable rule. diff --git a/trap.d/459.awk-pipe-failure-masked-by-or-true.md b/trap.d/459.awk-pipe-failure-masked-by-or-true.md deleted file mode 100644 index cac46846..00000000 --- a/trap.d/459.awk-pipe-failure-masked-by-or-true.md +++ /dev/null @@ -1,24 +0,0 @@ -The new regression test for #459 (tests/test-no-heredocs-459.sh) builds its -detector as `awk '...' "$1" | grep -nE -- "$PATTERN" || true`. The `|| true` is -there so the pipeline's overall exit status is never non-zero just because -`grep` found no match (a clean file is the expected common case, and `set -o -pipefail` is not in effect in this script -- only `set -uo pipefail` is, and -`pipefail` is the missing one here) -- but it also means `awk` failing for an -unrelated reason (the file vanished between `git ls-files scripts` and the -read, a permissions problem, anything) produces the exact same empty string as -a file that was genuinely read and found clean. The live sweep then prints "no -tracked file under scripts/ opens a real here-document" identically for -"checked every file and all were clean" and "could not actually read one of -them." Caught by the oss:auditor self-review spawn on this issue, filed as -non-blocking/misreports rather than fixed in the same pass, since it is a -narrow, pre-existing shape (`detect_heredoc_lines()`'s own `|| true`) rather -than something this diff's live sweep loop can plausibly hit today -- the file -list comes from `git ls-files` moments earlier in the same run. - -What would close it: check `[ -r "$1" ]` before scanning, or capture awk's own -exit status separately from grep's (e.g. `awk ... > "$tmp"; awk_rc=$?; grep -nE -... "$tmp"`), so an unreadable-file case renders distinguishably from a clean -one rather than collapsing into the same empty string. Possibly a case for a -small reusable "two outcomes is not three outcomes" pattern across this -repo's other `|| true`-guarded pipelines, if curation turns up siblings of this -same shape elsewhere in tests/. diff --git a/trap.d/461.release-check-crash-reads-as-pass.md b/trap.d/461.release-check-crash-reads-as-pass.md deleted file mode 100644 index 6b710bc9..00000000 --- a/trap.d/461.release-check-crash-reads-as-pass.md +++ /dev/null @@ -1,10 +0,0 @@ -Observed (release audit round 2 for v0.14.0, token jitrel-f53a3c70, finding B2, row misreports): -`tests/test-release-branch-461.sh` passes the check step on -`[ "$CHECK_RC" -eq 0 ] || [ -z "$UNEXPECTED" ]`. When check_release_tree.py crashes, its -traceback has no `^FAIL ` line, so UNEXPECTED is empty and the suite prints -"PASS: check_release_tree.py reports no offender in the built tree". The auditor ran that -condition with CHECK_RC=1 and a traceback as output and got the PASS line. CI is covered -(the workflow's own check step reads the exit code); a local run-all.sh is not. -test-release-branch-437.sh has the same shape from before this range. - -Settles it: a non-zero exit with no FAIL lines is a failure, not a pass. diff --git a/trap.d/461.release-equivalence-builds-head-not-source-ref.md b/trap.d/461.release-equivalence-builds-head-not-source-ref.md deleted file mode 100644 index e70b6260..00000000 --- a/trap.d/461.release-equivalence-builds-head-not-source-ref.md +++ /dev/null @@ -1,11 +0,0 @@ -Observed (release audit round 2 for v0.14.0, token jitrel-f53a3c70, finding B1, row misreports): -`.github/workflows/release-branch.yml`'s step "Equivalence test the compiled scripts (#461)" -runs `tests/test-release-branch-461.sh`, which builds `--ref HEAD` of the checkout. The -workflow itself builds and publishes `$SOURCE_REF`. On a tag push they are the same commit. -On `workflow_dispatch` with `ref=` (the input's own description invites -that), the checkout is the dispatching branch, so the equivalence step goes green on main's -tree while a different tree is published. check_release_tree.py and the smoke test do run on -the built tree; only the source-vs-compiled equivalence is missing on that route. - -Settles it: pass the built ref (or the built tree's path) into the test instead of HEAD. -Cost so far: nothing shipped wrong; found by reading, not by a failure. diff --git a/trap.d/466.privacy-page-claims-vs-code.md b/trap.d/466.privacy-page-claims-vs-code.md deleted file mode 100644 index 92864fa5..00000000 --- a/trap.d/466.privacy-page-claims-vs-code.md +++ /dev/null @@ -1,19 +0,0 @@ -Observed (release audit round 2 for v0.16.0, token jitrel-4b9aafdd, row misreports, carried -forward): four PRIVACY.md claims disagree with code that predates the page. - -- F1: the scratch file in $TMPDIR receives the log line (prompt start, tool text, path) in - EVERY project, not only opted-in ones. pre-*-hook.sh pass `-v log_tmp="$JIT_TMP"` - unconditionally; JIT_LOG_DISABLED only gates the append to hooks.log. -- F2: "the first 120 bytes of its command" is wrong. pre-tool logs - `substr(jit_log_text(f_file_path " " cmd_paths), 1, 120)`, where cmd_paths is every token - containing "/" anywhere in the command, lowercased and CamelCase-split. Exercised: a URL - with `?Token=...` past byte 165 of a command landed in hooks.log. A command with no "/" - logs nothing. -- F3: "markers older than seven days are removed" is false for bytes-shown-*.txt (#469). -- F4: "nothing outside the project is read, except doctor" misses jit_worktree_mismatch_line, - which runs `git rev-parse --show-toplevel` in the cwd and in CLAUDE_PROJECT_DIR on every - prompt/tool/path hook, opted in or not. - -Settles it: PRIVACY.md is linked at blob/main, so it is corrected on main right after the -v0.16.0 tag, no release needed. Lesson: a privacy page written from the code still missed -four facts; have the audit read it claim by claim, twice.