agentleFS
yannikzzyannikzz/ui-contract/skills/ui-contract-enforce/SKILL.md

ui-contract-enforce

Skill3 starsChanged 17 days agoMITView on GitHub

Install the enforcement that stops a coding agent putting a new feature in the wrong place - an entry clause in the repo-root CLAUDE.md, a directory-level clause, the region-name allowlist compiled into a union type, and an asset boundary forbidding hardcoded token values and locally-declared structural containers. Use once the structural spec exists.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Verify it by breaking it: Put an off-allowlist name into a product file, run the project's typecheck, and paste the error.

    line 69
  2. Very specific:

    Put a line at the top of layout.md §2 saying that changing the table requires changing both of the others.

    line 81

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Enforcement

Produces edits to the repo-root CLAUDE.md and to the type layer. No new document.

For pushing rules into the type system, call the Skill tool with "constraint-by-types". For the interview discipline, call the Skill tool with "decision-interview". For the deliverable's language and terminology, call the Skill tool with "deliverable-language".

Preconditions

Artifact paths come from docs/ui-contract/config.md's paths table. If that file doesn't exist, the specs are at the repo root. Never assume a path the config contradicts.

ConditionAction
layout.md does not existStop. There is nothing to enforce
layout.md §2 has no "must not hold" columnStop, and say why that column is required. It is the only part of the allowlist that becomes a compile-time constraint. Without it, enforcement degrades to "please read the document"
A RegionId-like type exists in code but layout.md does notStop and say so. Enforcing a type that was derived from the code locks in whatever the code already does

There is no wireframe precondition. Enforcement depends on the allowlist, not on anything being drawn — it runs fine in a repo that has never had a wireframe.

The one question this unit asks

How hard should enforcement be?

OptionYou getYou lose
Decision tree up front + CLAUDE.md hard constraint + allowlist as a union type (recommended)Zero extra tooling, and one layer is compile-time—
Plus a validation script or lint ruleHardestWith only a wireframe existing there isn't much to validate yet — easy to build too early
Documentation onlyLightestAgents don't read a document nobody told them to read — least of all when the task sounds small

Two passes, not one

The four layers below do not become installable at the same moment, so this unit runs twice.

PassWhenInstallsWhy it cannot wait / cannot come earlier
1Inside the structural halfEntry and DirectoryBoth depend on layout.md alone. The gap before the visual half can last weeks; during it, anything that touches the interface needs something telling it to read layout.md first, and an unwritten entry clause tells it nothing
2After the asset layer has moved into the production treeCompile time and Asset boundaryBoth are installed on the production source tree. Before the move there is no production tree to install on

Do not install the compile-time layer on a staged asset layer. A union that lives outside the product constrains only what lives beside it. Measured (2026-08-25): in the admin-console example, PageWithInspector, TableColumnKind, InspectorKind and PageKind are all declared in wireframe/src/wire.tsx; every one of them is a layout.md clause, and none of them reaches a line of production code. ui-contract-assets records the same measurement from the other side — kept word-for-word on both sides on purpose, so the two can be compared. It type-checks, it goes green, and it guards a directory that gets deleted.

The layers

Documents alone don't get read — least of all when the task sounds small ("just adding a button"). Miss a layer and things leak through.

LayerHow
EntryA section in the repo-root CLAUDE.md: before changing any interface structure, walk layout.md §1; stop and use the change process if it doesn't land; every region name must be in the allowlist
DirectoryA CLAUDE.md inside the wireframe (if there is one): what to read before touching anything, the deliberate-tradeoffs list, which file is the single source for structural parameters
Compile timeAllowlist → union type, installed on the product source tree and covered by the project's own typecheck. Writing a name that isn't in the table fails type-checking. See below — where this runs is part of the layer, not a detail
Asset boundaryToken values must not be hardcoded in feature code — they come from the token file. Structural containers must be imported from the asset layer, never re-declared locally

The compile-time layer has to sit on code that ships

A union type only fires where something type-checks against it. So "the allowlist is a union type" is half a mechanism; the other half is which typecheck it is part of, and that half has been left implicit.

A wireframe's typecheck does not count. The wireframe is a throwaway reference implementation — constraining it constrains nothing that ships, and if the project keeps its wireframe out of the repository, that typecheck does not run in CI at all. Either way the layer reports nothing and looks no different from one that is working.

Two things have to be true, and both are checkable:

  1. The union type lives in the asset layer, inside the product source tree, and the command a contributor or CI actually runs (tsc --noEmit, pnpm typecheck, whatever the project uses) covers that path.
  2. Something fails even when no product code happens to use a given region name yet. On a young project most of the allowlist has no call sites, so an empty product tree type-checks clean against any union at all — including one somebody weakened. The assertion file in constraint-by-types §2 is what closes this; without it, the layer's strength is proportional to how much code already exists, which is backwards.

Verify it by breaking it. Put an off-allowlist name into a product file, run the project's typecheck, and paste the error. A layer nobody has seen fail is a layer nobody has seen.

The asset boundary is the newest layer and the least obvious. Layering the specs into production code created a new drift surface: the asset layer is real production code, so business pressure pushes changes into it directly — someone adds a slot to a container to ship a feature, and the spec never hears about it. The boundary clause is what makes that visible at review time.

The three forms of the allowlist

FormWhereFor
Table (responsibility / must hold / must not hold)layout.md §2Humans and agents reading
Union typeThe asset layerThe compiler
Metadata (label + description)The asset layerThe annotation layer

Put a line at the top of layout.md §2 saying that changing the table requires changing both of the others.

Before you call this done

  • Region names are identical across the document, the type definition, and the code — grep every allowlist name and find it in all three
  • The repo-root CLAUDE.md entry clause exists and names the decision tree by section
  • Every "must not hold" entry that can be expressed as a type omission has been, with a comment marking the omission deliberate
  • Each of those omissions also has a line in the assertion file (constraint-by-types §2), so deleting the prohibition breaks the build instead of going silent
  • The union type is in the product source tree, and the project's own typecheck command covers it — name the command. A wireframe's typecheck does not satisfy this
  • The assertion file is inside that same typecheck's include. Nothing imports it, so a narrow include drops it silently
  • Enforcement was verified by breaking it: an off-allowlist name was written into a product file, the project's typecheck was run, and the real error is pasted
  • The asset boundary clause is written, and it names both halves (no hardcoded token values; no locally-declared structural containers)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.