cnlp-kit
v3.1.3
Published
CNL-P — a format for documents an agent has to read precisely. The standard, its profiles, and the checker that holds them.
Maintainers
Readme
CNLP-Kit
CNL-P — Controlled Natural Language for Prompts. A format for documents an agent has to
read precisely: agent skills, decision records, repository guides, review checklists, and the
documentation of a library the agent writes code against. It replaces prose with key: blocks,
so a reader stops inferring which sentence is intent, which is policy, and which is a step.
This repository is the format itself — the standard, the profiles, and the checker that holds them — with no tie to any project, language or agent runtime.
cnlp-format.md— the standard. Read it first withnpx cnlp print format; nothing else makes sense before it.profiles/— one profile per kind of document. A profile declares values, never rules.src/cnlp.js— the checker. Node builtins only, no dependencies.bin/cnlp.js— thecnlpcommand.
Install it
Node >= 22.14, and no runtime dependency of any kind — the checker is builtins only, which is what lets any repository run it.
npm i -g cnlp-kit # cnlp on your PATH; the same line upgrades it
npm i -D cnlp-kit # a pinned version, for a repository whose CI runs the checkTake the global one to work on a repository you do not want to add a dependency to, and the
dev-dependency wherever the check has to be reproducible. Both can hold at once: npx cnlp inside a
project with the local install runs that version rather than the global one.
Use it
Two commands. The first one installs and updates:
cnlp init # config, profiles directory, and the skills for the agents you pick
cnlp check # holds every document the corpus names to its profilecnlp init asks once which agents to write skills for — Claude Code, Codex — and remembers the
answer in cnlp.config.json. Run it again after every upgrade of the package: the skills are
versioned with the standard they teach, so init replaces every copy with the installed version.
Nothing else about your config moves on that second run.
--agents claude-code,codexskips the menu, for a script.- With no terminal to draw a menu in — Git Bash under MinTTY, or CI — it repeats the choice the config records, and installs for every agent when there is no config yet.
cnlp checkreports skills left behind by an upgrade, and its exit code stays the corpus's own answer: a stale copy is not a defect in a document.
On the first run cnlp init also looks at what the repository already has — AGENTS.md,
CLAUDE.md, skills/*/SKILL.md, docs/adr/*.md, docs/references/*.md, docs/docsets/*/… — and
writes a corpus entry for each layout that matches a file, naming the count it found. Nothing is
guessed from content: a decision record filed under docs/references/ reads as a reference until
you say otherwise. Edit the entries it wrote, or add your own:
{
"profiles": "cnlp/profiles",
"corpus": [
{ "profile": "skill", "glob": "skills/*/SKILL.md" },
{ "profile": "reference", "glob": "docs/references/*.md" },
{ "profile": "guide", "glob": "AGENTS.md" }
]
}profiles is optional and names a directory of your own profiles. A profile there resolves
before the packaged one of the same name, which is how a project adds its own vocabulary:
copy profiles/skill.md into it and fill custom_sections:.
cnlp init also prints the block to paste into AGENTS.md, CLAUDE.md or whatever file your
agent reads. Three lines, and they are the rules no skill announces: an installed skill states
its own task in its description, and a standing rule answers to no task description. It prints
rather than writes: your agent file is yours.
Convert documentation you already have
Run cnlp init, then the cnlp-migrate skill over the
directory — /cnlp-migrate docs/ in Claude Code, or read the body and follow it. It works on
any kind of document, not one: it groups what it finds by kind, reads the packaged profile nearest
each kind, and writes a new profile for a kind none of them fits — a new kind of document is one
profile and no change to the checker.
It does not start by rewriting. First it inventories the vocabulary your documents already use and
fills custom_sections: in a local copy of each profile, so the format bends to your words
before a single body moves. Then, per document, it lists the source's rules, rewrites the body, ticks
each rule against the result, and names anything in the result that has no origin in the source.
A contradiction it finds in the prose is reported, never resolved silently — that decision is yours.
Document a library or a framework
Add the docsets key and the four corpus entries shown under Document sets to
the config cnlp init already wrote, then run the cnlp-docset skill
against the library's sources at a fixed revision.
It builds in the order that keeps a half-finished set honest: the closed catalog of every public
capability first, every record at status: source, so the set itself says what has no page yet; then
the type names of one running example; then canonical_use:, one whole working program in the index;
then the concept, API and task pages. cnlp check resolves every reference and reports any page no
other page reaches. cnlp docs is the single command you point your agent at.
The commands
| | |
|---|---|
| cnlp init [<dir>] [--agents <a,b>] | install or update: config, profiles directory, skills for the agents you pick |
| cnlp check [--config <path>] | hold every document the corpus names to its profile; exit non-zero on any issue |
| cnlp docs [<set>] | write every document set's index, in reading order |
| cnlp print [<doc>] [--path] | write the standard, or a profile, to stdout |
The ownership rule
The package owns the standard, the checker, the generic profiles and the skills. The project owns its
cnlp.config.json, itscustom_sections, and any local profile. Nothing the project writes is ever overwritten.
The line falls between the two. A skill is generated from this package and versioned with the
standard it teaches, so cnlp init replaces every installed copy on every run — editing a copy
under .claude/skills/ is editing a file the next upgrade discards. To change what the kit does in
your project, edit a local profile or the config, which no command here ever rewrites: on a second
run init touches only the agents key it wrote itself.
This is the whole design. The format previously travelled as a zip of files copied into a target repository, where the corpus was configured by editing constants at the top of a file the next unpack would overwrite. One consumer got stranded 2 versions behind because upgrading meant a three-way merge. A package boundary is what makes upgrading a version bump.
The profiles that ship
| Profile | The document it shapes |
|---|---|
| profile | a profile — the profile of a profile, checked against itself |
| skill | an agent SKILL.md body |
| adr | an architecture decision record |
| guide | a repository guide such as AGENTS.md |
| reference | a checklist or protocol a skill loads by path |
| docset | the index of a library's documentation — one whole working program, and the closed list of what the library offers |
| concept | one model the API pages of that library rest on |
| api | one capability: its symbols, its contract, and one example |
| task | one outcome in steps, with the example that composes several calls |
The last four are one kind of document set: the documentation of one library, written so a reader loads a page at a time rather than a file that holds everything. See below.
skill, guide and reference ship with custom_sections: - none: the vocabulary is yours,
and the inventory step of cnlp-migrate is where it comes from. adr declares its own closed
block list and no custom section, because an ADR block outside that list is prose looking
for a home.
Severity is the caller's. The checker returns issues; cnlp check treats every issue as an
error. A project that gates on a lifecycle status — a warning while a decision is a draft, an
error once it is accepted — applies that policy in its own validator, calling bodyIssues
directly.
Document sets
A document set is a directory of documents written to be read a page at a time, where one
page sends the reader to another instead of restating it. §9 of the standard defines them: a
page id is <set>:<name>, a bare name is local, a prefixed one crosses into another set, and
page:, by: and docset: are the only forms a reference takes.
The index is not a table of contents. It carries canonical_use: — one complete working
program using every load-bearing element of the library — so the common case is written from
the index with no other page open, and walkthrough: names the page owning each part of it.
Its catalog: is the closed list: a capability with no page of its own keeps its line and says
where to read the source instead, so a reader can always tell absence from omission.
Point the config at them and §9 turns on:
{
"docsets": "docs/docsets",
"corpus": [
{ "profile": "docset", "glob": "docs/docsets/*/index.md" },
{ "profile": "concept", "glob": "docs/docsets/*/concepts/*.md" },
{ "profile": "api", "glob": "docs/docsets/*/api/*.md" },
{ "profile": "task", "glob": "docs/docsets/*/tasks/*.md" }
]
}docsets names the directory whose immediate subdirectories are one set each. With the key
present, cnlp check also resolves every reference, reports any that names no file, and
reports any page no other page reaches. Without it a project has no document set and nothing
about its run changes.
npx cnlp docs writes the index of every set present, the overridden set first — so a library
and the integration package that corrects it arrive together, and loading the second is not an
action anyone can forget. That command is what a project points its agent at.
Two worked sets live in examples/: a C# ECS framework and its engine integration.
They are the corpus that proves the four profiles, they are checked by the same cnlp check as
everything else, and they do not ship — files: in package.json names neither. docs/DOCSETS.md
records why the design is what it is and which file holds each decision as a rule.
Without Node
The checker is a convenience; the standard is the authority. A repository with no Node
runtime holds its profiles by review against cnlp-format.md and sets enforcement:
accordingly. Nothing else changes.
What is deliberately not here yet
- an intra-block line grammar. A profile can declare the form of a block but not the
shape of the lines inside one, so a project needing that writes a test of its own. The fix
is an
item_pattern:key on a section, and six rules are waiting on it: the canonical verb set in §5, thestatusandrelationvocabularies of a docset, the keys a status implies, and §9's rule that a name reaches exactly one page. Thestatusone fails open — an unrecognised value reads ascoveredand sends a reader to a page that is absent. - a checker for three of §9's rules. A fact stated on one page of a set is not restated on another, a set has one running example, and a name routes to exactly one page. All three are rules a review holds; §9 says so rather than implying the checker reaches them.
- a declarative severity policy. Today
enforcement:states the severity in prose and nothing reads it. - a language-neutral conformance corpus. Until it exists, "any technology" means any technology that can read the standard, not any technology that can check it.
profiles/plan.md. It is the acceptance test for the three above: if adding it costs one file and no code change, the generalization held.
quality-rules.md is a seed, not part of the standard: a rubric to prune and copy into the
skills that carry one. It travels with a byte-identity test you write yourself — without that
test the copies drift and the rubric stops being shared.
