@dtmd/temper 0.0.7 → 0.0.8
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.
- package/README.md +84 -53
- package/bin/temper.js +0 -0
- package/dist/src/assembly.d.ts +15 -1
- package/dist/src/assembly.js +2 -1
- package/dist/src/builtins.d.ts +682 -66
- package/dist/src/builtins.js +664 -92
- package/dist/src/claude-code.d.ts +2 -2
- package/dist/src/claude-code.js +1 -1
- package/dist/src/contract.d.ts +180 -29
- package/dist/src/contract.js +128 -15
- package/dist/src/declarations.d.ts +72 -5
- package/dist/src/declarations.js +389 -107
- package/dist/src/dial.d.ts +75 -0
- package/dist/src/dial.js +82 -0
- package/dist/src/emit.d.ts +35 -1
- package/dist/src/emit.js +369 -63
- package/dist/src/generated/AssemblyFactRow.d.ts +2 -2
- package/dist/src/generated/BoundRow.d.ts +2 -2
- package/dist/src/generated/ClauseRow.d.ts +73 -3
- package/dist/src/generated/CollectionAddressRow.d.ts +4 -0
- package/dist/src/generated/EmbeddedMember.d.ts +3 -3
- package/dist/src/generated/FeatureValue.d.ts +2 -2
- package/dist/src/generated/Features.d.ts +52 -3
- package/dist/src/generated/KindFactRow.d.ts +24 -7
- package/dist/src/generated/MentionRow.d.ts +5 -3
- package/dist/src/generated/NestedMemberRow.d.ts +32 -0
- package/dist/src/generated/PayloadMember.d.ts +6 -0
- package/dist/src/generated/RequirementRow.d.ts +4 -2
- package/dist/src/generated/SatisfiesRow.d.ts +2 -1
- package/dist/src/generated/Shape.d.ts +15 -0
- package/dist/src/generated/Shape.js +2 -0
- package/dist/src/generated/TemplateRow.d.ts +24 -0
- package/dist/src/generated/TemplateRow.js +2 -0
- package/dist/src/generated/ValueType.d.ts +11 -2
- package/dist/src/generated/Verifier.d.ts +20 -0
- package/dist/src/generated/Verifier.js +2 -0
- package/dist/src/generated/index.d.ts +3 -0
- package/dist/src/index.d.ts +8 -8
- package/dist/src/index.js +3 -3
- package/dist/src/kind.d.ts +135 -29
- package/dist/src/kind.js +35 -8
- package/dist/src/prose.d.ts +81 -25
- package/dist/src/prose.js +91 -21
- package/package.json +2 -2
package/dist/src/builtins.js
CHANGED
|
@@ -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,
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
76
|
-
* `format`.
|
|
77
|
-
*
|
|
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-
|
|
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-
|
|
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-
|
|
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).
|
|
131
|
-
* decidable
|
|
132
|
-
*
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
731
|
+
cite: "https://agentskills.io/specification#frontmatter (retrieved 2026-07-15)",
|
|
193
732
|
}),
|
|
194
|
-
clause(
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
844
|
+
cite: "https://code.claude.com/docs/en/memory#path-specific-rules (retrieved 2026-07-15)",
|
|
279
845
|
}),
|
|
280
|
-
clause(
|
|
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-
|
|
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
|
|
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-
|
|
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(
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
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
|
|
436
|
-
cite: "https://code.claude.com/docs/en/mcp (retrieved 2026-07-
|
|
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
|
];
|