Resident skill-description floor — committed baseline (#5444)
Part of epic #3229 (shrink the always-sent context budget). This is the
userland sibling of MCP tool-schema floor: that page pins the
floor fak’s MCP server advertises, this one pins the floor fak’s
.claude/skills catalog carries.
What this number is
Every skill under .claude/skills/*/SKILL.md declares a frontmatter
description. The skills index holds every one of those descriptions at rest, so
the resident tax of the catalog is the sum of the description fields, and it
grows linearly with skill count — one skill at a time, each addition invisible to
its own author.
fak skill footprint prices that floor offline and deterministically from the same
shipped capindex.SkillResolver cards the fak skill verbs read, so the number can
never drift from what the catalog actually holds. The fold lives in
internal/skillfootprint (Fold / Measure) alongside the gate, so the scorecard
and the ratchet are one measurement rather than two estimators.
Regenerate at any time:
fak skill footprint # human table, heaviest-first
fak skill footprint --json # schema fak-skill-footprint/1
fak skill footprint --top 8 # just the heaviest N
go test ./internal/skillfootprint # the enforcing test; -v logs the same figures
Baseline (measured)
skill footprint [interactive]: 58 skill(s); resident floor = 47236 bytes (~11809 tokens);
description floor = 47236 B; name-only floor = 787 B; at-rest card floor = 14196 bytes
at-rest intent slice (#5560): 10564 B (~2641 tokens) across 58 skill(s)
Heaviest resident descriptions — the trim targets:
| rank | bytes | skill |
|---|---|---|
| 1 | 1877 | study-repo |
| 2 | 1599 | resume-watchdog-audit |
| 3 | 1471 | scout-loop |
| 4 | 1359 | super-loop |
| 5 | 1332 | field-borrow |
| 6 | 1270 | trajectory-control |
| 7 | 1231 | disambiguation-score |
| 8 | 1223 | stability-score |
The full 58-skill breakdown is what fak skill footprint --top 0 prints; only the
head is pinned here so a drift is legible in review.
name-only floor = 787 B is the size of the headroom: 46.4 kB of the 47.2 kB
resident floor is description prose, and every skill stays invocable by name
without a single byte of it.
Provenance (Law A2 — every value carries its provenance)
This floor is denominated in bytes of frontmatter description text, as parsed
by internal/capindex’s SkillResolver. Two things follow, and neither may be
quietly dropped when the number is quoted:
- The
~11809 tokensfigure is an ESTIMATE, at the house ~4 bytes/token divisor (skillfootprint.BytesPerTokenEstimate, the same walk asEstimateAnthropicTokens). It is not a provider-billed count and must never be compared against one. - The byte floor is fak’s MODEL of the resident index, the
interactiveprofile #3234 defined — not a witnessed on-the-wire measurement. Harness skill listings have been observed rendering project skills name-only while built-in skills carry their full descriptions, which would put the on-the-wire project-skill cost nearer the 787 B name floor than the 47.2 kB description floor. Confirming what a given harness build actually ships is open follow-on work; the ratchet is worth holding either way, because it is fak’s own committed scorecard and because pinning today’s floor is what turns a later trim into a bankable win instead of headroom for the next skill.
The gate (#5444)
Measuring the floor does not keep it lean. #3234 shipped the measurement and closed; in the twenty days that followed, the measured floor grew from 36,237 B to 47,236 B (+30.4%) with nothing opposing it. A number that cannot refuse a change is taste, and taste lost 30% in three weeks.
internal/skillfootprint.CheckDescriptions gates the measured floor against a
committed ceiling, SkillDescriptionBudgetBytes (currently 47236), as a one-way
ratchet:
| Direction | Reason | What it means |
|---|---|---|
| measured > budget | SKILL_DESC_BUDGET_EXCEEDED |
a new skill, or a fattened description, grew the resident tax |
| measured < budget − 2000 | SKILL_DESC_BUDGET_STALE |
a trim won headroom that was never banked into the constant |
Both tokens are registered in dos.toml [reasons], so dos_check_reason
SKILL_DESC_BUDGET_EXCEEDED resolves them as known, refusable gates rather than
UNCLASSIFIED free-text drift.
How to justify growth. Raise SkillDescriptionBudgetBytes in the same commit
as the skill description that grew it, and re-pin the baseline block above. That is
the whole mechanism: the new resident tax becomes a diff line a reviewer sees, bound
to its cause, instead of being discovered a month later as a 30% regression. Prefer
trimming the description first — the skill is still invocable by name, so the
resident prose only has to say when to load it, not what it does.
The 2000-byte slack is roughly one heavy skill description (a shade over 4% of the
floor). It absorbs incidental churn — a reworded trigger, a retitled skill — while
still forcing a real reduction to be banked, the same discipline
internal/pythongate applies to the tools/*.py baseline: the ratchet only ever
tightens. The gate fails closed: an unreadable or empty skills tree folds to 0
bytes and refuses as SKILL_DESC_BUDGET_STALE rather than greening on a measurement
of nothing.
Witness
internal/skillfootprint.TestMeasureReadsTheRealSkillsTreeprices the real.claude/skillstree and asserts the floor is a faithful partition of the per-skill rows and non-trivial — the numbers above are reproducible, not hand-typed.internal/skillfootprint.TestSkillDescriptionBudgetPassesAtHEADis the enforcing test: the tree as shipped must pass its own committed budget.TestSkillDescriptionBudgetRefusesGrowthwitnessesSKILL_DESC_BUDGET_EXCEEDEDfiring on a catalog grown by one fat description;TestSkillDescriptionBudgetDemandsBankedWinwitnessesSKILL_DESC_BUDGET_STALErefusing an unbanked trim;TestSkillDescriptionBudgetBandBoundariespins the exact admit/refuse edges;TestSkillDescriptionBudgetFailsClosedpins the fail-closed posture.TestCommittedSkillBudgetMatchesMeasuredFloorproves the ceiling above is a measurement, not a hand-typed number that drifted from the tree.TestSkillDescBudgetReasonsRegisteredInDosTomlproves both tokens are declared indos.toml [reasons]as refusals, andTestBaselineDocPinsTheCommittedCeilingproves this page carries the same number the constant does.- For the card split (#5560), in
internal/capindex:TestResidentCardCarriesIntentNotFullDescriptionproves the serialized card holds the intent line and nodescriptionfield;TestFaultStillPagesTheWholeSkillBodyproves the elided prose is byte-for-byte recoverable from the faulted body;TestShrinkingTheRankingKeyCostsSelectionQualityis the A/B above and FAILS if a shrunken ranking key ever stops costing selection quality, so the trade is re-decided with evidence rather than silently;TestExplicitFrontmatterIntentWinspins theintent:override end to end.
The card split (#5560) — and why it does NOT move this number
capindex.CapCard now separates the two copies of the prose that used to be one
field (skill_resolver.go):
| field | who reads it | cost of a byte |
|---|---|---|
Intent |
serialized into CardBytes; what a listing renders |
a resident token, paid every session |
Trigger |
Catalog.scoreCard, contextq, selfquery’s ranker |
recall — it never leaves the process |
Intent is the leading sentence of the description, capped at
capindex.SkillIntentMaxBytes (320 B), overridable per skill with a frontmatter
intent: line. The full prose is still reachable two ways: as the ranking key, and
in the SKILL.md body that Fault pages in on selection. It is a residency split,
not a deletion.
Measured effect: the at-rest card floor falls 51,409 B → 14,196 B (−72%).
The ranking key was measured, not assumed. #5560 asked whether Trigger may
shrink too. capindex.TestShrinkingTheRankingKeyCostsSelectionQuality builds 56
probes from the corpus itself — rare terms that appear only in the tail of a
skill’s own description — and ranks them through the shipped Catalog.RankCards:
| ranking key | top-1 | probes matching nothing at all |
|---|---|---|
| full description (shipped) | 54/56 (96%) | 0 |
| leading sentence only | 4/56 (7%) | 27 |
So Trigger keeps the full description. Selection quality, not byte count, decides
that field. TestEverySkillStaysInvocableByName pins the other half: name
addressability survives even with no trigger at all, because scoreCard weights a
name match above a trigger match.
This is why SkillDescriptionBudgetBytes is unchanged at 47,236. The gated
DescFloor is the sum of the frontmatter description fields — the prose a harness
renders into the always-on skill listing, and the thing #5444 exists to refuse the
growth of. The card split moved where fak serializes that prose; it did not delete a
byte of frontmatter. Re-pinning the ceiling down onto the derived intent slice would
bank a win that was never won, and would blind the ratchet to a description that
doubles in its tail — the exact growth it was built to catch.
What is still NOT done
- The userland description migration — #3234’s undelivered item 3, and the only
lever that moves the 47,236 B number: shortening the frontmatter
descriptionfields themselves.score-2xcarries the worked example of the migration — oneintent:line added, itsdescriptionuntouched, so the gated floor is byte-identical and only the at-rest card shrank. The 5 skills whose leading sentence still overruns the 320 B cap and therefore elides (curate-cluster,issue-triage,sota-check,trajectory-audit,wave-harvest) are named bycapindex.TestResidentIntentInventoryand are the next adopters. Sweeping all 58 is deliberately not done here. - A witnessed on-the-wire count — see the provenance caveat above. Which slice a given harness build actually ships is still unconfirmed, and the card split does not change that.
Cross-links
- #3229 — epic: shrink the always-sent context budget.
- #3234 — the measurement this ratchet defends (
fak skill footprint). - #3612 — the
headlessname-only profile (the 787 B floor above). - #5560 — the
capindexresidency split above (IntentvsTrigger). - MCP tool-schema floor — the systemic sibling, with the
FLOOR_BUDGET_*/DESC_BUDGET_*ratchets this one is modelled on.