Skip to content
cgraf78Public

About

Diff-scoped lint, format, secret-scan, and test gate for Git/Sapling hooks, editors, and AI agents

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

104 Commits

Folders and files

Repository files navigation

sley

Tests License Bash Platform

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".

Installation

Install with a checkout-backed curl bootstrap:

curl -fsSL https://raw.githubusercontent.com/cgraf78/sley/main/install.sh | bash

This 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):

  • jq and python3;
  • checkrun for fix and check, which needs Python 3.11 or newer, yq v4, and the formatters and linters you want it to run;
  • gitleaks for sley 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.sh

Keep 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.

Commands

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.

Commit hook

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.

Lint exclusions

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.

Dependencies

  • Bash for the CLI entry point and sourceable API.
  • git or sl depending on the repository type being inspected.
  • python3 for required-check receipt caching and command grouping.
  • jq for JSON output, verify registries, and ready JSON summaries. A few manifest hints have limited fallback parsing without jq, but verify registry files require it.
  • cgraf78/checkrun is the default formatting and linting backend. Base Sley invokes the PATH-visible autoformat and autolint CLIs for sley fix, sley check, and sley hook commands unless an extension overrides the hook policy. Checkrun itself requires yq v4.
  • gitleaks is required for sley 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.

Bare Git worktrees (advanced)

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 $HOME

The 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.

Public API

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/sley is the PATH-visible CLI.
  • lib/sley/sley.sh is the sourceable Bash API.
  • lib/sley/nvim.lua is 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-visible sley. Its diagnostic parser consumes the JSON-lines contract emitted by Checkrun-backed sley hook lint-file --json; Checkrun owns that diagnostic schema so editor integrations share one producer contract.
  • share/sley/shell.sh is the sourceable interactive shell loader.
  • share/sley/schemas/verify.schema.json is the JSON Schema for sley verify registry 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_GATE may name one consumer-owned wrapper around the final Git readiness gate without copying Sley's sequencer policy. The Git commit-msg launcher 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_select resolves repo and scope context.
  • sley_hook_format_file and sley_hook_lint_file are the narrow save-time hook APIs.
  • sley_hook_validate_message and sley hook validate-message validate 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 --commit is the commit-gate API used by agent hooks. With --commit, a check or secrets phase 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,secrets on the commit command (for example SLEY_ALLOW_UNAVAILABLE=secrets git commit, or sl commit for 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 Git commit-msg secret scan honors the same value, and it covers every commit the one command creates, such as each commit a rebase replays.
  • _sley_shell_complete and _sley_zsh_complete are the Bash and zsh completion functions installed by the shell loader.
  • SLEY_VERIFY_SCHEMA is exported by the shell loader as the absolute path to the verify-registry schema; sley_verify_schema_path prints the same value for consumers that prefer a function API.
  • SLEY_BARE_REPO_GIT_DIR, SLEY_BARE_REPO_WORK_TREE, and SLEY_SKIP_BARE_REPO_FALLBACK=1 are the optional bare-repo fallback API for PATH-visible CLI integrations.
  • zsh completion registration defers until compinit is 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.

Neovim Adapter API

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 for sley hook format-file "$FILENAME".
  • nvim_lint_linter(opts) returns an nvim-lint linter spec for sley hook lint-file --json.
  • parse_diagnostics(output, opts) converts Sley's JSON-lines diagnostics into vim.diagnostic records.

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.

Extension Policy

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.

Implementation Layout

  • bin/sley is the self-contained CLI entry point.
  • repo.sh owns VCS primitives.
  • scope.sh owns scope and file selection.
  • hooks.sh owns hook APIs.
  • verify.sh owns local verification discovery.
  • ready.sh owns 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.

License

MIT. See LICENSE.

About

Diff-scoped lint, format, secret-scan, and test gate for Git/Sapling hooks, editors, and AI agents

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages