feat(zsh): order fpath deliberately (brew appended) and pin lookup order with a test - #362
Conversation
The block carried only "Avoid using brew shellenv for setup, because brew completions are messed up" — a verdict with no mechanism, which is not enough to decide whether it still holds. Recovered from history: the `fpath+="$(brew --prefix)/share/zsh/site-functions"` line was dropped in 1aae7e5 (2025-03-03) in the same hunk that dropped `eval "$(brew shellenv)"`, so the surviving comment covers PATH setup and never stated a reason for the fpath half. Re-derived and confirmed against the current brew, which emits `fpath[1,0]=...` plus `export FPATH`: - Position 1 places brew ahead of ~/.zfunc and /usr/share/zsh/*/functions. First match in fpath wins (verified with a flipped-order control), so brew's build-time snapshots would shadow 20 of the 38 completers ~/.zfunc generates fresh, and brew's `_git` would shadow zsh's native one. - `export FPATH` propagates that order to child shells: after eval, a bare `zsh -f` resolves _git to the brew copy. So the conclusion still holds and now says why. Also notes that the other historical suspicion — a 2023 comment blaming brew completions for fzf-tab misbehaving on kubectl — was root-caused as a load-order bug and fixed in d4fb8c5, so it is not part of the rationale. Comment only; no behaviour change.
|
🤖 Claude analyzed the CI failures but determined no code changes are needed. Failed workflow: https://github.com/laurigates/dotfiles/actions/runs/32120090592 This may indicate:
Please review the failure logs manually. |
… test Puts brew's zsh site-functions back on fpath — appended, never via `brew shellenv` — and fixes a precedence skew that predates it. Adds a regression test so neither order can be broken silently again. fpath is now ~/.zfunc > zsh's own functions > brew: - ~/.zfunc moves ahead of the system dirs. It is generated from the binary that actually runs, so it should win; it did not. _npm, _pip and _luarocks exist in both places, and zsh's bundled copies (5.9, 2022) were shadowing the generated ones. This is the completion-side mirror of the PATH rule. - brew goes last, reaching ~30 completers nothing else ships (_bat, _delta, _k9s, _yq, _zoxide, ...) while displacing nothing. First match wins, verified with a flipped-order control. - brew after zsh's own, not before: the only two names in both are _git and _luarocks, and brew's _git is git's bash-completion wrapper, which would displace zsh's native one and make dot_zfunc/__git_branch_names inert. Removes the stale unmanaged ~/.zfunc/_git (a Feb-2025 copy of that same wrapper). Harmless while ~/.zfunc sorted last; promoting the directory would have silently swapped git completion. Backed up before deleting. PATH invariant mise > brew > system was violated by 12 commands. `mise activate` prepends, so anything prepending after it wins. Ten were bin symlinks in ~/.bun/bin pointing into ~/node_modules — leftovers from #360, which moved the declarations into mise but left the old install in place. Removed the duplicated deps and the ten now-dangling symlinks; happy-coder, which mise does not manage, is untouched. All ten commands now resolve to mise. tests/test-shell-precedence.sh pins both orders against a live `zsh -i`: - Verified in both directions: it passes on this config and fails on the pre-change one (rendered from origin/main) with exactly the two expected failures, rather than being trusted by construction. - A self-check feeds the tier assertion a scrambled PATH and fails if it reports it clean, so the suite cannot go green with a neutered assertion. - The two remaining violations, kubectl (gcloud's path.zsh.inc prepends and bundles its own) and jnv (cargo), are allowlisted by name with the reason and the fix, and printed on every run rather than silently subtracted. - `find -L`: mise install dirs are symlinks (`latest -> ./1.36.2`) and find will not descend a symlinked starting point without it. The first version silently missed 20 tools, kubectl among them. - Uses ${f##*/} rather than basename(1), which forked per file: 58s -> 1.6s. Comments at `mise activate` and at each of the three prepends that run after it state the invariant and name the test, so the next edit that adds a PATH entry has the reason in front of it.
|
🤖 Claude analyzed the CI failures but determined no code changes are needed. Failed workflow: https://github.com/laurigates/dotfiles/actions/runs/32127075127 This may indicate:
Please review the failure logs manually. |
|
chezmoi ≥2.72 rejects leading-slash patterns (they resolve outside the destination dir); older releases tolerated them. CI installs chezmoi unpinned via Worth flagging for anyone reading the two "no code changes are needed" bot comments above: the error is fatal and total — The fix is slash-free patterns, which is what actually root-anchors ( No changes needed on this branch. Generated by Claude Code |
## What Unbreaks `Build (Ubuntu)`, which has failed on **`main`** since 2026-08-04 — every commit, not just PRs. ``` chezmoi: /home/runner/work/dotfiles/dotfiles/.chezmoiignore:9: /justfile: invalid path ``` ## Why now, when nothing in the repo changed chezmoi matches patterns against the target path **relative to the destination dir**, so a leading slash makes the path absolute — outside the destination. Newer chezmoi (≥2.72) rejects that; older releases tolerated it silently. CI installs chezmoi unpinned (`brew install chezmoi`), so a release changed the rule underneath us. `/justfile` itself has been in the file since #308 (2026-07-07) and was green for a month. The failure is worth understanding because it is **fatal and total**: `apply`, `ignored` and `managed` all refuse to run, and chezmoi reports only the **first** offending line — so seven bad patterns presented as one. That is why the job dies at `chezmoi apply` while `Linters`, `Benchmarks`, `SBOM` and the rest stay green, and why it looks like an isolated `justfile` problem rather than a whole-file one. ## The fix, and why bare patterns are correct A slash-free pattern **is** the root anchor — it was never the leading slash doing that work. Verified against a locally built chezmoi v2.72.0: | Pattern | Matches | | |---|---|---| | `justfile` | source-root `justfile` only — `.config/just/justfile` stays managed | ✅ what we want | | `/justfile` | nothing — fatal parse error | ❌ | | `**/justfile` | every `justfile` at any depth, **including the managed global one** | ❌ would break `just -g` | Directory patterns behave the same: `docs/` ignores only the root `docs/`, leaving `~/.config/nvim/docs/` managed. So all seven edits preserve the existing intent exactly — this is a syntax repair, not a semantics change. Both invariants re-checked against the real source tree after the change: - `chezmoi managed` still lists `.config/just/justfile` — the global justfile survives. - `chezmoi managed` still does **not** list `justfile` — the repo-root copy stays out of `$HOME`. - `chezmoi apply -v --dry-run --source=. --exclude=scripts` (CI's exact command) exits 0. ## `.chezmoiremove` The second offender, surfaced only once line 9 stopped aborting the parse: ``` .chezmoiremove:14: /tmp/claude_status_hub.log: invalid path ``` Deleted rather than de-slashed. `.chezmoiremove` can only name targets under `$HOME`, so a genuine `/tmp` path is not expressible at all — and `tmp/claude_status_hub.log` would silently re-point it at `~/tmp`, a scratch dir `.chezmoiignore` deliberately ignores, deleting an unrelated file. Nothing else in the tree references that log; it is dead weight from a 2025-09-30 cleanup. A comment records the reasoning so the entry is not "repaired" back into existence. ## Docs `.claude/rules/chezmoi-conventions.md` documented `/justfile` as *the* root-anchoring idiom — following the rule as written re-breaks CI. Corrected to the slash-free form, with the anchoring table above and a note that a leading slash is now a hard error. The `.chezmoiignore` comment block also claimed the global justfile is `dot_user.justfile.tmpl` → `~/.user.justfile`, which the same rule file elsewhere explains is wrong (`~/.user.justfile` is not on `just`'s search path). Corrected to `private_dot_config/just/justfile` → `~/.config/just/justfile` while touching those lines. ## Note Found while investigating the red check on #362. It is unrelated to that PR's diff — split out here so it unblocks every PR rather than riding inside a zsh change. #362 stays red until this merges. ## Follow-up worth considering (not in this PR) CI installs chezmoi unpinned, which is what let an upstream release break `main` silently. Pinning it (mise already manages `chezmoi = "latest"`) would turn this class of breakage into a deliberate upgrade. --- _Generated by [Claude Code](https://claude.ai/code/session_01Sa3o7Pia4UZ1zKH5NFRQ24)_ Co-authored-by: Claude <noreply@anthropic.com>
|
| Status | Count |
|---|---|
| 🔍 Total | 170 |
| ✅ Successful | 160 |
| ⏳ Timeouts | 0 |
| 🔀 Redirected | 0 |
| 👻 Excluded | 9 |
| ❓ Unknown | 0 |
| 🚫 Errors | 1 |
Errors per input
Errors in docs/macos-automation.md
- [ERROR] https://macos-defaults.com/ | Failed: Network error: error sending request for url (https://macos-defaults.com/)
Full Github Actions output
Please fix the broken links before merging.
The `Test zsh shell initialization` step wraps `source ~/.zshrc` in `timeout 10s`. That budget was already almost exhausted before this branch touched anything: the step took ~6s on a cold runner on 2026-07-30 and ~9s on 2026-08-18 — 90% of the limit — so any change that adds startup cost turns it red, and exit code 124 reads as a hang rather than as "one second slower than last week". Adding brew's site-functions to fpath gives compinit a fourth directory to scan, which was enough to cross it. The timeout exists to catch a config change that BLOCKS on input, not to police startup time, so it is raised to 60s — still fast to fail on a real hang, with room for normal drift. The comment records the measured baselines so it is not tightened back on the assumption that 10s was ever comfortable. Worth stating plainly: this does not make shell startup faster, and the 6s -> 9s drift on main is not explained by this branch. Nothing in CI currently watches that number — the benchmarks workflow times a minimal /tmp/test.zshrc, not the applied ~/.zshrc — so the drift is invisible by design. Noted in the comment as a separate problem rather than folded into this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sa3o7Pia4UZ1zKH5NFRQ24
|
| Status | Count |
|---|---|
| 🔍 Total | 170 |
| ✅ Successful | 160 |
| ⏳ Timeouts | 0 |
| 🔀 Redirected | 0 |
| 👻 Excluded | 9 |
| ❓ Unknown | 0 |
| 🚫 Errors | 1 |
Errors per input
Errors in docs/adrs/0012-justfile-command-runner.md
- [ERROR] https://just.systems/man/en/what-are-the-idiosyncrasies-of-make-that-just-avoids.html | Failed: Network error: error sending request for url (https://just.systems/man/en/what-are-the-idiosyncrasies-of-make-that-just-avoids.html)
Full Github Actions output
Please fix the broken links before merging.
|
🤖 Claude analyzed the CI failures but determined no code changes are needed. Failed workflow: https://github.com/laurigates/dotfiles/actions/runs/32179632608 This may indicate:
Please review the failure logs manually. |
What
Puts brew's zsh
site-functionsback onfpath— appended, never viabrew shellenv— fixes a precedence skew that predates it, finishes the~/.bun/binhalf of #360, and adds a regression test so none of it can be broken silently again.Started as a comment recording why the directory was off
fpath. The archaeology showed the omission was collateral rather than a decision, and that the real hazard is the prepend, not the directory — so the comment turned into a change.Why it was off fpath
fpath+="$(brew --prefix)/share/zsh/site-functions"was deleted in1aae7e5(2025-03-03), in the same hunk that deletedeval "$(brew shellenv)". The surviving note — "brew completions are messed up" — was authored against the commented-outshellenvline in the PATH block. Nothing ever stated a reason for thefpathhalf; it went out because it lived inside the block being removed.Re-derived against current brew, which emits
fpath[1,0]=…plusexport FPATH. Both halves bite, and both indict the prepend specifically:~/.zfuncand/usr/share/zsh/*/functions. First match wins, so brew's build-time snapshots would displace 20 of the completers~/.zfuncgenerates fresh, and brew's_gitwould displace zsh's native one.export FPATHpropagates that order to child shells: after the eval, a barezsh -f— which reads no config at all — resolves_gitto the brew copy.Appending has neither property.
fpath order
Now
~/.zfunc→ zsh's own → brew, each position load-bearing:~/.zfuncfirst_npm,_pip,_luarocks(5.9 = 2022) were shadowing the generated copies. Completion-side mirror of the PATH rule._gitand_luarocks. Brew's_gitis git's bash-completion wrapper; ahead of zsh's native one it would replace it wholesale and makedot_zfunc/__git_branch_names(#361) inert._bat,_delta,_k9s,_yq,_zoxide, …) while displacing nothing.Verified after apply:
_git→/usr/share/zsh/5.9/functions/_git,_npm→~/.zfunc/_npm,_yq/_zoxide/_bat→ brew.Also removes the stale unmanaged
~/.zfunc/_git, a Feb-2025 copy of that same bash wrapper. Harmless while~/.zfuncsorted last; promoting the directory would have silently swapped git completion. Backed up first.PATH: mise > brew > system
Twelve commands violated it.
mise activateprepends, so anything prepending after it wins:~/.bun/binpointing into~/node_modules— leftovers from chore(mise): migrate npm-only CLI tools from ~/package.json #360, which moved the declarations into mise but left the old install behind (the same cleanup that PR did by hand forccr). The duplicated deps and the ten now-dangling symlinks are gone;happy-coder, which mise does not manage, is untouched. All ten resolve to mise now.kubectl(gcloud'spath.zsh.incprepends aftermise activateand bundles its own) andjnv(cargo install;~/.cargo/binis prepended in zshenv).The test
tests/test-shell-precedence.sh, wired intomise run testastest:shell-precedence. It reads a livezsh -i, because both invariants are properties of a shell rather than of the source tree, and skips loudly on a machine without mise or brew.What makes it trustworthy rather than decorative:
origin/main, not a hand-written bypass — with exactly the two expected failures.finddoes not descend a symlinked starting point without-L, and mise's install dirs are symlinks (latest -> ./1.36.2) — the first version silently missed 20 tools,kubectlamong them. Andbasename(1)forked per file: 58s → 1.6s with${f##*/}.Also recorded
A 2023-era comment (
7491c8c) blamed brew completions for fzf-tab misbehaving onkubectl. That was a load-order bug, root-caused and repaired ind4fb8c5"fix(zsh): load fzf-tab after compinit". The comment says so, so the next reader does not count an already-repaired bug as part of the rationale.Note
Builds on the same file as #361 but is independent of it; #361's completion override was re-verified working under the new fpath order.