kendex.ai

Marketplaces / vanillagreencom/kendex / size-ratchet

size-ratchet

Tighten-only file-size gate: tracked files over their threshold (default 400, per-class via SIZE_RATCHET_CLASSES) are frozen in a baseline TSV that only moves down. Load to add, tune, or debug the ratchet, its baseline, or SIZE_RATCHET_* settings.

skill · automation · safety 100/100 (clean) · @ a7ca9af

Install in kendex: kendex add --skill size-ratchet after subscribing to vanillagreencom/kendex.

Size Ratchet

Problem with this skill? Run kendex report — it files to the owning repo automatically. Do not hand-file.

One check, one direction: no tracked file gets bigger than its threshold, and the files already over it only shrink. Existing offenders are frozen in a baseline at their current line counts; everything else must stay at or under the threshold its path class carries. The baseline is a ratchet, not a ledger: rows only go down or away, and the only way a number goes up is a human editing the row in a reviewed diff.

.agents/skills/size-ratchet/scripts/size-ratchet            # check (pre-PR / CI)
.agents/skills/size-ratchet/scripts/size-ratchet --staged   # check what a commit records (git hook)
.agents/skills/size-ratchet/scripts/size-ratchet --update   # tighten the baseline
.agents/skills/size-ratchet/scripts/size-ratchet --seed     # write the FIRST baseline

--staged judges the commit's snapshot: index blobs, and index policy. Details in DEVELOPMENT.md.

Verdicts

check scans every tracked file (git ls-files) minus the exclusion list and fails (exit 1) on any of:

FailureMeaning
new offenderOver its threshold with no baseline row.
baselined file grewActual lines exceed the file's baseline row.
baseline looser than realityA row higher than the file's actual count, a row for a file now at/under its threshold, or a row for a file no longer tracked (or now excluded). Slack in the baseline is itself a failure — the ratchet must move down.

Every diagnostic names the file, its count and the baseline row it violated, and — wherever a threshold decided the verdict — that threshold and whether it came from a class pattern or the default, plus the remedies: split at a concept seam, or raise the baseline row in this diff with justification.

Exit codes: 0 clean, 1 violations, 2 usage/config/collection error (malformed baseline or excludes, bad threshold, a tracked path containing a tab or newline, or a file the gate could not measure). Line counts are newline counts (wc -l). A tracked file absent from the worktree (unstaged deletion, sparse checkout) is counted from the INDEX blob — a partial tree can neither smuggle a new offender past the gate nor loosen a baselined row; an index blob that cannot be read is a collection error (exit 2, naming the file), never a skip. A submodule gitlink at a tracked path is not a countable file.

--update — tighten only

--update rewrites the baseline to current reality in the downward direction only: rows are lowered to the actual count or removed (file shrank to/under its own threshold, was deleted, or is now excluded). It never adds a row and never raises a number — a grown file keeps its old row and keeps failing, and a new offender stays a failure. Deliberate growth or a new freeze is a hand-edit of the baseline TSV, visible in the diff. After the rewrite the check re-runs, so --update exits 1 while growth or new offenders remain.

--seed — bootstrap only

--seed writes the FIRST baseline from the gate's own collector: every tracked, non-excluded file over its deciding threshold enters at its current count, sorted, with a self-row when the baseline outgrows its own threshold. A baseline that already has rows in the worktree, the index or HEAD refuses — the ratchet is live there, and growth stays a reviewed hand-edit. Commit the seeded file.

Configuration

Resolution order for every key: explicit environment > .env.local (personal, untracked) > .kendex/settings.toml > the repo's committed kendex.settings.toml (flat KEY = "value" under [env]) > .env > built-in default. Only an ABSENT source is skipped: a source that exists but is unusable — unreadable, a directory, FIFO, socket or device, or a symlink that does not resolve — is a config error (exit 2), never a fall-through to the next layer. SIZE_RATCHET_SETTINGS_FILE=/dev/null selects no settings source at all (.env.local, the settings file and .env are all skipped), leaving environment variables and the defaults.

KeyDefaultMeaning
SIZE_RATCHET_THRESHOLD400Line threshold for paths matching no class.
SIZE_RATCHET_CLASSES(none)pattern=threshold entries separated by ;, first match wins.
SIZE_RATCHET_BASELINEtools/size-ratchet-baseline.tsvBaseline path (also --baseline FILE).
SIZE_RATCHET_EXCLUDEStools/size-ratchet-excludesExclusion-list path (also --excludes FILE).

Path classes — a file's threshold is the first SIZE_RATCHET_CLASSES pattern it matches, else SIZE_RATCHET_THRESHOLD. Patterns are the excludes file's globs and every other semantic is per file, so a class only moves the number a path is judged against:

SIZE_RATCHET_CLASSES = "tests/*=800;*/tests/*=800;*.test.*=800"

A directory name needs both forms: */tests/* requires a slash-delimited prefix, so a root-level tests/ matches only tests/*.

A malformed entry (no =, an empty pattern, a non-positive-integer threshold) is a config error naming the entry; an unset or empty value is single-threshold behavior.

Baseline formatpath<TAB>lines, LC_ALL=C sorted, unique paths, counts above the path's threshold. Excludes formatpattern<TAB>reason per line (shell glob against the full repo-relative path; * crosses /); blank lines and # comments are ignored, and a pattern without a reason is a config error — every exclusion carries its justification (generated, vendored, fixtures, lockfiles).

Formats, path classes, and seeding a first baseline: README.md. Collection internals and the migration note for repos already using this format: DEVELOPMENT.md.