Check only what you changed, the same way in your editor, your commit hook, and your AI agent.
sley ready selects the files in your current change, not the whole
repository, and runs these phases on just those files:
- lint and validation through checkrun,
which picks the tool for each file type from the ones already on your
PATH; - a redacted secret scan through gitleaks;
- the verification commands your repository marks as required in
.sley/verify.json, plus Checkrun's project analyzers (checkrun verify) when they apply.
sley fix formats the same files, and sley ready --fix formats before it
checks. sley verify also lists test commands it finds in manifests such as
package.json, Cargo.toml, go.mod, Makefile, and justfile, but those
are suggestions: sley ready runs only required commands.
The same entry point backs the shipped Git and Sapling commit hooks, Neovim and VS Code save-time adapters, and AI-agent edit hooks such as agentguard. Unlike pre-commit, lefthook, or lint-staged, there is no per-repository hook list or tool-version pinning to maintain for formatting and linting, and editors, hooks, and agents share one definition of "ready to commit".
Install with a checkout-backed curl bootstrap:
curl -fsSL https://raw.githubusercontent.com/cgraf78/sley/main/install.sh | bashThis keeps a durable managed checkout under $XDG_DATA_HOME when that path is
absolute, or under $HOME/.local/share otherwise, and publishes its command
and manual-page links. It does not use a release asset or copy a second runtime
tree. Git and Bash 4 or newer are required; the supported Bash must be the first
bash on PATH.
Sley drives other tools, so install these too (details under Dependencies):
jqandpython3;- checkrun for
fixandcheck, which needs Python 3.11 or newer,yqv4, and the formatters and linters you want it to run; gitleaksforsley secrets.
Install them before you activate the commit hook: while checkrun, yq, or
gitleaks is missing, the hook blocks every commit.
To choose and manage the checkout yourself instead:
git clone https://github.com/cgraf78/sley.git
cd sley
bash install.shKeep that checkout in place. The installer creates a sley command symlink
under $HOME/.local/bin and a manual-page symlink under
$HOME/.local/share/man/man1. Set PREFIX to relocate both trees, or set
BIN_DIR and MAN_DIR independently. Re-running either installation path is
safe and retargets existing symlinks, but it refuses to replace a non-symlink
path. Rerunning the curl command first safely fast-forwards its clean managed
checkout. Moving or deleting a manually managed checkout breaks its installed
links.
The command resolves lib/sley/ from the checkout. The installer leaves that
library and share/sley/ checkout-owned and creates no completion tree. Sley's
Bash and zsh completion functions remain in share/sley/shell.sh; source that
loader by its absolute path in the checkout, or through
shdeps if you use it. Git and
Sapling hook activation likewise continues to select the provider-owned
launchers under share/sley/hooks/ explicitly.
sley status # repo type, root, ref, and dirty counts
sley changes # changed-file listing
sley fix # mutating format phase
sley check # read-only lint/validation phase
sley secrets # redacted secret scan where supported
sley verify # local verification command discovery and execution
sley ready # aggregate pre-submit readiness report
sley hook # low-level editor, agent, and VCS hook plumbing
For example, take a Git repository whose .sley/verify.json requires
make test for Python files:
{
"rules": [
{ "paths": ["*.py"], "commands": [{ "cmd": "make test", "kind": "test", "required": true, "tier": "fast" }] }
]
}After adding an unused import to calc.py, sley ready checks only that file
and exits 1 because the lint finding blocks:
$ sley ready
sley ready: selecting changed files
sley ready: selected 1 changed files
sley ready: running phases: status, check, secrets, verify
status: 0 (pass)
repo: git
root: /tmp/sley-demo
ref: main
dirty: true
check: 1 (blocking)
F401 [*] `os` imported but unused
--> calc.py:1:8
|
1 | import os
| ^^
help: Remove unused import: `os`
|
- import os
1 |
|
Found 1 error.
[*] 1 fixable with `--fix`.
secrets: 0 (pass)
sley secrets: scanned 1 file
verify: 0 (pass)
sley verify: running required fast command: make test
sley verify: selected changed files:
calc.py
sley verify: required verification commands:
fast: make test
python3 -c "import calc; assert calc.add(2, 2) == 4"
sley verify: exit code: 0
sley verify reports where each suggested command came from. Human output labels
sources such as ancestor: sub/package.json, repo-root: package.json, and
builtin: checkrun verify; sley verify --json includes the same data in
source_contexts.
Registry rules and individual command objects may set "enabled": false to
temporarily suppress entries without deleting their configuration.
The default scope follows the active change context, not the current working
directory. Use --path ., --path PATH, --repo-wide, and
--include-untracked to override that scope.
No default sley command formats or checks the whole repository. In large
repos, the active diff is the useful project signal even when commands are run
from the repo root.
To run the gate on every commit in a Git repository, point Git at Sley's
shipped hooks. For the curl installation, the checkout is
${XDG_DATA_HOME:-$HOME/.local/share}/cgraf78/checkouts/sley; for a manual
clone, use its path instead:
git config core.hooksPath "${XDG_DATA_HOME:-$HOME/.local/share}/cgraf78/checkouts/sley/share/sley/hooks/git"core.hooksPath replaces the repository's .git/hooks directory, so any hooks
there stop running. The hooks run sley ready --fix --commit on the staged
files for ordinary commits, merge commits, git am, and the commits that
cherry-pick, revert, and rebase replay, and they secret-scan each commit
message. If formatting changes a staged file, the commit stops and asks you to
stage the result. Sapling users and anyone composing hooks with other tools
should read share/sley/hooks/README.md.
Further reading: docs/workflow-contract.md
describes how Sley, Checkrun, and editor adapters divide responsibilities, and
examples/README.md has copyable configuration and
hook-policy examples.
User-managed files under $XDG_CONFIG_HOME/sley/lint-ignore.d/*.paths, falling
back to ~/.config/sley/lint-ignore.d/*.paths, can keep selected files out of
Sley's lint dispatch. This is user-global policy: every configured prefix is
considered in every repository, so choose paths specific enough to avoid
unintended matches elsewhere. Each nonempty, non-comment line is a literal path
relative to the current repository root. It matches that exact path and
descendants separated by /; an entry for vendor/generated does not match
vendor/generated-old. Leading ./ and trailing / are normalized. Other
whitespace is literal, and comments must occupy a whole line. Absolute paths,
repository roots, empty path components, and . or .. traversal are rejected
with a warning and never broaden the ignored set.
# Generated elsewhere and linted by CI.
vendor/generated
web/legacy
Lint exclusions apply consistently to sley check, the check phase of
sley ready, and the sley hook lint and sley hook lint-file entry points.
Filtering happens before dispatch to the configured lint provider, including a
provider supplied by an extension. Sley reports the number of excluded files;
if every selected file is excluded, it does not start the lint provider.
This policy is deliberately lint-only. The original selected file set still
drives formatting, status, secrets, validation, and verification. Directly
invoking the underlying lint provider also remains unfiltered, so repository CI
and explicit provider commands retain their own coverage. Run sley check --no-lint-ignore or sley ready --no-lint-ignore to lint every selected file,
or set SLEY_LINT_IGNORE=0 for low-level hook commands. Set
SLEY_LINT_IGNORE_DIR to select an explicit configuration directory.
See examples/lint-ignore.d/10-generated.paths
for a copyable configuration. The examples test executes that file through the
production parser so its documented behavior cannot drift silently.
sley fix --commit formats commit-input files: staged files in Git and pending
files in Sapling. In Git, it formats worktree files but does not update the
index. If formatting changes a staged file, it reports the file and tells the
caller to run git add. If a selected file has both staged and unstaged
changes, it refuses the whole operation to preserve partial-staging intent.
- Bash for the CLI entry point and sourceable API.
gitorsldepending on the repository type being inspected.python3for required-check receipt caching and command grouping.jqfor JSON output, verify registries, and ready JSON summaries. A few manifest hints have limited fallback parsing withoutjq, but verify registry files require it.cgraf78/checkrunis the default formatting and linting backend. Base Sley invokes the PATH-visibleautoformatandautolintCLIs forsley fix,sley check, andsley hookcommands unless an extension overrides the hook policy. Checkrun itself requiresyqv4.gitleaksis required forsley secrets.
Optional workflow commands such as pytest, cargo test, go test, make,
just, and buck2 are discovered from project manifests or verify registries
and are only required when a selected rule asks Sley to run them. Low-level
formatters, linters, type checkers, and project/security analyzers such as
ruff, mypy, actionlint, zizmor, cargo-audit, and govulncheck belong
behind Checkrun or an explicit verify registry command. Base Sley adds a
required checkrun verify -- <changed-files> command when checkrun is
available, so generic Checkrun-owned analyzers are selected automatically from
the same changed-file scope as the rest of readiness. Keep repo-specific
workflow commands in verify registries.
Consumers that want Sley to operate on a bare Git worktree can set the standard
GIT_DIR and GIT_WORK_TREE environment variables before invoking it. Set
SLEY_SKIP_UNTRACKED=1 when the worktree is large and untracked-file discovery
would be too expensive for status or readiness checks; it also leaves untracked
files out of sley verify cache keys.
For shells and editor integrations that want the PATH-visible sley command to
fall back to a bare Git worktree when no normal repo owns the current directory,
Sley also exposes an optional fallback contract:
export SLEY_BARE_REPO_GIT_DIR="$HOME/.my-bare-git-dir"
export SLEY_BARE_REPO_WORK_TREE="$HOME" # optional; defaults to $HOMEThe fallback is off unless SLEY_BARE_REPO_GIT_DIR is set. When the current
directory is inside SLEY_BARE_REPO_WORK_TREE, the launcher exports GIT_DIR,
GIT_WORK_TREE, and SLEY_SKIP_UNTRACKED=1 before running the shared Sley API.
Editor probes that need to ask only "is this a normal repo?" can set
SLEY_SKIP_BARE_REPO_FALLBACK=1 to ignore an inherited fallback.
New integrations should source lib/sley/sley.sh from the checkout and call
public sley_* functions. With shdeps, resolve the path through it:
. "$(shdeps dep-file cgraf78/sley lib/sley/sley.sh)"bin/sleyis the PATH-visible CLI.lib/sley/sley.shis the sourceable Bash API.lib/sley/nvim.luais the optional Neovim adapter API. It does not depend on shdeps or local editor policy; callers pass command paths and policy callbacks explicitly when they do not want the default PATH-visiblesley. Its diagnostic parser consumes the JSON-lines contract emitted by Checkrun-backedsley hook lint-file --json; Checkrun owns that diagnostic schema so editor integrations share one producer contract.share/sley/shell.shis the sourceable interactive shell loader.share/sley/schemas/verify.schema.jsonis the JSON Schema forsley verifyregistry files.share/sley/hooks/contains provider-owned Git and Sapling commit launchers. Consumers activate those executables from their VCS configuration or hooks directory;SLEY_GIT_COMMIT_GATEmay name one consumer-owned wrapper around the final Git readiness gate without copying Sley's sequencer policy. The Gitcommit-msglauncher also exposes the optional template and executable message-validator providers documented in that directory.share/sley/vscode/sley-tools-0.0.1/is the deployable VS Code extension payload. Consumers register that versioned directory directly; Sley owns its formatting, diagnostics, capability-discovery, and lifecycle behavior.sley_selectresolves repo and scope context.sley_hook_format_fileandsley_hook_lint_fileare the narrow save-time hook APIs.sley_hook_validate_messageandsley hook validate-messagevalidate one proposed commit message through either a structural template or an external executable. They do not choose policy or require repository detection.sley ready --fix --quiet --commitis the commit-gate API used by agent hooks. With--commit, acheckorsecretsphase that cannot run (exit 2, such as a missing gitleaks or a broken linter config) fails the gate instead of being reported as unavailable; outside the gate those phases stay advisory.SLEY_ALLOW_UNAVAILABLE=check,=secrets, or=check,secretson the commit command (for exampleSLEY_ALLOW_UNAVAILABLE=secrets git commit, orsl commitfor Sapling) tolerates that phase's exit 2 and reports it as bypassed, but only when the phase confirms it reported no findings. Findings always block, even when exit 2 outranks them, as does an exit 2 the phase cannot vouch for (a lint hook extension running its own linter, an older Checkrun, or a secret scanner that ran and failed); other phases still gate. The Gitcommit-msgsecret scan honors the same value, and it covers every commit the one command creates, such as each commit a rebase replays._sley_shell_completeand_sley_zsh_completeare the Bash and zsh completion functions installed by the shell loader.SLEY_VERIFY_SCHEMAis exported by the shell loader as the absolute path to the verify-registry schema;sley_verify_schema_pathprints the same value for consumers that prefer a function API.SLEY_BARE_REPO_GIT_DIR,SLEY_BARE_REPO_WORK_TREE, andSLEY_SKIP_BARE_REPO_FALLBACK=1are the optional bare-repo fallback API for PATH-visible CLI integrations.- zsh completion registration defers until
compinitis available, so shell startup files may source the loader before their zsh completion phase. - Native VCS hooks delegate mechanical checks to
sley ready --fix --commit.
sley verify discovers and runs local workflow commands from manifests,
verify registries, the built-in Checkrun verify bridge, and Sley extensions. It
is intentionally separate from Checkrun's formatter, linter, diagnostic, and
project analyzer policy. Sley passes selected changed files to
checkrun verify; Checkrun decides which generic analyzer tools apply.
Repo-specific workflow commands still belong in Sley's verify registry or
extension API rather than manifest guessing.
Repository-owned verify registries live at .sley/verify.json and
.sley/verify.d/*.json in any ancestor directory of a selected changed file,
up to and including the repository root. Their rules apply to the containing
repository without repo matching, and their path patterns may be relative to
either the repository root or the directory containing .sley. They are
discovered before user registries.
User verify registries live at $XDG_CONFIG_HOME/sley/verify.json and
$XDG_CONFIG_HOME/sley/verify.d/*.json, falling back to the same paths under
~/.config. Set SLEY_VERIFY_CONFIG to add one explicit registry file or
directory before those defaults. Success receipts live under
$XDG_CACHE_HOME/sley/verify, falling back to ~/.cache/sley/verify; set
SLEY_VERIFY_CACHE_DIR to choose an explicit cache directory.
A receipt's key covers the selected files' content, the base identity chosen
by cache.base_policy, the committed versions of files outside the selection
(Git), and the names and content of untracked, non-ignored files inside the
path scope (the whole repository unless --path narrows it). With a Git upstream, committing exactly the verified content keeps
the receipt as long as the selection is unchanged. The
selected-content policy limits the key to the selected files. A command that
leaves a changed untracked artifact behind still passes but is not cached, so
ignore such artifacts to make the command cacheable.
Sley uses an XDG root only when it is non-empty and absolute. A missing, empty,
or relative root falls back to HOME, and resolution fails with an actionable
error when that fallback is required but HOME is unavailable. Explicit
SLEY_* paths retain their exact value, including relative paths.
sley verify always initializes extension discovery before collecting command
sources. An explicit SLEY_VERIFY_CONFIG does not disable extensions; when no
absolute config root or HOME is available, set SLEY_EXTENSION_DIR explicitly
as well.
The same boundary applies to hooks and editor integrations: Sley should decide repo scope, caller timing, and readiness phase composition, while Checkrun decides filetype inference, low-level tool selection, and diagnostic shape.
sley_select sets SLEY_REPO_TYPE, SLEY_REPO_ROOT, SLEY_CHANGE_SCOPE,
SLEY_INCLUDE_UNTRACKED, SLEY_REPO_WIDE, SLEY_PATH_SCOPE, and
SLEY_SELECTED_FILES.
Hook integrations should call sley_hook_* functions and use SLEY_CALLER and
SLEY_SCOPED for extension context.
The Neovim module is a protocol adapter, not a complete plugin. It translates Sley's public hook commands and JSON diagnostic contract into common Neovim plugin shapes while leaving policy in the consuming config.
local sley_nvim = dofile("lib/sley/nvim.lua")
formatters = {
sley = sley_nvim.conform_formatter({
command = "sley",
}),
}
linters = {
sley = sley_nvim.nvim_lint_linter({
command = "sley",
condition = function()
return true
end,
}),
}Public functions:
conform_formatter(opts)returns a Conform formatter spec forsley hook format-file "$FILENAME".nvim_lint_linter(opts)returns an nvim-lint linter spec forsley hook lint-file --json.parse_diagnostics(output, opts)converts Sley's JSON-lines diagnostics intovim.diagnosticrecords.
Supported options are intentionally small: command, args, condition,
source, default_severity, parser_opts (a complete parse_diagnostics
options table for nvim_lint_linter that replaces source and
default_severity), and pass-through plugin fields. The module does
not choose filetypes, configure keymaps, resolve shdeps dependencies, inspect
workspace roots, or decide local disable flags.
Environment-specific hook policy lives in ordered files under
$XDG_CONFIG_HOME/sley/extensions.d/*.sh when XDG_CONFIG_HOME is absolute,
or ~/.config/sley/extensions.d/*.sh otherwise. These files may override the
documented sley_hook_* functions. Set SLEY_EXTENSION_DIR when an integration
needs to source extensions from a test fixture or another managed config
location.
Extensions that override sley_hook_lint or sley_hook_lint_file receive the
already-filtered file set when Sley invokes them through check, ready, or
the hook CLI. Lint-ignore policy therefore belongs in the declarative config
directory rather than being reimplemented by individual providers.
Extensions may also provide sley_ext_verify_commands <files> to print additional
sley verify command items as JSON lines. Those items use the same shape as
verification registry commands, including required, tier, and optional
cache metadata, so sley ready can run environment-specific checks without
base sley knowing about those tools.
Sley runs each readiness phase in a managed process group and contains ordinary descendants that remain in that group. Extension hooks and phases must remain in that managed process group through completion and cancellation. Code that daemonizes, creates a new session, or otherwise leaves the group is unsupported; cleanup remains the extension author's responsibility.
bin/sleyis the self-contained CLI entry point.repo.showns VCS primitives.scope.showns scope and file selection.hooks.showns hook APIs.verify.showns local verification discovery.ready.showns aggregate readiness orchestration.share/sley/vscode/owns the versioned VS Code integration payload and its consumer contract.
Sley does not define Neovim user commands. Consuming configs may wrap
sley status, sley check, or sley ready in their own commands for
human-facing repo workflows. Save-time formatting and diagnostics use the same
hook APIs as agent and VCS hooks.
MIT. See LICENSE.