Guidance for AI agents and automated tooling working in this repository.
Complements CONTRIBUTING.md (human contributors) with the concrete commands,
conventions, and guardrails an agent needs for safe, consistent changes.
Vim Plugin Manager is a lightweight Vim plugin manager built on Git
submodules and Vim 8's native package system (pack/plugins/{start,opt}). The
codebase is pure VimScript, organized into small single-responsibility modules.
There is no compilation step. The project is loaded directly by Vim.
- Run the test suite (Vader):
vader.vim is cloned automatically at the pinned SHA on first run. To use an existing clone:
make test # Interactive TUI make test-ci # Headless (same output as CI)
make test-ci VADER_DIR=./vader.vim. - The
testtarget generates a throwaway.vaderrc.vimand runsvim -Nu .vaderrc.vim -c 'Vader! tests/*.vader'(interactive TUI). - The
test-citarget runs the same suite viavim -es(headless/ex mode) producing clean plain-text output with no ANSI sequences. - Clean test artifacts:
make clean
- VimScript linting: optional, via
vim-vint(not run in CI). Config:.vintrc.yaml(correctness=error, style=off). - CI runs
test-ci:.github/workflows/test.yml(GitHub Actions) and.gitlab-ci.yml(GitLab). Keep both green. - CI matrix covers AlmaLinux 9/10, Debian Bookworm/Trixie, Ubuntu 24.04/26.04,
Arch Linux. Gentoo runs non-blocking (
allow_failure: true). The AlmaLinux 9 job runs on Vim 8.2 and proves the 8.2 floor is respected. - Real async CI runs via
make test-asyncon both GitHub Actions and GitLab CI. Headlessvim -esdoes not run the event loop; the async job usesscript -qec "vim -N -u tests/async_smoke.vim"(util-linux pty) so Vim's event loop runs and real job callbacks fire. The smoke test exercisesstart_jobwith opts.callback,async#git, and the concurrency queue. Blocking on both CI systems.
Requirements: Vim 8.2+ (with +job and +channel), Git 2.39+. Neovim is not supported (it has lazy.nvim, packer.nvim and vim-plug). Windows and macOS are not supported: the project targets Linux only (Debian, Ubuntu, Arch, Gentoo, RHEL/AlmaLinux/Rocky).
The minimum Vim version floor is 8.2 and must not be raised without a concrete
reason. It is set by RHEL 9 / AlmaLinux 9 / Rocky 9, which ship Vim 8.2.2637.
RHEL 9 is supported until 2032. The codebase uses no Vim 9.0+ features; a
vim9script migration is explicitly deferred (dominant cost is git/network I/O,
not script execution). The guard if v:version < 802 in
plugin/plugin_manager.vim is intentional - do not raise it to 900.
For VimScript:
- 2-space indentation. Keep lines under 100 characters where practical.
snake_casefor functions and variables.- Internal (script-local) functions and variables are prefixed with
s:. - Public functions follow
plugin_manager#<module>#<function>(). - Each file starts with a header comment:
" path/to/file.vim - Short description " Maintainer: G.K.E. <gke@6admin.io>
- Document functions with a short comment above them.
- Prefer Vim script native idioms over shelling out when feasible.
- Follow Semantic Versioning 2.0.0 for tags: PATCH = bug fix retrocompatible only. MINOR = addition d'API publique retrocompatible ou deprecation. MAJOR = breaking change.
- The public API surface is:
api.vimfunctions,:PluginManager*/:Plugin/:PluginBegin/:PluginEndcommands, andg:plugin_manager_*config variables. Everything else (core/*,git.vim,async.vim,ui.viminternals,cmd/*.vim) is internal and may change without deprecation. - Directory scoping: git commands run through
git#execute, which injectsgit -C <dir>when a directory is provided (available since git 1.8.5; project floor is 2.39). Arbitrary shell commands (exec hooks, cp, rsync) run throughcore#util#run_in_dir, which usescd <dir> && <cmd>(POSIX subshell, never touches Vim's process cwd). Never use:cd/:lcdin plugin code. Command strings must be single invocations (no shell||,&&,;).
plugin/plugin_manager.vim Entry point: config defaults + command definitions
autoload/plugin_manager/
core.vim Error handling (throw, handle_error, parse_error)
core/log.vim Log management (debug, trace, view, clear, rotation)
core/cache.vim Update check cache (read, write, TTL)
core/util.vim Paths, config, URL parsing, filesystem, plugin options
git.vim Git operations, .gitmodules cache, submodule status
async.vim Non-blocking async jobs (Vim job/channel) with a concurrency queue
ui.vim Sidebar rendering, spinners, operation tracking
api.vim Public API facade
cmd.vim Command dispatcher + legacy-format adapters
cmd/*.vim add, remove, update, status, list, helptags, reload,
backup, restore, remote, declare, check
ftdetect/ ftplugin/ syntax/ Sidebar buffer (filetype=pluginmanager)
doc/ :help documentation
tests/ Vader tests
Control flow: :PluginManager <cmd> -> plugin_manager#cmd#dispatch() ->
plugin_manager#api#* -> plugin_manager#cmd#<cmd>#execute(). UI feedback goes
through ui.vim; Git through git.vim; background work through async.vim.
Internal errors use a structured, 4-field format:
PM_ERROR:<component>:<CODE>:<message>
- Raise errors with
plugin_manager#core#throw(component, code, message), not a barethrow 'PM_ERROR:...'. The valid codes per component live ins:error_typesincore.vim. - Catch and present errors with
plugin_manager#core#handle_error(v:exception, component). - Logging is handled centrally; do not write to the log file directly.
- Use the operation API:
plugin_manager#ui#start_operation(),#update_operation(),#complete_operation(). - Use
plugin_manager#ui#get_symbol()for glyphs (never reference the script-locals:symbolsfrom outsideui.vim). - Do not reintroduce the removed legacy UI helpers (
start_task,update_task,complete_task,box,themed_header,show_message,format_message).
- Never add network access (e.g.
git fetch) on startup or on a timer unless it is strictly opt-in behind ag:plugin_manager_*flag defaulting to off. - Do not auto-commit or auto-push without honoring the relevant config flags
(
g:plugin_manager_auto_commit_on_update). - Keep changes minimal and within the most relevant module; create a new module only when needed.
- When adding a config option, declare it in
plugin/plugin_manager.vimwith a sensible default and read it viaplugin_manager#core#util#get_config(). - Add or update Vader tests for new logic - test-first whenever practical: write the failing test before the implementation, and before the fix for bug fixes (regression test). When not practical (docs, CI plumbing, smoke fixtures), the PR must justify it. Prefer tests that do not require network access (mock with local fixtures).
- Update documentation (
README.md,doc/plugin_manager.txt,CHANGELOG.md) when behavior changes.
Use Conventional Commits: type(scope): subject.
Valid types:
feat:new features (prefer over historicalfeature:)fix:bug fixesdocs:documentation changestest:test additions or changesrefactor:code refactoringstyle:formatting changeschore:routine maintenanceci:CI/CD workflow changesbuild:build system or dependency changes
Scopes (optional, recommended when relevant):
core, async, ui, git, cmd, api, github, gitlab, deps.
Examples:
feat(git): add non-blocking status via async fetch
fix(core): implement missing s:check_log_rotation function
ci(github): bump checkout to v6 and cache to v5 for Node 24
test: add headless test-ci target with clean Vader output
docs: document GitFlow branching and Conventional Commits
Only commit when explicitly requested.
GitHub Flow + tags. One protected branch (main) and release tags; no
develop branch, no vX.Y maintenance branches. The ruleset "main-protection"
requires the ci-green check, resolved conversations, and forbids force
pushes and deletions; PRs are squash-merged (one commit per PR). Maintainers
bypass the ruleset.
main-- stable code, taggedvX.Y.Zfor releases. Releases are tags onmain; hotfixes are prioritizedfix/*branches targetingmain.- External contributors: fork +
feature/*,fix/*,docs/*,chore/*branch + pull request (template + Copilot review + maintainer review + squash merge). One branch per topic, rebased onmainwhen asked. - Maintainers: local
feature/*/fix/*/chore/*branches merged with--no-ff(preserves branch topology), pushed directly (bypass).
Multiple agent sessions may work on this repository. These rules exist because two sessions mutating one working tree caused: commits landing on a foreign branch (the checkout changed under the first session), release tags created before the merge strategy was known (a squash merge orphaned them), and a PR merged while the other session was still planning its own push.
- Worktree by default: any session whose work will produce commits
starts in its own worktree:
(run from the main clone; branch naming per CONTRIBUTING:
git worktree add ~/projets/wt/vim-plugin-manager/<topic> -b <topic>
fix/*,feat/*,chore/*). All work, commits and pushes happen there. The main clone stays onmainand is never mutated by sessions. One worktree = one session. - Worktree lifecycle: push and PR integration happen from the
worktree; when the work is merged, clean up with
git worktree remove ~/projets/wt/vim-plugin-manager/<topic>and delete the branch. - Verify before acting: before any commit, run
git branch --show-current,git statusandgit reflog -5. Re-verify after every pause. - Tags only after merge: release tags are created on the exact
maincommit AFTER the merge lands, never before. A squash merge rewrites history: tags created on pre-merge commits become orphans. - Dependabot PRs: dependabot prepares (grouped PRs, rebases, cooldown),
the maintainer validates:
gh pr review --approvethen merge. No auto-merge: the rulesetrequire_extra_approval_for_unattributed_changesis the guardrail, the human approval is the policy.
Releases are automated via .github/workflows/release.yml:
- Commit all changes and ensure the working tree is clean before tagging.
- Update
CHANGELOG.mdfor the new version (the single source of truth). - Per-file
Version:headers are not maintained - the canonical version is the Git tag and CHANGELOG entry. - Tag and push:
make tag VERSION=vX.Y.Z(creates an annotated tag thatgit push --follow-tagswill carry to all three remotes). Manual equivalent:git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin main --follow-tags. Push one tag at a time: pushing multiple tags simultaneously can cause an older tag's release workflow to finish last, making GitHub mark it as "Latest" instead of the newest version. - Pushing a
vX.Y.Ztag to GitHub triggers a job that runsmake archiveand publishes a GitHub Release with the.tar.gzasset and auto-generated notes.