gcf-aif-ext 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/commands/gcf.js +173 -0
  4. package/extension.json +24 -0
  5. package/hooks/pre-commit.js +91 -0
  6. package/injections/commit-status.md +6 -0
  7. package/injections/plan-context.md +8 -0
  8. package/injections/review-pillars.md +7 -0
  9. package/lib/anchors.js +109 -0
  10. package/lib/audit-core.js +102 -0
  11. package/lib/context.js +77 -0
  12. package/lib/frontmatter.js +26 -0
  13. package/lib/gcf-paths.js +60 -0
  14. package/lib/git.js +19 -0
  15. package/lib/id.js +42 -0
  16. package/lib/index-gen.js +51 -0
  17. package/lib/linter.js +61 -0
  18. package/lib/playtest-list.js +13 -0
  19. package/lib/safe-path.js +71 -0
  20. package/lib/transitions.js +30 -0
  21. package/lib/validate-card.js +48 -0
  22. package/lib/verdict-fs.js +126 -0
  23. package/node_modules/cnlp-kit/CHANGELOG.md +625 -0
  24. package/node_modules/cnlp-kit/LICENSE +21 -0
  25. package/node_modules/cnlp-kit/README.md +242 -0
  26. package/node_modules/cnlp-kit/bin/cnlp.js +334 -0
  27. package/node_modules/cnlp-kit/cnlp-format.md +470 -0
  28. package/node_modules/cnlp-kit/package.json +40 -0
  29. package/node_modules/cnlp-kit/profiles/adr.md +129 -0
  30. package/node_modules/cnlp-kit/profiles/api.md +87 -0
  31. package/node_modules/cnlp-kit/profiles/concept.md +70 -0
  32. package/node_modules/cnlp-kit/profiles/docset.md +103 -0
  33. package/node_modules/cnlp-kit/profiles/guide.md +51 -0
  34. package/node_modules/cnlp-kit/profiles/profile.md +69 -0
  35. package/node_modules/cnlp-kit/profiles/reference.md +61 -0
  36. package/node_modules/cnlp-kit/profiles/skill.md +75 -0
  37. package/node_modules/cnlp-kit/profiles/task.md +65 -0
  38. package/node_modules/cnlp-kit/quality-rules.md +69 -0
  39. package/node_modules/cnlp-kit/skills/cnlp-docset/SKILL.md +73 -0
  40. package/node_modules/cnlp-kit/skills/cnlp-migrate/SKILL.md +64 -0
  41. package/node_modules/cnlp-kit/src/cnlp.js +534 -0
  42. package/node_modules/cnlp-kit/src/config.js +352 -0
  43. package/node_modules/yaml/LICENSE +13 -0
  44. package/node_modules/yaml/README.md +172 -0
  45. package/node_modules/yaml/bin.mjs +11 -0
  46. package/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
  47. package/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
  48. package/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
  49. package/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
  50. package/node_modules/yaml/browser/dist/compose/composer.js +219 -0
  51. package/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
  52. package/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  53. package/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
  54. package/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
  55. package/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
  56. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +226 -0
  57. package/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
  58. package/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  59. package/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
  60. package/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  61. package/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
  62. package/node_modules/yaml/browser/dist/doc/Document.js +335 -0
  63. package/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
  64. package/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
  65. package/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
  66. package/node_modules/yaml/browser/dist/doc/directives.js +176 -0
  67. package/node_modules/yaml/browser/dist/errors.js +57 -0
  68. package/node_modules/yaml/browser/dist/index.js +17 -0
  69. package/node_modules/yaml/browser/dist/log.js +11 -0
  70. package/node_modules/yaml/browser/dist/nodes/Alias.js +118 -0
  71. package/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
  72. package/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
  73. package/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
  74. package/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
  75. package/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  76. package/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  77. package/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
  78. package/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
  79. package/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
  80. package/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
  81. package/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
  82. package/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
  83. package/node_modules/yaml/browser/dist/parse/cst.js +98 -0
  84. package/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
  85. package/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
  86. package/node_modules/yaml/browser/dist/parse/parser.js +975 -0
  87. package/node_modules/yaml/browser/dist/public-api.js +102 -0
  88. package/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
  89. package/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
  90. package/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
  91. package/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
  92. package/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
  93. package/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
  94. package/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
  95. package/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
  96. package/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
  97. package/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
  98. package/node_modules/yaml/browser/dist/schema/tags.js +96 -0
  99. package/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
  100. package/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  101. package/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  102. package/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  103. package/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
  104. package/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  105. package/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  106. package/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
  107. package/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  108. package/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  109. package/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  110. package/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
  111. package/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
  112. package/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  113. package/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  114. package/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
  115. package/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  116. package/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
  117. package/node_modules/yaml/browser/dist/util.js +11 -0
  118. package/node_modules/yaml/browser/dist/visit.js +233 -0
  119. package/node_modules/yaml/browser/index.js +5 -0
  120. package/node_modules/yaml/browser/package.json +3 -0
  121. package/node_modules/yaml/dist/cli.d.ts +8 -0
  122. package/node_modules/yaml/dist/cli.mjs +201 -0
  123. package/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
  124. package/node_modules/yaml/dist/compose/compose-collection.js +90 -0
  125. package/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
  126. package/node_modules/yaml/dist/compose/compose-doc.js +45 -0
  127. package/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
  128. package/node_modules/yaml/dist/compose/compose-node.js +112 -0
  129. package/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
  130. package/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
  131. package/node_modules/yaml/dist/compose/composer.d.ts +63 -0
  132. package/node_modules/yaml/dist/compose/composer.js +224 -0
  133. package/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  134. package/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
  135. package/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  136. package/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
  137. package/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  138. package/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
  139. package/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
  140. package/node_modules/yaml/dist/compose/resolve-end.js +39 -0
  141. package/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  142. package/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
  143. package/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  144. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +228 -0
  145. package/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
  146. package/node_modules/yaml/dist/compose/resolve-props.js +148 -0
  147. package/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  148. package/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
  149. package/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  150. package/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
  151. package/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  152. package/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
  153. package/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
  154. package/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
  155. package/node_modules/yaml/dist/doc/Document.d.ts +141 -0
  156. package/node_modules/yaml/dist/doc/Document.js +337 -0
  157. package/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
  158. package/node_modules/yaml/dist/doc/anchors.js +76 -0
  159. package/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
  160. package/node_modules/yaml/dist/doc/applyReviver.js +57 -0
  161. package/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
  162. package/node_modules/yaml/dist/doc/createNode.js +90 -0
  163. package/node_modules/yaml/dist/doc/directives.d.ts +49 -0
  164. package/node_modules/yaml/dist/doc/directives.js +178 -0
  165. package/node_modules/yaml/dist/errors.d.ts +21 -0
  166. package/node_modules/yaml/dist/errors.js +62 -0
  167. package/node_modules/yaml/dist/index.d.ts +25 -0
  168. package/node_modules/yaml/dist/index.js +50 -0
  169. package/node_modules/yaml/dist/log.d.ts +3 -0
  170. package/node_modules/yaml/dist/log.js +19 -0
  171. package/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
  172. package/node_modules/yaml/dist/nodes/Alias.js +120 -0
  173. package/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
  174. package/node_modules/yaml/dist/nodes/Collection.js +151 -0
  175. package/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
  176. package/node_modules/yaml/dist/nodes/Node.js +40 -0
  177. package/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
  178. package/node_modules/yaml/dist/nodes/Pair.js +39 -0
  179. package/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
  180. package/node_modules/yaml/dist/nodes/Scalar.js +27 -0
  181. package/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  182. package/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
  183. package/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  184. package/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
  185. package/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  186. package/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
  187. package/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
  188. package/node_modules/yaml/dist/nodes/identity.js +53 -0
  189. package/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
  190. package/node_modules/yaml/dist/nodes/toJS.js +39 -0
  191. package/node_modules/yaml/dist/options.d.ts +350 -0
  192. package/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
  193. package/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
  194. package/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
  195. package/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
  196. package/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
  197. package/node_modules/yaml/dist/parse/cst-visit.js +99 -0
  198. package/node_modules/yaml/dist/parse/cst.d.ts +109 -0
  199. package/node_modules/yaml/dist/parse/cst.js +112 -0
  200. package/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
  201. package/node_modules/yaml/dist/parse/lexer.js +723 -0
  202. package/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
  203. package/node_modules/yaml/dist/parse/line-counter.js +41 -0
  204. package/node_modules/yaml/dist/parse/parser.d.ts +84 -0
  205. package/node_modules/yaml/dist/parse/parser.js +980 -0
  206. package/node_modules/yaml/dist/public-api.d.ts +44 -0
  207. package/node_modules/yaml/dist/public-api.js +107 -0
  208. package/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
  209. package/node_modules/yaml/dist/schema/Schema.js +39 -0
  210. package/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
  211. package/node_modules/yaml/dist/schema/common/map.js +19 -0
  212. package/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
  213. package/node_modules/yaml/dist/schema/common/null.js +17 -0
  214. package/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
  215. package/node_modules/yaml/dist/schema/common/seq.js +19 -0
  216. package/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
  217. package/node_modules/yaml/dist/schema/common/string.js +16 -0
  218. package/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
  219. package/node_modules/yaml/dist/schema/core/bool.js +21 -0
  220. package/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
  221. package/node_modules/yaml/dist/schema/core/float.js +47 -0
  222. package/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
  223. package/node_modules/yaml/dist/schema/core/int.js +42 -0
  224. package/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
  225. package/node_modules/yaml/dist/schema/core/schema.js +25 -0
  226. package/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
  227. package/node_modules/yaml/dist/schema/json/schema.js +64 -0
  228. package/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
  229. package/node_modules/yaml/dist/schema/tags.d.ts +48 -0
  230. package/node_modules/yaml/dist/schema/tags.js +99 -0
  231. package/node_modules/yaml/dist/schema/types.d.ts +92 -0
  232. package/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  233. package/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
  234. package/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  235. package/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  236. package/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  237. package/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
  238. package/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  239. package/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
  240. package/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
  241. package/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
  242. package/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
  243. package/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  244. package/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  245. package/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  246. package/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  247. package/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
  248. package/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  249. package/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
  250. package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  251. package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  252. package/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  253. package/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
  254. package/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
  255. package/node_modules/yaml/dist/stringify/stringify.js +132 -0
  256. package/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  257. package/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
  258. package/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  259. package/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
  260. package/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  261. package/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
  262. package/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  263. package/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
  264. package/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  265. package/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
  266. package/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
  267. package/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
  268. package/node_modules/yaml/dist/test-events.d.ts +4 -0
  269. package/node_modules/yaml/dist/test-events.js +134 -0
  270. package/node_modules/yaml/dist/util.d.ts +16 -0
  271. package/node_modules/yaml/dist/util.js +28 -0
  272. package/node_modules/yaml/dist/visit.d.ts +102 -0
  273. package/node_modules/yaml/dist/visit.js +236 -0
  274. package/node_modules/yaml/package.json +97 -0
  275. package/node_modules/yaml/util.js +2 -0
  276. package/package.json +54 -0
  277. package/profiles/card.md +65 -0
  278. package/profiles/pillars.md +31 -0
  279. package/profiles/system.md +56 -0
  280. package/schemas/card.schema.json +30 -0
  281. package/skills/gcf-adopt/SKILL.md +41 -0
  282. package/skills/gcf-audit/SKILL.md +27 -0
  283. package/skills/gcf-card/SKILL.md +35 -0
  284. package/skills/gcf-debate/SKILL.md +33 -0
  285. package/skills/gcf-ingest/SKILL.md +38 -0
  286. package/skills/gcf-init/SKILL.md +32 -0
  287. package/skills/gcf-verdict/SKILL.md +32 -0
  288. package/skills/gcf-vision/SKILL.md +33 -0
  289. package/templates/PILLARS.md +8 -0
  290. package/templates/backlog.md +4 -0
  291. package/templates/card.md +34 -0
  292. package/templates/cnlp.config.json +8 -0
  293. package/templates/config.yml +8 -0
  294. package/templates/system.md +22 -0
@@ -0,0 +1,242 @@
1
+ # CNLP-Kit
2
+
3
+ **CNL-P** — *Controlled Natural Language for Prompts*. A format for documents an agent has to
4
+ read precisely: agent skills, decision records, repository guides, review checklists, and the
5
+ documentation of a library the agent writes code against. It replaces prose with `key:` blocks,
6
+ so a reader stops inferring which sentence is intent, which is policy, and which is a step.
7
+
8
+ This repository is the format itself — the standard, the profiles, and the checker that holds
9
+ them — with no tie to any project, language or agent runtime.
10
+
11
+ - **`cnlp-format.md`** — the standard. Read it first with `npx cnlp print format`; nothing else makes sense before it.
12
+ - **`profiles/`** — one profile per kind of document. A profile declares values, never rules.
13
+ - **`src/cnlp.js`** — the checker. Node builtins only, no dependencies.
14
+ - **`bin/cnlp.js`** — the `cnlp` command.
15
+
16
+ ## Install it
17
+
18
+ Node >= 22.14, and no runtime dependency of any kind — the checker is builtins only, which is what
19
+ lets any repository run it.
20
+
21
+ ```
22
+ npm i -g cnlp-kit # cnlp on your PATH; the same line upgrades it
23
+ npm i -D cnlp-kit # a pinned version, for a repository whose CI runs the check
24
+ ```
25
+
26
+ Take the global one to work on a repository you do not want to add a dependency to, and the
27
+ dev-dependency wherever the check has to be reproducible. Both can hold at once: `npx cnlp` inside a
28
+ project with the local install runs that version rather than the global one.
29
+
30
+ ## Use it
31
+
32
+ Two commands. The first one installs **and** updates:
33
+
34
+ ```
35
+ cnlp init # config, profiles directory, and the skills for the agents you pick
36
+ cnlp check # holds every document the corpus names to its profile
37
+ ```
38
+
39
+ `cnlp init` asks once which agents to write skills for — Claude Code, Codex — and remembers the
40
+ answer in `cnlp.config.json`. Run it again after every upgrade of the package: the skills are
41
+ versioned with the standard they teach, so `init` replaces every copy with the installed version.
42
+ Nothing else about your config moves on that second run.
43
+
44
+ - `--agents claude-code,codex` skips the menu, for a script.
45
+ - With no terminal to draw a menu in — Git Bash under MinTTY, or CI — it repeats the choice the
46
+ config records, and installs for every agent when there is no config yet.
47
+ - `cnlp check` reports skills left behind by an upgrade, and its exit code stays the corpus's own
48
+ answer: a stale copy is not a defect in a document.
49
+
50
+ On the first run `cnlp init` also looks at what the repository already has — `AGENTS.md`,
51
+ `CLAUDE.md`, `skills/*/SKILL.md`, `docs/adr/*.md`, `docs/references/*.md`, `docs/docsets/*/…` — and
52
+ writes a corpus entry for each layout that matches a file, naming the count it found. Nothing is
53
+ guessed from content: a decision record filed under `docs/references/` reads as a reference until
54
+ you say otherwise. Edit the entries it wrote, or add your own:
55
+
56
+ ```json
57
+ {
58
+ "profiles": "cnlp/profiles",
59
+ "corpus": [
60
+ { "profile": "skill", "glob": "skills/*/SKILL.md" },
61
+ { "profile": "reference", "glob": "docs/references/*.md" },
62
+ { "profile": "guide", "glob": "AGENTS.md" }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ `profiles` is optional and names a directory of your own profiles. A profile there resolves
68
+ **before** the packaged one of the same name, which is how a project adds its own vocabulary:
69
+ copy `profiles/skill.md` into it and fill `custom_sections:`.
70
+
71
+ `cnlp init` also prints the block to paste into `AGENTS.md`, `CLAUDE.md` or whatever file your
72
+ agent reads. Three lines, and they are the rules no skill announces: an installed skill states
73
+ its own task in its description, and a standing rule answers to no task description. It prints
74
+ rather than writes: your agent file is yours.
75
+
76
+ ### Convert documentation you already have
77
+
78
+ Run `cnlp init`, then the `cnlp-migrate` skill over the
79
+ directory — `/cnlp-migrate docs/` in Claude Code, or read the body and follow it. It works on
80
+ any kind of document, not one: it groups what it finds by kind, reads the packaged profile nearest
81
+ each kind, and writes a new profile for a kind none of them fits — a new kind of document is one
82
+ profile and no change to the checker.
83
+
84
+ It does not start by rewriting. First it inventories the vocabulary your documents already use and
85
+ fills `custom_sections:` in a **local** copy of each profile, so the format bends to your words
86
+ before a single body moves. Then, per document, it lists the source's rules, rewrites the body, ticks
87
+ each rule against the result, and names anything in the result that has no origin in the source.
88
+ A contradiction it finds in the prose is reported, never resolved silently — that decision is yours.
89
+
90
+ ### Document a library or a framework
91
+
92
+ Run the `cnlp-docset` skill against whatever you found, and name a directory to write into. The
93
+ set is a folder of markdown and nothing else — **no config file, no dependency, no project around
94
+ it** — and `cnlp check <dir>` reads it by path:
95
+
96
+ ```sh
97
+ cnlp check ./nanoid-docset # 12 file(s) conform
98
+ ```
99
+
100
+ That is the producing side. **Adopting** a set is the other half and a separate act: a repository
101
+ that will hold one among its own documents runs `cnlp init`, adds the `docsets` key and the four
102
+ corpus entries shown under [Document sets](#document-sets), and checks it with everything else from
103
+ then on.
104
+
105
+ **Hand it any mix of sources.** A library arrives differently every time: a repository you can clone,
106
+ a specification, a documentation site with no readable source, a pile of markdown someone collected,
107
+ a vendor dump written for an agent. The skill ranks what you give it by what a source can prove —
108
+ declarations first, then a machine-readable spec, then published prose, then the library's own
109
+ examples, then a derived agent file — and takes each capability's contract from the strongest source
110
+ that states it. What the set rests on lands in `identity:` `derived_from:`, so a reader can tell a
111
+ page written from a signature from one written from prose, and the skill will not quietly write a
112
+ prose claim as though it came from a declaration.
113
+
114
+ It builds in the order that keeps a half-finished set honest: the closed catalog of every public
115
+ capability first, every record at `status: source`, so the set itself says what has no page yet; then
116
+ the type names of one running example; then `canonical_use:`, one whole working program in the index;
117
+ then the concept, API and task pages. `cnlp check` resolves every reference and reports any page no
118
+ other page reaches. `cnlp docs` is the single command you point your agent at.
119
+
120
+ ### The commands
121
+
122
+ | | |
123
+ |---|---|
124
+ | `cnlp init [<dir>] [--agents <a,b>]` | install or update: config, profiles directory, skills for the agents you pick |
125
+ | `cnlp check [<dir>] [--config <path>]` | hold every document to its profile: a directory read by path, or every corpus entry of the config. Exit non-zero on any issue |
126
+ | `cnlp docs [<set>]` | write every document set's index, in reading order |
127
+ | `cnlp print [<doc>] [--path]` | write the standard, or a profile, to stdout |
128
+ | `cnlp scaffold <profile> <data.json>` | write a document from data, in the blocks and forms the profile declares |
129
+ | `cnlp diff <profile> <doc> <data.json>` | report what moved between a document and a fresh read of its sources |
130
+
131
+ ## The ownership rule
132
+
133
+ > The package owns the standard, the checker, the generic profiles **and the skills**.
134
+ > The project owns its `cnlp.config.json`, its `custom_sections`, and any local profile.
135
+ > **Nothing the project writes is ever overwritten.**
136
+
137
+ The line falls between the two. A skill is generated from this package and versioned with the
138
+ standard it teaches, so `cnlp init` replaces every installed copy on every run — editing a copy
139
+ under `.claude/skills/` is editing a file the next upgrade discards. To change what the kit does in
140
+ your project, edit a local profile or the config, which no command here ever rewrites: on a second
141
+ run `init` touches only the `agents` key it wrote itself.
142
+
143
+ This is the whole design. The format previously travelled as a zip of files copied into a
144
+ target repository, where the corpus was configured by editing constants at the top of a file
145
+ the next unpack would overwrite. One consumer got stranded 2 versions behind because upgrading
146
+ meant a three-way merge. A package boundary is what makes upgrading a version bump.
147
+
148
+ ## The profiles that ship
149
+
150
+ | Profile | The document it shapes |
151
+ |---|---|
152
+ | `profile` | a profile — the profile of a profile, checked against itself |
153
+ | `skill` | an agent `SKILL.md` body |
154
+ | `adr` | an architecture decision record |
155
+ | `guide` | a repository guide such as `AGENTS.md` |
156
+ | `reference` | a checklist or protocol a skill loads by path |
157
+ | `docset` | the index of a library's documentation — one whole working program, and the closed list of what the library offers |
158
+ | `concept` | one model the API pages of that library rest on |
159
+ | `api` | one capability: its symbols, its contract, and one example |
160
+ | `task` | one outcome in steps, with the example that composes several calls |
161
+
162
+ The last four are one kind of document set: the documentation of one library, written so a
163
+ reader loads a page at a time rather than a file that holds everything. See below.
164
+
165
+ `skill`, `guide` and `reference` ship with `custom_sections: - none`: the vocabulary is yours,
166
+ and the inventory step of `cnlp-migrate` is where it comes from. `adr` declares its own closed
167
+ block list and no custom section, because an ADR block outside that list is prose looking
168
+ for a home.
169
+
170
+ Severity is the caller's. The checker returns issues; `cnlp check` treats every issue as an
171
+ error. A project that gates on a lifecycle status — a warning while a decision is a draft, an
172
+ error once it is accepted — applies that policy in its own validator, calling `bodyIssues`
173
+ directly.
174
+
175
+ ## Document sets
176
+
177
+ A **document set** is a directory of documents written to be read a page at a time, where one
178
+ page sends the reader to another instead of restating it. §9 of the standard defines them: a
179
+ page id is `<set>:<name>`, a bare name is local, a prefixed one crosses into another set, and
180
+ `page:`, `by:` and `docset:` are the only forms a reference takes.
181
+
182
+ The index is not a table of contents. It carries `canonical_use:` — one complete working
183
+ program using every load-bearing element of the library — so the common case is written from
184
+ the index with no other page open, and `walkthrough:` names the page owning each part of it.
185
+ Its `catalog:` is the closed list: a capability with no page of its own keeps its line and says
186
+ where to read the source instead, so a reader can always tell absence from omission.
187
+
188
+ Point the config at them and §9 turns on:
189
+
190
+ ```json
191
+ {
192
+ "docsets": "docs/docsets",
193
+ "corpus": [
194
+ { "profile": "docset", "glob": "docs/docsets/*/index.md" },
195
+ { "profile": "concept", "glob": "docs/docsets/*/concepts/*.md" },
196
+ { "profile": "api", "glob": "docs/docsets/*/api/*.md" },
197
+ { "profile": "task", "glob": "docs/docsets/*/tasks/*.md" }
198
+ ]
199
+ }
200
+ ```
201
+
202
+ `docsets` names the directory whose immediate subdirectories are one set each. With the key
203
+ present, `cnlp check` also resolves every reference, reports any that names no file, and
204
+ reports any page no other page reaches. Without it a project has no document set and nothing
205
+ about its run changes.
206
+
207
+ `npx cnlp docs` writes the index of every set present, the overridden set first — so a library
208
+ and the integration package that corrects it arrive together, and loading the second is not an
209
+ action anyone can forget. That command is what a project points its agent at.
210
+
211
+ Two worked sets live in `examples/`: a C# ECS framework and its engine integration.
212
+ They are the corpus that proves the four profiles, they are checked by the same `cnlp check` as
213
+ everything else, and they do not ship — `files:` in `package.json` names neither. `docs/DOCSETS.md`
214
+ records why the design is what it is and which file holds each decision as a rule.
215
+
216
+ ## Without Node
217
+
218
+ The checker is a convenience; the standard is the authority. A repository with no Node
219
+ runtime holds its profiles by review against `cnlp-format.md` and sets `enforcement:`
220
+ accordingly. Nothing else changes.
221
+
222
+ ## What is deliberately not here yet
223
+
224
+ - **the half of an intra-block grammar that is a line *shape*.** `record_values:` closes a
225
+ record key to a vocabulary, which took the two rules that failed open — `status` and
226
+ `relation` — off this list. What a profile still cannot declare is the form of a line: the
227
+ canonical verb a step opens with, the keys a `status` implies, and §9's rule that a name
228
+ reaches exactly one page. Those want an `item_pattern:` key, and a project needing one today
229
+ writes a test of its own.
230
+ - **a checker for three of §9's rules.** A fact stated on one page of a set is not restated on
231
+ another, a set has one running example, and a name routes to exactly one page. All three are
232
+ rules a review holds; §9 says so rather than implying the checker reaches them.
233
+ - **a declarative severity policy.** Today `enforcement:` states the severity in prose and
234
+ nothing reads it.
235
+ - **a language-neutral conformance corpus.** Until it exists, "any technology" means any
236
+ technology that can read the standard, not any technology that can check it.
237
+ - **`profiles/plan.md`.** It is the acceptance test for the three above: if adding it costs
238
+ one file and no code change, the generalization held.
239
+
240
+ `quality-rules.md` is a seed, not part of the standard: a rubric to prune and copy into the
241
+ skills that carry one. It travels with a byte-identity test you write yourself — without that
242
+ test the copies drift and the rubric stops being shared.
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ // cnlp — check a project's documents against their profiles, and print the documents that
3
+ // ship here. `print` is how anything outside this package reaches the standard: it lives
4
+ // inside node_modules, which is not a path a document can name and expect a reader to find.
5
+
6
+ import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
7
+ import { existsSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import readline from 'node:readline';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { PACKAGE_ROOT, recordDrift, renderBody, resolveDoc } from '../src/cnlp.js';
12
+ import { AGENTS, CONFIG_NAME, STAMP_NAME, checkProject, configForPath, docsetsOf, findConfig, profileFor, proposeCorpus, readConfig, readingOrder, skillDrift, skillsDirFor } from '../src/config.js';
13
+
14
+ const USAGE = `cnlp — the CNL-P checker
15
+
16
+ cnlp init [<dir>] [--agents <a,b>] install or update this kit in a project
17
+ cnlp check [<dir>] [--config <path>] check a directory of documents, or every corpus entry
18
+ cnlp docs [<set>] write the index of every document set, or of one
19
+ cnlp print [<doc>] [--path] write a shipped document to stdout, or its path
20
+ cnlp scaffold <profile> <data> write a document from a JSON file, in the profile's shape
21
+ cnlp diff <profile> <doc> <data> report what moved between a document and a fresh read
22
+
23
+ <doc> is "format" for the standard, or a profile name such as "skill".
24
+ <a,b> is a comma list of ${AGENTS.map((a) => a.value).join(', ')}.
25
+ `;
26
+
27
+ // The starter carries `docsets` because a key absent from it reads as a capability that does not
28
+ // exist: a project would have to find §9 to learn that a document set is available at all. The
29
+ // corpus here is the fallback: `init` writes what it found instead, when it found anything.
30
+ const STARTER = {
31
+ profiles: 'cnlp/profiles',
32
+ docsets: 'docs/docsets',
33
+ corpus: [{ profile: 'skill', glob: 'skills/*/SKILL.md' }],
34
+ };
35
+
36
+ const VERSION = JSON.parse(await readFile(fileURLToPath(new URL('package.json', PACKAGE_ROOT)), 'utf8')).version;
37
+
38
+ const argv = process.argv.slice(2);
39
+ const VALUE_FLAGS = ['--config', '--agents', '--out'];
40
+ const flag = (name) => argv.includes(name);
41
+ const value = (name) => {
42
+ const at = argv.indexOf(name);
43
+ return at >= 0 ? argv[at + 1] : undefined;
44
+ };
45
+ const positional = argv.filter((a, i) => !a.startsWith('--') && !VALUE_FLAGS.includes(argv[i - 1]));
46
+
47
+ const fail = (message) => {
48
+ console.error(message);
49
+ process.exit(1);
50
+ };
51
+
52
+ // A terminal menu, from node:readline and nothing else — a dependency here would be a dependency
53
+ // in every repository that installs the kit. A run with no terminal to draw in gets no menu and no
54
+ // prompt: the caller resolves the choice before asking, and this is never reached.
55
+ function multiSelect(message, choices, checked) {
56
+ const selected = new Set(choices.map((c, i) => (checked.includes(c.value) ? i : -1)).filter((i) => i >= 0));
57
+ let cursor = 0;
58
+ const render = (first) => {
59
+ if (!first) process.stdout.write(`\x1b[${choices.length + 1}A`);
60
+ process.stdout.write(`\x1b[2K${message}\n`);
61
+ choices.forEach((c, i) => {
62
+ process.stdout.write(`\x1b[2K${i === cursor ? '>' : ' '} ${selected.has(i) ? '[x]' : '[ ]'} ${c.label}\n`);
63
+ });
64
+ };
65
+ readline.emitKeypressEvents(process.stdin);
66
+ process.stdin.setRawMode(true);
67
+ render(true);
68
+ return new Promise((resolve) => {
69
+ const done = (out) => {
70
+ process.stdin.removeListener('keypress', onKey);
71
+ process.stdin.setRawMode(false);
72
+ process.stdin.pause();
73
+ resolve(out);
74
+ };
75
+ const onKey = (_str, key) => {
76
+ if (!key) return;
77
+ if (key.name === 'up' || key.name === 'down') {
78
+ cursor = (cursor + (key.name === 'up' ? -1 : 1) + choices.length) % choices.length;
79
+ render(false);
80
+ } else if (key.name === 'space') {
81
+ if (selected.has(cursor)) selected.delete(cursor);
82
+ else selected.add(cursor);
83
+ render(false);
84
+ } else if (key.name === 'return') {
85
+ done(choices.filter((_, i) => selected.has(i)).map((c) => c.value));
86
+ } else if (key.ctrl && key.name === 'c') {
87
+ done([]);
88
+ process.exit(0);
89
+ }
90
+ };
91
+ process.stdin.on('keypress', onKey);
92
+ });
93
+ }
94
+
95
+ const [command] = positional;
96
+
97
+ if (!command || flag('--help') || flag('-h')) {
98
+ console.log(USAGE);
99
+ // An asked-for usage is a success; a missing command is not.
100
+ process.exit(flag('--help') || flag('-h') ? 0 : 1);
101
+ }
102
+
103
+ if (command === 'check') {
104
+ // A path names what to check and needs no config: a document set produced for somebody else is
105
+ // a folder of markdown, and asking it to carry a config makes the tool homestead in its output.
106
+ const target = positional[1];
107
+ const configPath = target ? null : (value('--config') ?? findConfig());
108
+ if (!target && !configPath) {
109
+ fail(`no ${CONFIG_NAME} found here or above ${process.cwd()} — name a directory to check, or run \`cnlp init\` to write a config`);
110
+ }
111
+ let result;
112
+ let config;
113
+ try {
114
+ config = target ? await configForPath(target) : await readConfig(configPath);
115
+ result = await checkProject(config);
116
+ } catch (e) {
117
+ fail(e.message);
118
+ }
119
+ // Paths below are relative to a root the caller never named, so name it once.
120
+ if (target) console.log(`reading ${config.corpus.length} entr(ies) under ${config.root}`);
121
+ for (const { file, line, message } of result.issues) {
122
+ console.error(`${file}:${line}: ${message}`);
123
+ }
124
+ // A skill copy is versioned with the standard it teaches, so one left behind by an upgrade
125
+ // instructs an agent in rules this version changed. It is not a defect in a document the corpus
126
+ // names, so it reports and does not decide the exit code.
127
+ for (const { dir, held } of await skillDrift(config.root, config.agents, VERSION)) {
128
+ const from = held ? `are from ${held}` : 'carry no version';
129
+ console.error(`${path.relative(config.root, dir) || dir}: skills ${from}, this package is ${VERSION} — run \`cnlp init\``);
130
+ }
131
+ if (result.issues.length) {
132
+ fail(`\n${result.issues.length} issue(s) in ${result.checked.length} file(s)`);
133
+ }
134
+ console.log(`${result.checked.length} file(s) conform`);
135
+ process.exit(0);
136
+ }
137
+
138
+ // The entry point an agent is pointed at. It writes every active index in 1 run, so loading a
139
+ // companion is not a second action anybody can forget: a companion states which pages of the set
140
+ // it extends stop holding, and a reader who never loaded it writes against the wrong page.
141
+ if (command === 'docs') {
142
+ const configPath = value('--config') ?? findConfig();
143
+ if (!configPath) {
144
+ fail(`no ${CONFIG_NAME} found here or above ${process.cwd()} — run \`cnlp init\` to write one`);
145
+ }
146
+ let sets;
147
+ let config;
148
+ try {
149
+ config = await readConfig(configPath);
150
+ sets = await docsetsOf(config);
151
+ } catch (e) {
152
+ fail(e.message);
153
+ }
154
+ const wanted = positional[1];
155
+ if (wanted && !sets.has(wanted)) {
156
+ fail(`no document set named ${JSON.stringify(wanted)} — this project carries ${[...sets.keys()].sort().join(', ') || 'none'}`);
157
+ }
158
+ const order = wanted ? [sets.get(wanted)] : readingOrder(sets);
159
+ if (order.length === 0) {
160
+ fail(`${config.docsets}/ holds no document set — a set is a subdirectory with an index.md`);
161
+ }
162
+ for (const set of order) {
163
+ process.stdout.write(`===== docset: ${set.name} (${set.index}) =====\n\n${set.raw}\n`);
164
+ }
165
+ process.exit(0);
166
+ }
167
+
168
+ if (command === 'print') {
169
+ const doc = positional[1] ?? 'format';
170
+ let abs;
171
+ try {
172
+ abs = resolveDoc(doc);
173
+ } catch (e) {
174
+ fail(e.message);
175
+ }
176
+ if (!existsSync(abs)) fail(`no such document: ${doc}`);
177
+ if (flag('--path')) console.log(abs);
178
+ else process.stdout.write(await readFile(abs, 'utf8'));
179
+ process.exit(0);
180
+ }
181
+
182
+ // The mechanical half of authoring: a profile already declares the block order and the form of
183
+ // each one, so emitting a document from data needs no knowledge of what kind of document it is.
184
+ // What it will not do is fill a block the data does not carry — `cnlp check` reporting that block
185
+ // as missing is the list of what the author still owes, and a placeholder would hide it.
186
+ async function profileAndData(name, dataPath) {
187
+ if (!existsSync(dataPath)) fail(`no such file: ${dataPath}`);
188
+ const configPath = value('--config') ?? findConfig();
189
+ let profilesDir = null;
190
+ if (configPath) {
191
+ try {
192
+ ({ profilesDir } = await readConfig(configPath));
193
+ } catch (e) {
194
+ fail(e.message);
195
+ }
196
+ }
197
+ try {
198
+ return { profile: await profileFor(name, profilesDir), data: JSON.parse(await readFile(dataPath, 'utf8')) };
199
+ } catch (e) {
200
+ return fail(`${dataPath}: ${e.message}`);
201
+ }
202
+ }
203
+
204
+ if (command === 'scaffold') {
205
+ const [, name, dataPath] = positional;
206
+ if (!name || !dataPath) fail('usage: cnlp scaffold <profile> <data.json> [--out <path>]');
207
+ const { profile, data } = await profileAndData(name, dataPath);
208
+ let out;
209
+ try {
210
+ out = renderBody(data, profile);
211
+ } catch (e) {
212
+ fail(`${dataPath}: ${e.message}`);
213
+ }
214
+ const target = value('--out');
215
+ if (target) {
216
+ await mkdir(path.dirname(path.resolve(target)), { recursive: true });
217
+ await writeFile(target, out);
218
+ console.log(`wrote ${target} — run \`cnlp check\` for the blocks it still owes`);
219
+ } else process.stdout.write(out);
220
+ process.exit(0);
221
+ }
222
+
223
+ // A document states what its sources said at 1 revision, and nothing in the document reports that
224
+ // the sources moved. Re-read them, hand the fresh data here, and what changed is named rather than
225
+ // left for a reader to hit. Audit and refresh are the same comparison: this reports, and the author
226
+ // decides which pages to rewrite.
227
+ if (command === 'diff') {
228
+ const [, name, docPath, dataPath] = positional;
229
+ if (!name || !docPath || !dataPath) fail('usage: cnlp diff <profile> <document.md> <data.json>');
230
+ if (!existsSync(docPath)) fail(`no such file: ${docPath}`);
231
+ const { profile, data } = await profileAndData(name, dataPath);
232
+ let drift;
233
+ try {
234
+ drift = recordDrift(await readFile(docPath, 'utf8'), data, profile);
235
+ } catch (e) {
236
+ fail(e.message);
237
+ }
238
+ for (const { block, id, change } of drift) console.error(`${docPath}: ${block}: ${id}: ${change}`);
239
+ if (drift.length === 0) console.log(`${docPath} matches what the sources state now`);
240
+ process.exit(drift.length === 0 ? 0 : 1);
241
+ }
242
+
243
+ // Install and update are 1 command, because a project wants neither half alone: a config with no
244
+ // skills gives an agent no instructions, and skills with no config give the checker no corpus.
245
+ if (command === 'init') {
246
+ const dir = path.resolve(positional[1] ?? '.');
247
+ const at = path.join(dir, CONFIG_NAME);
248
+ const existing = existsSync(at) ? JSON.parse(await readFile(at, 'utf8')) : null;
249
+
250
+ // First hit wins: the flag a script passes, then the menu, then the choice already recorded,
251
+ // then every agent. A run with no terminal never reaches the menu and never stops to ask.
252
+ const named = value('--agents')?.split(',').map((a) => a.trim()).filter(Boolean);
253
+ for (const a of named ?? []) {
254
+ if (!AGENTS.some((known) => known.value === a)) {
255
+ fail(`unknown agent ${JSON.stringify(a)} — this kit knows ${AGENTS.map((k) => k.value).join(', ')}`);
256
+ }
257
+ }
258
+ // A recorded empty list is a choice — "install for no agent" — and repeats like any other.
259
+ const recorded = Array.isArray(existing?.agents) ? existing.agents : null;
260
+ let agents = named;
261
+ if (!agents && process.stdin.isTTY) {
262
+ agents = await multiSelect('Install the skills for which agents? (up/down move, space toggles, enter confirms)', AGENTS, recorded ?? []);
263
+ }
264
+ if (!agents) agents = recorded ?? AGENTS.map((a) => a.value);
265
+
266
+ await mkdir(path.join(dir, 'cnlp', 'profiles'), { recursive: true });
267
+ // A project that deleted its docsets key deleted the directory with it; recreating it here
268
+ // would undo that choice on the very next update run.
269
+ const docsets = existing ? existing.docsets : STARTER.docsets;
270
+ if (docsets) await mkdir(path.join(dir, docsets), { recursive: true });
271
+
272
+ let exampleCorpus = false;
273
+ if (existing) {
274
+ // The corpus is the one thing the project owns here, so an update rewrites the agent list
275
+ // and nothing else. Rereading the layout would undo every glob its author edited.
276
+ await writeFile(at, `${JSON.stringify({ ...existing, agents }, null, 2)}\n`, 'utf8');
277
+ console.log(`updated ${at}`);
278
+ } else {
279
+ // Read the layout before writing the corpus: a starter naming files a project does not have
280
+ // makes `cnlp check` fail on the first run, and the operator edits before ever seeing it pass.
281
+ const found = await proposeCorpus(dir);
282
+ const config = { ...STARTER, agents, corpus: found.length ? found.map(({ profile, glob }) => ({ profile, glob })) : STARTER.corpus };
283
+ await writeFile(at, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
284
+ for (const { profile, glob, files } of found) {
285
+ console.log(`found ${String(files).padStart(3)} file(s) for "${profile}" ${glob}`);
286
+ }
287
+ if (!found.length) {
288
+ exampleCorpus = true;
289
+ console.log('found no document this kit already knows the layout of — the corpus written is an example to edit');
290
+ }
291
+ console.log(`written ${at}`);
292
+ }
293
+
294
+ // A skill is generated from this package and versioned with the standard it teaches, so every
295
+ // copy is replaced on every run: a copy left behind teaches rules a later version changed. Edit
296
+ // a profile or the config to change what the kit does here, never a copied skill.
297
+ const from = fileURLToPath(new URL('skills/', PACKAGE_ROOT));
298
+ const skills = (await readdir(from)).sort();
299
+ for (const agent of agents) {
300
+ const to = skillsDirFor(dir, agent);
301
+ await mkdir(to, { recursive: true });
302
+ for (const name of skills) {
303
+ const target = path.join(to, name);
304
+ const action = existsSync(target) ? 'updated' : 'written';
305
+ await rm(target, { recursive: true, force: true });
306
+ await cp(path.join(from, name), target, { recursive: true });
307
+ console.log(`${action} ${target}`);
308
+ }
309
+ await writeFile(path.join(to, STAMP_NAME), `${VERSION}\n`, 'utf8');
310
+ }
311
+ if (!agents.length) console.log('no agent selected — no skill installed');
312
+
313
+ // An example corpus names files this project does not have, and `cnlp check` refuses a glob
314
+ // matching no file — so the next step it prints is the edit, never a run that fails.
315
+ if (exampleCorpus) {
316
+ console.log(`\nNext: edit the corpus in ${CONFIG_NAME} to name your documents, then run \`cnlp check\`.`);
317
+ } else {
318
+ console.log('\nNext: run `cnlp check` to see where the corpus stands.');
319
+ }
320
+ console.log('Re-run `cnlp init` after every upgrade of this package: it is what brings the skill');
321
+ console.log('copies to the installed version.');
322
+ console.log('A project documenting no library deletes "docsets" and that directory.');
323
+ // Printed and not written: an agent file is a file the project owns, and its content is a
324
+ // decision about how that project's agent works. The operator pastes it where their agent reads.
325
+ // 3 lines and not 5: an installed skill announces its own task through its description, so the
326
+ // block states only what a skill cannot — a standing rule answers to no task description.
327
+ console.log(`\nPaste this into AGENTS.md, CLAUDE.md, or whatever file your agent reads:\n
328
+ - the CNL-P standard is the authority on document form: read it with \`npx cnlp print format\`
329
+ - run \`npx cnlp check\` before a commit that touches a document the corpus names
330
+ - load a library's documentation with \`npx cnlp docs\`, which writes every set's index in reading order`);
331
+ process.exit(0);
332
+ }
333
+
334
+ fail(`unknown command ${JSON.stringify(command)}\n\n${USAGE}`);