@dtmd/temper 0.0.7 → 0.0.9

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 (44) hide show
  1. package/README.md +84 -53
  2. package/bin/temper.js +0 -0
  3. package/dist/src/assembly.d.ts +15 -1
  4. package/dist/src/assembly.js +2 -1
  5. package/dist/src/builtins.d.ts +682 -66
  6. package/dist/src/builtins.js +664 -92
  7. package/dist/src/claude-code.d.ts +2 -2
  8. package/dist/src/claude-code.js +1 -1
  9. package/dist/src/contract.d.ts +180 -29
  10. package/dist/src/contract.js +128 -15
  11. package/dist/src/declarations.d.ts +72 -5
  12. package/dist/src/declarations.js +389 -107
  13. package/dist/src/dial.d.ts +75 -0
  14. package/dist/src/dial.js +82 -0
  15. package/dist/src/emit.d.ts +35 -1
  16. package/dist/src/emit.js +369 -63
  17. package/dist/src/generated/AssemblyFactRow.d.ts +2 -2
  18. package/dist/src/generated/BoundRow.d.ts +2 -2
  19. package/dist/src/generated/ClauseRow.d.ts +73 -3
  20. package/dist/src/generated/CollectionAddressRow.d.ts +4 -0
  21. package/dist/src/generated/EmbeddedMember.d.ts +3 -3
  22. package/dist/src/generated/FeatureValue.d.ts +2 -2
  23. package/dist/src/generated/Features.d.ts +52 -3
  24. package/dist/src/generated/KindFactRow.d.ts +24 -7
  25. package/dist/src/generated/MentionRow.d.ts +5 -3
  26. package/dist/src/generated/NestedMemberRow.d.ts +32 -0
  27. package/dist/src/generated/PayloadMember.d.ts +6 -0
  28. package/dist/src/generated/RequirementRow.d.ts +4 -2
  29. package/dist/src/generated/SatisfiesRow.d.ts +2 -1
  30. package/dist/src/generated/Shape.d.ts +15 -0
  31. package/dist/src/generated/Shape.js +2 -0
  32. package/dist/src/generated/TemplateRow.d.ts +24 -0
  33. package/dist/src/generated/TemplateRow.js +2 -0
  34. package/dist/src/generated/ValueType.d.ts +11 -2
  35. package/dist/src/generated/Verifier.d.ts +20 -0
  36. package/dist/src/generated/Verifier.js +2 -0
  37. package/dist/src/generated/index.d.ts +3 -0
  38. package/dist/src/index.d.ts +8 -8
  39. package/dist/src/index.js +3 -3
  40. package/dist/src/kind.d.ts +135 -29
  41. package/dist/src/kind.js +35 -8
  42. package/dist/src/prose.d.ts +81 -25
  43. package/dist/src/prose.js +91 -21
  44. package/package.json +4 -4
@@ -10,14 +10,35 @@
10
10
  * — never from the root.
11
11
  */
12
12
  import { kind } from "./kind.js";
13
- import { allowedChars, clause, deny, enumOf, forbiddenKeys, maxLen, maxLines, minLen, nameMatchesDir, required, uniqueName, } from "./contract.js";
13
+ import { allowedChars, clause, closedKeys, degree, deny, enumOf, extent, forbiddenKeys, globValid, maxLen, mentionReachable, minLen, nameMatchesDir, optional, required, shape, type, uniqueName, when, } from "./contract.js";
14
+ /**
15
+ * `supporting-doc` — a skill's bundled reference document, at the nested-file locus:
16
+ * its path composes from its host skill's unit and the host's template pattern, so it
17
+ * governs no glob of its own. Fields-free and frontmatterless (no `format` — the whole
18
+ * file is body), identity from the filename, and channel-less: a supporting file reaches
19
+ * the world only through the skill whose body references it, never on a channel of its
20
+ * own (code.claude.com/docs/en/skills, "Add supporting files", retrieved 2026-07-16).
21
+ */
22
+ export const supportingDoc = kind({
23
+ name: "supporting-doc",
24
+ locus: { kind: "nested-file" },
25
+ unitShape: "file",
26
+ registration: [],
27
+ });
14
28
  /**
15
29
  * `skill` — `.claude/skills/<name>/SKILL.md`, a directory unit, YAML frontmatter
16
30
  * carrying `name` then `description`; registers on both documented invocation
17
31
  * channels — user-invoked (`/name`) and description-trigger — modulated per
18
32
  * member by the `disable-model-invocation`/`user-invocable` fields
19
33
  * (code.claude.com/docs/en/skills, agentskills.io/specification, retrieved
20
- * 2026-07-07).
34
+ * 2026-07-15).
35
+ *
36
+ * Its one template layer names the bundled reference documents a skill's directory
37
+ * carries: `supporting-doc` children at the directory's own markdown, the documented
38
+ * placement (`my-skill/reference.md` beside `SKILL.md`; same source, "Add supporting
39
+ * files"). The pattern claims the markdown subset the prose-only child kind can hold —
40
+ * a supporting file of another type (the docs' own `scripts/helper.py`) matches nothing
41
+ * and stays unmodeled rather than mis-typed.
21
42
  */
22
43
  export const skill = kind({
23
44
  name: "skill",
@@ -26,11 +47,12 @@ export const skill = kind({
26
47
  unitShape: "directory",
27
48
  registration: [{ via: "user-invoked" }, { via: "description-trigger", field: "description" }],
28
49
  identityField: "name",
50
+ templates: [{ kind: supportingDoc, path: "*.md" }],
29
51
  });
30
52
  /**
31
53
  * `command` — `.claude/commands/*.md`, the skill surface's legacy file placement
32
54
  * (Claude Code merged commands into skills; code.claude.com/docs/en/skills,
33
- * retrieved 2026-07-07): a lone file (identity from the stem, so no
55
+ * retrieved 2026-07-15): a lone file (identity from the stem, so no
34
56
  * `identityField` — like `rule`), the skill's field schema by import, registering
35
57
  * on the same two documented invocation channels as `skill`.
36
58
  */
@@ -47,7 +69,7 @@ export const command = kind({
47
69
  * `name` then `description`; identity is the `name` field (never the filename),
48
70
  * the named-field mode; registers on the description-trigger channel only — no
49
71
  * user-invoked slash command (code.claude.com/docs/en/sub-agents, retrieved
50
- * 2026-07-07).
72
+ * 2026-07-15).
51
73
  */
52
74
  export const agent = kind({
53
75
  name: "agent",
@@ -60,7 +82,7 @@ export const agent = kind({
60
82
  /**
61
83
  * `rule` — `.claude/rules/<name>.md`, a lone file (identity from the stem), YAML
62
84
  * frontmatter; registers a path scope (code.claude.com/docs/en/memory, retrieved
63
- * 2026-07-02).
85
+ * 2026-07-15).
64
86
  */
65
87
  export const rule = kind({
66
88
  name: "rule",
@@ -72,9 +94,11 @@ export const rule = kind({
72
94
  /**
73
95
  * `memory` — a root `<name>.md` (`CLAUDE.md`, `AGENTS.md`), a lone file loaded
74
96
  * unconditionally, with **no frontmatter** (code.claude.com/docs/en/memory,
75
- * retrieved 2026-07-02): the whole file is the body, so the kind declares no
76
- * `format`. Its discovery locus is any-depth (a `CLAUDE.md` at any directory);
77
- * a module-carried memory projects the root file.
97
+ * retrieved 2026-07-15): the whole file is the body, so the kind declares no
98
+ * `format`. A project `CLAUDE.md` may sit at either `./CLAUDE.md` or
99
+ * `./.claude/CLAUDE.md` — equal documented locations (same source) — and the
100
+ * any-depth locus (a `CLAUDE.md` at any directory) covers both; a
101
+ * module-carried memory projects the root file.
78
102
  */
79
103
  export const memory = kind({
80
104
  name: "memory",
@@ -86,7 +110,7 @@ export const memory = kind({
86
110
  * `hook` — a `settings.json` `hooks.<Event>` registration member: a fields-only kind (no
87
111
  * body slot), its members discovered off the `.claude/settings.json` manifest at the
88
112
  * `hooks.<Event>` collection address, keyed by lifecycle event; registers on the `event`
89
- * channel (code.claude.com/docs/en/hooks, retrieved 2026-07-10). The first manifest kind
113
+ * channel (code.claude.com/docs/en/hooks, retrieved 2026-07-15). The first manifest kind
90
114
  * temper ships — the read side of 0021's manifest-authoring surface.
91
115
  */
92
116
  export const hook = kind({
@@ -95,13 +119,13 @@ export const hook = kind({
95
119
  unitShape: "file",
96
120
  registration: [{ via: "event", field: "event" }],
97
121
  shape: "fields",
98
- collectionAddress: { manifest: "settings.json", keyPath: "hooks.<Event>" },
122
+ collectionAddress: { manifest: "settings.json", keyPath: "hooks.<Event>", entryShape: "group-array(hooks;matcher)" },
99
123
  });
100
124
  /**
101
125
  * `mcpServer` — a `.mcp.json` `mcpServers.*` registration member: a fields-only kind (no
102
126
  * body slot), its members discovered off the `.mcp.json` manifest at the `mcpServers.*`
103
127
  * collection address, keyed by server name; registers on the `connection` channel
104
- * (code.claude.com/docs/en/mcp, retrieved 2026-07-10). The second manifest kind temper
128
+ * (code.claude.com/docs/en/mcp, retrieved 2026-07-15). The second manifest kind temper
105
129
  * ships, and the first whose entries are objects — each server's fields fold into the
106
130
  * member the read surfaces.
107
131
  */
@@ -111,13 +135,520 @@ export const mcpServer = kind({
111
135
  unitShape: "file",
112
136
  registration: [{ via: "connection" }],
113
137
  shape: "fields",
114
- collectionAddress: { manifest: ".mcp.json", keyPath: "mcpServers.*" },
138
+ collectionAddress: { manifest: ".mcp.json", keyPath: "mcpServers.*", entryShape: "object" },
139
+ });
140
+ /**
141
+ * `installedPlugin` — a `settings.json` `enabledPlugins` registration member: a
142
+ * fields-only kind (no body slot), its members discovered off the `.claude/settings.json`
143
+ * manifest at the `enabledPlugins.*` collection address, keyed by plugin identity;
144
+ * registers on the `enablement` channel — the entry's own presence is the registration
145
+ * (code.claude.com/docs/en/plugins-reference, retrieved 2026-07-16). The third manifest
146
+ * kind temper ships, and the first whose entries are scalars.
147
+ */
148
+ export const installedPlugin = kind({
149
+ name: "installed-plugin",
150
+ locus: { kind: "at", root: ".claude", glob: "settings.json" },
151
+ unitShape: "file",
152
+ registration: [{ via: "enablement", field: "enabled" }],
153
+ shape: "fields",
154
+ collectionAddress: { manifest: "settings.json", keyPath: "enabledPlugins.*", entryShape: "scalar(enabled)" },
155
+ // The marketplace half of the `<plugin>@<marketplace>` key is a declared edge to the
156
+ // `known-marketplace` member it names (decision 0039). The half is not an authored field
157
+ // — the engine splits it off the composite key at read (`src/kind.rs`, the read-time fold
158
+ // that surfaces it under `marketplace`) — so the edge resolves on the reference graph like
159
+ // any other, and an enablement naming a marketplace no registration declares dangles.
160
+ edgeFields: [{ field: "marketplace", to: ["known-marketplace"] }],
161
+ });
162
+ /**
163
+ * The default contract for `installed-plugin` — **deliberately empty**. The format
164
+ * documents almost no contract, so it earns an almost-empty default: the honest encoding,
165
+ * not a gap.
166
+ *
167
+ * A charset clause on the `<plugin>@<marketplace>` key has nothing decidable to range
168
+ * over: the key is the member's identity, and the two sources that describe it do not
169
+ * settle a charset — the plugins-reference documents the identity as
170
+ * `formatter@my-marketplace` and schemastore's `claude-code-settings.json` constrains its
171
+ * `enabledPlugins` keys with no `propertyNames` pattern at all (both retrieved
172
+ * 2026-07-16), so a clause against either spelling would forge findings on valid harnesses.
173
+ * The marketplace half of the key is not un-typed, though: it is a declared edge to the
174
+ * `known-marketplace` member it names, resolved on the reference graph rather than by a
175
+ * contract clause — an enablement naming a marketplace no registration declares is a
176
+ * dangling-edge finding, never a convention (decision 0039).
177
+ *
178
+ * `enabled` needs no `required` clause: the type already holds it, and a member that
179
+ * omits it projects the `true` Claude Code itself writes.
180
+ */
181
+ export const installedPluginDefaultContract = [];
182
+ /**
183
+ * `knownMarketplace` — a `settings.json` `extraKnownMarketplaces` registration member: a
184
+ * fields-only kind (no body slot), its members discovered off the `.claude/settings.json`
185
+ * manifest at the `extraKnownMarketplaces.*` collection address, keyed by marketplace name;
186
+ * registers on the `registry` channel — the entry's own presence is the registration, and
187
+ * whether the marketplace it names actually resolves is a fetch-time fact temper cannot
188
+ * decide, so the channel is never provably dead (code.claude.com/docs/en/plugin-marketplaces,
189
+ * retrieved 2026-07-17). The fourth registration member temper ships.
190
+ */
191
+ export const knownMarketplace = kind({
192
+ name: "known-marketplace",
193
+ locus: { kind: "at", root: ".claude", glob: "settings.json" },
194
+ unitShape: "file",
195
+ registration: [{ via: "registry" }],
196
+ shape: "fields",
197
+ collectionAddress: { manifest: "settings.json", keyPath: "extraKnownMarketplaces.*", entryShape: "object" },
198
+ });
199
+ /**
200
+ * The default contract for `known-marketplace` — **deliberately empty**. The format
201
+ * documents no decidable schema beyond the shape the {@link KnownMarketplace} type already
202
+ * holds: `source` is the same union {@link Marketplace} carries and reuses its typing,
203
+ * `autoUpdate` is a bare optional boolean, and the key is the member's identity (a
204
+ * marketplace name) rather than a declared field a clause could range over. Nothing decidable
205
+ * survives that the type does not already enforce, so the honest encoding is the empty
206
+ * contract, not a forged clause — the `installedPluginDefaultContract` precedent.
207
+ */
208
+ export const knownMarketplaceDefaultContract = [];
209
+ /**
210
+ * `plugin-manifest` — `.claude-plugin/plugin.json`, a whole-file JSON document (never
211
+ * frontmatter) whose top-level keys are its fields; identity is read from `name`, the
212
+ * named-field mode, because the file's stem is `plugin` for every manifest ever written.
213
+ * It owns its file, so it carries no collection address, and it is channel-less: a
214
+ * manifest carries distribution metadata rather than session content, so it reaches the
215
+ * model on no channel of its own — what it reaches is the installer
216
+ * (code.claude.com/docs/en/plugins-reference, retrieved 2026-07-16).
217
+ */
218
+ export const pluginManifest = kind({
219
+ name: "plugin-manifest",
220
+ locus: { kind: "at", root: ".claude-plugin", glob: "plugin.json" },
221
+ format: "json-document",
222
+ unitShape: "named-field",
223
+ registration: [],
224
+ identityField: "name",
225
+ });
226
+ /**
227
+ * The default contract for `plugin-manifest` — the documented profile of `claude plugin
228
+ * validate --strict`, which is the portable bar: Claude Code's own runtime is deliberately
229
+ * forgiving of unrecognized fields, `--strict` is the CI bar that catches them, and where
230
+ * the two diverge each clause's guidance says so (all facts
231
+ * code.claude.com/docs/en/plugins-reference, retrieved 2026-07-16).
232
+ *
233
+ * The whole profile ships: every documented rule the vocabulary can decide is a clause
234
+ * below, and nothing decidable is held.
235
+ *
236
+ * **Unrecognized top-level fields** — the substance of `--strict` — are the `closedKeys()`
237
+ * clause at the end, over the `required`/`optional` rows above it: Claude Code ignores a
238
+ * key it does not recognize so a manifest doubling as a `package.json` still loads, and
239
+ * `claude plugin validate` warns rather than fails, but `--strict` turns those warnings
240
+ * into the CI errors this contract is the profile of. The declared key set is the union of
241
+ * the two documented sources — the reference's own field tables and the published schema
242
+ * — because a key *either* one recognizes is a key no clause may indict.
243
+ *
244
+ * The component-path fields are gated below rather than held: `type` declares a *set* of
245
+ * lattice kinds, so a field documented `string|array` is checked as the union it is. Each
246
+ * clause declares the widest union its documentation states — a narrower set would reject
247
+ * a documented form, which is a false positive no clause may produce, and the strictest
248
+ * *documented* profile of a union-typed field is the union.
249
+ *
250
+ * Deliberately absent as undecidable: whether the `description` reads well, whether
251
+ * `keywords` aid discovery, whether `name` names the pack aptly.
252
+ *
253
+ * Authoring notes the clauses cannot carry: leave `version` unset while iterating, so the
254
+ * commit SHA drives updates and users are not stranded on a stale pin; set it once the
255
+ * plugin has a release cycle, and bump it every time — pushing commits without bumping is
256
+ * a no-op. Reach for `defaultEnabled: false` when the plugin costs money or scope on load.
257
+ */
258
+ export const pluginManifestDefaultContract = [
259
+ clause(required("name"), {
260
+ severity: "required",
261
+ guidance: "A manifest declares a `name` — the pack's identity and the namespace its components surface under (`plugin-dev:agent-creator`). Omitting the manifest entirely is supported and Claude Code then derives the name from the directory; a manifest that exists and has no `name` is the one case the loader rejects outright: `name: Invalid input: expected string, received undefined`.",
262
+ cite: "https://code.claude.com/docs/en/plugins-reference#required-fields (retrieved 2026-07-16)",
263
+ }),
264
+ clause(minLen("name", 1), {
265
+ severity: "required",
266
+ guidance: "A present-but-empty name cannot namespace a component or key an install. Drop the field to let Claude Code derive the name from the directory, or give it a real one.",
267
+ cite: "https://code.claude.com/docs/en/plugins-reference#required-fields (retrieved 2026-07-16)",
268
+ }),
269
+ clause(allowedChars("name", { ranges: ["a-z", "0-9"], chars: "-" }), {
270
+ severity: "required",
271
+ guidance: "Kebab-case, no spaces — lowercase letters, digits, and hyphens. The name is a namespace prefix and an install key, so a space or a capital makes it unquotable in the places it is typed. Use `displayName` for the human-readable label: it may carry spaces and any casing, and is never used for lookup.",
272
+ cite: "https://code.claude.com/docs/en/plugins-reference#required-fields (retrieved 2026-07-16)",
273
+ }),
274
+ clause(forbiddenKeys(["themes", "monitors"]), {
275
+ severity: "required",
276
+ guidance: "`themes` and `monitors` are experimental components and belong under the `experimental` key. Declaring them at the top level still loads today and `claude plugin validate` only warns — but `--strict` fails it, and a future release will require `experimental.*`, so a top-level spelling is a migration already scheduled against you.",
277
+ cite: "https://code.claude.com/docs/en/plugins-reference#experimental-components (retrieved 2026-07-16)",
278
+ }),
279
+ clause(type("keywords", ["list"]), {
280
+ severity: "required",
281
+ guidance: "`keywords` is an array of discovery tags, and a bare string is not a shorter spelling of a one-tag list: a wrong-typed field is a load error, so the plugin does not load at all — everywhere, not merely under `--strict`, which is the one rule here the forgiving runtime does not wave through. Write `[\"deployment\"]` for a single tag.",
282
+ cite: "https://code.claude.com/docs/en/plugins-reference#unrecognized-fields (retrieved 2026-07-16)",
283
+ }),
284
+ clause(type("skills", ["string", "list"]), {
285
+ severity: "required",
286
+ guidance: "`skills` names custom skill directories — each holding a `<name>/SKILL.md` — as one `\"./custom/skills/\"` path or a list of them. Unlike its neighbours it *adds* to the default `skills/` scan rather than replacing it, so listing `\"./skills/\"` alongside your extra path is redundant, not required.",
287
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
288
+ }),
289
+ clause(type("commands", ["string", "list", "map"]), {
290
+ severity: "required",
291
+ guidance: "`commands` names flat `.md` skill files or directories, as one path, a list of them, or an object keyed by command name (each entry carrying `source` or inline `content`). Setting it *replaces* the default `commands/` scan — to keep the default and add to it, list it explicitly: `[\"./commands/\", \"./extras/\"]`. The reference table names only the string and array forms; the object form is the published schema's, so a manifest using it is valid and this clause admits all three.",
292
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17); object form per https://json.schemastore.org/claude-code-plugin-manifest.json (retrieved 2026-07-16)",
293
+ }),
294
+ clause(type("agents", ["string", "list"]), {
295
+ severity: "required",
296
+ guidance: "`agents` names custom agent files as one path or a list of them, and *replaces* the default `agents/` scan — a manifest that sets it and expects `agents/` to still be read ships a plugin missing every agent in that folder. Claude Code v2.1.140 and later warns about the ignored folder in `claude plugin list`.",
297
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
298
+ }),
299
+ clause(type("hooks", ["string", "list", "map"]), {
300
+ severity: "required",
301
+ guidance: "`hooks` is a path to a hook config JSON file, a list of them, or the config inline as an object keyed by event name. The inline object is the whole `hooks.json` body, not a fragment of it — the same shape `hooks/hooks.json` carries.",
302
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
303
+ }),
304
+ clause(type("mcpServers", ["string", "list", "map"]), {
305
+ severity: "required",
306
+ guidance: "`mcpServers` is a path to an MCP config JSON file, a list of them, or the config inline as an object keyed by server name — the same shape `.mcp.json` carries. A `channels` entry's `server` must match a key here, so the inline object is the form to reach for when the two are authored together.",
307
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
308
+ }),
309
+ clause(type("lspServers", ["string", "list", "map"]), {
310
+ severity: "required",
311
+ guidance: "`lspServers` is a path to an LSP config JSON file, a list of them, or the config inline as an object keyed by server name — the same shape `.lsp.json` carries. These drive code intelligence (go to definition, find references), so a wrong-typed value costs the capability silently rather than loudly.",
312
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
313
+ }),
314
+ // Every documented top-level key the manifest recognizes, `name` (required, above)
315
+ // aside — the allow-list `closedKeys()` reads, declared here once and consumed there
316
+ // rather than restated as a second list that could disagree with this one. The clauses
317
+ // above refine some of these keys' values; a refinement never declares its key, so a key
318
+ // absent from this block is one `closedKeys()` indicts.
319
+ clause(optional("$schema"), {
320
+ severity: "required",
321
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
322
+ }),
323
+ clause(optional("displayName"), {
324
+ severity: "required",
325
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
326
+ }),
327
+ clause(optional("version"), {
328
+ severity: "required",
329
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
330
+ }),
331
+ clause(optional("description"), {
332
+ severity: "required",
333
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
334
+ }),
335
+ clause(optional("author"), {
336
+ severity: "required",
337
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
338
+ }),
339
+ clause(optional("homepage"), {
340
+ severity: "required",
341
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
342
+ }),
343
+ clause(optional("repository"), {
344
+ severity: "required",
345
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
346
+ }),
347
+ clause(optional("license"), {
348
+ severity: "required",
349
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
350
+ }),
351
+ clause(optional("keywords"), {
352
+ severity: "required",
353
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
354
+ }),
355
+ clause(optional("defaultEnabled"), {
356
+ severity: "required",
357
+ cite: "https://code.claude.com/docs/en/plugins-reference#metadata-fields (retrieved 2026-07-17)",
358
+ }),
359
+ clause(optional("skills"), {
360
+ severity: "required",
361
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
362
+ }),
363
+ clause(optional("commands"), {
364
+ severity: "required",
365
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
366
+ }),
367
+ clause(optional("agents"), {
368
+ severity: "required",
369
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
370
+ }),
371
+ clause(optional("hooks"), {
372
+ severity: "required",
373
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
374
+ }),
375
+ clause(optional("mcpServers"), {
376
+ severity: "required",
377
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
378
+ }),
379
+ clause(optional("outputStyles"), {
380
+ severity: "required",
381
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
382
+ }),
383
+ clause(optional("lspServers"), {
384
+ severity: "required",
385
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
386
+ }),
387
+ clause(optional("experimental"), {
388
+ severity: "required",
389
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
390
+ }),
391
+ clause(optional("userConfig"), {
392
+ severity: "required",
393
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
394
+ }),
395
+ clause(optional("channels"), {
396
+ severity: "required",
397
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
398
+ }),
399
+ clause(optional("dependencies"), {
400
+ severity: "required",
401
+ cite: "https://code.claude.com/docs/en/plugins-reference#component-path-fields (retrieved 2026-07-17)",
402
+ }),
403
+ // Recognized at the top level, and separately denied there by the `forbiddenKeys` clause
404
+ // above: the migration is where they are declared, not whether the key is known. Leaving
405
+ // them out of the allow-list would have `closedKeys()` call them unrecognized — false of
406
+ // the format, and a second finding for one mistake.
407
+ clause(optional("themes"), {
408
+ severity: "required",
409
+ cite: "https://code.claude.com/docs/en/plugins-reference#experimental-components (retrieved 2026-07-17)",
410
+ }),
411
+ clause(optional("monitors"), {
412
+ severity: "required",
413
+ cite: "https://code.claude.com/docs/en/plugins-reference#experimental-components (retrieved 2026-07-17)",
414
+ }),
415
+ // The published schema's own property, absent from the reference's field tables. The
416
+ // allow-list spans both sources: this contract already treats the schema as documentation
417
+ // (the `commands` object form is cited to it), so indicting a key it declares would be a
418
+ // false positive.
419
+ clause(optional("settings"), {
420
+ severity: "required",
421
+ cite: "https://json.schemastore.org/claude-code-plugin-manifest.json (retrieved 2026-07-16)",
422
+ }),
423
+ clause(closedKeys(), {
424
+ severity: "required",
425
+ guidance: "This key is not one the manifest format documents. Claude Code ignores an unrecognized top-level field, so the plugin still loads — that is deliberate, and it is what lets one `plugin.json` double as a VS Code or Cursor extension manifest, an npm `package.json`, or an MCPB/DXT bundle manifest. `claude plugin validate` reports it as a warning; `--strict` fails it, which is the CI bar this contract holds, so the usual cause is a typo or a field left over from another tool. If the key is deliberate foreign metadata, this clause is the one to drop from your adopted contract.",
426
+ cite: "https://code.claude.com/docs/en/plugins-reference#unrecognized-fields (retrieved 2026-07-17)",
427
+ }),
428
+ ];
429
+ /**
430
+ * `marketplace` — `.claude-plugin/marketplace.json`, a whole-file JSON document whose
431
+ * top-level keys are its fields; identity from `name`, the named-field mode, because the
432
+ * stem is `marketplace` for every catalog ever written. Like its `plugin-manifest`
433
+ * sibling it owns its file (no collection address) and is channel-less: a catalog is read
434
+ * by the installer, never surfaced to the model
435
+ * (code.claude.com/docs/en/plugin-marketplaces, retrieved 2026-07-16).
436
+ */
437
+ export const marketplace = kind({
438
+ name: "marketplace",
439
+ locus: { kind: "at", root: ".claude-plugin", glob: "marketplace.json" },
440
+ format: "json-document",
441
+ unitShape: "named-field",
442
+ registration: [],
443
+ identityField: "name",
115
444
  });
445
+ /**
446
+ * The reserved marketplace names — reserved for official Anthropic use and refused to a
447
+ * third-party marketplace, re-read from the docs at encode time rather than trusted from
448
+ * memory (code.claude.com/docs/en/plugin-marketplaces, "Marketplace schema", retrieved
449
+ * 2026-07-16). The list grows: before v2.1.205 `first-party-plugins` and `healthcare`
450
+ * were not reserved, so the update ritual is to walk this array against the page, never
451
+ * to re-derive it.
452
+ */
453
+ const RESERVED_MARKETPLACE_NAMES = [
454
+ "claude-code-marketplace",
455
+ "claude-code-plugins",
456
+ "claude-plugins-official",
457
+ "claude-plugins-community",
458
+ "claude-community",
459
+ "anthropic-marketplace",
460
+ "anthropic-plugins",
461
+ "agent-skills",
462
+ "anthropic-agent-skills",
463
+ "knowledge-work-plugins",
464
+ "life-sciences",
465
+ "claude-for-legal",
466
+ "claude-for-financial-services",
467
+ "financial-services-plugins",
468
+ "first-party-plugins",
469
+ "healthcare",
470
+ ];
471
+ /**
472
+ * The default contract for `marketplace` — the strictest documented profile of the catalog
473
+ * format (all facts code.claude.com/docs/en/plugin-marketplaces, retrieved 2026-07-16).
474
+ *
475
+ * The reserved-names clause is the load-bearing one, and it gates a *loud* failure: Claude
476
+ * Code re-checks reserved names on every load, not only on `/plugin marketplace add`, so a
477
+ * catalog published under a name that later becomes reserved stops loading for every user
478
+ * who already added it. That is the one clause here worth more than a lint.
479
+ *
480
+ * The `source` union's per-form required fields are now gated via `when` clauses: decision
481
+ * 0041's Rust implementation shipped in src/contract.rs (Predicate::When, src/engine.rs:1207
482
+ * decide logic), and the SDK's `when()` export has been available since 884a704 — both well
483
+ * before these comments were last touched. The per-source-form requirements now hold via guarded
484
+ * clauses at `plugins[*].source`: the string form needs `leading-dot-slash` shape; each object
485
+ * form (`github`, `url`, `git-subdir`, `npm`) needs its required fields.
486
+ *
487
+ * Deliberately absent as undecidable, and never a clause (`specs/intent.md`, invariant 2):
488
+ * the docs *also* block names that "impersonate official marketplaces" (`official-claude-plugins`,
489
+ * `anthropic-plugins-v2` are the page's own examples). Impersonation is semantic judgment —
490
+ * there is no predicate that decides it, and a clause that guessed would fire on true
491
+ * negatives. The enumerated deny list is the decidable subset; the impersonation rule rides
492
+ * as guidance below.
493
+ *
494
+ * Authoring notes the clauses cannot carry: a relative-path `source` resolves against a
495
+ * *local copy* of the marketplace, so it silently fails to resolve for users who added the
496
+ * marketplace by direct URL to `marketplace.json` — only that one file is downloaded. Reach
497
+ * for `github`, `url`, or `npm` when the catalog is distributed by URL. Where a git source
498
+ * pins both `ref` and `sha`, the `sha` is the effective pin. A marketplace entry's
499
+ * `defaultEnabled` beats the same field in the plugin's own `plugin.json`, while `version`
500
+ * runs the other way — `plugin.json` wins.
501
+ */
502
+ export const marketplaceDefaultContract = [
503
+ clause(required("name"), {
504
+ severity: "required",
505
+ guidance: "A marketplace declares a `name` — the identity users type when installing from it (`/plugin install my-tool@your-marketplace`) and the key each user registers it under. There is no directory fallback the way `plugin.json` has one.",
506
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#required-fields (retrieved 2026-07-16)",
507
+ }),
508
+ clause(minLen("name", 1), {
509
+ severity: "required",
510
+ guidance: "A present-but-empty name cannot key an install or qualify a plugin reference. Give it a real one.",
511
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#required-fields (retrieved 2026-07-16)",
512
+ }),
513
+ clause(allowedChars("name", { ranges: ["a-z", "0-9"], chars: "-" }), {
514
+ severity: "required",
515
+ guidance: "Kebab-case, no spaces — lowercase letters, digits, and hyphens. The name is typed after an `@` to qualify every plugin installed from the marketplace, so a space or a capital makes it unquotable exactly where users type it.",
516
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#required-fields (retrieved 2026-07-16)",
517
+ }),
518
+ clause(deny("name", RESERVED_MARKETPLACE_NAMES), {
519
+ severity: "required",
520
+ guidance: "This name is reserved for official Anthropic use and a third-party marketplace cannot load under it. Claude Code re-checks the reserved list on every load, not only when the marketplace is added — so publishing under one of these names strands every user who already added you: the marketplace stops loading and reports that it is registered from an untrusted source, and they must remove and re-add it. The list also grows (before v2.1.205, `first-party-plugins` and `healthcare` were not on it). Names that merely *impersonate* an official marketplace — `official-claude-plugins`, `anthropic-plugins-v2` — are blocked too, but impersonation is a judgment no clause can decide: this clause holds the enumerated list, and steering clear of the lookalikes is yours.",
521
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#required-fields (retrieved 2026-07-16)",
522
+ }),
523
+ clause(required("owner"), {
524
+ severity: "required",
525
+ guidance: "A marketplace names its maintainer. This clause decides the `owner` object's presence; the clause below decides that its `name` is filled.",
526
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#owner-fields (retrieved 2026-07-17)",
527
+ }),
528
+ clause(required("owner.name"), {
529
+ severity: "required",
530
+ guidance: "`owner.name` is the maintainer or team behind the catalog, and it is required — an `owner` object carrying only `email` does not satisfy the schema. `owner.email` beside it is optional.",
531
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#owner-fields (retrieved 2026-07-17)",
532
+ }),
533
+ clause(required("plugins"), {
534
+ severity: "required",
535
+ guidance: "The `plugins` array is the catalog — a marketplace without it lists nothing. An empty array is a valid, if empty, catalog.",
536
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#required-fields (retrieved 2026-07-17)",
537
+ }),
538
+ clause(required("plugins[*].name"), {
539
+ severity: "required",
540
+ guidance: "Every catalog entry declares a `name` — the plugin identifier users type when installing (`/plugin install my-plugin@marketplace`), kebab-case and without spaces. An entry with no name cannot be installed, and this clause names the entry that omitted it by its own index.",
541
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-entries (retrieved 2026-07-17)",
542
+ }),
543
+ clause(required("plugins[*].source"), {
544
+ severity: "required",
545
+ guidance: "Every catalog entry declares a `source` — where the plugin is fetched from. A listed plugin with no source resolves to nothing. Note that a relative-path source resolves against a *local copy* of the marketplace, so it fails to resolve for users who added the marketplace by direct URL to `marketplace.json`.",
546
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-entries (retrieved 2026-07-17)",
547
+ }),
548
+ when(type("plugins[*].source", ["string"]), [
549
+ clause(shape("plugins[*].source", "leading-dot-slash"), {
550
+ severity: "required",
551
+ guidance: "A relative-path source must start with `./` to resolve correctly against the marketplace root. A path without `./` is treated as a URL or package name, which is not the intent for local files.",
552
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
553
+ }),
554
+ ], {
555
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
556
+ }),
557
+ when(enumOf("plugins[*].source.source", ["github"]), [
558
+ clause(required("source.repo"), {
559
+ severity: "required",
560
+ guidance: "A `github` source entry must specify the `repo` field — the repository in `owner/repo` format — so the plugin can be fetched from GitHub.",
561
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
562
+ }),
563
+ ], {
564
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
565
+ }),
566
+ when(enumOf("plugins[*].source.source", ["url"]), [
567
+ clause(required("source.url"), {
568
+ severity: "required",
569
+ guidance: "A `url` source entry must specify the `url` field — the HTTPS URL to a git repository — so the plugin can be cloned from that location.",
570
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
571
+ }),
572
+ ], {
573
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
574
+ }),
575
+ when(enumOf("plugins[*].source.source", ["git-subdir"]), [
576
+ clause(required("source.url"), {
577
+ severity: "required",
578
+ guidance: "A `git-subdir` source entry must specify the `url` field — the HTTPS URL to the git repository containing the plugin.",
579
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
580
+ }),
581
+ clause(required("source.path"), {
582
+ severity: "required",
583
+ guidance: "A `git-subdir` source entry must specify the `path` field — the path within the repository to the plugin's root directory.",
584
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
585
+ }),
586
+ ], {
587
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
588
+ }),
589
+ when(enumOf("plugins[*].source.source", ["npm"]), [
590
+ clause(required("source.package"), {
591
+ severity: "required",
592
+ guidance: "An `npm` source entry must specify the `package` field — the npm package name (e.g., `@scope/package`) — so the plugin can be installed from the npm registry.",
593
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
594
+ }),
595
+ ], {
596
+ cite: "https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources (retrieved 2026-07-16)",
597
+ }),
598
+ ];
599
+ /**
600
+ * `settings-local` — `.claude/settings.local.json`, a whole-file JSON document at the
601
+ * **local** commitment class: read in place at check and gated, never an `emit` input or
602
+ * target, its rows derived at read time and no row of it ever landing in the lock. Its
603
+ * top-level keys are its fields; identity is the fixed singleton stem `settings.local` (the
604
+ * `file` unit shape — every machine's overlay is the one file at this path, so no declared
605
+ * key names it). Channel-less: machine configuration read by the harness, never surfaced to
606
+ * the model (code.claude.com/docs/en/settings, retrieved 2026-07-16; decisions
607
+ * 0032/0034/0036).
608
+ */
609
+ export const settingsLocal = kind({
610
+ name: "settings-local",
611
+ locus: { kind: "at", root: ".claude", glob: "settings.local.json", commitment: "local" },
612
+ format: "json-document",
613
+ unitShape: "file",
614
+ registration: [],
615
+ });
616
+ /**
617
+ * The default contract for `settings-local` — deliberately near-empty. The settings format
618
+ * documents a large, version-evolving key set (many keys managed-scope-only, most of them
619
+ * scalar preferences), so a `closedKeys()` allow-list would strand every valid local
620
+ * overlay the moment upstream adds a key: 0036 settles the residue *opaque*, not indicted.
621
+ * What stays decidable is the shape of the few structural container keys a local overlay
622
+ * carries — `permissions`, `env`, and `hooks` are each a documented JSON *object*, and a
623
+ * value that is not one cannot be applied — so each is gated as a `map` and everything else
624
+ * rides opaque. (All facts code.claude.com/docs/en/settings, retrieved 2026-07-16.)
625
+ *
626
+ * Deliberately absent as undecidable: whether a permission rule reads correctly, whether an
627
+ * env var is one this machine needs, whether the chosen `model` exists — semantic judgment,
628
+ * never a gate clause.
629
+ */
630
+ export const settingsLocalDefaultContract = [
631
+ clause(type("permissions", ["map"]), {
632
+ severity: "required",
633
+ guidance: "`permissions` is the tool-permission object — `{ allow, ask, deny }`, each an array of rule strings. A value that is not an object carries no rules Claude Code can read, so the permission overlay this file exists to hold silently applies nothing.",
634
+ cite: "https://code.claude.com/docs/en/settings#permission-settings (retrieved 2026-07-16)",
635
+ }),
636
+ clause(type("env", ["map"]), {
637
+ severity: "required",
638
+ guidance: "`env` is a map of environment variables — string keys to string values — applied to every session. A non-object value is not a shorter spelling of one variable; it is a shape the loader cannot expand, so none of the variables take effect.",
639
+ cite: "https://code.claude.com/docs/en/settings#available-settings (retrieved 2026-07-16)",
640
+ }),
641
+ clause(type("hooks", ["map"]), {
642
+ severity: "required",
643
+ guidance: "`hooks` is the hooks-configuration object, keyed by lifecycle event. A locally-registered hook lives inside this object as opaque residue — it is not modeled as a `hook` member of its own, since that kind reads the committed `settings.json`. A value that is not an object registers no hooks at all.",
644
+ cite: "https://code.claude.com/docs/en/settings#available-settings (retrieved 2026-07-16)",
645
+ }),
646
+ ];
116
647
  /**
117
648
  * The default contract for `skill` — Anthropic's documented skill contract: the Agent
118
649
  * Skills open standard (agentskills.io), Anthropic's platform upload
119
650
  * validation, and Claude Code's own docs.
120
- * All sources retrieved 2026-07-09.
651
+ * All sources retrieved 2026-07-15.
121
652
  *
122
653
  * Checks the strictest documented profile: the spec and upload validation are
123
654
  * hard, Claude Code's runtime is deliberately forgiving ("All fields are
@@ -127,11 +658,9 @@ export const mcpServer = kind({
127
658
  *
128
659
  * Deliberately absent — undecidable, so never gate clauses: whether the
129
660
  * description actually triggers well or reads third-person (semantic);
130
- * vagueness/no-op detection (semantic); gerund naming (judgment). Two
131
- * decidable spec rules are also absent, pending a vocabulary addition (a
132
- * narrow shape predicate governs additions): the name
133
- * must not start/end with a hyphen or contain consecutive hyphens; likewise
134
- * the platform's "no XML tags in the description."
661
+ * vagueness/no-op detection (semantic); gerund naming (judgment). Nothing
662
+ * decidable is held: the name's hyphen placement and the platform's "no XML
663
+ * tags in the description" are the two `shape` clauses below.
135
664
  *
136
665
  * Authoring notes the clauses cannot carry: prefer gerund or noun-phrase
137
666
  * names (`processing-pdfs`, `pdf-processing`) over vague ones (`helper`,
@@ -144,62 +673,100 @@ export const skillDefaultContract = [
144
673
  clause(required("name"), {
145
674
  severity: "required",
146
675
  guidance: "Every skill declares a `name` — the slug the harness binds to. Claude Code alone would default it from the directory name, but a nameless skill is not portable: the spec and Anthropic's upload validation both require it.",
147
- cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-09)",
676
+ cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-15)",
148
677
  }),
149
678
  clause(minLen("name", 1), {
150
679
  severity: "required",
151
680
  guidance: "A present-but-empty name fails the spec's 1-64 character bound.",
152
- cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-09)",
681
+ cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-15)",
153
682
  }),
154
683
  clause(allowedChars("name", { ranges: ["a-z", "0-9"], chars: "-" }), {
155
684
  severity: "required",
156
685
  guidance: "Lowercase letters, digits, and hyphens only — `PDF-Processing` is the spec's own counter-example. The charset also keeps XML out of the name, which Anthropic's upload validation separately forbids.",
157
- cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-09)",
686
+ cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-15)",
158
687
  }),
159
688
  clause(maxLen("name", 64), {
160
689
  severity: "required",
161
690
  guidance: "Keep the name short and slug-like; it becomes a directory and an id.",
162
- cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-09)",
691
+ cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-15)",
692
+ }),
693
+ clause(shape("name", "hyphen-placement"), {
694
+ severity: "required",
695
+ guidance: "A hyphen separates segments, so it may not lead, trail, or double — `-pdf`, `pdf-`, and `pdf--processing` are the spec's own counter-examples. The charset clause above admits the hyphen; this places it.",
696
+ cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-17)",
163
697
  }),
164
698
  clause(deny("name", ["anthropic", "claude"]), {
165
699
  severity: "required",
166
700
  guidance: "Reserved words, enforced by Anthropic's platform upload validation (not by the open spec, and not by Claude Code's runtime — which itself ships a `claude-api` skill). Keep them out if the skill will ever travel through the API or claude.ai.",
167
- cite: "https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#skill-structure (retrieved 2026-07-09)",
701
+ cite: "https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#skill-structure (retrieved 2026-07-15)",
168
702
  }),
169
703
  clause(nameMatchesDir(), {
170
704
  severity: "required",
171
705
  guidance: "The spec requires the name to match its parent directory. Claude Code decouples the two (the frontmatter name is a display label; the directory names the slash command, except for a plugin-root SKILL.md) — but a mismatch is a portability trap and a reader trap even where it loads.",
172
- cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-09)",
706
+ cite: "https://agentskills.io/specification#name-field (retrieved 2026-07-15)",
173
707
  }),
174
708
  clause(required("description"), {
175
709
  severity: "required",
176
710
  guidance: "The description is how the model chooses this skill from potentially 100+ available — it is the skill's API. Claude Code would fall back to the body's first paragraph; the spec and upload validation require it declared.",
177
- cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-09)",
711
+ cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-15)",
178
712
  }),
179
713
  clause(minLen("description", 1), {
180
714
  severity: "required",
181
715
  guidance: "Say both what the skill does and when to use it, with the keywords a user would naturally say. Write in third person — the text is injected into the system prompt, and inconsistent point-of-view causes discovery problems.",
182
- cite: "https://agentskills.io/specification#description-field (retrieved 2026-07-09)",
716
+ cite: "https://agentskills.io/specification#description-field (retrieved 2026-07-15)",
183
717
  }),
184
718
  clause(maxLen("description", 1024), {
185
719
  severity: "required",
186
720
  guidance: "The spec's cap. Claude Code additionally truncates the skill listing at 1,536 combined characters (description + when_to_use) — truncation, not rejection, but text past the fold cannot help the model choose.",
187
- cite: "https://agentskills.io/specification#description-field (retrieved 2026-07-09)",
721
+ cite: "https://agentskills.io/specification#description-field (retrieved 2026-07-15)",
722
+ }),
723
+ clause(shape("description", "no-xml-tags"), {
724
+ severity: "required",
725
+ guidance: "Anthropic's platform upload validation rejects a description carrying an XML tag — the description is injected into the system prompt, where a tag is markup rather than text. Neither the open spec nor Claude Code's runtime enforces it, so this is the clause that keeps a skill portable through the API and claude.ai. Prose spelling a comparison (`use when x < y`) is not a tag and does not fire.",
726
+ cite: "https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#skill-structure (retrieved 2026-07-17)",
188
727
  }),
189
728
  clause(maxLen("compatibility", 500), {
190
729
  severity: "required",
191
730
  guidance: "Optional field; when present the spec caps it at 500 characters. Most skills do not need it.",
192
- cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-09)",
731
+ cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-15)",
193
732
  }),
194
- clause(maxLines(500), {
733
+ clause(extent("lines", 500), {
195
734
  severity: "advisory",
196
- guidance: "Progressive disclosure: keep SKILL.md under 500 lines and move detailed reference material to separate files, one level deep. Once a skill loads, its body stays in context across turns — every line is a recurring token cost. The context window is a public good.",
197
- cite: "https://agentskills.io/specification#progressive-disclosure (retrieved 2026-07-09)",
735
+ guidance: "Progressive disclosure: keep SKILL.md under 500 rendered lines and move detailed reference material to separate files, one level deep. Measured render-side — the lines the projected artifact contributes to context, not the source body before any include resolves. Once a skill loads, its body stays in context across turns — every line is a recurring token cost. The context window is a public good.",
736
+ cite: "https://agentskills.io/specification#progressive-disclosure (retrieved 2026-07-15)",
198
737
  }),
199
738
  clause(forbiddenKeys(["globs", "alwaysApply"]), {
200
739
  severity: "required",
201
740
  guidance: "Cursor `.mdc` keys. Nothing in the Agent Skills spec or Claude Code's documented frontmatter accepts them — a skill authored with them is carrying dead configuration that another tool's semantics silently fail to apply.",
202
- cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-09)",
741
+ cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-15)",
742
+ }),
743
+ clause(globValid("paths"), {
744
+ severity: "required",
745
+ guidance: "The optional `paths` scope gates every invocation channel until Claude reads a file its globs match; each entry is a glob (brace expansion supported). An unparseable pattern — an unclosed `[`, say — is invalid under globset and silently matches nothing, so the gate never opens and the skill never registers, with no error surfaced. Fix the pattern or drop the field.",
746
+ cite: "https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)",
747
+ }),
748
+ ];
749
+ /**
750
+ * The default contract for `supporting-doc` — one clause, because the format documents
751
+ * exactly one thing about a supporting file that is decidable, and it is not a fact
752
+ * about the file's own contents: a supporting file is prose Claude reads when the
753
+ * skill's body points at it, so an unreferenced one is never read at all
754
+ * (code.claude.com/docs/en/skills, "Add supporting files", retrieved 2026-07-16). The
755
+ * format carries no frontmatter schema, no required field and no cap of its own, so
756
+ * nothing else joins it — an almost-empty format gets an almost-empty contract, and
757
+ * manufacturing a second clause would fake a check the format does not carry.
758
+ *
759
+ * The reach bound is a property of the file's *place in the graph*, not its bytes, and
760
+ * the fact holds of every supporting document — so it is spelled as the by-kind
761
+ * universal binding at the `each` grain (`model/contract.md`, "selection"), never as a
762
+ * requirement: a requirement is the opt-in selector, and routing a vendor fact through
763
+ * one would make the harness's own truth a consumer's ceremony.
764
+ */
765
+ export const supportingDocDefaultContract = [
766
+ clause(degree({ incoming: { min: 1 } }), {
767
+ severity: "advisory",
768
+ guidance: "Reference the file from `SKILL.md` — a supporting file the skill's body never points at is invisible: Claude has no way to learn what it holds or when to load it, and it ships as dead weight in the bundle. Any resolved edge from the host skill counts, a mention included; what the edge cannot decide is whether the reference tells Claude *when* to follow it, and that sentence is the point of writing one.",
769
+ cite: "https://code.claude.com/docs/en/skills (retrieved 2026-07-16)",
203
770
  }),
204
771
  ];
205
772
  /**
@@ -208,12 +775,12 @@ export const skillDefaultContract = [
208
775
  * that ranges over the directory relationship does not apply; every other
209
776
  * documented skill-schema recommendation, name-requiredness included, still
210
777
  * governs a command by the same import (code.claude.com/docs/en/skills,
211
- * retrieved 2026-07-07).
778
+ * retrieved 2026-07-15).
212
779
  */
213
780
  export const commandDefaultContract = skillDefaultContract.filter((entry) => entry.predicate.key !== "name-matches-dir");
214
781
  /**
215
782
  * The default contract for `agent` — Anthropic's documented subagent contract
216
- * (code.claude.com/docs/en/sub-agents, retrieved 2026-07-07): `name` and
783
+ * (code.claude.com/docs/en/sub-agents, retrieved 2026-07-15): `name` and
217
784
  * `description` are the only required fields, `name` is a "unique identifier
218
785
  * using lowercase letters and hyphens" (no digits, unlike a skill's `name`), and
219
786
  * "keep `name` values unique across the whole tree" — a same-scope collision
@@ -227,29 +794,28 @@ export const agentDefaultContract = [
227
794
  clause(required("name"), {
228
795
  severity: "required",
229
796
  guidance: "Every subagent declares a `name` — its unique identifier. Claude Code binds identity to this field alone, never the filename, so a nameless subagent cannot be delegated to.",
230
- cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-07)",
797
+ cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-15)",
231
798
  }),
232
799
  clause(allowedChars("name", { ranges: ["a-z"], chars: "-" }), {
233
800
  severity: "required",
234
801
  guidance: "Lowercase letters and hyphens only — no digits, unlike a skill's `[a-z0-9-]` name. Hooks receive this value as `agent_type`.",
235
- cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-07)",
802
+ cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-15)",
236
803
  }),
237
804
  clause(uniqueName(), {
238
805
  severity: "required",
239
806
  guidance: "Keep `name` values unique across the whole tree — when two files in one scope declare the same name, Claude Code loads only one of them, silently shadowing the other.",
240
- cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-07)",
807
+ cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-15)",
241
808
  }),
242
809
  clause(required("description"), {
243
810
  severity: "required",
244
811
  guidance: "The description is how Claude decides when to delegate to this subagent — write it so the trigger is unambiguous.",
245
- cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-07)",
812
+ cite: "https://code.claude.com/docs/en/sub-agents (retrieved 2026-07-15)",
246
813
  }),
247
814
  ];
248
815
  /**
249
816
  * The default contract for `rule` — Anthropic's documented contract for a Claude Code
250
817
  * rules file, sourced from the memory docs (`.claude/rules/` landed in
251
- * v2.0.64; `packages/rule.anthropic/PACKAGE.md`, the curated authoring
252
- * reference this migrates verbatim). All sources retrieved 2026-07-09.
818
+ * v2.0.64). All sources retrieved 2026-07-15.
253
819
  *
254
820
  * `paths` is the one documented frontmatter key for rules: glob patterns
255
821
  * (brace expansion supported) that scope the rule to matching files. Rules
@@ -259,7 +825,7 @@ export const agentDefaultContract = [
259
825
  * optional field asserts nothing decidable, so it carries no clause of its
260
826
  * own: `required` is the one
261
827
  * presence predicate, and its absence is not itself a predicate.)
262
- * https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-09)
828
+ * https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)
263
829
  *
264
830
  * What the clauses cannot carry, as guidance: keep a rule to facts Claude
265
831
  * should hold whenever the rule is in scope — concrete enough to verify ("use
@@ -275,22 +841,31 @@ export const ruleDefaultContract = [
275
841
  clause(forbiddenKeys(["description", "globs", "alwaysApply"]), {
276
842
  severity: "required",
277
843
  guidance: "Cursor `.mdc` keys. Claude Code's documented rules schema is `paths`-only; a rule authored with Cursor frontmatter is configuration another tool's semantics silently fail to honor — the rule loads, the scoping you meant does not. (That Claude Code ignores unknown keys is observed behavior, not documented contract — the documented schema is the citation.)",
278
- cite: "https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-09)",
844
+ cite: "https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)",
279
845
  }),
280
- clause(maxLines(200), {
846
+ clause(globValid("paths"), {
847
+ severity: "required",
848
+ guidance: "`paths` is the one documented rules key: globs (brace expansion supported) that scope the rule to matching files. An unparseable pattern — an unclosed `[`, say — is invalid under globset and silently matches nothing, so the rule never loads where you meant it to, with no error surfaced. Fix the pattern or drop it.",
849
+ cite: "https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)",
850
+ }),
851
+ clause(extent("lines", 200), {
281
852
  severity: "advisory",
282
- guidance: "Unconditional rules are always-on context, paid every session: the docs' size target is under 200 lines per memory file — 'longer files consume more context and reduce adherence.' (Distinct from the hard 200-line/25KB cutoff, which applies only to auto-memory MEMORY.md; rules load in full regardless of length.) For each line ask: would removing it cause Claude to make mistakes? If not, cut it.",
283
- cite: "https://code.claude.com/docs/en/memory#write-effective-instructions (retrieved 2026-07-09)",
853
+ guidance: "Unconditional rules are always-on context, paid every session: the docs' size target is under 200 rendered lines per memory file — 'longer files consume more context and reduce adherence.' Measured render-side, off the projected artifact rather than the source body. (Distinct from the hard 200-line/25KB cutoff, which applies only to auto-memory MEMORY.md; rules load in full regardless of length.) For each line ask: would removing it cause Claude to make mistakes? If not, cut it.",
854
+ cite: "https://code.claude.com/docs/en/memory#write-effective-instructions (retrieved 2026-07-15)",
855
+ }),
856
+ clause(mentionReachable("paths", "paths"), {
857
+ severity: "advisory",
858
+ guidance: "A mention of a gated member is actionable only where that member can be invoked. A `paths` gate removes its member from every invocation channel until Claude reads a matching file, and invoking a gated member from outside its gate hard-errors (`Unknown skill`) — the harness then tells the user it doesn't exist. So a rule that loads where its target cannot be invoked hands Claude an obligation it cannot act on. Two remedies: scope this rule's `paths` to the target's gate, or ungate the target. Advisory because the containment test is literal — every glob here must appear verbatim in the gate — so a semantically narrower glob (`src/**/*.ts` inside `src/**`) false-fires; retune or drop this clause in your own contract when it does.",
859
+ cite: "https://code.claude.com/docs/en/skills (retrieved 2026-07-16; gating hard-error verified against 2.1.211)",
284
860
  }),
285
861
  ];
286
862
  /**
287
863
  * The default contract for the qualified `claude-code.memory` kind — Anthropic's
288
- * documented contract for a project `CLAUDE.md` (`packages/memory.anthropic/PACKAGE.md`,
289
- * the curated authoring reference this migrates verbatim). Retrieved 2026-07-09.
864
+ * documented contract for a project `CLAUDE.md`. Retrieved 2026-07-15.
290
865
  *
291
866
  * Deliberately near-empty, because the format is: `CLAUDE.md` is plain
292
867
  * markdown with no documented frontmatter and no required fields
293
- * (code.claude.com/docs/en/memory, retrieved 2026-07-09), so there is no
868
+ * (code.claude.com/docs/en/memory, retrieved 2026-07-15), so there is no
294
869
  * schema to gate — manufacturing a required field or a forbidden-key list
295
870
  * would fake a check the format does not carry. The single clause is a context-cost
296
871
  * budget; everything else the contract could say is guidance.
@@ -311,38 +886,15 @@ export const ruleDefaultContract = [
311
886
  * (gitignored), appended after `CLAUDE.md` at its level.
312
887
  */
313
888
  export const memoryAnthropicDefaultContract = [
314
- clause(maxLines(200), {
889
+ clause(extent("lines", 200), {
315
890
  severity: "advisory",
316
- guidance: "CLAUDE.md is always-on context, paid every session. The memory docs' size target is under 200 lines per memory file — 'longer files consume more context and reduce adherence.' For each line ask: would removing it cause Claude to make mistakes? If not, cut it. (Advisory: Claude Code loads the file in full regardless of length; this is a context-cost budget, not a hard cutoff.)",
317
- cite: "https://code.claude.com/docs/en/memory#write-effective-instructions (retrieved 2026-07-09)",
891
+ guidance: "CLAUDE.md is always-on context, paid every session. The memory docs' size target is under 200 rendered lines per memory file — 'longer files consume more context and reduce adherence.' Measured render-side, off the projected artifact rather than the source body. For each line ask: would removing it cause Claude to make mistakes? If not, cut it. (Advisory: Claude Code loads the file in full regardless of length; this is a context-cost budget, not a hard cutoff.)",
892
+ cite: "https://code.claude.com/docs/en/memory#write-effective-instructions (retrieved 2026-07-15)",
318
893
  }),
319
894
  ];
320
- /**
321
- * The default contract for the qualified `agents-md.memory` kind — the AGENTS.md
322
- * standard's contract for a memory file, which is that there is almost none
323
- * (`packages/memory.agents-md/PACKAGE.md`, the curated authoring reference
324
- * this migrates). Guidance-only, and that is the honest encoding: `AGENTS.md`
325
- * "is just standard Markdown" with no required fields, no sections, and no
326
- * frontmatter (agents.md, retrieved 2026-07-09); the format deliberately
327
- * constrains nothing. A default contract that manufactured a required field, a size
328
- * gate, or a forbidden-key list would assert a contract the standard
329
- * disclaims. Even the tempting size
330
- * number is a *tool's* rule, not the format's: agents read the closest
331
- * `AGENTS.md` in the tree (nested, nearest-wins; agents.md, retrieved
332
- * 2026-07-09); Codex concatenates the chain root-to-cwd and stops once
333
- * combined size hits a byte budget, not a per-file line count
334
- * (`project_doc_max_bytes`, 32 KiB default;
335
- * developers.openai.com/codex/guides/agents-md, retrieved 2026-07-09);
336
- * Gemini CLI reads `GEMINI.md` by default and only treats `AGENTS.md` as an
337
- * alias when configured via `context.fileName` (geminicli.com/docs/cli/gemini-md,
338
- * retrieved 2026-07-09); Claude Code does not read `AGENTS.md` natively —
339
- * bridge it with a `CLAUDE.md` that `@AGENTS.md`-imports it
340
- * (code.claude.com/docs/en/memory, retrieved 2026-07-09).
341
- */
342
- export const memoryAgentsMdDefaultContract = [];
343
895
  /**
344
896
  * Every documented Claude Code hook lifecycle event — the closed set a `hooks.<Event>`
345
- * key is drawn from (code.claude.com/docs/en/hooks, "Hook events", retrieved 2026-07-10).
897
+ * key is drawn from (code.claude.com/docs/en/hooks, "Hook events", retrieved 2026-07-15).
346
898
  * The allowlist the `hook` default contract's one decidable clause ranges over; the
347
899
  * update ritual when the docs add an event is to re-fetch and extend this set, never to
348
900
  * re-derive from memory.
@@ -381,18 +933,21 @@ const DOCUMENTED_HOOK_EVENTS = [
381
933
  ];
382
934
  /**
383
935
  * The default contract for `hook` — Anthropic's documented hooks contract
384
- * (code.claude.com/docs/en/hooks, retrieved 2026-07-10). A hook surfaces at
936
+ * (code.claude.com/docs/en/hooks, retrieved 2026-07-15). A hook surfaces at
385
937
  * `hooks.<Event>`, so the member the gate reads is the lifecycle event itself, its name
386
938
  * carried as the `event` field off the collection key. The one decidable, cited property
387
939
  * of that member is its event: a key outside the documented set is dead configuration —
388
940
  * Claude Code silently never fires a hook under an unrecognized event, so the strictest
389
941
  * documented profile is that the event is one temper's cited docs name.
390
942
  *
391
- * Deliberately absent — the handler's own schema (`type`/`command`/`url`/`timeout`, the
392
- * matcher grammar) lives one array level deeper than `hooks.<Event>`, inside each event's
393
- * matcher-group list, which the collection address does not walk into; a clause over it
394
- * would range over a field the read never surfaces, so it is no clause at all. What the
395
- * clauses cannot carry, as guidance: keep a handler's `type` among
943
+ * **Re-examined against decision 0041's widened vocabulary and confirmed to still hold**:
944
+ * The handler's own schema (`type`/`command`/`url`/`timeout`, the matcher grammar) lives
945
+ * one array level deeper than `hooks.<Event>`, inside each event's matcher-group list. The
946
+ * collection address `hooks.<Event>` does not walk into arrays, and even with the guard
947
+ * vocabulary's when/enumOf/type extensions, addressing still cannot spell a path into the
948
+ * handler array (e.g., `hooks.<Event>[0].type`). A clause over it would range over a field
949
+ * the read never surfaces, so it is no clause at all — the addressing-reach gap remains.
950
+ * What the clauses cannot carry, as guidance: keep a handler's `type` among
396
951
  * `command`/`http`/`mcp_tool`/`prompt`/`agent`; a `command` handler needs a `command`, an
397
952
  * `http` handler a `url`; the `matcher` filters tool-scoped events and is inert on events
398
953
  * that carry no tool (`UserPromptSubmit`, `Stop`, and their siblings).
@@ -401,12 +956,12 @@ export const hookDefaultContract = [
401
956
  clause(enumOf("event", DOCUMENTED_HOOK_EVENTS), {
402
957
  severity: "required",
403
958
  guidance: "A hook keys under its lifecycle event; an event outside the documented set is dead configuration — Claude Code silently never fires a hook under an unrecognized event. If this is a newly-documented event, re-fetch code.claude.com/docs/en/hooks and extend temper's cited set rather than working around the finding.",
404
- cite: "https://code.claude.com/docs/en/hooks (retrieved 2026-07-10)",
959
+ cite: "https://code.claude.com/docs/en/hooks (retrieved 2026-07-15)",
405
960
  }),
406
961
  ];
407
962
  /**
408
963
  * Every documented `.mcp.json` server transport — the closed set a server entry's `type`
409
- * is drawn from (code.claude.com/docs/en/mcp, retrieved 2026-07-10). `stdio` is the
964
+ * is drawn from (code.claude.com/docs/en/mcp, retrieved 2026-07-15). `stdio` is the
410
965
  * default when `type` is absent; `streamable-http` is the MCP spec's own name for `http`,
411
966
  * accepted as an alias so configurations copied from server docs work unchanged; `sse` is
412
967
  * documented but deprecated; `ws` is the WebSocket transport. The update ritual when the
@@ -415,24 +970,41 @@ export const hookDefaultContract = [
415
970
  const DOCUMENTED_MCP_TRANSPORTS = ["stdio", "http", "streamable-http", "sse", "ws"];
416
971
  /**
417
972
  * The default contract for `mcpServer` — Anthropic's documented `.mcp.json` contract
418
- * (code.claude.com/docs/en/mcp, retrieved 2026-07-10). A server surfaces at `mcpServers.*`,
973
+ * (code.claude.com/docs/en/mcp, retrieved 2026-07-15). A server surfaces at `mcpServers.*`,
419
974
  * keyed by name, its transport-specific fields folded into the member. The one decidable,
420
975
  * cited property that holds across every transport is `type`: a value outside the
421
976
  * documented set is a transport Claude Code cannot honor, so the strictest documented
422
977
  * profile is that a present `type` names one temper's cited docs carry. An absent `type`
423
978
  * passes — Claude Code reads it as `stdio`, the documented default.
424
979
  *
425
- * Deliberately absent — the per-transport requirements are conditional on `type`, which no
426
- * single-field clause can decide: a `url` with no `type` is a configuration error (Claude
427
- * Code reads it as a stdio server and skips it), a stdio server needs a `command`, and a
428
- * remote server needs a `url` — each a two-field implication the closed predicate
429
- * vocabulary cannot express, so it rides guidance rather than a clause that would range
430
- * over a field the shape of the check cannot see.
980
+ * The per-transport requirements are now gated via `when` clauses: a stdio server (type
981
+ * absent or `stdio`) needs a `command`, and a remote server (type `http`, `streamable-http`,
982
+ * `sse`, or `ws`) needs a `url`. Decision 0041's Rust implementation shipped in
983
+ * src/contract.rs and the SDK's `when()` export has been available since 884a704 — both
984
+ * well before these comments were last written.
431
985
  */
432
986
  export const mcpServerDefaultContract = [
433
987
  clause(enumOf("type", DOCUMENTED_MCP_TRANSPORTS), {
434
988
  severity: "required",
435
- guidance: "A server's `type` names its transport; a value outside the documented set is one Claude Code cannot honor. Absent reads as `stdio` — but an entry that carries a `url` with no `type` is then a configuration error, because Claude Code treats it as a stdio server and skips it: add `type: \"http\"` (or `sse`/`ws`). A stdio server needs a `command`; a remote server needs a `url`. If this is a newly-documented transport, re-fetch code.claude.com/docs/en/mcp and extend temper's cited set rather than working around the finding.",
436
- cite: "https://code.claude.com/docs/en/mcp (retrieved 2026-07-10)",
989
+ guidance: "A server's `type` names its transport; a value outside the documented set is one Claude Code cannot honor. Absent reads as `stdio`, the documented default. If this is a newly-documented transport, re-fetch code.claude.com/docs/en/mcp and extend temper's cited set rather than working around the finding.",
990
+ cite: "https://code.claude.com/docs/en/mcp (retrieved 2026-07-15)",
991
+ }),
992
+ when(enumOf("type", ["stdio"]), [
993
+ clause(required("command"), {
994
+ severity: "required",
995
+ guidance: "A stdio server must specify a `command` — the executable path and arguments to run the server process. Without it, Claude Code cannot start the server. (Note: when `type` is absent, Claude Code reads it as `stdio`; a `command` is also required in that case.)",
996
+ cite: "https://code.claude.com/docs/en/mcp#transport-options (retrieved 2026-07-15)",
997
+ }),
998
+ ], {
999
+ cite: "https://code.claude.com/docs/en/mcp#transport-options (retrieved 2026-07-15)",
1000
+ }),
1001
+ when(enumOf("type", ["http", "streamable-http", "sse", "ws"]), [
1002
+ clause(required("url"), {
1003
+ severity: "required",
1004
+ guidance: "A remote server (http, streamable-http, sse, or ws transport) must specify a `url` — the endpoint to connect to. Without it, Claude Code cannot establish the connection.",
1005
+ cite: "https://code.claude.com/docs/en/mcp#transport-options (retrieved 2026-07-15)",
1006
+ }),
1007
+ ], {
1008
+ cite: "https://code.claude.com/docs/en/mcp#transport-options (retrieved 2026-07-15)",
437
1009
  }),
438
1010
  ];