Conversation
Co-authored-by: Sal <ss-o@users.noreply.github.com>
`.zi-tmp-subst-on` replaces the `autoload' builtin for the whole duration of a
plug-in's load so that a plug-in can autoload its own functions without its
directory being in $fpath. The replacement wrote a stub with $PLUGIN_DIR baked
into the body without ever checking that the function actually lives there, so
every `autoload' issued while the substitution was installed got the currently
loading plug-in's directory baked in, whoever the function belonged to.
The usual source of such calls is compinit, which replays a bulk `autoload -Uz'
for every completion function recorded in .zcompdump. Functions whose real
provider directory was not yet in $fpath, most commonly because the provider is
loaded later, were left pointing at the wrong directory and failed with
`function definition file not found' on first call.
Look the function up under the plug-in's own directory and its own $fpath
entries first, the same ownership test the -C branch already performs. When it
is not found there, hand the call to `builtin autoload', which resolves lazily
against the live $fpath and therefore succeeds once the real provider is loaded.
The -C form arrives only from an explicit autoload'' ice, so it keeps its own
search and is byte-identical.
Three cases needed the ownership test to recognise directories that are in fact
the plug-in's own, each previously masked because every consumer appended the
global $fpath afterwards:
- `(R)' rather than `(r)' for $fpath_elements, so a plug-in registering more
than one $fpath subdirectory does not have all but the first hidden.
- ${PLUGIN_DIR:A} matched as well as $PLUGIN_DIR, because the Plug Standard
idiom `fpath+=( ${0:A:h}/lib )' resolves symlinks while $PLUGIN_DIR keeps the
path zi was given.
- `-f $apth.zwc' accepted alongside `-f $apth/$func', because an $fpath entry
may be backed by a directory digest rather than by plain files.
tests/plugin-autoload-ownership.zsh covers all five behaviours and is registered
in zsh-n.yml. Each of the three recognition hunks was confirmed load-bearing by
reverting it in isolation.
Closes #471
fix(autoload): drop the dead FPATH assignment in the +X branch
The `+X' branch opened with
local +h FPATH=$PLUGINS_DIR${fpath_elements:+:...}:$FPATH
built from $PLUGINS_DIR, which has never existed; the plug-in directory is
$PLUGIN_DIR and the plug-ins root is ZI[PLUGINS_DIR]. The value was never used
either: `local +h -a fpath' on the next line declares a fresh local array, and
because fpath and FPATH are tied that discards the scalar, after which
`fpath=( $PLUGIN_DIR $fpath_elements $fpath )' establishes the real search path.
Tracing the branch confirms it. FPATH immediately before `builtin autoload +X'
carries no empty leading field and does not contain the working directory, and
removing the assignment changes nothing observable: immediate autoload still
resolves the plug-in's own functions, and neither $fpath nor $FPATH leaks to the
caller, because the array declaration localises both.
Remove it rather than repair the name. It is inert, it names a parameter that
does not exist, and it sits in the middle of the search-path construction it
appears to belong to, which has already caused one misdiagnosis.
tests/plugin-autoload-fpath-scope.zsh pins the invariant that made the removal
safe: immediate `autoload +X' resolves from the plug-in directory and from an
$fpath subdirectory the plug-in registers, never from the working directory, and
restores the caller's $fpath and $FPATH. It passes before and after, which is
the point.
Closes #475
fix(autoload): make the autoload'' ice and @autoload work again Two independent defects left every form of the ice broken except the plain and `#' ones. The (#b) backreference substitution that rewrites `a -> b' into `a -S b' needs extended_glob. Neither .zi-load-plugin nor @autoload sets it, so the substitution matched nothing and the value reached :zi-tmp-subst-autoload untranslated: the unspaced form arrived as one literal name, and the spaced form word-split into three, `a', `->' and `b'. The -C flag was still added, because plain alternation does not need extended_glob, so the -C branch then searched $fpath for names that cannot exist. Build both argument lists inside an anonymous function that sets the option and localises match, mbegin and mend. The -C branch's generated wrapper installed the loaded body under the wrong key. The call line reads ${(q)${custom[count*2]}:-$func}, which falls back to $func when the element is unset, but the assignment line read ${${(q)custom[count*2]}:-$func}, which quotes first; quoting an unset element yields a two-character string, so the :- fallback never fired and the wrapper assigned to functions['']. The real function was never redefined, so calling it re-entered the wrapper until FUNCNEST. Match the call line's nesting. The two are not independent in effect. Once the substitution runs, every name also gets `-S <name>', so custom is always populated and the empty key stops arising; either fix alone resolves the bang form. The nesting fix is kept because the assignment is wrong on its own terms. tests/plugin-autoload-ice.zsh covers the plain, spaced rename, unspaced rename and bang forms of the ice, plus @autoload in its plain and rename forms, and is registered in zsh-n.yml. Each ice form needs its own plug-in directory and its own function name, because zi skips an already-loaded plug-in and the substitution never touches a function that already exists. Closes #476
fix(load): restore the enclosing plug-in after a nested load .zi-load and .zi-load-snippet set ZI[CUR_USR], ZI[CUR_PLUGIN] and ZI[CUR_USPL2] on entry and cleared them unconditionally on exit, under the comment "mark no load is in progress". Both are re-entrant: a plug-in body may call `zi load', `zi light' or `zi snippet'. The inner call cleared the state the outer call still needed, so everything zi did on the outer plug-in's behalf after a nested load was attributed to no plug-in at all, including report entries and the bindkey, zstyle, alias and widget tracking keys used by unload. Save the previous values and restore them instead. At the top level the saved values are empty, so "no load is in progress" still holds there. Restore from an `always' block rather than at the old assignment site. Both functions return early on several paths, cloneonly'' among them, and a plain restore at the end would leave those paths clobbering the outer plug-in exactly as before. `always' also matches how the rest of the file handles this shape. tests/nested-load-state.zsh covers three nesting shapes, a plain nested plug-in load, one that returns early through cloneonly'', and a nested snippet, and asserts the top level is still clear afterwards. It is registered in zsh-n.yml. This does not make the autoload in the issue's reproduction resolve. That symptom has a second, independent cause: .zi-tmp-subst-on returns early when a substitution is already installed, while .zi-tmp-subst-off tears it down as soon as a nested load of the same mode finishes, so the outer plug-in's later `autoload' calls are no longer intercepted at all. Reported separately. Refs #473
…ce (#481) fix(subst): count load nesting so substitutions install and remove once ZI[TMP_SUBST] carried two incompatible encodings. .zi-tmp-subst-on stored a mode name in it, while three inlined compdef-only blocks in .zi-load-snippet and .zi-load-plugin used it as a depth counter, with `ZI[TMP_SUBST]=1', `(( ++ ZI[TMP_SUBST] ))' and `(( -- ZI[TMP_SUBST] == 0 ))'. Loading is re-entrant, so the two met. Two failures followed. A plug-in that loads another plug-in hit the on/off pair: the inner .zi-tmp-subst-on returned early because a substitution was already installed, then the inner .zi-tmp-subst-off matched on mode and tore it down while the outer plug-in was still running. Everything the outer plug-in did afterwards stopped being intercepted, including FPATH-clean autoloading and the bindkey, zstyle, alias, zle and compdef tracking that report and unload depend on. A plug-in that loads a snippet hit the inlined blocks. `(( ++ ZI[TMP_SUBST] ))' on the string `load' evaluates it as an unset parameter name, so the mode became 1, the matching decrement reached 0, and the compdef teardown ran and set the key to inactive. The outer .zi-tmp-subst-off then returned early on `inactive', so autoload, bindkey, zstyle and alias were never restored and stayed replaced by zi's substitutions at the top level for the rest of the shell's life. Give the depth its own key. ZI[TMP_SUBST] keeps meaning the installed mode or inactive, ZI[TMP_SUBST_DEPTH] counts entries. Every site that installs increments, every site that tears down decrements, the substitutions are installed on the transition out of inactive and removed only when the count returns to zero. No site returns between its install and its teardown, so the pairing is exact; that was checked before choosing counting over an always block. tests/subst-nesting.zsh asserts that neither a nested plug-in load nor a nested snippet load leaks a substitution to the top level, that the depth returns to zero, and that a plug-in's own autoload still resolves after it has loaded another plug-in. It tests for a body dispatching to :zi-tmp-subst-*, not for the mere presence of the function, because compdef legitimately exists once compinit has run. This also completes the reproduction in #473, which #479 fixed only halfway: the enclosing plug-in keeps its identity and its substitutions. Closes #480 Closes #473
copilot/ and codex/ join dependabot/ and renovate/ in the always-allowed prefixes, because a coding agent picks those names rather than the pull-request author. The pattern becomes ^(feature|bug|hotfix)-[1-9][0-9]*(-[a-z0-9]+)*$, so a descriptive slug may follow the issue id. The issue id stays mandatory for author-chosen branches and next keeps its persistent-integration exemption, so decisions/0019 is unchanged in substance. Parent: z-shell/.github#590 Closes #484
Validate Commits matched the disallowed trailer as a pipeline under pipefail with grep -q. grep -q exits on its first match, so for a commit message larger than the pipe buffer git was still writing, took SIGPIPE, and returned 141. pipefail promoted that to the pipeline result and the if turned a real match into a miss, so the banned trailer passed. Use grep -c with a numeric test. grep -c reads to end of input, so the writer always finishes and no SIGPIPE arises, and it keeps grep's own ERE engine and -i semantics rather than swapping in bash matching. Verified on four real commits: a 3 MB message carrying a bot trailer reports clean under the old construct and flagged under the new one, while a small bot trailer, a large human trailer, and a large clean message are unchanged. Closes #486
z-shell/.github decisions/0022 moves traceability off the branch name and onto the pull request. This repository carries the same commit-lint jobs with their patterns inlined, so it moves in step or the two copies drift. Validate Branch Name becomes a shape check: <type>-<id> with an optional slug, which stays valid and recommended, or <type>/<slug> over the Conventional Commits types plus feature, bug and hotfix. next keeps its persistent-integration exemption. Validate Issue Link is added, matching the organization copy. It passes on an automation branch, next, a meta:no-issue label, or an issue reference in the body, and says which applied. It reads only the pull_request event payload, so it needs no token. code is not in the Conventional Commits type set, so code/ branches remain rejected here as in the organization repository. meta:no-issue reaches this repository through label sync; until then the exemption path is unusable and the check falls through to the body reference. Closes #489
Co-authored-by: Sal <ss-o@users.noreply.github.com>
Co-authored-by: Sal <ss-o@users.noreply.github.com>
This was referenced Sep 3, 2026
Closed
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.
Promotion readiness record
Candidate identity
Prior
mainSHA:38c63c274ce6bc4d05f5ab595c7731dd4eab9660Candidate
nextSHA:d84132175d87edb8fc42de81bbcea7b1de9a6a07Candidate tree SHA:
ac6f85dd41768003db5f2f3fa94fab2cd03b24d3Complete compare:
main...nextgit diff --name-only d841321...38c63c2produces no output.git rev-parse 'd841321^{tree}'equalsac6f85dd41768003db5f2f3fa94fab2cd03b24d3.git log --oneline d841321..38c63c2is also empty, somainholds no stable-only commit the candidate lacks.Required validation
Zsh syntax and compileZD integrationTrunkCodeQLClean install and startupPromotion gate38c63c2, headd841321.Guard main branch sourceandPromotion gaterequired contexts pass. 155 checks green, none failing or skipped.Readiness review
Why now
Released
maincarries #471:compinitrun from inside a plug-in body leaves foreign completion functions unresolvable, which breaksAloxaf/fzf-taband takes completions with it. #472 fixed it onnextbefore v2.0.0 shipped and has never been promoted, so every consumer of the stable ref has been exposed since.This is now independently demonstrated rather than argued. z-shell/zd#116 adds shell-state integration tests, and its
shell-statejob fails against zimainwithinner-compinit-brokenwhile passing against this candidate. ADR-0016 records that forzi,mainis the ref consumers pull directly and staleness has a direct, ongoing cost.Contents
Five user-facing fixes and one behavioural test addition:
+Xbranch; see #491 belowautoload''ice and@autoloadwork again$fpathin the+Xbranch, fixing the regression #477 introduced-wbranch's persistent$fpathappendThe remainder is CI and repository work: #470, #474, #485, #487, #490, and one
mainreconciliation.Note the #477 and #491 pairing. #477 removed a line as dead that was not, breaking immediate
autoload +Xof any function a plug-in does not own; #491 restores the behaviour. Both are inside this candidate, so the net effect of the pair against priormainis the intended fix plus regression coverage, not a live defect.Unresolved issues
None blocking. Open zi issues at the time of writing are #113, #386, #387, #388, #429, #430, #445, #446, #447, #448, #449, #468, #482, #483. All predate this candidate or are unrelated to it, and none is a regression introduced by it.
User-facing and migration notes
No migration required. Every behavioural change in this candidate restores intended behaviour that regressed; none changes a documented interface. Users on stable gain working completions in plug-in-body
compinitarrangements, workingautoload''ice and@autoload, correct enclosing-plug-in state after nested loads, and correct substitution install/remove pairing.Public-contract follow-ups
None identified. The
Public contract impactcheck ran on each constituent pull request and reported no consumer-visible surface change requiring documentation.Merge contract
Promotion uses Create a merge commit. Never squash or rebase.
Co-authored-bytrailer.delete_branch_on_mergeisfalse; remotenextwill survive the merge.mainruleset allows only merge commits and does not require linear history.nextruleset does not require linear history.Rollback readiness
@ss-omainhead.z-shell/zdshell-stateor any other ZD ZUnit job fails against the newmain.hotfix-*pull request that reverts the promotion merge through protectedmain.Guard main branch sourceandPromotion gate.mainhead, then the hotfix will be merged forward intonext.Stable consumption boundary
This updates the Git-consumed stable
mainref. It creates no semantic tag or GitHub release. v2.0.0 remains the newest tag; any tag covering this content is a separately approved action.