@shrkcrft/cli 0.1.0-alpha.6 → 0.1.0-alpha.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.
Files changed (48) hide show
  1. package/dist/commands/check.command.d.ts.map +1 -1
  2. package/dist/commands/check.command.js +19 -2
  3. package/dist/commands/command-catalog.d.ts +7 -3
  4. package/dist/commands/command-catalog.d.ts.map +1 -1
  5. package/dist/commands/command-catalog.js +112 -33
  6. package/dist/commands/commands.command.d.ts.map +1 -1
  7. package/dist/commands/commands.command.js +4 -4
  8. package/dist/commands/constructs.command.d.ts.map +1 -1
  9. package/dist/commands/constructs.command.js +5 -22
  10. package/dist/commands/diff-check.command.d.ts +30 -0
  11. package/dist/commands/diff-check.command.d.ts.map +1 -0
  12. package/dist/commands/diff-check.command.js +210 -0
  13. package/dist/commands/doctor.command.d.ts.map +1 -1
  14. package/dist/commands/doctor.command.js +41 -7
  15. package/dist/commands/export.command.d.ts.map +1 -1
  16. package/dist/commands/export.command.js +76 -3
  17. package/dist/commands/help.command.d.ts +4 -3
  18. package/dist/commands/help.command.d.ts.map +1 -1
  19. package/dist/commands/help.command.js +74 -16
  20. package/dist/commands/helper.command.js +1 -1
  21. package/dist/commands/import.command.d.ts.map +1 -1
  22. package/dist/commands/import.command.js +121 -5
  23. package/dist/commands/init.command.d.ts.map +1 -1
  24. package/dist/commands/init.command.js +151 -7
  25. package/dist/commands/packs-new.d.ts +1 -1
  26. package/dist/commands/packs-new.d.ts.map +1 -1
  27. package/dist/commands/packs-new.js +5 -36
  28. package/dist/commands/packs.command.d.ts.map +1 -1
  29. package/dist/commands/packs.command.js +2 -10
  30. package/dist/commands/profiles.command.js +4 -4
  31. package/dist/commands/release.command.js +13 -13
  32. package/dist/commands/search.command.js +1 -1
  33. package/dist/commands/task-context.command.js +0 -16
  34. package/dist/export/claude-commands-export.d.ts +60 -0
  35. package/dist/export/claude-commands-export.d.ts.map +1 -0
  36. package/dist/export/claude-commands-export.js +276 -0
  37. package/dist/export/export-formats.d.ts +1 -1
  38. package/dist/export/export-formats.d.ts.map +1 -1
  39. package/dist/export/export-formats.js +139 -12
  40. package/dist/main.d.ts.map +1 -1
  41. package/dist/main.js +104 -11
  42. package/dist/output/watch-loop.d.ts +9 -1
  43. package/dist/output/watch-loop.d.ts.map +1 -1
  44. package/dist/output/watch-loop.js +13 -3
  45. package/package.json +20 -20
  46. package/dist/commands/plugin.command.d.ts +0 -11
  47. package/dist/commands/plugin.command.d.ts.map +0 -1
  48. package/dist/commands/plugin.command.js +0 -394
@@ -0,0 +1,276 @@
1
+ /**
2
+ * `.claude/commands/` generator — emits per-project slash commands
3
+ * for Claude Code. Companion to `claude-skill` export, but with a
4
+ * different inversion semantics:
5
+ *
6
+ * - **claude-skill** loads rules into Claude's prompt automatically
7
+ * based on description match. Passive — Claude reads the rules.
8
+ * - **claude-commands** registers slash commands the USER invokes
9
+ * (`/new-service`, `/check-changes`). Active — the command IS the
10
+ * recipe; Claude follows it step-by-step.
11
+ *
12
+ * Generated commands fall into two buckets:
13
+ *
14
+ * **Static commands** (always present, project-agnostic recipes):
15
+ * - `/follow-shrk` — short reminder of the apply-gate flow.
16
+ * - `/check-changes` — runs `shrk check boundaries --changed-only`
17
+ * scoped to the current diff and reports back.
18
+ * - `/shrk-brief` — runs `shrk brief` and uses the output for the
19
+ * current task.
20
+ * - `/explain-file <path>` — per-file rules / paths / boundary
21
+ * lookup (pairs with `shrk advise <path>` from Phase 3).
22
+ *
23
+ * **Per-template commands** (one per id in `sharkcraft/templates.ts`):
24
+ * - `/new-<template-id>` — Claude runs `shrk gen <template-id>
25
+ * <name> --dry-run --save-plan ...`, reviews the plan, and
26
+ * applies via `shrk apply ... --verify-signature --validate`.
27
+ *
28
+ * Generated files are self-contained markdown — no `@shrkcrft/*`
29
+ * imports, no shell expansions. Each one is a complete "recipe in a
30
+ * file" that Claude Code reads when the user types the slash command.
31
+ */
32
+ // ─── Static commands (always emitted) ────────────────────────────────────────
33
+ const STATIC_FOLLOW_SHRK = `---
34
+ description: Reminder of the shrk apply-gate flow for this project. Use when generating or modifying code so the change passes the same boundary / validation gates the project CI uses.
35
+ ---
36
+
37
+ # Follow the shrk apply-gate flow
38
+
39
+ This repo uses [SharkCraft](https://github.com/shrkcrft/sharkcraft) as the
40
+ gate for AI-written code. Skip the gate and CI will fail with the same
41
+ errors anyway — save the round-trip.
42
+
43
+ ## The loop
44
+
45
+ 1. **Get focused context first.**
46
+ - Run: \`shrk task "<one-sentence task>"\` for a task packet (relevant rules + templates + verification commands).
47
+ - Or: \`shrk brief\` for the single-page project brief.
48
+
49
+ 2. **Scaffold via a template (not freehand).**
50
+ - List options: \`shrk templates list\`.
51
+ - Dry-run + save plan: \`shrk gen <template-id> <name> --dry-run --save-plan /tmp/plan.json\`.
52
+
53
+ 3. **Apply the plan through the CLI.**
54
+ - \`shrk apply /tmp/plan.json --verify-signature --validate\`.
55
+ - Never write files directly through MCP — MCP is read-only in this repo.
56
+
57
+ 4. **Verify before declaring done.**
58
+ - \`shrk check boundaries --changed-only\` — fails if the diff broke any layer rule.
59
+ - \`shrk check imports\` — fails on lazy requires / cross-package deep imports.
60
+ - Project verification commands (from \`shrk task\`'s \`actionHints.verificationCommands\`).
61
+
62
+ ## When this loop doesn't apply
63
+
64
+ - Tiny changes that don't touch source files (docs, comments).
65
+ - Read-only investigations.
66
+ - Anything where shrk would clearly be in the way.
67
+
68
+ In all other cases: **follow the loop**. The gates are short; the rework if you skip them is long.
69
+ `;
70
+ const STATIC_CHECK_CHANGES = `---
71
+ description: Run shrk's boundary + import-hygiene checks on the current git diff. Use after making any file change to confirm the change didn't violate the project's architecture rules before declaring the task done.
72
+ ---
73
+
74
+ # Check the current diff for boundary violations
75
+
76
+ Run the diff-scoped boundary check:
77
+
78
+ \`\`\`bash
79
+ shrk check boundaries --changed-only
80
+ \`\`\`
81
+
82
+ Run the import-hygiene check on the changed files:
83
+
84
+ \`\`\`bash
85
+ shrk check imports
86
+ \`\`\`
87
+
88
+ ## Interpreting the output
89
+
90
+ - **Exit 0, "0 violations":** safe to declare done.
91
+ - **Exit non-zero with violations listed:** fix each violation before continuing. Each violation has a \`suggestedFix\` line — apply it.
92
+ - **A violation on a file you didn't change:** that's a pre-existing violation in the repo. Ignore it; the \`--changed-only\` filter only fails on violations your diff introduced.
93
+
94
+ If the violations look like false positives, consult \`sharkcraft/boundaries.ts\` (the rule definitions) — don't disable them ad-hoc.
95
+ `;
96
+ const STATIC_SHRK_BRIEF = `---
97
+ description: Pull shrk's single-page project brief — focused rules, paths, verification commands. Use when starting work in this codebase so you have the project's actual conventions in context before writing any code.
98
+ ---
99
+
100
+ # Pull the shrk brief for this project
101
+
102
+ Run the project brief:
103
+
104
+ \`\`\`bash
105
+ shrk brief
106
+ \`\`\`
107
+
108
+ This returns a compact markdown brief covering:
109
+ - Project overview (name, frameworks, package manager).
110
+ - Top rules that apply to code generation in this repo.
111
+ - Path conventions (where different file types belong).
112
+ - Action hints (commands, MCP tools, verification commands).
113
+ - Forbidden actions (what NOT to do).
114
+
115
+ For a per-task version (only the rules + paths + templates relevant to one task):
116
+
117
+ \`\`\`bash
118
+ shrk task "<one-sentence task description>"
119
+ \`\`\`
120
+
121
+ Both are read-only — no files are touched. Use the output to shape your plan before making any changes.
122
+ `;
123
+ const STATIC_EXPLAIN_FILE = `---
124
+ description: Look up the rules, path conventions, and boundary rules that apply to a specific file in this codebase. Use before editing an unfamiliar file so you follow the project's per-area conventions instead of generic patterns.
125
+ ---
126
+
127
+ # Explain what applies to a file in this codebase
128
+
129
+ For a given file path (e.g. \`apps/users/src/profile.service.ts\`):
130
+
131
+ \`\`\`bash
132
+ shrk why <file-path>
133
+ \`\`\`
134
+
135
+ Returns:
136
+ - Which package / layer the file belongs to.
137
+ - Which path conventions apply (e.g. "services live in \`apps/<x>/src/services/\`").
138
+ - Which rules are scoped to this file's path.
139
+ - Which boundary rules constrain this file's imports.
140
+ - Cross-references to related knowledge entries.
141
+
142
+ Use this *before* editing. The output is the project's actual conventions for that area, not your guess based on the file's content.
143
+ `;
144
+ // ─── Per-template command generator ──────────────────────────────────────────
145
+ /**
146
+ * Slugify a template id into a slash-command name. Template ids
147
+ * conventionally look like `typescript.service`; the slash command is
148
+ * `new-typescript-service` (or `new-service` if the segment after the
149
+ * dot is unique and shorter).
150
+ */
151
+ function templateSlash(templateId) {
152
+ // Use the LAST dot-separated segment as the primary name —
153
+ // `typescript.service` → `new-service`. Falls back to the full id
154
+ // when there's no dot or the segment is too generic (single char).
155
+ const parts = templateId.split('.');
156
+ const tail = parts[parts.length - 1] ?? templateId;
157
+ const safeName = tail.length >= 3 ? tail : templateId.replace(/\./g, '-');
158
+ return `new-${safeName}`
159
+ .toLowerCase()
160
+ .replace(/[^a-z0-9-]+/g, '-')
161
+ .replace(/-+/g, '-')
162
+ .replace(/^-+|-+$/g, '');
163
+ }
164
+ function templateCommandBody(template) {
165
+ const displayName = template.name ?? template.id;
166
+ const description = template.description
167
+ ? template.description.replace(/\s+/g, ' ').trim()
168
+ : `Scaffold a new ${displayName} using the project's actual template (\`${template.id}\`). Follows the shrk plan → apply → validate flow.`;
169
+ return `---
170
+ description: ${JSON.stringify(description)}
171
+ ---
172
+
173
+ # /${templateSlash(template.id)} — scaffold ${displayName}
174
+
175
+ This command scaffolds a new ${displayName} using the project's actual template
176
+ defined in \`sharkcraft/templates.ts\`. The template encodes this repo's
177
+ conventions for path, naming, and structure — the result will match how
178
+ the rest of the codebase is organized, not generic patterns.
179
+
180
+ ## The flow
181
+
182
+ When the user invokes \`/${templateSlash(template.id)} <name>\`:
183
+
184
+ 1. **Generate a plan (no writes yet):**
185
+ \`\`\`bash
186
+ shrk gen ${template.id} <name> --dry-run --save-plan /tmp/${templateSlash(template.id)}.plan.json
187
+ \`\`\`
188
+
189
+ 2. **Read the plan back** from \`/tmp/${templateSlash(template.id)}.plan.json\` and show the user which files will be created.
190
+
191
+ 3. **Confirm.** Wait for the user to approve. If they want changes, adjust the plan or re-run \`shrk gen\` with different flags.
192
+
193
+ 4. **Apply through the validated CLI path:**
194
+ \`\`\`bash
195
+ shrk apply /tmp/${templateSlash(template.id)}.plan.json --verify-signature --validate
196
+ \`\`\`
197
+
198
+ 5. **Verify** the diff didn't break any boundary rules:
199
+ \`\`\`bash
200
+ shrk check boundaries --changed-only
201
+ \`\`\`
202
+
203
+ ## If the template doesn't fit
204
+
205
+ If the user's request doesn't match the \`${template.id}\` template
206
+ shape, fall back to:
207
+
208
+ - \`shrk templates list\` — see all available templates.
209
+ - \`shrk gen <other-template> <name> --dry-run\` — try a different template.
210
+ - Hand-author only as last resort, and run \`/check-changes\` after.
211
+
212
+ The whole point of using the template is consistency with the rest of
213
+ this codebase. Skipping it means the new code will look different from
214
+ what's already there.
215
+ `;
216
+ }
217
+ /**
218
+ * Build the full set of `.claude/commands/*.md` files for a project.
219
+ *
220
+ * Pure — caller writes the bytes. Same shape as the `synthesize-*`
221
+ * functions in this codebase: input inspection → output file list.
222
+ */
223
+ export function buildClaudeCommands(inspection, options = {}) {
224
+ const files = [];
225
+ files.push({
226
+ path: '.claude/commands/follow-shrk.md',
227
+ slash: 'follow-shrk',
228
+ source: 'static',
229
+ content: STATIC_FOLLOW_SHRK,
230
+ });
231
+ files.push({
232
+ path: '.claude/commands/check-changes.md',
233
+ slash: 'check-changes',
234
+ source: 'static',
235
+ content: STATIC_CHECK_CHANGES,
236
+ });
237
+ files.push({
238
+ path: '.claude/commands/shrk-brief.md',
239
+ slash: 'shrk-brief',
240
+ source: 'static',
241
+ content: STATIC_SHRK_BRIEF,
242
+ });
243
+ files.push({
244
+ path: '.claude/commands/explain-file.md',
245
+ slash: 'explain-file',
246
+ source: 'static',
247
+ content: STATIC_EXPLAIN_FILE,
248
+ });
249
+ // Per-template commands — bounded so a pack with 50 templates
250
+ // doesn't dump 50 slash commands into the user's palette. Sorted
251
+ // by id for deterministic emit order.
252
+ const cap = options.maxTemplateCommands ?? 20;
253
+ const templates = [...inspection.templates].sort((a, b) => a.id.localeCompare(b.id));
254
+ const seenSlash = new Set(files.map((f) => f.slash));
255
+ for (const t of templates) {
256
+ if (files.filter((f) => f.source === 'template').length >= cap)
257
+ break;
258
+ let slash = templateSlash(t.id);
259
+ if (seenSlash.has(slash)) {
260
+ // Two templates with the same tail (e.g. `ts.service` and
261
+ // `py.service` both → `new-service`) — fall back to the full id
262
+ // to disambiguate the second one.
263
+ slash = `new-${t.id.replace(/\./g, '-').toLowerCase()}`;
264
+ if (seenSlash.has(slash))
265
+ continue;
266
+ }
267
+ seenSlash.add(slash);
268
+ files.push({
269
+ path: `.claude/commands/${slash}.md`,
270
+ slash,
271
+ source: 'template',
272
+ content: templateCommandBody(t),
273
+ });
274
+ }
275
+ return { files };
276
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ISharkcraftInspection } from '@shrkcrft/inspector';
2
- export type ExportFormat = 'agents-md' | 'claude-md' | 'cursor-rules' | 'copilot-instructions';
2
+ export type ExportFormat = 'agents-md' | 'claude-md' | 'claude-skill' | 'cursor-rules' | 'copilot-instructions';
3
3
  export interface ExportOptions {
4
4
  format: ExportFormat;
5
5
  /** Optional task to scope the export. */
@@ -1 +1 @@
1
- {"version":3,"file":"export-formats.d.ts","sourceRoot":"","sources":["../../src/export/export-formats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAIjE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,cAAc,GACd,sBAAsB,CAAC;AAE3B,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,CAAC;IACrB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqHD,wBAAgB,YAAY,CAC1B,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,aAAa,GACrB,YAAY,CAsBd;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,YAAY,CAOnE;AAED,eAAO,MAAM,kBAAkB,EAAE,SAAS,YAAY,EAKpD,CAAC"}
1
+ {"version":3,"file":"export-formats.d.ts","sourceRoot":"","sources":["../../src/export/export-formats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAIjE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,cAAc,GACd,cAAc,GACd,sBAAsB,CAAC;AAE3B,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,CAAC;IACrB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC;IACtB,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqPD,wBAAgB,YAAY,CAC1B,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE,aAAa,GACrB,YAAY,CAmCd;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,YAAY,CAQnE;AAED,eAAO,MAAM,kBAAkB,EAAE,SAAS,YAAY,EAMpD,CAAC"}
@@ -1,11 +1,28 @@
1
1
  import { aggregateActionHints, KnowledgeType } from '@shrkcrft/knowledge';
2
2
  import { priorityWeight } from '@shrkcrft/knowledge';
3
- const DEFAULT_OUTPUT_PATH = {
4
- 'agents-md': 'AGENTS.md',
5
- 'claude-md': 'CLAUDE.md',
6
- 'cursor-rules': '.cursor/rules/sharkcraft.mdc',
7
- 'copilot-instructions': '.github/copilot-instructions.md',
8
- };
3
+ function projectSlug(name) {
4
+ if (!name)
5
+ return 'project';
6
+ return (name
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, '-')
9
+ .replace(/^-+|-+$/g, '')
10
+ .slice(0, 48) || 'project');
11
+ }
12
+ function defaultOutputFor(format, inspection) {
13
+ switch (format) {
14
+ case 'agents-md':
15
+ return 'AGENTS.md';
16
+ case 'claude-md':
17
+ return 'CLAUDE.md';
18
+ case 'claude-skill':
19
+ return `.claude/skills/${projectSlug(inspection.config?.projectName)}/SKILL.md`;
20
+ case 'cursor-rules':
21
+ return '.cursor/rules/sharkcraft.mdc';
22
+ case 'copilot-instructions':
23
+ return '.github/copilot-instructions.md';
24
+ }
25
+ }
9
26
  function selectTopByPriority(entries, limit) {
10
27
  return [...entries]
11
28
  .sort((a, b) => priorityWeight(b.priority) - priorityWeight(a.priority))
@@ -100,23 +117,131 @@ function renderBody(inspection, options) {
100
117
  }
101
118
  return sections.join('\n\n');
102
119
  }
120
+ /**
121
+ * Filter rules / paths to the high-signal subset for an inlined skill.
122
+ *
123
+ * Every rule the skill carries pays in Claude's context every time the
124
+ * skill is loaded. Low-priority items dilute the signal of the
125
+ * load-bearing ones (`Critical` safety + `High` architecture). We keep
126
+ * only Critical + High by default, falling back to "all" only if the
127
+ * filtered list is empty (e.g. a repo with no high-priority entries
128
+ * yet shouldn't ship an empty skill).
129
+ */
130
+ function filterHighSignal(entries) {
131
+ const filtered = entries.filter((e) => {
132
+ const p = String(e.priority).toLowerCase();
133
+ return p === 'critical' || p === 'high';
134
+ });
135
+ return filtered.length > 0 ? filtered : [...entries];
136
+ }
137
+ /**
138
+ * Render the body in tight, decision-driving form for the Claude Code
139
+ * Skill format. Skills get loaded into Claude's prompt when the skill's
140
+ * `description` matches the current task, so the body must be short and
141
+ * directly actionable — no preamble, no metadata about generation, no
142
+ * pipelines (Claude can run `shrk task` for those when needed).
143
+ *
144
+ * Sections:
145
+ * 1. Where files belong (path conventions, top N) — most-leveraged signal.
146
+ * 2. Rules to follow (Critical + High only) — pads context otherwise.
147
+ * 3. Do / Don't — forbidden actions and verification commands.
148
+ * 4. How to write code in this repo — the gen → review → apply loop
149
+ * verbatim, since Claude needs to know shrk is the write path.
150
+ */
151
+ function renderSkillBody(inspection, options) {
152
+ const { rules: allRules, paths: allPaths } = gatherRulesAndPaths(inspection, {
153
+ ...options,
154
+ // Pull more upfront, then filter down to high-signal — keeps the
155
+ // softcap-at-15 default but the inlined skill stays terse.
156
+ maxRules: options.maxRules ?? 24,
157
+ maxPaths: options.maxPaths ?? 18,
158
+ });
159
+ // Skills are inlined into the prompt — every entry costs Claude
160
+ // context. Only ship the items the user said are load-bearing.
161
+ const rules = filterHighSignal(allRules).slice(0, options.maxRules ?? 12);
162
+ const paths = filterHighSignal(allPaths).slice(0, options.maxPaths ?? 10);
163
+ const sections = [];
164
+ if (inspection.config?.description) {
165
+ sections.push(`## About this codebase\n\n${inspection.config.description}`);
166
+ }
167
+ const pathsBody = renderPathsSection(paths);
168
+ if (pathsBody) {
169
+ sections.push(`## Where files belong\n\nWhen creating or moving a file, place it according to these conventions:\n\n${pathsBody}`);
170
+ }
171
+ const rulesBody = renderRulesSection(rules);
172
+ if (rulesBody) {
173
+ sections.push(`## Rules this codebase follows\n\nApply these whenever you generate, modify, or review code:\n\n${rulesBody}`);
174
+ }
175
+ const agg = aggregateActionHints(inspection.knowledgeEntries);
176
+ const doDont = [];
177
+ if (agg.forbiddenActions.length) {
178
+ doDont.push('**Do not:**');
179
+ for (const f of agg.forbiddenActions.slice(0, 8))
180
+ doDont.push(`- ${f}`);
181
+ doDont.push('');
182
+ }
183
+ if (agg.verificationCommands.length) {
184
+ doDont.push('**Verify changes with:**');
185
+ for (const c of agg.verificationCommands.slice(0, 6))
186
+ doDont.push(`- \`${c}\``);
187
+ }
188
+ if (doDont.length) {
189
+ sections.push(`## Do / Don't\n\n${doDont.join('\n').trim()}`);
190
+ }
191
+ sections.push(`## How to write code in this repo\n\n` +
192
+ `This repository uses [SharkCraft](https://github.com/shrkcrft/sharkcraft) to gate writes — the CLI is the only write path, and writes go through a plan → review → apply loop.\n\n` +
193
+ `1. **Get a task packet** before non-trivial work: \`shrk task "<one-sentence task>"\` returns the focused rules, paths, templates, and verification commands for that task.\n` +
194
+ `2. **Scaffold via templates** instead of writing files freehand: \`shrk gen <template-id> <name> --dry-run --save-plan plan.json\` produces a signed plan.\n` +
195
+ `3. **Apply the plan** through the CLI: \`shrk apply plan.json --verify-signature\` — never write through MCP.\n` +
196
+ `4. **Check the result**: \`shrk check boundaries\` + the verification commands listed above.\n\n` +
197
+ `For ad-hoc questions about the codebase, query the MCP server (\`shrk mcp serve\`) or run \`shrk context --task "<query>"\` for token-budgeted context.`);
198
+ return sections.join('\n\n');
199
+ }
200
+ /**
201
+ * Build the YAML frontmatter for a Claude Code Skill. The `description`
202
+ * field is what Claude uses to decide whether to load this skill — keep
203
+ * it specific (project name + what's covered) so it doesn't trigger on
204
+ * unrelated tasks.
205
+ */
206
+ function renderSkillFrontmatter(inspection) {
207
+ const slug = projectSlug(inspection.config?.projectName);
208
+ const projectName = inspection.config?.projectName ?? slug;
209
+ const description = `Codebase rules, path conventions, and review gates for ${projectName}. ` +
210
+ `Use this skill whenever generating, modifying, or reviewing code in this repository — ` +
211
+ `it tells you the per-file path conventions, the architecture boundaries, the verification commands, ` +
212
+ `and the safe write path (CLI plan → review → apply).`;
213
+ return `---\nname: ${slug}\ndescription: ${JSON.stringify(description)}\n---`;
214
+ }
103
215
  export function renderExport(inspection, options) {
104
- const body = renderBody(inspection, options);
105
- const suggestedPath = DEFAULT_OUTPUT_PATH[options.format];
216
+ const suggestedPath = defaultOutputFor(options.format, inspection);
106
217
  let content = '';
107
218
  switch (options.format) {
108
- case 'agents-md':
219
+ case 'agents-md': {
220
+ const body = renderBody(inspection, options);
109
221
  content = `# Agents Guide\n\n${PREAMBLE}\n\n${body}\n`;
110
222
  break;
111
- case 'claude-md':
223
+ }
224
+ case 'claude-md': {
225
+ const body = renderBody(inspection, options);
112
226
  content = `# CLAUDE.md\n\n${PREAMBLE}\n\nThis file is read by Claude Code (claude.ai/code) at session start. Treat it as a compatibility view of the project's SharkCraft knowledge — use the MCP server (\`shrk mcp serve\`) for the live, queryable source of truth.\n\n${body}\n`;
113
227
  break;
228
+ }
229
+ case 'claude-skill': {
230
+ // Claude Code Skill: YAML frontmatter declares when to load this,
231
+ // body is tight decision-driving content. No preamble, no boilerplate
232
+ // — every token in here costs Claude context when loaded.
233
+ const frontmatter = renderSkillFrontmatter(inspection);
234
+ const projectName = inspection.config?.projectName ?? 'this codebase';
235
+ const body = renderSkillBody(inspection, options);
236
+ content = `${frontmatter}\n\n# ${projectName} — codebase guide\n\n${body}\n`;
237
+ break;
238
+ }
114
239
  case 'cursor-rules':
115
240
  // Cursor MDC frontmatter: leave alwaysApply off so the user can tune.
116
- content = `---\ndescription: SharkCraft project rules (auto-generated)\nalwaysApply: false\n---\n\n${PREAMBLE}\n\n${body}\n`;
241
+ content = `---\ndescription: SharkCraft project rules (auto-generated)\nalwaysApply: false\n---\n\n${PREAMBLE}\n\n${renderBody(inspection, options)}\n`;
117
242
  break;
118
243
  case 'copilot-instructions':
119
- content = `# Copilot instructions\n\n${PREAMBLE}\n\n${body}\n`;
244
+ content = `# Copilot instructions\n\n${PREAMBLE}\n\n${renderBody(inspection, options)}\n`;
120
245
  break;
121
246
  }
122
247
  return { format: options.format, suggestedPath, content };
@@ -124,12 +249,14 @@ export function renderExport(inspection, options) {
124
249
  export function isExportFormat(value) {
125
250
  return (value === 'agents-md' ||
126
251
  value === 'claude-md' ||
252
+ value === 'claude-skill' ||
127
253
  value === 'cursor-rules' ||
128
254
  value === 'copilot-instructions');
129
255
  }
130
256
  export const ALL_EXPORT_FORMATS = Object.freeze([
131
257
  'agents-md',
132
258
  'claude-md',
259
+ 'claude-skill',
133
260
  'cursor-rules',
134
261
  'copilot-instructions',
135
262
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EACL,eAAe,EAIhB,MAAM,uBAAuB,CAAC;AAyV/B,wBAAgB,aAAa,IAAI,eAAe,CA2V/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AA6FD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA4CxE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EACL,eAAe,EAIhB,MAAM,uBAAuB,CAAC;AAyV/B,wBAAgB,aAAa,IAAI,eAAe,CA2V/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAkGD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA4CxE"}
package/dist/main.js CHANGED
@@ -20,6 +20,7 @@ import { presetsListCommand, presetsGetCommand, presetsExplainCommand, presetsRe
20
20
  import { taskCommand } from "./commands/task.command.js";
21
21
  import { preflightCommand } from "./commands/preflight.command.js";
22
22
  import { checkCommand } from "./commands/check.command.js";
23
+ import { diffCheckCommand } from "./commands/diff-check.command.js";
23
24
  import { driftCommand } from "./commands/drift.command.js";
24
25
  import { graphCommand } from "./commands/graph.command.js";
25
26
  import { coverageCommand } from "./commands/coverage.command.js";
@@ -56,7 +57,6 @@ import { biomeCommand } from "./commands/biome.command.js";
56
57
  import { ideCommand } from "./commands/ide.command.js";
57
58
  import { makeCommandsCommand } from "./commands/commands.command.js";
58
59
  import { safetyCommand } from "./commands/safety.command.js";
59
- import { pluginCommand } from "./commands/plugin.command.js";
60
60
  import { profilesCommand } from "./commands/profiles.command.js";
61
61
  import { auditProjectCouplingCommand } from "./commands/audit.command.js";
62
62
  import { conventionsCommand } from "./commands/conventions.command.js";
@@ -162,6 +162,7 @@ export function buildRegistry() {
162
162
  registry.register(taskCommand);
163
163
  registry.register(explainCommand);
164
164
  registry.register(checkCommand);
165
+ registry.register(diffCheckCommand);
165
166
  // changed-only preflight orchestrator.
166
167
  registry.register(preflightCommand);
167
168
  registry.register(driftCommand);
@@ -192,7 +193,6 @@ export function buildRegistry() {
192
193
  registry.register(eslintCommand);
193
194
  registry.register(biomeCommand);
194
195
  registry.register(ideCommand);
195
- registry.register(pluginCommand);
196
196
  registry.register(profilesCommand);
197
197
  registry.registerSubcommand('audit', auditProjectCouplingCommand);
198
198
  registry.register(conventionsCommand);
@@ -506,7 +506,14 @@ async function runCliInner(argv) {
506
506
  return registry.get('help').run(parseArgs([], { globalCwd }));
507
507
  }
508
508
  if (first === '--full-help') {
509
- return registry.get('help').run(parseArgs(['--full'], { globalCwd }));
509
+ // Pass through `--all` (catalog dump) and `--verbose` if the user
510
+ // included them after `--full-help`.
511
+ const extra = [];
512
+ if (argv.includes('--all'))
513
+ extra.push('--all');
514
+ if (argv.includes('--verbose') || argv.includes('-v'))
515
+ extra.push('--verbose');
516
+ return registry.get('help').run(parseArgs(['--full', ...extra], { globalCwd }));
510
517
  }
511
518
  if (first === '--version' || first === '-v') {
512
519
  return registry.get('version').run(parseArgs([], { globalCwd }));
@@ -550,7 +557,7 @@ async function runCliInner(argv) {
550
557
  return 2;
551
558
  }
552
559
  const attempted = probe.slice(0, 2).join(' ');
553
- process.stderr.write(`Unknown command: ${attempted}\n`);
560
+ process.stderr.write(`shrk doesn't have a \`${attempted}\` command.\n`);
554
561
  printDidYouMean(attempted);
555
562
  return 2;
556
563
  }
@@ -658,21 +665,107 @@ async function checkSurfaceGate(matchedPath, cwd) {
658
665
  return null;
659
666
  }
660
667
  }
668
+ // Score thresholds for did-you-mean output. Picked from observed
669
+ // scoring: 1-char-off typos score 8+ on plain commands; 3-4-char-off
670
+ // near-misses score ~5; loose / token-overlap matches score 1-3.
671
+ // Junk matches (frobnicate → bundle diff) can score 8 too because of
672
+ // token overlap, so we sharpen by also requiring the suggestion's
673
+ // command name to be reasonably close in length to the attempt.
674
+ const SUGGEST_CONFIDENT_SCORE = 7;
675
+ const SUGGEST_VISIBLE_SCORE = 3;
676
+ function reorderCandidates(attempted, candidates) {
677
+ // Stable sort: higher score first, then shorter command (more likely
678
+ // canonical), then lexicographic. The base suggester returns ties in
679
+ // arbitrary order — this makes the top suggestion more predictable.
680
+ const ranked = [...candidates];
681
+ ranked.sort((a, b) => {
682
+ if (b.score !== a.score)
683
+ return b.score - a.score;
684
+ if (a.command.length !== b.command.length) {
685
+ return a.command.length - b.command.length;
686
+ }
687
+ return a.command < b.command ? -1 : a.command > b.command ? 1 : 0;
688
+ });
689
+ return ranked;
690
+ }
691
+ /** Edit distance (Levenshtein). Used to gate did-you-mean confidence. */
692
+ function editDistance(a, b) {
693
+ const m = a.length;
694
+ const n = b.length;
695
+ if (m === 0)
696
+ return n;
697
+ if (n === 0)
698
+ return m;
699
+ const dp = new Array(n + 1);
700
+ for (let j = 0; j <= n; j += 1)
701
+ dp[j] = j;
702
+ for (let i = 1; i <= m; i += 1) {
703
+ let prev = dp[0];
704
+ dp[0] = i;
705
+ for (let j = 1; j <= n; j += 1) {
706
+ const tmp = dp[j];
707
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
708
+ dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + cost);
709
+ prev = tmp;
710
+ }
711
+ }
712
+ return dp[n];
713
+ }
714
+ /**
715
+ * Suggestion is "confident" when the candidate's top token is close
716
+ * to the attempt in edit-distance terms — `doctorz`→`doctor` (1 edit)
717
+ * or `inspct`→`inspect` (1 edit) qualify; `frobnicate`→`bundle` (10
718
+ * edits) does not, even when the suggester scores them similarly
719
+ * because of incidental token overlap in descriptions.
720
+ *
721
+ * Threshold: edit distance ≤ max(1, attempt.length / 4) AND raw score
722
+ * meets `SUGGEST_VISIBLE_SCORE`. This catches typical fingers-on-keys
723
+ * typos while rejecting "you typed something totally different."
724
+ */
725
+ function isConfidentMatch(attempted, suggestion) {
726
+ if (suggestion.score < SUGGEST_VISIBLE_SCORE)
727
+ return false;
728
+ const lower = attempted.toLowerCase();
729
+ const head = (suggestion.command.split(/\s+/)[0] ?? suggestion.command).toLowerCase();
730
+ const dist = editDistance(lower, head);
731
+ const tolerance = Math.max(1, Math.floor(lower.length / 4));
732
+ return dist <= tolerance;
733
+ }
661
734
  function printDidYouMean(attempted) {
662
- const candidates = suggestDidYouMean(COMMAND_CATALOG, [attempted], 3);
663
- if (candidates.length === 0) {
664
- process.stderr.write("Tip: run `shrk commands suggest \"<partial>\"` or `shrk help` to discover commands.\n");
735
+ const rawCandidates = suggestDidYouMean(COMMAND_CATALOG, [attempted], 5);
736
+ const reordered = reorderCandidates(attempted, rawCandidates).filter((c) => c.score >= SUGGEST_VISIBLE_SCORE);
737
+ if (reordered.length === 0) {
738
+ process.stderr.write('Run `shrk help` to see the curated commands, or `shrk --full-help` for the full catalog.\n');
739
+ const footer = errorFooterFor('unknown-command', { task: attempted });
740
+ if (footer)
741
+ process.stderr.write(renderErrorFooter(footer));
742
+ return;
743
+ }
744
+ // Confident single-match path: surface ONE suggestion clearly.
745
+ const top = reordered[0];
746
+ if (isConfidentMatch(attempted, top)) {
747
+ process.stderr.write(`Did you mean \`shrk ${top.command}\`?\n`);
748
+ process.stderr.write(` ${top.description}\n`);
749
+ // Second-tier suggestions only if they're also strong.
750
+ const others = reordered.slice(1, 3).filter((c) => isConfidentMatch(attempted, c));
751
+ if (others.length > 0) {
752
+ process.stderr.write('Other close matches:\n');
753
+ for (const c of others) {
754
+ process.stderr.write(` shrk ${c.command} — ${c.description}\n`);
755
+ }
756
+ }
665
757
  const footer = errorFooterFor('unknown-command', { task: attempted });
666
758
  if (footer)
667
759
  process.stderr.write(renderErrorFooter(footer));
668
760
  return;
669
761
  }
670
- process.stderr.write('Did you mean:\n');
671
- for (const c of candidates) {
762
+ // Low-confidence: show up to 3 as "closest matches", honest about
763
+ // not knowing which is right.
764
+ process.stderr.write('Closest matches in the catalog:\n');
765
+ for (const c of reordered.slice(0, 3)) {
672
766
  process.stderr.write(` shrk ${c.command} — ${c.description}\n`);
673
767
  }
674
- // append the standardised next-command footer so the user
675
- // always has a deterministic exit route.
768
+ process.stderr.write("If none of those look right, run `shrk help` or `shrk \"<task>\"` to route as a free-form task.\n");
676
769
  const footer = errorFooterFor('unknown-command', { task: attempted });
677
770
  if (footer)
678
771
  process.stderr.write(renderErrorFooter(footer));