@jeremiewerner/motto 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jérémie Werner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/motto.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * motto CLI entry point — thin shell.
4
+ *
5
+ * Parses the subcommand with node:util parseArgs (allowPositionals, strict:true
6
+ * so unknown flags throw and are caught to print usage). Dispatches to the lint
7
+ * orchestrator or reserves the build slot for Phase 3.
8
+ *
9
+ * Output format (D2-11):
10
+ * Clean: "✓ N skills OK" to stdout, exit 0.
11
+ * Errors: "✗ <skill>: <message>" per error to stdout, exit 1.
12
+ * Usage: "usage: motto <lint|build>" to stderr, exit 1.
13
+ *
14
+ * Exit codes: always set via process.exitCode (never process.exit(1)) to
15
+ * avoid truncating buffered stdout (Pitfall 7). The only exception is
16
+ * process.exit() (no arg) in the parseArgs catch block, which is safe because
17
+ * we have already written all output to stderr before calling it.
18
+ */
19
+
20
+ import { parseArgs } from 'node:util';
21
+ import { lintProject } from '../src/lint.js';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Parse argv — strict:true throws on any unknown flag (e.g. --help)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ let parsed;
28
+ try {
29
+ parsed = parseArgs({
30
+ args: process.argv.slice(2),
31
+ options: {},
32
+ allowPositionals: true,
33
+ strict: true,
34
+ });
35
+ } catch {
36
+ // Unknown flag (strict:true) — print usage and exit (Pitfall 4, D2-16)
37
+ process.stderr.write('usage: motto <lint|build>\n');
38
+ process.exitCode = 1;
39
+ process.exit(); // exits with exitCode 1; safe: all output written before this call
40
+ }
41
+
42
+ const sub = parsed.positionals[0];
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Subcommand dispatch
46
+ // ---------------------------------------------------------------------------
47
+
48
+ if (sub === 'lint') {
49
+ // cwd-only, v1 (D2-15)
50
+ const result = await lintProject(process.cwd());
51
+ if (result.ok) {
52
+ process.stdout.write(`✓ ${result.count} skills OK\n`);
53
+ } else {
54
+ for (const e of result.errors) {
55
+ process.stdout.write(`✗ ${e.skill}: ${e.message}\n`);
56
+ }
57
+ process.exitCode = 1; // D2-11; process.exit(1) NOT used (Pitfall 7)
58
+ }
59
+ } else if (sub === 'build') {
60
+ // cwd-only, mirrors lint branch (D3-15)
61
+ const { buildProject } = await import('../src/build.js');
62
+ const result = await buildProject(process.cwd());
63
+ if (result.ok) {
64
+ // D3-16: output dir + one-line summary
65
+ process.stdout.write(
66
+ `✓ built ${result.outDir} — ${result.skillCount} skills, ${result.bucketCount} plugin(s)\n`,
67
+ );
68
+ } else {
69
+ for (const e of result.errors) {
70
+ process.stdout.write(`✗ ${e.skill}: ${e.message}\n`);
71
+ }
72
+ process.exitCode = 1; // never process.exit(1) — preserves stdout flush (Pitfall 7)
73
+ }
74
+ } else {
75
+ // Unknown subcommand or no subcommand at all (D2-16)
76
+ process.stderr.write('usage: motto <lint|build>\n');
77
+ process.exitCode = 1;
78
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "motto",
3
+ "version": "0.0.3",
4
+ "description": "Motto is a framework for authoring, validating, and packaging Claude Code Agent Skills into self-contained, distributable plugins."
5
+ }
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: author-skill
3
+ description: Teaches how to write a conforming SKILL.md for the Motto framework. Use this skill when authoring a new Motto skill or investigating a lint error.
4
+ audience: public
5
+ shared_references:
6
+ - skill-schema
7
+ ---
8
+
9
+ # Authoring a Motto Skill
10
+
11
+ **Role:** You are a hands-on guide for writing Motto SKILL.md files. Walk the author through each required frontmatter field, the body spine contract, and shared references step by step, citing the exact lint error messages they will see if something is wrong.
12
+
13
+ A Motto skill is a `SKILL.md` file that Motto validates and packages into a Claude Code Agent Skill plugin. You write the source; Motto handles distribution.
14
+
15
+ ## Required Frontmatter Fields
16
+
17
+ Every SKILL.md must open with a frontmatter block — a `---`-delimited YAML section — containing three required fields.
18
+
19
+ ### `name`
20
+
21
+ The skill's identifier. Must be a **letter-start kebab-case** string matching `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` and must equal the skill's folder name exactly. See the bundled `skill-schema` reference for the full regex, reserved-word list, and all four lint error messages in the name validation cascade.
22
+
23
+ ```yaml
24
+ name: my-skill
25
+ ```
26
+
27
+ ### `description`
28
+
29
+ A short, non-empty string stating what the skill does and when an agent should trigger it. Keep it under 1024 characters and avoid XML-like tags (`<`, `>`).
30
+
31
+ ```yaml
32
+ description: Explains how to configure my-skill. Use this skill when setting up a new my-skill configuration.
33
+ ```
34
+
35
+ **Lint error if missing:** `description is required`
36
+
37
+ ### `audience`
38
+
39
+ Must be exactly `public` or `private`.
40
+
41
+ - `public` — skill is shipped in the public plugin bucket (`dist/public/`)
42
+ - `private` — skill is shipped in the private plugin bucket (`dist/private/`) only
43
+
44
+ ```yaml
45
+ audience: public
46
+ ```
47
+
48
+ **Lint error if wrong:** `audience must be public|private`
49
+
50
+ ## The Body Spine
51
+
52
+ After the closing `---` delimiter, the skill body must satisfy two independent checks. If both fail, both errors are reported in a single lint run.
53
+
54
+ **H1 title:** The first non-blank line must be an H1 heading — `# Title` with at least one non-space character after `# `.
55
+
56
+ **Role line:** The body must contain at least one line beginning with `**Role:`. Write a complete, behavioral sentence — "You are a..." — not just a label. An empty `**Role:**` passes the validator but produces unusable agent instruction content.
57
+
58
+ ```markdown
59
+ # My Skill Title
60
+
61
+ **Role:** You are a guide who walks the author through every step of X.
62
+ ```
63
+
64
+ ## Optional Frontmatter
65
+
66
+ ### `shared_references`
67
+
68
+ A YAML array of bare basenames — no `.md` extension, no path separators. Each entry must correspond to an existing `shared/references/<entry>.md` file.
69
+
70
+ ```yaml
71
+ shared_references:
72
+ - skill-schema
73
+ - another-reference
74
+ ```
75
+
76
+ `motto build` copies each referenced file into the skill's `references/` directory in the output, making the skill self-contained when distributed. If a skill already has a local `references/<entry>.md`, build will report a collision and stop.
77
+
78
+ **Lint error if file is missing:** `shared_references entry "X" not found in shared/references/`
79
+
80
+ ### `template` and `dependencies`
81
+
82
+ Accepted and passed through verbatim; not validated in v0.0.2.
83
+
84
+ ## Annotated Minimal Example
85
+
86
+ The following is a minimal, correct SKILL.md showing every required field and the mandatory body spine:
87
+
88
+ ```yaml
89
+ ---
90
+ name: my-skill
91
+ description: Shows how to configure my-skill. Use this skill when setting up my-skill for the first time.
92
+ audience: public
93
+ ---
94
+
95
+ # My Skill
96
+
97
+ **Role:** You are a guide who walks the author through configuring my-skill step by step.
98
+
99
+ Main body content goes here.
100
+ ```
101
+
102
+ The folder containing this file must be named `my-skill` to match the `name` field.
103
+
104
+ ## Common Lint Errors
105
+
106
+ | Error message | Cause | Fix |
107
+ |---------------|-------|-----|
108
+ | `name is required` | `name` field is missing | Add `name: your-skill-name` |
109
+ | `name must be letter-start kebab-case (...)` | Uppercase, underscores, leading digit, or double hyphen | Use lowercase letters, hyphens only, starting with a letter |
110
+ | `name must not contain the reserved substrings "anthropic" or "claude": "X"` | Skill name contains a reserved substring | Rename the skill folder and `name` field |
111
+ | `name "X" must equal its folder name "Y"` | `name` field does not match the folder name | Rename the folder or fix the `name` field |
112
+ | `description is required` | `description` field is missing or empty | Add a non-empty `description` |
113
+ | `audience must be public\|private` | `audience` is missing or has an unexpected value | Set `audience: public` or `audience: private` |
114
+ | `body must begin with an H1 title line (# Title) as its first non-blank line` | Body does not start with `# Title` | Make the first non-blank line after `---` an H1 |
115
+ | `body must contain a **Role:** line` | No line starting with `**Role:` in the body | Add `**Role:** You are...` to the body |
116
+ | `shared_references entry "X" is an unsafe basename (must not contain "/" or ".")` | Entry contains a path separator or extension | Use a bare basename like `skill-schema`, not `./skill-schema.md` |
117
+ | `shared_references entry "X" not found in shared/references/` | Declared shared ref file does not exist | Create `shared/references/X.md` or fix the basename |
@@ -0,0 +1,163 @@
1
+ # Motto Skill Schema Reference
2
+
3
+ This file is the canonical rule source for the Motto skill schema (v0.0.2).
4
+ It is bundled verbatim by `motto build` into each public skill's `references/` directory.
5
+ All claims are derived from `src/schema.js`, `src/config.js`, `src/frontmatter.js`, and `src/lint.js`.
6
+
7
+ ---
8
+
9
+ ## 1. `name` Field
10
+
11
+ **Rule:** A skill's `name` must be a letter-start kebab-case identifier.
12
+
13
+ Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`
14
+ - Starts with exactly one lowercase letter (not a digit, not a hyphen)
15
+ - Followed by zero or more lowercase letters or digits
16
+ - Then zero or more groups of (one hyphen + one or more lowercase letters/digits)
17
+
18
+ **Additional constraints:**
19
+ - Must equal the skill's folder name exactly. If the folder is `my-skill`, the `name` field must be `my-skill`.
20
+ - Max 64 characters (Claude Code platform limit; enforced by the Motto validator as a cascade step).
21
+ - Must not contain the substrings `anthropic` or `claude` (substring match, not word boundary).
22
+
23
+ **Name validation cascades — it stops at the first failure:**
24
+
25
+ | Step | Check | Lint error emitted |
26
+ |------|-------|--------------------|
27
+ | 1 | `name` is missing, empty, or falsy (also covers non-string falsy values) | `name is required` |
28
+ | 2 | `name` is a truthy non-string (boolean, number, array, object) | `name must be a string (got <typeof>)` |
29
+ | 3 | `name` fails the kebab regex | `name must be letter-start kebab-case (/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/): "X"` |
30
+ | 4 | `name` exceeds 64 characters | `name must not exceed 64 characters (got N): "X"` |
31
+ | 5 | `name` contains a reserved substring | `name must not contain the reserved substrings "anthropic" or "claude": "X"` |
32
+ | 6 | `name` does not match the folder | `name "X" must equal its folder name "Y"` |
33
+
34
+ Valid examples: `my-skill`, `abc`, `a1`, `abc-def-123`
35
+ Invalid examples: `0bad` (leading digit), `My_Skill` (uppercase/underscore), `my--skill` (double dash), `-bad` (leading dash)
36
+
37
+ ---
38
+
39
+ ## 2. `description` Field
40
+
41
+ **Rule:** Must be present and non-empty.
42
+
43
+ ```
44
+ description: A concise description of what this skill does and when to use it.
45
+ ```
46
+
47
+ **Authoring guidance:**
48
+ - State both what the skill does AND when an agent should trigger it.
49
+ - Max 1024 characters (enforced by the Motto validator).
50
+ - Must not contain XML-tag shapes (e.g. `<example>`, `</p>`, `<br/>`). Plain comparison/math
51
+ text such as `a<b` is not flagged; only strings matching the tag-shape regex are rejected.
52
+
53
+ **Lint errors:**
54
+ - `description is required`
55
+ - `description must not exceed 1024 characters (got N)`
56
+ - `description must not contain XML tags (e.g. <example>)`
57
+
58
+ ---
59
+
60
+ ## 3. `audience` Field
61
+
62
+ **Rule:** Must be exactly the string `public` or `private`. Any other value (including missing) is an error.
63
+
64
+ ```yaml
65
+ audience: public # shipped in dist/public/ bucket
66
+ audience: private # shipped in dist/private/ bucket only
67
+ ```
68
+
69
+ **Lint error if wrong:** `audience must be public|private`
70
+
71
+ **Build behavior:**
72
+ - `public` skills are copied to `dist/public/<skill-name>/`
73
+ - `private` skills are copied to `dist/private/<skill-name>/`
74
+ - `plugins.private` must be set in `motto.yaml` before any private skill can be built
75
+
76
+ ---
77
+
78
+ ## 4. Body Spine — Two Independent Checks
79
+
80
+ The skill body (the Markdown after the closing `---` delimiter) must satisfy two checks. Both run independently — both errors are reported even if both fail at once.
81
+
82
+ **Check 1 — H1 title:** The first non-blank line of the body must match `^# \S` (an H1 heading with at least one non-space character after `# `).
83
+
84
+ ```markdown
85
+ # My Skill Title
86
+ ```
87
+
88
+ **Lint error if missing:** `body must begin with an H1 title line (# Title) as its first non-blank line`
89
+
90
+ **Check 2 — Role line:** The body must contain at least one line that starts with `**Role:` (multiline match).
91
+
92
+ ```markdown
93
+ **Role:** You are a hands-on guide who walks the author through...
94
+ ```
95
+
96
+ The Role line content after `:` is not validated. An empty `**Role:**` passes the regex but produces unusable agent instruction content. Write Role lines as complete, behavioral sentences.
97
+
98
+ **Lint error if missing:** `body must contain a **Role:** line`
99
+
100
+ ---
101
+
102
+ ## 5. `shared_references` Field
103
+
104
+ **Rule:** Optional. When present, must be a YAML array of strings. Each entry must be a safe basename.
105
+
106
+ ```yaml
107
+ shared_references:
108
+ - skill-schema
109
+ - another-reference
110
+ ```
111
+
112
+ **Per-entry rules:**
113
+ - Must NOT contain `/` or `.` — bare basenames only, no path separators or extensions
114
+ - Must correspond to an existing `shared/references/<entry>.md` file
115
+
116
+ **Per-entry lint errors:**
117
+ - `shared_references entry "X" is an unsafe basename (must not contain "/" or ".")`
118
+ - `shared_references entry "X" not found in shared/references/`
119
+
120
+ **Build behavior:** `motto build` copies `shared/references/<entry>.md` into each skill's `dist/<audience>/<skill-name>/references/` directory, making the skill self-contained.
121
+
122
+ **Collision guard:** If a skill already has a local `references/<entry>.md`, build will report a collision and stop.
123
+
124
+ ---
125
+
126
+ ## 6. `template` and `dependencies` Fields
127
+
128
+ These fields are accepted and passed through verbatim. They are NOT validated in Motto v0.0.2.
129
+
130
+ ```yaml
131
+ template: some-template
132
+ dependencies:
133
+ - another-skill
134
+ ```
135
+
136
+ ---
137
+
138
+ ## 7. Frontmatter Envelope
139
+
140
+ Every SKILL.md must open with a bare `---` on line 1 and close with a matching bare `---`.
141
+
142
+ ```
143
+ ---
144
+ name: my-skill
145
+ description: What this skill does and when to use it.
146
+ audience: public
147
+ ---
148
+
149
+ # My Skill Title
150
+
151
+ **Role:** You are...
152
+ ```
153
+
154
+ **Rules:**
155
+ - The opening `---` must be the very first line (no blank lines, no BOM — BOM is stripped automatically).
156
+ - A matching closing `---` is required.
157
+ - YAML between the delimiters is parsed with the `yaml` v2 library (YAML 1.2 core schema) — `yes`/`no`/`on`/`off` are NOT treated as booleans.
158
+ - A stray `---` inside the body is flagged only if the content between it and the body start parses as a clean, non-empty YAML mapping. Wrapping example SKILL.md blocks in fenced code blocks avoids this edge case.
159
+
160
+ **Lint errors:**
161
+ - `missing frontmatter: file must begin with a '--- ... ---' block`
162
+ - `unterminated frontmatter: no closing '---' delimiter found`
163
+ - `stray '---' delimiter in frontmatter: the block must contain exactly one closing '---'`
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: setup-project
3
+ description: Walks through initializing a Motto project — directory layout, motto.yaml configuration, and running lint and build. Use this skill when starting a new Motto project.
4
+ audience: public
5
+ shared_references:
6
+ - skill-schema
7
+ ---
8
+
9
+ # Setting Up a Motto Project
10
+
11
+ **Role:** You are a Motto project setup guide. Walk the author through the source directory layout, every motto.yaml field, the lint and build commands, and what the dist/ output contains.
12
+
13
+ ## Directory Layout
14
+
15
+ A Motto project uses a fixed directory structure:
16
+
17
+ ```
18
+ my-project/
19
+ skills/
20
+ my-skill/
21
+ SKILL.md ← one skill per folder
22
+ references/ ← optional local references (skill-specific)
23
+ another-skill/
24
+ SKILL.md
25
+ shared/
26
+ references/ ← shared references bundled into multiple skills at build time
27
+ skill-schema.md
28
+ motto.yaml ← project configuration
29
+ dist/ ← generated output; never edit manually
30
+ ```
31
+
32
+ Rules:
33
+ - Each skill lives in its own folder under `skills/`. The folder name must match the `name` field in the skill's `SKILL.md`.
34
+ - `shared/references/` holds Markdown reference files that can be declared by multiple skills and bundled by `motto build`.
35
+ - `dist/` is created (and wiped) by `motto build`. Do not commit it.
36
+
37
+ ## `motto.yaml` Fields
38
+
39
+ The root `motto.yaml` configures the project and both plugin buckets.
40
+
41
+ ```yaml
42
+ name: my-project
43
+ version: "0.1.0"
44
+ description: A short description of what this project's skills do.
45
+ plugins:
46
+ public: my-project-skills
47
+ private: my-project-private # only required when private-audience skills exist
48
+ ```
49
+
50
+ | Field | Required | Rule | Example |
51
+ |-------|----------|------|---------|
52
+ | `name` | Yes | Truthy string; used as project identifier | `motto` |
53
+ | `version` | Yes | Truthy string; recommend semver; quote it to ensure a YAML string | `"0.1.0"` |
54
+ | `description` | Yes | Truthy string; feeds into each built plugin.json | `"A framework for..."` |
55
+ | `plugins.public` | Yes | Letter-start kebab-case (`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`); cannot contain `anthropic` or `claude` | `my-project-skills` |
56
+ | `plugins.private` | Only when private skills exist | Same kebab-case rule as `plugins.public` | `my-project-private` |
57
+
58
+ See the `skill-schema` reference for the full kebab regex and name constraint rules.
59
+
60
+ ## Running Lint
61
+
62
+ ```
63
+ node bin/motto.js lint
64
+ ```
65
+
66
+ or, once installed globally:
67
+
68
+ ```
69
+ motto lint
70
+ ```
71
+
72
+ **Clean output:**
73
+
74
+ ```
75
+ ✓ 3 skills OK
76
+ ```
77
+
78
+ **Error output (example):**
79
+
80
+ ```
81
+ ✗ my-skill: name must be letter-start kebab-case ...
82
+ ✗ my-skill: body must contain a **Role:** line
83
+ ✗ (project): missing plugins.public
84
+
85
+ 3 errors
86
+ ```
87
+
88
+ Lint validates the config (`motto.yaml`), loads shared references, then validates every skill in `skills/`. All errors from all skills are reported in a single run.
89
+
90
+ ## Running Build
91
+
92
+ ```
93
+ node bin/motto.js build
94
+ ```
95
+
96
+ or:
97
+
98
+ ```
99
+ motto build
100
+ ```
101
+
102
+ Build runs lint first. If lint fails, no files are written to `dist/`. On success, `dist/` is wiped and rebuilt.
103
+
104
+ ### What `dist/` Contains
105
+
106
+ ```
107
+ dist/
108
+ public/
109
+ .claude-plugin/
110
+ plugin.json ← { "name": "my-project-skills", "version": "...", "description": "..." }
111
+ my-skill/
112
+ SKILL.md ← verbatim copy from skills/my-skill/SKILL.md
113
+ references/
114
+ skill-schema.md ← copied from shared/references/ (if declared)
115
+ private/
116
+ .claude-plugin/
117
+ plugin.json ← { "name": "my-project-private", "version": "...", "description": "..." }
118
+ secret-skill/
119
+ SKILL.md
120
+ ```
121
+
122
+ - `plugin.json` is generated from `motto.yaml` fields (`name`, `version`, `description`).
123
+ - Shared references declared in a skill's `shared_references` field are bundled into that skill's `references/` directory.
124
+ - Private skills appear only in `dist/private/`. The public plugin never contains private skill content.
125
+
126
+ ## Installing the Built Plugin in Claude Code
127
+
128
+ After building, point Claude Code at the output directory for the bucket you want to install:
129
+
130
+ - **Public plugin:** install from `dist/public/`
131
+ - **Private plugin:** install from `dist/private/`
132
+
133
+ Use the `claude plugin add` command (or the Claude Code UI) with the path to the bucket directory. Each bucket contains a valid `.claude-plugin/plugin.json` manifest.
@@ -0,0 +1,163 @@
1
+ # Motto Skill Schema Reference
2
+
3
+ This file is the canonical rule source for the Motto skill schema (v0.0.2).
4
+ It is bundled verbatim by `motto build` into each public skill's `references/` directory.
5
+ All claims are derived from `src/schema.js`, `src/config.js`, `src/frontmatter.js`, and `src/lint.js`.
6
+
7
+ ---
8
+
9
+ ## 1. `name` Field
10
+
11
+ **Rule:** A skill's `name` must be a letter-start kebab-case identifier.
12
+
13
+ Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`
14
+ - Starts with exactly one lowercase letter (not a digit, not a hyphen)
15
+ - Followed by zero or more lowercase letters or digits
16
+ - Then zero or more groups of (one hyphen + one or more lowercase letters/digits)
17
+
18
+ **Additional constraints:**
19
+ - Must equal the skill's folder name exactly. If the folder is `my-skill`, the `name` field must be `my-skill`.
20
+ - Max 64 characters (Claude Code platform limit; enforced by the Motto validator as a cascade step).
21
+ - Must not contain the substrings `anthropic` or `claude` (substring match, not word boundary).
22
+
23
+ **Name validation cascades — it stops at the first failure:**
24
+
25
+ | Step | Check | Lint error emitted |
26
+ |------|-------|--------------------|
27
+ | 1 | `name` is missing, empty, or falsy (also covers non-string falsy values) | `name is required` |
28
+ | 2 | `name` is a truthy non-string (boolean, number, array, object) | `name must be a string (got <typeof>)` |
29
+ | 3 | `name` fails the kebab regex | `name must be letter-start kebab-case (/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/): "X"` |
30
+ | 4 | `name` exceeds 64 characters | `name must not exceed 64 characters (got N): "X"` |
31
+ | 5 | `name` contains a reserved substring | `name must not contain the reserved substrings "anthropic" or "claude": "X"` |
32
+ | 6 | `name` does not match the folder | `name "X" must equal its folder name "Y"` |
33
+
34
+ Valid examples: `my-skill`, `abc`, `a1`, `abc-def-123`
35
+ Invalid examples: `0bad` (leading digit), `My_Skill` (uppercase/underscore), `my--skill` (double dash), `-bad` (leading dash)
36
+
37
+ ---
38
+
39
+ ## 2. `description` Field
40
+
41
+ **Rule:** Must be present and non-empty.
42
+
43
+ ```
44
+ description: A concise description of what this skill does and when to use it.
45
+ ```
46
+
47
+ **Authoring guidance:**
48
+ - State both what the skill does AND when an agent should trigger it.
49
+ - Max 1024 characters (enforced by the Motto validator).
50
+ - Must not contain XML-tag shapes (e.g. `<example>`, `</p>`, `<br/>`). Plain comparison/math
51
+ text such as `a<b` is not flagged; only strings matching the tag-shape regex are rejected.
52
+
53
+ **Lint errors:**
54
+ - `description is required`
55
+ - `description must not exceed 1024 characters (got N)`
56
+ - `description must not contain XML tags (e.g. <example>)`
57
+
58
+ ---
59
+
60
+ ## 3. `audience` Field
61
+
62
+ **Rule:** Must be exactly the string `public` or `private`. Any other value (including missing) is an error.
63
+
64
+ ```yaml
65
+ audience: public # shipped in dist/public/ bucket
66
+ audience: private # shipped in dist/private/ bucket only
67
+ ```
68
+
69
+ **Lint error if wrong:** `audience must be public|private`
70
+
71
+ **Build behavior:**
72
+ - `public` skills are copied to `dist/public/<skill-name>/`
73
+ - `private` skills are copied to `dist/private/<skill-name>/`
74
+ - `plugins.private` must be set in `motto.yaml` before any private skill can be built
75
+
76
+ ---
77
+
78
+ ## 4. Body Spine — Two Independent Checks
79
+
80
+ The skill body (the Markdown after the closing `---` delimiter) must satisfy two checks. Both run independently — both errors are reported even if both fail at once.
81
+
82
+ **Check 1 — H1 title:** The first non-blank line of the body must match `^# \S` (an H1 heading with at least one non-space character after `# `).
83
+
84
+ ```markdown
85
+ # My Skill Title
86
+ ```
87
+
88
+ **Lint error if missing:** `body must begin with an H1 title line (# Title) as its first non-blank line`
89
+
90
+ **Check 2 — Role line:** The body must contain at least one line that starts with `**Role:` (multiline match).
91
+
92
+ ```markdown
93
+ **Role:** You are a hands-on guide who walks the author through...
94
+ ```
95
+
96
+ The Role line content after `:` is not validated. An empty `**Role:**` passes the regex but produces unusable agent instruction content. Write Role lines as complete, behavioral sentences.
97
+
98
+ **Lint error if missing:** `body must contain a **Role:** line`
99
+
100
+ ---
101
+
102
+ ## 5. `shared_references` Field
103
+
104
+ **Rule:** Optional. When present, must be a YAML array of strings. Each entry must be a safe basename.
105
+
106
+ ```yaml
107
+ shared_references:
108
+ - skill-schema
109
+ - another-reference
110
+ ```
111
+
112
+ **Per-entry rules:**
113
+ - Must NOT contain `/` or `.` — bare basenames only, no path separators or extensions
114
+ - Must correspond to an existing `shared/references/<entry>.md` file
115
+
116
+ **Per-entry lint errors:**
117
+ - `shared_references entry "X" is an unsafe basename (must not contain "/" or ".")`
118
+ - `shared_references entry "X" not found in shared/references/`
119
+
120
+ **Build behavior:** `motto build` copies `shared/references/<entry>.md` into each skill's `dist/<audience>/<skill-name>/references/` directory, making the skill self-contained.
121
+
122
+ **Collision guard:** If a skill already has a local `references/<entry>.md`, build will report a collision and stop.
123
+
124
+ ---
125
+
126
+ ## 6. `template` and `dependencies` Fields
127
+
128
+ These fields are accepted and passed through verbatim. They are NOT validated in Motto v0.0.2.
129
+
130
+ ```yaml
131
+ template: some-template
132
+ dependencies:
133
+ - another-skill
134
+ ```
135
+
136
+ ---
137
+
138
+ ## 7. Frontmatter Envelope
139
+
140
+ Every SKILL.md must open with a bare `---` on line 1 and close with a matching bare `---`.
141
+
142
+ ```
143
+ ---
144
+ name: my-skill
145
+ description: What this skill does and when to use it.
146
+ audience: public
147
+ ---
148
+
149
+ # My Skill Title
150
+
151
+ **Role:** You are...
152
+ ```
153
+
154
+ **Rules:**
155
+ - The opening `---` must be the very first line (no blank lines, no BOM — BOM is stripped automatically).
156
+ - A matching closing `---` is required.
157
+ - YAML between the delimiters is parsed with the `yaml` v2 library (YAML 1.2 core schema) — `yes`/`no`/`on`/`off` are NOT treated as booleans.
158
+ - A stray `---` inside the body is flagged only if the content between it and the body start parses as a clean, non-empty YAML mapping. Wrapping example SKILL.md blocks in fenced code blocks avoids this edge case.
159
+
160
+ **Lint errors:**
161
+ - `missing frontmatter: file must begin with a '--- ... ---' block`
162
+ - `unterminated frontmatter: no closing '---' delimiter found`
163
+ - `stray '---' delimiter in frontmatter: the block must contain exactly one closing '---'`