@crouton-kit/crouter 0.1.9 → 0.2.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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "crtr",
3
+ "version": "0.2.0",
4
+ "description": "Builtin crouter skills — authoring guidance shipped with the binary"
5
+ }
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: crouter-development/marketplaces
3
+ type: playbook
4
+ description: How to author a crtr marketplace — marketplace.json index, plugin entries, symlink-based install, auto-bump CI, dual-publishing. Use when creating a marketplace or contributing plugins to one.
5
+ keywords: [marketplace, marketplace.json, plugin, index, publishing]
6
+ ---
7
+
8
+ # Authoring crtr marketplaces
9
+
10
+ A **marketplace** is a git repo that indexes multiple plugins. Users register it once, then `crtr marketplace install <mkt>:<plugin>` symlinks any plugin from it. One `crtr marketplace update` pulls updates for every installed plugin at once.
11
+
12
+ Audience: LLM agents creating a marketplace or adding plugins to an existing one.
13
+
14
+ ## When you need a marketplace (vs a single-plugin repo)
15
+
16
+ A solo plugin ships from any git URL via `crtr plugin install <url>` — no marketplace needed.
17
+
18
+ Reach for a marketplace when:
19
+ - You want to publish ≥3 plugins under one brand or theme.
20
+ - Users should install selectively without cloning each plugin's repo.
21
+ - You want centralized version-bump automation across many plugins.
22
+
23
+ If you have one plugin, ship it standalone. Promote to a marketplace later.
24
+
25
+ ## Directory layout
26
+
27
+ ```
28
+ <marketplace-repo>/
29
+ ├── .crouter-marketplace/
30
+ │ └── marketplace.json # marketplace manifest — required
31
+ └── plugins/
32
+ ├── plugin-a/
33
+ │ ├── .crouter-plugin/plugin.json
34
+ │ └── skills/...
35
+ └── plugin-b/
36
+ └── ...
37
+ ```
38
+
39
+ Each entry under `plugins/` is a complete plugin (see [[crouter-development/plugins]]). The marketplace adds an index on top.
40
+
41
+ ## The manifest
42
+
43
+ `.crouter-marketplace/marketplace.json`:
44
+
45
+ ```json
46
+ {
47
+ "name": "my-marketplace",
48
+ "version": "0.3.0",
49
+ "owner": { "name": "Your Name", "email": "you@example.com" },
50
+ "plugins": [
51
+ {
52
+ "name": "plugin-a",
53
+ "version": "0.2.1",
54
+ "source": "https://github.com/<owner>/<repo>",
55
+ "description": "One sentence."
56
+ },
57
+ { "name": "plugin-b", "version": "0.1.0", "source": "…", "description": "…" }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ | Field | Required | Notes |
63
+ |---|---|---|
64
+ | `name` | yes | Lowercase kebab. |
65
+ | `version` | yes | Semver. Bumps when any plugin bumps or when the marketplace manifest itself changes. |
66
+ | `owner` | optional | |
67
+ | `plugins` | yes | Array. Each entry: `name`, `version`, `source`, `description`. |
68
+
69
+ The `plugins[]` array IS the index — what's installable. A plugin on disk but not in the index won't resolve. A plugin in the index but missing from disk errors on install.
70
+
71
+ ## Install mechanics — symlinks, not clones
72
+
73
+ When a user runs `crtr marketplace install my-marketplace:plugin-a`:
74
+ 1. Crouter looks up `plugin-a` in the marketplace's manifest.
75
+ 2. **Symlinks** `<marketplace-clone>/plugins/plugin-a/` → `<user-scope>/plugins/plugin-a/`.
76
+ 3. Records the install in the scope config with `source_marketplace: my-marketplace`.
77
+
78
+ Therefore one `crtr marketplace update my-marketplace` (which does `git pull` in the marketplace clone) updates every installed plugin from it — no per-plugin re-install, no second clone.
79
+
80
+ ## Version-bump automation (recommended)
81
+
82
+ Bumping N plugin manifests + the index by hand is error-prone. The canonical pattern is a GitHub Actions workflow that bumps versions from commit subjects (conventional commits):
83
+
84
+ | Commit subject | Bump |
85
+ |---|---|
86
+ | `feat!: …` or `BREAKING CHANGE` in body | major |
87
+ | `feat: …` | minor |
88
+ | anything else (`fix:`, `chore:`, `docs:`, …) | patch |
89
+
90
+ Per commit:
91
+ - For each plugin folder touched, both `plugins/<name>/.crouter-plugin/plugin.json` and the matching entry in `marketplace.json` get bumped.
92
+ - Top-level `marketplace.json` version bumps when any plugin bumps OR the manifest itself was edited directly.
93
+ - Newly-added plugins are skipped — they keep the version you committed.
94
+ - Commits whose subject starts with `chore: release` are skipped to avoid loops.
95
+
96
+ If you adopt this: **never edit `version` fields by hand**. CI will overwrite. Document this in the marketplace's CLAUDE.md.
97
+
98
+ The crouter-official-marketplace repo's `.github/workflows/auto-bump.yml` is the reference implementation. Copy it.
99
+
100
+ ## Adding a plugin to a marketplace
101
+
102
+ ```bash
103
+ cd <marketplace-repo>
104
+
105
+ # Create the plugin (see [[crouter-development/plugins]] for plugin layout)
106
+ mkdir -p plugins/my-new-plugin/.crouter-plugin plugins/my-new-plugin/skills
107
+ $EDITOR plugins/my-new-plugin/.crouter-plugin/plugin.json
108
+
109
+ # Add at least one skill
110
+ crtr skill new my-new-plugin:first-skill --type playbook --description "Use when …"
111
+
112
+ # Add the plugin to the marketplace index
113
+ $EDITOR .crouter-marketplace/marketplace.json
114
+ # (append to plugins[] with name, initial version, source, description)
115
+
116
+ # Validate
117
+ crtr doctor
118
+
119
+ # Commit — CI bumps versions if you've wired up auto-bump
120
+ git add -A
121
+ git commit -m "feat: add my-new-plugin"
122
+ git push
123
+ ```
124
+
125
+ Existing users pick it up on their next `crtr marketplace update <marketplace-name>` (or on the next auto-update tick if they've set `auto_update.content: "apply"`).
126
+
127
+ ## Updating an existing plugin
128
+
129
+ Edit content under `plugins/<name>/`. Commit with a conventional-commit subject. CI bumps the plugin manifest and the marketplace index entry together. No manual sync.
130
+
131
+ ## Removing a plugin
132
+
133
+ Delete the plugin's directory AND its entry in `marketplace.json` → `plugins[]`. Commit with `feat!: remove <plugin>` to signal a major bump (the marketplace's contract changed for anyone depending on that plugin).
134
+
135
+ ## Marketplace registration scopes
136
+
137
+ A marketplace itself registers per-scope:
138
+
139
+ | Scope | Path | Use case |
140
+ |---|---|---|
141
+ | user | `~/.crouter/marketplaces/<name>/` | Private to your machine — your personal subscriptions |
142
+ | project | `<project>/.crouter/marketplaces/<name>/` | Checked into the repo — anyone cloning gets the same marketplace pinned |
143
+
144
+ `crtr marketplace add <git-url> --scope user` is the default. Use `--scope project` when the marketplace is integral to how the repo expects to be developed.
145
+
146
+ ## Validation
147
+
148
+ `crtr doctor` checks marketplaces:
149
+ - `marketplace.json` is valid JSON.
150
+ - Every entry in `plugins[]` corresponds to a real directory under `plugins/`.
151
+ - Each plugin under `plugins/` passes plugin-level validation.
152
+
153
+ A plugin on disk but missing from the manifest is a **warning** (probably forgotten); a plugin in the manifest but missing on disk is an **error** (install would break).
154
+
155
+ ## Cross-publishing with Claude Code
156
+
157
+ Some marketplaces ship a parallel `.claude-plugin/` tree so plugins can load directly into Claude Code without crtr. Optional. Sync the two manifests if you adopt it.
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: crouter-development/plugins
3
+ type: playbook
4
+ description: How to author a crtr plugin — plugin.json manifest, directory layout, scopes, install mechanics, versioning. Use when creating a new plugin, packaging skills for distribution, or debugging install/resolution.
5
+ keywords: [plugin, plugin.json, manifest, install, scope]
6
+ ---
7
+
8
+ # Authoring crtr plugins
9
+
10
+ A **plugin** is a directory shipping skills (and, in future, other artifact types like commands and hooks). Plugins are how you package skills for sharing across machines, projects, and people.
11
+
12
+ Audience: LLM agents creating or maintaining a crtr plugin.
13
+
14
+ ## When you need a plugin (vs scope-owned skills)
15
+
16
+ Scope-owned skills live at `~/.crouter/skills/` (user) or `<project>/.crouter/skills/` (project). They're personal and per-machine/per-repo.
17
+
18
+ Reach for a **plugin** when:
19
+ - You want to share skills across multiple projects or with other people.
20
+ - You want versioning + update mechanics (`crtr plugin update`).
21
+ - You want a marketplace to index the work — see [[crouter-development/marketplaces]].
22
+
23
+ If it's a one-off note for yourself, scope-owned skills are simpler. Promote to a plugin later.
24
+
25
+ ## Directory layout
26
+
27
+ ```
28
+ <plugin-name>/
29
+ ├── .crouter-plugin/
30
+ │ └── plugin.json # manifest — required
31
+ └── skills/
32
+ └── <skill-name>/
33
+ └── SKILL.md
34
+ ```
35
+
36
+ The `<plugin-name>` directory IS the plugin. The manifest's `name` field must match the directory name (install renames if needed). Future artifact types (`commands/`, `hooks/`, etc.) will be sibling dirs to `skills/`.
37
+
38
+ ## The manifest
39
+
40
+ `.crouter-plugin/plugin.json`:
41
+
42
+ ```json
43
+ {
44
+ "name": "my-plugin",
45
+ "version": "0.1.0",
46
+ "description": "One sentence — shown in `crtr plugin list`.",
47
+ "source": "https://github.com/<owner>/<repo>",
48
+ "owner": {
49
+ "name": "Your Name",
50
+ "email": "you@example.com"
51
+ }
52
+ }
53
+ ```
54
+
55
+ | Field | Required | Notes |
56
+ |---|---|---|
57
+ | `name` | yes | Must match the directory name. Lowercase kebab. |
58
+ | `version` | yes | Semver. Marketplace CI may bump automatically — see marketplaces skill. |
59
+ | `description` | yes | One sentence. |
60
+ | `source` | recommended | Git URL where the plugin lives. Used by `crtr plugin update`. |
61
+ | `owner` | optional | Author info. |
62
+
63
+ ## Scopes
64
+
65
+ A plugin can live in either scope:
66
+
67
+ | Scope | Path | Use case |
68
+ |---|---|---|
69
+ | user | `~/.crouter/plugins/<name>/` | Personal, available in all your projects |
70
+ | project | `<project>/.crouter/plugins/<name>/` | Pinned to a specific repo — checked in or vendored |
71
+
72
+ Project-scope plugins outrank user-scope on resolution. Both outrank marketplace-installed plugins. The builtin `crtr` plugin (ships with the CLI) sits at the bottom.
73
+
74
+ ## Install mechanics
75
+
76
+ Three ways a plugin lands in a scope:
77
+
78
+ 1. **From a git URL** (`crtr plugin install <url> --scope user`):
79
+ - Clones into `<scope>/plugins/<name>/` using the manifest's name.
80
+ - `crtr plugin update <name>` does `git pull`.
81
+ - Independent of any marketplace.
82
+
83
+ 2. **From a marketplace** (`crtr marketplace install <mkt>:<name> --scope user`):
84
+ - **Symlinks** the marketplace's `plugins/<name>/` into `<scope>/plugins/<name>/`.
85
+ - One `crtr marketplace update <mkt>` pulls updates for every installed plugin from that marketplace.
86
+ - See [[crouter-development/marketplaces]].
87
+
88
+ 3. **Authored in place** (you're writing the plugin in a working repo):
89
+ - Symlink for tight dev loop: `ln -s $(pwd) ~/.crouter/plugins/<name>`.
90
+ - Or `crtr plugin install file://$(pwd) --scope project` to clone-install.
91
+
92
+ ## Local development loop
93
+
94
+ ```bash
95
+ # Scaffold dir + manifest + first skill
96
+ mkdir -p my-plugin/.crouter-plugin my-plugin/skills
97
+ $EDITOR my-plugin/.crouter-plugin/plugin.json # write the manifest
98
+ cd my-plugin
99
+ crtr skill new my-plugin:my-first-skill --type playbook --description "Use when …"
100
+
101
+ # Symlink for fast iteration — no clone, edits land immediately
102
+ ln -s $(pwd) ~/.crouter/plugins/my-plugin
103
+
104
+ # Verify
105
+ crtr plugin list # my-plugin appears
106
+ crtr plugin show my-plugin # lists its skills
107
+ crtr skill list --plugin my-plugin
108
+ crtr doctor # validates manifest + every skill
109
+ ```
110
+
111
+ When ready to share: push to a git remote; anyone can `crtr plugin install <url>`.
112
+
113
+ ## Versioning
114
+
115
+ Standard semver:
116
+
117
+ | Change | Bump |
118
+ |---|---|
119
+ | Typo, wording polish | patch (0.1.0 → 0.1.1) |
120
+ | New skill, new section, new example | minor (0.1.0 → 0.2.0) |
121
+ | Removed skill, renamed skill, changed manifest schema | major (0.1.0 → 1.0.0) |
122
+
123
+ `crtr plugin update <name>` reads the new version after pulling and updates the local config. Plugins published through a marketplace may have their `version` field bumped automatically by CI — see [[crouter-development/marketplaces]].
124
+
125
+ ## Enable/disable
126
+
127
+ `crtr plugin disable <name>` flips the per-scope config without removing files. Disabled plugins are hidden from `crtr skill list` and don't resolve via `crtr skill show <name>`. Re-enable with `crtr plugin enable <name>`.
128
+
129
+ Individual skills inside an enabled plugin can also be disabled: `crtr skill disable <plugin>:<skill>`.
130
+
131
+ ## What goes in a plugin
132
+
133
+ Good plugin scope:
134
+ - A coherent set of related skills (3–15 typical) sharing a theme.
135
+ - All skills serve the same user persona or workflow.
136
+ - Versioned together — a bump means a bump for the whole set.
137
+
138
+ Bad plugin scope:
139
+ - One mega-plugin with every skill you've ever written. Hard to install selectively, hard to version.
140
+ - A plugin per single skill. No value-add over scope-owned skills.
141
+
142
+ ## Cross-plugin etiquette
143
+
144
+ If your skill conceptually depends on another plugin's skill, link via `## Related` with `` `<plugin>/<skill>` ``. Don't fork content; link it.
145
+
146
+ ## Validation
147
+
148
+ `crtr doctor` checks every plugin:
149
+ - Manifest exists and is valid JSON.
150
+ - Manifest `name` matches the directory name.
151
+ - Every skill under `skills/` passes the skill-validation contract (frontmatter parses, `name` matches dir path, `type` in enum). Run `crtr skill` (no args) for the full format reference.
152
+ - Sibling artifact dirs (`commands/`, `hooks/`, etc.) — validated by their respective specs as those land.
153
+
154
+ ## Cross-publishing with Claude Code
155
+
156
+ Some plugins also publish a `.claude-plugin/` manifest alongside `.crouter-plugin/` so they can be loaded directly into Claude Code without going through crtr. Optional. Only worth doing when your skills/commands meaningfully stand alone in the Claude Code surface. Keep manifests in sync if you do.
@@ -1,10 +1,12 @@
1
1
  import { join } from 'node:path';
2
+ import { SKILL_TYPES, isSkillType } from '../types.js';
2
3
  import { out, jsonOut, handleError, stdoutColor } from '../core/output.js';
3
- import { scopeRoot, pluginsDir, marketplacesDir, listScopes } from '../core/scope.js';
4
+ import { scopeRoot, pluginsDir, marketplacesDir, listScopes, builtinSkillsRoot } from '../core/scope.js';
4
5
  import { readConfig, updateConfig } from '../core/config.js';
5
6
  import { listInstalledPlugins, listSkillsInPlugin } from '../core/resolver.js';
6
- import { pathExists, listDirs, removePath } from '../core/fs-utils.js';
7
+ import { pathExists, listDirs, removePath, readText } from '../core/fs-utils.js';
7
8
  import { readPluginManifest, readMarketplaceManifest } from '../core/manifest.js';
9
+ import { parseFrontmatter } from '../core/frontmatter.js';
8
10
  import { lsRemote } from '../core/git.js';
9
11
  import { ExitCode } from '../types.js';
10
12
  function pass(scope, name, message) {
@@ -16,7 +18,46 @@ function fail(scope, name, message) {
16
18
  function warn(scope, name, message) {
17
19
  return { scope, name, status: 'warn', message };
18
20
  }
21
+ function readRawTypeField(skillPath) {
22
+ const content = readText(skillPath);
23
+ const { raw } = parseFrontmatter(content);
24
+ if (!raw)
25
+ return undefined;
26
+ const m = raw.match(/^type:\s*(.+?)\s*$/m);
27
+ if (!m)
28
+ return undefined;
29
+ let v = m[1].trim();
30
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
31
+ v = v.slice(1, -1);
32
+ }
33
+ return v;
34
+ }
35
+ function runChecksForBuiltin() {
36
+ const root = builtinSkillsRoot();
37
+ const plugins = listInstalledPlugins('builtin');
38
+ if (plugins.length === 0) {
39
+ return [fail('builtin', 'builtin:crtr:root', `builtin-skills root missing or has no valid plugin.json: ${root}`)];
40
+ }
41
+ const results = [
42
+ pass('builtin', 'builtin:crtr:root', `builtin-skills root present: ${root}`),
43
+ ];
44
+ for (const plugin of plugins) {
45
+ results.push(pass('builtin', `builtin:${plugin.name}:manifest`, `manifest valid`));
46
+ const skills = listSkillsInPlugin(plugin);
47
+ for (const skill of skills) {
48
+ if (!skill.frontmatter.name) {
49
+ results.push(fail('builtin', `builtin:${plugin.name}:skill:${skill.name}:frontmatter`, `frontmatter missing or name field empty`));
50
+ }
51
+ else {
52
+ results.push(pass('builtin', `builtin:${plugin.name}:skill:${skill.name}:frontmatter`, `frontmatter valid`));
53
+ }
54
+ }
55
+ }
56
+ return results;
57
+ }
19
58
  function runChecksForScope(scope, opts) {
59
+ if (scope === 'builtin')
60
+ return runChecksForBuiltin();
20
61
  const results = [];
21
62
  const root = scopeRoot(scope);
22
63
  if (!root)
@@ -138,6 +179,17 @@ function runChecksForScope(scope, opts) {
138
179
  else {
139
180
  results.push(pass(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `frontmatter valid`));
140
181
  }
182
+ const typeCheckName = `plugin:${name}:skill:${skill.name}:type`;
183
+ const rawType = readRawTypeField(skill.path);
184
+ if (rawType === undefined) {
185
+ results.push(warn(scope, typeCheckName, `missing type field — add one of: ${SKILL_TYPES.join(' | ')}`));
186
+ }
187
+ else if (!isSkillType(rawType)) {
188
+ results.push(fail(scope, typeCheckName, `invalid type "${rawType}" — valid: ${SKILL_TYPES.join(' | ')}`));
189
+ }
190
+ else {
191
+ results.push(pass(scope, typeCheckName, `type: ${rawType}`));
192
+ }
141
193
  }
142
194
  }
143
195
  // Git remote check (slow, opt-in)
@@ -178,10 +178,13 @@ export function registerPluginCommands(program) {
178
178
  .option('--yes', 'skip confirmation in non-TTY mode')
179
179
  .action(async (name, opts) => {
180
180
  try {
181
+ if (name === 'crtr') {
182
+ throw usage(`cannot uninstall builtin plugin "crtr" — it ships with the binary`);
183
+ }
181
184
  if (!isTTY() && !opts.yes) {
182
185
  throw usage(`uninstall requires --yes in non-TTY mode: crtr plugin uninstall ${name} --yes`);
183
186
  }
184
- const scopes = listScopes(opts.scope);
187
+ const scopes = listScopes(opts.scope).filter((s) => s !== 'builtin');
185
188
  let removed = false;
186
189
  for (const scope of scopes) {
187
190
  const pDir = pluginsDir(scope);
@@ -1,11 +1,11 @@
1
1
  import { join } from 'node:path';
2
2
  import { writeFileSync } from 'node:fs';
3
- import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, } from '../types.js';
3
+ import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, } from '../types.js';
4
4
  import { skillConfigKey } from '../types.js';
5
5
  import { notFound, usage, general } from '../core/errors.js';
6
6
  import { out, hint, info, jsonOut, handleError, } from '../core/output.js';
7
7
  import { listScopes, requireScopeRoot, resolveScopeArg, projectScopeRoot, scopeSkillsDir, } from '../core/scope.js';
8
- import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, } from '../core/resolver.js';
8
+ import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, listSkillSiblings, listSkillChildren, } from '../core/resolver.js';
9
9
  import { updateConfig, ensureScopeInitialized } from '../core/config.js';
10
10
  import { parseFrontmatter, serializeFrontmatter } from '../core/frontmatter.js';
11
11
  import { ensureDir, pathExists, readText, walkFiles } from '../core/fs-utils.js';
@@ -23,14 +23,53 @@ const KNOWN_VERBS = new Set([
23
23
  'disable',
24
24
  'search',
25
25
  ]);
26
- const AUTHORING_GUIDE_SKILL = 'authoring-skills';
27
26
  function buildShowFooter(skillPath) {
28
27
  return (`crtr: edit this skill directly at ${skillPath} — ` +
29
- `for SKILL.md authoring guidance run \`crtr skill ${AUTHORING_GUIDE_SKILL}\``);
28
+ `for SKILL.md format + authoring workflow run \`crtr skill\` (no args)`);
30
29
  }
31
30
  function wrapSkill(name, path, content) {
32
31
  return `<skill name="${name}" path="${path}">\n${content.endsWith('\n') ? content : content + '\n'}</skill>`;
33
32
  }
33
+ function formatNeighborQualifier(s) {
34
+ return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}:${s.name}` : `${s.plugin}/${s.name}`;
35
+ }
36
+ function buildNeighborsSection(skill) {
37
+ const siblings = listSkillSiblings(skill);
38
+ const children = listSkillChildren(skill);
39
+ if (siblings.length === 0 && children.length === 0)
40
+ return null;
41
+ const lines = [
42
+ '## Neighbors',
43
+ '*Auto-discovered from filesystem. Use `--no-neighbors` to suppress.*',
44
+ '',
45
+ ];
46
+ if (siblings.length > 0) {
47
+ lines.push('**Siblings:**');
48
+ for (const s of siblings) {
49
+ const desc = s.frontmatter.description !== undefined ? s.frontmatter.description : '';
50
+ lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
51
+ }
52
+ if (children.length > 0)
53
+ lines.push('');
54
+ }
55
+ if (children.length > 0) {
56
+ lines.push('**Nested:**');
57
+ for (const s of children) {
58
+ const desc = s.frontmatter.description !== undefined ? s.frontmatter.description : '';
59
+ lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
60
+ }
61
+ }
62
+ return lines.join('\n');
63
+ }
64
+ function appendNeighbors(skill, body, suppress) {
65
+ if (suppress)
66
+ return body;
67
+ const section = buildNeighborsSection(skill);
68
+ if (section === null)
69
+ return body;
70
+ const sep = body.endsWith('\n') ? '\n' : '\n\n';
71
+ return body + sep + `<neighbors>\n${section}\n</neighbors>\n`;
72
+ }
34
73
  const SKILL_IDENTIFIER_HELP = 'Skill identifier forms (accepted by show, path, where, enable, disable):\n' +
35
74
  ' <name> bare name — resolves scope-root first, then plugins\n' +
36
75
  ' <plugin>:<name> explicit plugin (canonical)\n' +
@@ -52,7 +91,8 @@ export function registerSkillCommands(program) {
52
91
  try {
53
92
  const skillObj = resolveSkill(nameOrVerb);
54
93
  const content = readText(skillObj.path);
55
- const body = opts.frontmatter ? content : parseFrontmatter(content).body;
94
+ const rawBody = opts.frontmatter ? content : parseFrontmatter(content).body;
95
+ const body = appendNeighbors(skillObj, rawBody, false);
56
96
  out(wrapSkill(skillObj.name, skillObj.path, body));
57
97
  hint(buildShowFooter(skillObj.path));
58
98
  }
@@ -117,6 +157,7 @@ export function registerSkillCommands(program) {
117
157
  .option('--scope <scope>', 'user|project')
118
158
  .option('--plugin <name>', 'filter by plugin name')
119
159
  .option('--frontmatter', 'include YAML frontmatter in the printed body')
160
+ .option('--no-neighbors', 'suppress the auto-appended ## Neighbors section')
120
161
  .option('--json', 'emit JSON')
121
162
  .addHelpText('after', '\nExamples:\n' +
122
163
  ' crtr skill show rules # bare name\n' +
@@ -133,7 +174,9 @@ export function registerSkillCommands(program) {
133
174
  }
134
175
  const skillObj = resolveSkill(name, resolveOpts);
135
176
  const content = readText(skillObj.path);
136
- const body = opts.frontmatter ? content : parseFrontmatter(content).body;
177
+ const rawBody = opts.frontmatter ? content : parseFrontmatter(content).body;
178
+ const suppressNeighbors = opts.neighbors === false;
179
+ const body = appendNeighbors(skillObj, rawBody, suppressNeighbors);
137
180
  if (opts.json) {
138
181
  jsonOut({
139
182
  name: skillObj.name,
@@ -141,7 +184,7 @@ export function registerSkillCommands(program) {
141
184
  scope: skillObj.scope,
142
185
  path: skillObj.path,
143
186
  content,
144
- authoring_guide_command: `crtr skill ${AUTHORING_GUIDE_SKILL}`,
187
+ authoring_guide_command: `crtr skill`,
145
188
  });
146
189
  return;
147
190
  }
@@ -235,12 +278,20 @@ export function registerSkillCommands(program) {
235
278
  .description('scaffold a new skill — <name> (scope-direct) or <plugin>:<name>')
236
279
  .option('--scope <scope>', 'user|project (default: project then user)')
237
280
  .option('--description <text>', 'skill description for frontmatter')
281
+ .option('--type <type>', `skill type for frontmatter — one of: ${SKILL_TYPES.join(' | ')}`)
238
282
  .action(async (qualifier, opts) => {
239
283
  try {
240
284
  const { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
241
285
  if (!skillName) {
242
286
  throw usage('skill name required');
243
287
  }
288
+ let skillType;
289
+ if (opts.type !== undefined) {
290
+ if (!isSkillType(opts.type)) {
291
+ throw usage(`unknown skill type: ${opts.type} / valid: ${SKILL_TYPES.join(' | ')}`);
292
+ }
293
+ skillType = opts.type;
294
+ }
244
295
  const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
245
296
  // Scope-direct: no plugin qualifier, or explicit `_:` sentinel
246
297
  if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
@@ -266,11 +317,13 @@ export function registerSkillCommands(program) {
266
317
  const fm = serializeFrontmatter({
267
318
  name: skillName,
268
319
  description: opts.description,
320
+ type: skillType,
269
321
  });
270
322
  writeFileSync(skillFile, fm, 'utf8');
271
323
  out(skillFile);
272
- hint(`crtr: scaffolded ${scope}-scope skill ${skillName} edit directly, then ` +
273
- `\`crtr skill ${AUTHORING_GUIDE_SKILL}\` for SKILL.md authoring guidance`);
324
+ const templateHint = skillType !== undefined ? skillType : '<type>';
325
+ hint(`crtr: scaffolded ${scope}-scope skill ${skillName} edit directly; ` +
326
+ `\`crtr skill template ${templateHint}\` for body skeleton`);
274
327
  return;
275
328
  }
276
329
  let plugin;
@@ -292,11 +345,13 @@ export function registerSkillCommands(program) {
292
345
  const fm = serializeFrontmatter({
293
346
  name: skillName,
294
347
  description: opts.description,
348
+ type: skillType,
295
349
  });
296
350
  writeFileSync(skillFile, fm, 'utf8');
297
351
  out(skillFile);
298
- hint(`crtr: scaffolded ${skillFile} edit directly, then ` +
299
- `\`crtr skill ${AUTHORING_GUIDE_SKILL}\` for SKILL.md authoring guidance`);
352
+ const templateHint = skillType !== undefined ? skillType : '<type>';
353
+ hint(`crtr: scaffolded ${skillFile} edit directly; ` +
354
+ `\`crtr skill template ${templateHint}\` for body skeleton`);
300
355
  }
301
356
  catch (e) {
302
357
  handleError(e);
@@ -305,7 +360,7 @@ export function registerSkillCommands(program) {
305
360
  // create — pick a template type
306
361
  skill
307
362
  .command('create [topic...]')
308
- .description('pick a template type for a new skill (primer | playbook | freeform)')
363
+ .description(`pick a template type for a new skill (${SKILL_TYPES.join(' | ')})`)
309
364
  .action(async (topic) => {
310
365
  const arg = topic && topic.length > 0 ? topic.join(' ') : '';
311
366
  out(skillCreatePrompt(arg));
@@ -313,7 +368,7 @@ export function registerSkillCommands(program) {
313
368
  // template — full workflow + skeleton for one template type
314
369
  skill
315
370
  .command('template <type> [topic...]')
316
- .description('full workflow + skeleton for a template type (primer | playbook | freeform)')
371
+ .description(`full workflow + skeleton for a template type (${SKILL_TYPES.join(' | ')})`)
317
372
  .action(async (type, topic) => {
318
373
  const arg = topic && topic.length > 0 ? topic.join(' ') : '';
319
374
  out(skillTemplatePrompt(type, arg));
@@ -1,4 +1,4 @@
1
- import type { SkillFrontmatter } from '../types.js';
1
+ import { type SkillFrontmatter } from '../types.js';
2
2
  export interface ParsedFrontmatter {
3
3
  data: SkillFrontmatter | null;
4
4
  body: string;
@@ -1,3 +1,4 @@
1
+ import { isSkillType } from '../types.js';
1
2
  const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
2
3
  export function parseFrontmatter(source) {
3
4
  const match = source.match(FRONTMATTER_RE);
@@ -126,6 +127,7 @@ function parseSimpleYaml(yaml) {
126
127
  name: typeof out.name === 'string' ? out.name : '',
127
128
  description: typeof out.description === 'string' ? out.description : undefined,
128
129
  keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
130
+ type: isSkillType(out.type) ? out.type : undefined,
129
131
  };
130
132
  return fm;
131
133
  }
@@ -141,6 +143,9 @@ export function serializeFrontmatter(data) {
141
143
  if (data.description !== undefined) {
142
144
  lines.push(`description: ${quoteIfNeeded(data.description)}`);
143
145
  }
146
+ if (data.type !== undefined) {
147
+ lines.push(`type: ${data.type}`);
148
+ }
144
149
  if (data.keywords && data.keywords.length) {
145
150
  const inline = `[${data.keywords.map(quoteIfNeeded).join(', ')}]`;
146
151
  lines.push(`keywords: ${inline}`);
@@ -13,6 +13,8 @@ export declare function effectiveSkillEnabled(pluginName: string, skillName: str
13
13
  export declare function listSkillsInPlugin(plugin: InstalledPlugin, cfgs?: ScopeConfigs): Skill[];
14
14
  export declare function listScopeRootSkills(scope: Scope, cfgs?: ScopeConfigs): Skill[];
15
15
  export declare function listAllSkills(scopeFilter?: Scope): Skill[];
16
+ export declare function listSkillSiblings(skill: Skill): Skill[];
17
+ export declare function listSkillChildren(skill: Skill): Skill[];
16
18
  export interface SkillResolutionOpts {
17
19
  scope?: Scope;
18
20
  pluginFilter?: string;
@@ -5,8 +5,29 @@ import { listDirs, pathExists, readText, walkFiles, } from './fs-utils.js';
5
5
  import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
6
6
  import { parseFrontmatter } from './frontmatter.js';
7
7
  import { ambiguous, notFound, usage } from './errors.js';
8
- import { marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
8
+ import { builtinSkillsRoot, marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
9
+ function getBuiltinPlugin() {
10
+ const root = builtinSkillsRoot();
11
+ if (!pathExists(root))
12
+ return null;
13
+ const manifest = readPluginManifest(root);
14
+ if (!manifest)
15
+ return null;
16
+ return {
17
+ name: manifest.name,
18
+ scope: 'builtin',
19
+ root,
20
+ manifest,
21
+ enabled: true,
22
+ builtin: true,
23
+ version: manifest.version,
24
+ };
25
+ }
9
26
  export function listInstalledPlugins(scope) {
27
+ if (scope === 'builtin') {
28
+ const builtin = getBuiltinPlugin();
29
+ return builtin ? [builtin] : [];
30
+ }
10
31
  const dir = pluginsDir(scope);
11
32
  if (!dir || !pathExists(dir))
12
33
  return [];
@@ -40,13 +61,14 @@ export function listAllPlugins() {
40
61
  if (projectScopeRoot())
41
62
  scopes.push('project');
42
63
  scopes.push('user');
64
+ scopes.push('builtin');
43
65
  return scopes.flatMap(listInstalledPlugins);
44
66
  }
45
67
  export function findPluginByName(name, scope) {
46
68
  if (scope) {
47
69
  return listInstalledPlugins(scope).find((p) => p.name === name) ?? null;
48
70
  }
49
- for (const s of ['project', 'user'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
71
+ for (const s of ['project', 'user', 'builtin'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
50
72
  const match = listInstalledPlugins(s).find((p) => p.name === name);
51
73
  if (match)
52
74
  return match;
@@ -100,6 +122,8 @@ export function listSkillsInPlugin(plugin, cfgs) {
100
122
  return skills.sort((a, b) => a.name.localeCompare(b.name));
101
123
  }
102
124
  export function listScopeRootSkills(scope, cfgs) {
125
+ if (scope === 'builtin')
126
+ return [];
103
127
  const skillsRoot = scopeSkillsDir(scope);
104
128
  if (!skillsRoot || !pathExists(skillsRoot))
105
129
  return [];
@@ -138,6 +162,41 @@ export function listAllSkills(scopeFilter) {
138
162
  ...plugins.filter((p) => p.enabled).flatMap((p) => listSkillsInPlugin(p, cfgs)),
139
163
  ];
140
164
  }
165
+ function enumerateNeighborPool(skill) {
166
+ if (skill.plugin === SCOPE_SKILL_PLUGIN) {
167
+ return listScopeRootSkills(skill.scope);
168
+ }
169
+ const plugin = listInstalledPlugins(skill.scope).find((p) => p.name === skill.plugin);
170
+ if (!plugin)
171
+ return [];
172
+ return listSkillsInPlugin(plugin);
173
+ }
174
+ export function listSkillSiblings(skill) {
175
+ const pool = enumerateNeighborPool(skill);
176
+ const segs = skill.name.split('/');
177
+ const depth = segs.length;
178
+ const parentPrefix = segs.slice(0, -1).join('/');
179
+ return pool.filter((s) => {
180
+ if (s.name === skill.name)
181
+ return false;
182
+ const sSegs = s.name.split('/');
183
+ if (sSegs.length !== depth)
184
+ return false;
185
+ if (parentPrefix === '')
186
+ return !s.name.includes('/');
187
+ return s.name.startsWith(parentPrefix + '/');
188
+ });
189
+ }
190
+ export function listSkillChildren(skill) {
191
+ const pool = enumerateNeighborPool(skill);
192
+ const prefix = skill.name + '/';
193
+ return pool.filter((s) => {
194
+ if (!s.name.startsWith(prefix))
195
+ return false;
196
+ const rest = s.name.slice(prefix.length);
197
+ return rest.length > 0 && !rest.includes('/');
198
+ });
199
+ }
141
200
  export function resolveSkill(rawName, opts = {}) {
142
201
  const parsed = parseSkillQualifier(rawName);
143
202
  if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
@@ -353,6 +412,8 @@ export function parseSkillQualifier(raw) {
353
412
  }
354
413
  function orderPluginsByResolution(plugins) {
355
414
  const score = (p) => {
415
+ if (p.scope === 'builtin')
416
+ return 4;
356
417
  const fromMarketplace = Boolean(p.sourceMarketplace);
357
418
  if (p.scope === 'project' && !fromMarketplace)
358
419
  return 0;
@@ -1,4 +1,5 @@
1
1
  import type { Scope } from '../types.js';
2
+ export declare function builtinSkillsRoot(): string;
2
3
  export declare function userScopeRoot(): string;
3
4
  export declare function findProjectScopeRoot(startDir?: string): string | null;
4
5
  export declare function projectScopeRoot(startDir?: string): string | null;
@@ -1,9 +1,17 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { existsSync, statSync } from 'node:fs';
3
3
  import { join, resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { CRTR_DIR_NAME } from '../types.js';
5
6
  import { usage } from './errors.js';
6
7
  let cachedProjectRoot;
8
+ export function builtinSkillsRoot() {
9
+ // Resolve relative to this file: src/core/scope.ts → src/builtin-skills/ OR dist/core/scope.js → dist/builtin-skills/
10
+ const thisFile = fileURLToPath(import.meta.url);
11
+ const coreDir = dirname(thisFile);
12
+ const pkgDir = dirname(coreDir); // src/ or dist/
13
+ return join(pkgDir, 'builtin-skills');
14
+ }
7
15
  export function userScopeRoot() {
8
16
  return join(homedir(), CRTR_DIR_NAME);
9
17
  }
@@ -37,6 +45,8 @@ export function projectScopeRoot(startDir) {
37
45
  return findProjectScopeRoot(startDir);
38
46
  }
39
47
  export function scopeRoot(scope) {
48
+ if (scope === 'builtin')
49
+ return builtinSkillsRoot();
40
50
  return scope === 'user' ? userScopeRoot() : projectScopeRoot();
41
51
  }
42
52
  export function requireScopeRoot(scope) {
@@ -71,9 +81,9 @@ export function resolveScopeArg(scopeArg) {
71
81
  if (scopeArg === undefined)
72
82
  return 'all';
73
83
  const value = scopeArg.toLowerCase();
74
- if (value === 'user' || value === 'project' || value === 'all')
84
+ if (value === 'user' || value === 'project' || value === 'builtin' || value === 'all')
75
85
  return value;
76
- throw usage(`invalid --scope: ${scopeArg} (expected user|project|all)`);
86
+ throw usage(`invalid --scope: ${scopeArg} (expected user|project|builtin|all)`);
77
87
  }
78
88
  export function listScopes(scopeArg) {
79
89
  const v = resolveScopeArg(scopeArg);
@@ -82,6 +92,7 @@ export function listScopes(scopeArg) {
82
92
  if (projectScopeRoot())
83
93
  out.push('project');
84
94
  out.push('user');
95
+ out.push('builtin');
85
96
  return out;
86
97
  }
87
98
  return [v];
@@ -48,14 +48,49 @@ crtr skill where <name> # {scope, plugin, path} JSON
48
48
  ## Author (progressive disclosure)
49
49
 
50
50
  \`\`\`
51
- crtr skill create [topic...] # pick a template type
52
- crtr skill template <type> [topic] # workflow + skeleton for that type
53
- crtr skill new <name> # bare scaffold, scope-direct
51
+ crtr skill create [topic...] # pick a template type
52
+ crtr skill template <type> [topic] # workflow + skeleton for that type
53
+ crtr skill new <name> --type <type> # bare scaffold with typed frontmatter
54
54
  \`\`\`
55
55
 
56
+ Five types — pick by what the agent does after reading:
57
+
58
+ - \`playbook\` — decide (judgment, heuristics, when-to-use)
59
+ - \`primer\` — navigate (codebase facts, architecture)
60
+ - \`reference\` — look up (stable facts, tables)
61
+ - \`runbook\` — execute (numbered procedure)
62
+ - \`freeform\` — none of the above (catchall)
63
+
56
64
  Don't load \`create\` and \`template\` in the same turn — \`create\` decides the
57
65
  type, then call \`template\`.
58
66
 
67
+ ## Format
68
+
69
+ A skill is a directory; \`SKILL.md\` is its entry file. The dir IS the skill —
70
+ siblings are assets, nested dirs are themselves skills.
71
+
72
+ Frontmatter (required: \`name\`, \`type\`, \`description\`):
73
+ - \`name\` — must equal the dir path under \`skills/\`. Slashes for nested
74
+ (\`skills/web/frontend/design/SKILL.md\` → \`name: web/frontend/design\`).
75
+ - \`type\` — one of the five above.
76
+ - \`description\` — one sentence, front-load with "Use when…" — drives discovery.
77
+ - \`keywords\` (optional) — array of strings, improves \`crtr skill search\`.
78
+
79
+ Intermediate dirs (\`web/\`, \`web/frontend/\`) don't need their own SKILL.md —
80
+ nesting is purely path-based. Budget ~150 lines per SKILL.md body; spill
81
+ deeper material into sibling files (\`reference.md\`, \`examples.md\`).
82
+
83
+ Validate with \`crtr doctor\` — checks frontmatter, name-vs-path match, type
84
+ enum, sibling-link reachability.
85
+
86
+ ## Neighbors auto-append
87
+
88
+ \`crtr skill show <name>\` appends a \`## Neighbors\` section listing siblings
89
+ (same parent dir) and nested skills. Skill bodies should write \`## Related\`
90
+ **only** for cross-plugin or distant refs — within-plugin links are redundant.
91
+
92
+ Suppress with \`crtr skill show <name> --no-neighbors\`.
93
+
59
94
  ## Toggle
60
95
 
61
96
  \`\`\`
@@ -81,11 +116,22 @@ ${topicLine}
81
116
 
82
117
  ## Templates
83
118
 
84
- - \`primer\` codebase/architectural knowledge ("how does X work, why, what
85
- are the gotchas"). Triggers parallel-explore research.
86
- - \`playbook\` — methodology/judgment ("how to think about X"). The
87
- \`authoring:skills\`-style skill that teaches decisions, not facts.
88
- - \`freeform\` — anything else (glossary, decision record, runbook, prefs).
119
+ Pick by what the agent does after reading the skill:
120
+
121
+ - \`playbook\` — **decide**. Judgment, heuristics, when-to-use / when-not-to-use.
122
+ Examples: skill-authoring, debugging methodology. Most skills are this.
123
+ - \`primer\` — **navigate**. Codebase/architectural facts. *"How does this
124
+ subsystem work, why, what are the gotchas."* Triggers parallel-explore.
125
+ - \`reference\` — **look up**. Stable facts: protocol fields, API surface,
126
+ glossaries, lookup tables. Source of truth is external (spec, docs).
127
+ - \`runbook\` — **execute**. Numbered procedure with decision points and
128
+ rollback. Examples: deploy, incident response, review workflow.
129
+ - \`freeform\` — **none of the above**. Catchall for decision records, prefs,
130
+ miscellany.
131
+
132
+ Litmus: *"when X, do Y"* → playbook. *"these are the fields of Y"* →
133
+ reference. *"step 1, step 2, step 3"* → runbook. *"how X is built inside
134
+ this repo"* → primer.
89
135
 
90
136
  ## Next
91
137
 
@@ -103,6 +149,7 @@ ${topicLine}
103
149
 
104
150
  - Subsystem is small/self-evident → suggest CLAUDE.md note instead of primer.
105
151
  - "Topic" is really a one-off task → don't capture; just do the work.
152
+ - Content is one-off lookup that lives elsewhere → link to it, don't mirror.
106
153
  `;
107
154
  }
108
155
  export function skillTemplatePrompt(type, topic) {
@@ -111,9 +158,13 @@ export function skillTemplatePrompt(type, topic) {
111
158
  return primerTemplatePrompt(topic);
112
159
  if (t === 'playbook')
113
160
  return playbookTemplatePrompt(topic);
161
+ if (t === 'reference')
162
+ return referenceTemplatePrompt(topic);
163
+ if (t === 'runbook')
164
+ return runbookTemplatePrompt(topic);
114
165
  if (t === 'freeform')
115
166
  return freeformTemplatePrompt(topic);
116
- return `unknown template type: ${type}\nvalid: primer | playbook | freeform\nrun \`crtr skill create\` to pick.\n`;
167
+ return `unknown template type: ${type}\nvalid: playbook | primer | reference | runbook | freeform\nrun \`crtr skill create\` to pick.\n`;
117
168
  }
118
169
  function topicLine(topic) {
119
170
  return topic
@@ -176,7 +227,7 @@ assumptions.**
176
227
  ## 5. Scaffold
177
228
 
178
229
  \`\`\`
179
- crtr skill new <name> --scope project --description "<what+when, ≤250 chars, front-loaded triggers>"
230
+ crtr skill new <name> --type primer --scope project --description "<what+when, ≤250 chars, front-loaded triggers>"
180
231
  \`\`\`
181
232
 
182
233
  ## 6. Write the body
@@ -204,11 +255,12 @@ Domain terms, invariants, non-obvious constraints.
204
255
 
205
256
  ## Gotchas
206
257
  Non-obvious coupling. Looks-broken-but-isn't. Past footguns.
207
-
208
- ## Related
209
- - \`<other-skill>\` — interaction
210
258
  \`\`\`
211
259
 
260
+ **No \`## Related\` for within-plugin siblings** — the CLI auto-appends a
261
+ \`## Neighbors\` section on \`crtr skill show\`. Add a manual \`## Related\`
262
+ only for cross-plugin or distant refs.
263
+
212
264
  **Density rules:**
213
265
  - \`file:line\` over prose
214
266
  - Tables where structure fits
@@ -229,7 +281,7 @@ Sharpen description if discovery misses. Cut body if bloated.
229
281
  ## Updates
230
282
 
231
283
  If updating existing primer: diff draft vs current, call out changes + why
232
- before writing. Update related skills' \`## Related\` sections.
284
+ before writing.
233
285
  `;
234
286
  }
235
287
  function playbookTemplatePrompt(topic) {
@@ -275,7 +327,7 @@ PR over many small ones for refactors here, because review churn dominates"*
275
327
  ## 3. Scaffold
276
328
 
277
329
  \`\`\`
278
- crtr skill new <name> --scope <user|project> --description "<what it teaches + when to load, ≤250 chars, front-loaded triggers>"
330
+ crtr skill new <name> --type playbook --scope <user|project> --description "<what it teaches + when to load, ≤250 chars, front-loaded triggers>"
279
331
  \`\`\`
280
332
 
281
333
  ## 4. Density rules
@@ -314,11 +366,12 @@ to read the whole thing for value, you've buried the judgment.
314
366
 
315
367
  ## Failure modes
316
368
  - **<name>**: what it looks like; how to avoid
317
-
318
- ## Related
319
- - \`<other-skill>\` — interaction
320
369
  \`\`\`
321
370
 
371
+ **No \`## Related\` for within-plugin siblings** — the CLI auto-appends a
372
+ \`## Neighbors\` section on \`crtr skill show\`. Add a manual \`## Related\`
373
+ only for cross-plugin or distant refs.
374
+
322
375
  ## 6. Progressive disclosure
323
376
 
324
377
  If deep reference is needed:
@@ -341,24 +394,11 @@ crtr skill show <name>
341
394
  crtr skill search <keyword>
342
395
  \`\`\`
343
396
 
344
- ## Deep-dive reference
345
-
346
- For canonical SKILL.md authoring (frontmatter fields, argument passing,
347
- dynamic context, subagent forking, hooks):
348
-
349
- \`\`\`
350
- crtr skill show authoring-skills
351
- \`\`\`
352
-
353
- The playbook above gives you structure + density rules. \`authoring-skills\`
354
- covers the SKILL.md surface itself.
355
-
356
397
  ## Constraints
357
398
 
358
- - Topic fails litmus? → \`crtr skill template freeform\` or \`primer\`.
399
+ - Topic fails litmus? → \`crtr skill template freeform\`, \`reference\`, or \`runbook\`.
359
400
  - No unconfirmed heuristics — if not from user experience or clear principle,
360
401
  leave it out.
361
- - Update related skills' \`## Related\` if interactions exist.
362
402
  `;
363
403
  }
364
404
  function freeformTemplatePrompt(topic) {
@@ -391,7 +431,7 @@ or grep, omit it.
391
431
  ## 3. Scope + name + scaffold
392
432
 
393
433
  \`\`\`
394
- crtr skill new <name> --scope <user|project> --description "<what+when, ≤250 chars, front-loaded triggers>"
434
+ crtr skill new <name> --type freeform --scope <user|project> --description "<what+when, ≤250 chars, front-loaded triggers>"
395
435
  \`\`\`
396
436
 
397
437
  ## 4. Body — pick the closest skeleton
@@ -441,11 +481,226 @@ crtr skill search <keyword>
441
481
 
442
482
  ## Switch templates if needed
443
483
 
444
- If the content is actually methodology or codebase knowledge:
484
+ Content actually fits a typed template?
445
485
 
446
486
  \`\`\`
447
- crtr skill template playbook ${topic ? topic : '<topic>'}
448
- crtr skill template primer ${topic ? topic : '<topic>'}
487
+ crtr skill template playbook ${topic ? topic : '<topic>'} # decide
488
+ crtr skill template primer ${topic ? topic : '<topic>'} # navigate codebase
489
+ crtr skill template reference ${topic ? topic : '<topic>'} # look up stable facts
490
+ crtr skill template runbook ${topic ? topic : '<topic>'} # execute a procedure
449
491
  \`\`\`
450
492
  `;
451
493
  }
494
+ function referenceTemplatePrompt(topic) {
495
+ return `# Reference — lookup-fact skill
496
+
497
+ **Audience: future LLM agent sessions.** Captures *stable lookup facts* an
498
+ agent will grep or scan: protocol fields, API surfaces, glossaries, enum
499
+ tables, status-code maps. Source of truth lives *outside* the skill (RFC,
500
+ spec, vendor docs); the skill is a fast in-repo cache.
501
+
502
+ ${topicLine(topic)}
503
+
504
+ ## Litmus test
505
+
506
+ > Would you *grep* this rather than *read* it?
507
+
508
+ If you'd skim end-to-end → it's not reference. If you'd jump straight to
509
+ the row you need → reference. If you'd make a decision after reading →
510
+ \`crtr skill template playbook\`. If you'd execute steps → \`runbook\`.
511
+
512
+ **Reference markers:** mostly tables · stable across releases · authoritative
513
+ source elsewhere · agent loads to *answer*, not to *think*.
514
+
515
+ ## 1. Confirm reference, not playbook
516
+
517
+ Use \`AskUserQuestion\` (≤4, multi-choice, best-guess first):
518
+
519
+ - The lookup the agent will perform (e.g., "what does HTTP 423 mean")
520
+ - Stability: does this change every release? If yes, push back — link the
521
+ upstream source instead of mirroring it.
522
+ - Authoritative source URL (cite in the body)
523
+
524
+ Skip if the topic is clearly facts (RFCs, public API). **Never invent
525
+ field/flag/code values** — pull verbatim from source.
526
+
527
+ ## 2. Scope + name
528
+
529
+ - **Scope**: \`user\` for cross-project facts. \`project\` for repo-specific.
530
+ - **Name**: noun-phrase. \`http-status-codes\` not \`learn-http-status\`.
531
+ - Check \`crtr skill where <name>\`.
532
+
533
+ ## 3. Scaffold
534
+
535
+ \`\`\`
536
+ crtr skill new <name> --type reference --scope <user|project> --description "<what to look up + when to load, ≤250 chars>"
537
+ \`\`\`
538
+
539
+ ## 4. Density rules
540
+
541
+ Reference skills are *load and scan*, not *load and read*. Optimize for jump-to-row.
542
+
543
+ - Tables for anything multi-row. Columns: most-queried field first.
544
+ - One topic per file. Split if it doesn't fit one screen.
545
+ - Source URL at the top — agent verifies before trusting cached facts.
546
+ - No prose paragraphs longer than 2 lines.
547
+ - Skip *why* — playbooks teach why. Reference teaches what.
548
+
549
+ ## 5. Body skeleton
550
+
551
+ \`\`\`markdown
552
+ # <topic> reference
553
+
554
+ **Source of truth:** <URL or spec name>
555
+ **Last verified:** <date — when an agent should re-check>
556
+
557
+ ## <table 1 title>
558
+ | <field> | <value> | <notes> |
559
+ |---------|---------|---------|
560
+ | … | … | … |
561
+
562
+ ## <table 2 title>
563
+
564
+
565
+ ## Edge cases / gotchas
566
+ - Brief bullets. *What* is misleading, not *why*.
567
+ \`\`\`
568
+
569
+ **No \`## Related\` for within-plugin siblings** — auto-appended by the CLI.
570
+
571
+ ## 6. Progressive disclosure
572
+
573
+ If reference is large, split:
574
+
575
+ \`\`\`
576
+ <skill-dir>/
577
+ SKILL.md # top-level index + most-queried table
578
+ full-table.md # the full set
579
+ examples.md # rarely-needed worked examples
580
+ \`\`\`
581
+
582
+ SKILL.md links to siblings (\`see [full-table.md](full-table.md)\`).
583
+
584
+ ## 7. Verify
585
+
586
+ \`\`\`
587
+ crtr skill where <name>
588
+ crtr skill show <name>
589
+ crtr skill search <keyword>
590
+ \`\`\`
591
+
592
+ Search must surface the skill on a typical lookup query. Sharpen the
593
+ description if it doesn't.
594
+
595
+ ## Constraints
596
+
597
+ - No invented values. If you can't cite the source, leave the row out.
598
+ - Topic teaches *judgment*, not facts? → \`crtr skill template playbook\`.
599
+ - Topic is a *procedure*? → \`crtr skill template runbook\`.
600
+ - Source updates faster than you'll update the skill? → don't capture; link.
601
+ `;
602
+ }
603
+ function runbookTemplatePrompt(topic) {
604
+ return `# Runbook — procedure skill
605
+
606
+ **Audience: future LLM agent sessions.** Captures a *procedure* the agent
607
+ executes: numbered steps, decision points, verification, rollback. Examples:
608
+ deploy, incident response, review workflow, release cut.
609
+
610
+ ${topicLine(topic)}
611
+
612
+ ## Litmus test
613
+
614
+ > After loading this, does the agent *do steps in order*?
615
+
616
+ If yes → runbook. If the agent makes a decision and stops → \`playbook\`. If
617
+ the agent looks up a value → \`reference\`. If neither fits → \`freeform\`.
618
+
619
+ **Runbook markers:** numbered steps · ordering matters · has rollback or
620
+ verification · maps an outcome to a known sequence.
621
+
622
+ ## 1. Capture the procedure
623
+
624
+ Use \`AskUserQuestion\` (≤4, multi-choice, best-guess first):
625
+
626
+ - The trigger (when does the agent run this?)
627
+ - Steps in order — explicit, atomic. *"Update X"* is not atomic; *"run
628
+ \\\`pnpm db:migrate\\\` and confirm exit 0"* is.
629
+ - Decision points within the sequence (when does the agent branch?)
630
+ - Rollback / verification (how to confirm success; how to undo on failure)
631
+
632
+ Push back on vagueness. *"Deploy to prod"* is a label; *"run \\\`pnpm build\\\`,
633
+ push to \\\`main\\\`, wait for green CI, click promote"* is a runbook step.
634
+
635
+ ## 2. Scope + name
636
+
637
+ - **Scope**: \`project\` for repo-specific procedures. \`user\` for cross-project.
638
+ - **Name**: verb-phrase. \`deploy-to-prod\` not \`production-deployment-guide\`.
639
+ - Check \`crtr skill where <name>\`.
640
+
641
+ ## 3. Scaffold
642
+
643
+ \`\`\`
644
+ crtr skill new <name> --type runbook --scope <user|project> --description "<when to run + outcome, ≤250 chars, front-loaded trigger>"
645
+ \`\`\`
646
+
647
+ ## 4. Density rules
648
+
649
+ - Steps are commands, not advice. Each step is something the agent can verify.
650
+ - Decision points get explicit branches, not "use judgment."
651
+ - Verification belongs in-line, after the step that produces the change.
652
+ - Rollback is mandatory if the procedure changes prod state.
653
+
654
+ ## 5. Body skeleton
655
+
656
+ \`\`\`markdown
657
+ # <procedure>
658
+
659
+ ## When to run
660
+ - <trigger>
661
+
662
+ ## Pre-flight
663
+ - [ ] <thing that must be true before starting>
664
+
665
+ ## Steps
666
+
667
+ 1. **<atomic action>** — \\\`<command>\\\`
668
+ Verify: <observation that confirms success>
669
+ If <error condition>: <what to do>
670
+
671
+ 2. **<atomic action>** — …
672
+
673
+ ## Decision points
674
+
675
+ - After step N, if X then go to step M; else continue.
676
+
677
+ ## Verification (post-flight)
678
+ - [ ] <how to confirm the procedure succeeded end-to-end>
679
+
680
+ ## Rollback
681
+ 1. <reverse-order undo steps with their own verification>
682
+ \`\`\`
683
+
684
+ **No \`## Related\` for within-plugin siblings** — auto-appended by the CLI.
685
+
686
+ ## 6. Verify
687
+
688
+ \`\`\`
689
+ crtr skill where <name>
690
+ crtr skill show <name>
691
+ crtr skill search <keyword>
692
+ \`\`\`
693
+
694
+ Walk through the runbook mentally. Each step verifiable? Each decision
695
+ explicit? Rollback covers the state changes? If any answer is no, fix
696
+ before shipping.
697
+
698
+ ## Constraints
699
+
700
+ - Steps without verification are wishes. Add the verify line or cut the step.
701
+ - "Use your judgment at step 4" → either turn step 4 into a playbook
702
+ reference, or write the decision criteria explicitly.
703
+ - A procedure that's actually one command isn't a runbook — make it a
704
+ CLAUDE.md note.
705
+ `;
706
+ }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type Scope = 'user' | 'project';
1
+ export type Scope = 'user' | 'project' | 'builtin';
2
2
  export declare const ExitCode: {
3
3
  readonly SUCCESS: 0;
4
4
  readonly GENERAL: 1;
@@ -71,10 +71,14 @@ export interface ScopeState {
71
71
  last_self_check?: string;
72
72
  bootstrap_done?: boolean;
73
73
  }
74
+ export declare const SKILL_TYPES: readonly ["playbook", "primer", "reference", "runbook", "freeform"];
75
+ export type SkillType = (typeof SKILL_TYPES)[number];
76
+ export declare function isSkillType(v: unknown): v is SkillType;
74
77
  export interface SkillFrontmatter {
75
78
  name: string;
76
79
  description?: string;
77
80
  keywords?: string[];
81
+ type?: SkillType;
78
82
  }
79
83
  export interface Skill {
80
84
  name: string;
@@ -92,6 +96,7 @@ export interface InstalledPlugin {
92
96
  root: string;
93
97
  manifest: PluginManifest;
94
98
  enabled: boolean;
99
+ builtin?: boolean;
95
100
  sourceMarketplace?: string;
96
101
  version?: string;
97
102
  }
package/dist/types.js CHANGED
@@ -7,6 +7,10 @@ export const ExitCode = {
7
7
  NETWORK: 5,
8
8
  };
9
9
  export const SCHEMA_VERSION = 1;
10
+ export const SKILL_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
11
+ export function isSkillType(v) {
12
+ return typeof v === 'string' && SKILL_TYPES.includes(v);
13
+ }
10
14
  export const PLUGIN_MANIFEST_DIR = '.crouter-plugin';
11
15
  export const PLUGIN_MANIFEST_FILE = 'plugin.json';
12
16
  export const MARKETPLACE_MANIFEST_DIR = '.crouter-marketplace';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.1.9",
3
+ "version": "0.2.3",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "bin"
24
24
  ],
25
25
  "scripts": {
26
- "build": "tsc",
26
+ "build": "tsc && rm -rf dist/builtin-skills && cp -R src/builtin-skills dist/builtin-skills",
27
27
  "dev": "tsx src/cli.ts",
28
28
  "link": "npm link",
29
29
  "prepublishOnly": "npm run build"