@hegemonart/get-design-done 1.49.0 → 1.50.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +6 -3
- package/CHANGELOG.md +77 -0
- package/README.md +2 -0
- package/SKILL.md +2 -2
- package/agents/design-auditor.md +20 -0
- package/agents/design-debt-crawler.md +8 -0
- package/dist/claude-code/.claude/skills/audit/SKILL.md +5 -5
- package/dist/claude-code/.claude/skills/brief/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/compare/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/connections/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/darkmode/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/design/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/discover/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/do/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/explore/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/fast/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/health/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/live/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/new-skill/SKILL.md +90 -0
- package/dist/claude-code/.claude/skills/plan/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/progress/SKILL.md +9 -1
- package/dist/claude-code/.claude/skills/quick/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/scan/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/ship/SKILL.md +1 -1
- package/dist/claude-code/.claude/skills/verify/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/verify/verify-procedure.md +5 -5
- package/package.json +9 -2
- package/reference/anti-slop-rubric.md +173 -0
- package/reference/audit-scoring.md +4 -0
- package/reference/debt-categories.md +20 -1
- package/reference/registry.json +15 -1
- package/reference/skill-authoring-contract.md +97 -15
- package/reference/skill-graph.md +125 -0
- package/reference/visual-tells.md +152 -6
- package/scripts/lib/manifest/scaffolder.cjs +261 -0
- package/scripts/lib/manifest/schemas/skills.schema.json +14 -0
- package/scripts/lib/manifest/skills.json +50 -24
- package/skills/audit/SKILL.md +5 -5
- package/skills/brief/SKILL.md +1 -1
- package/skills/compare/SKILL.md +1 -1
- package/skills/connections/SKILL.md +1 -1
- package/skills/darkmode/SKILL.md +1 -1
- package/skills/design/SKILL.md +1 -1
- package/skills/discover/SKILL.md +1 -1
- package/skills/do/SKILL.md +1 -1
- package/skills/explore/SKILL.md +1 -1
- package/skills/fast/SKILL.md +1 -1
- package/skills/health/SKILL.md +2 -2
- package/skills/live/SKILL.md +1 -1
- package/skills/new-skill/SKILL.md +90 -0
- package/skills/plan/SKILL.md +1 -1
- package/skills/progress/SKILL.md +9 -1
- package/skills/quick/SKILL.md +1 -1
- package/skills/scan/SKILL.md +1 -1
- package/skills/ship/SKILL.md +1 -1
- package/skills/verify/SKILL.md +2 -2
- package/skills/verify/verify-procedure.md +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Visual Tells Catalog (
|
|
1
|
+
# Visual Tells Catalog (v2)
|
|
2
2
|
|
|
3
3
|
The default-AI aesthetic has a fingerprint. When a model generates a front-end
|
|
4
4
|
without a brand brief, it falls back to the same handful of moves it saw most in
|
|
@@ -6,6 +6,12 @@ its training set. This catalog names those moves so the cheap regex floor in
|
|
|
6
6
|
`hooks/gdd-design-quality-check.js` can flag them on every `.tsx` / `.vue` /
|
|
7
7
|
`.svelte` / `.astro` write.
|
|
8
8
|
|
|
9
|
+
This catalog grows commit by commit. The v1 floor named the eight loudest tells the
|
|
10
|
+
hook matches today; v2 adds five more identified through hands-on usage. Newer
|
|
11
|
+
categories may be catalog-only (documented and cross-linked, not yet wired to a hook
|
|
12
|
+
rule) until a regex earns its place. Each entry below names a **primary axis** from
|
|
13
|
+
`reference/anti-slop-rubric.md`, so a tell connects to the verb axis it most degrades.
|
|
14
|
+
|
|
9
15
|
Severity here is WARN (advisory), in the vocabulary of `reference/audit-scoring.md`.
|
|
10
16
|
A WARN is not a FLAG: it does not block the write and it does not fail a gate. It
|
|
11
17
|
is a nudge that asks "did you choose this, or did the model default to it?" Each
|
|
@@ -227,11 +233,151 @@ and never animate keyboard-driven actions. See the Motion Anti-Patterns section
|
|
|
227
233
|
|
|
228
234
|
---
|
|
229
235
|
|
|
236
|
+
## stock-photo-people
|
|
237
|
+
|
|
238
|
+
Rule id: `stock-photo-people` (catalog-only)
|
|
239
|
+
|
|
240
|
+
Generic smiling-team and handshake stock photography dropped in where a real product
|
|
241
|
+
view belongs. The diverse-team-around-a-laptop shot, the lone founder gazing out a
|
|
242
|
+
window, the abstract handshake: these read as filler the moment a viewer asks what the
|
|
243
|
+
product actually does. Like the isometric fallback, the tell is that no real screen was
|
|
244
|
+
available, so a stock human filled the hole.
|
|
245
|
+
|
|
246
|
+
Instances:
|
|
247
|
+
|
|
248
|
+
1. A hero photo of four people laughing at one laptop, captioned with a value prop.
|
|
249
|
+
2. `src="/images/team-meeting.jpg"` standing in for a product screenshot.
|
|
250
|
+
3. A testimonial section using stock headshots instead of real customer faces.
|
|
251
|
+
4. An unsplash or shutterstock asset path in a primary marketing surface.
|
|
252
|
+
5. The same stock photo reused across unrelated sections of one page.
|
|
253
|
+
|
|
254
|
+
Diagnostic regex (where applicable): `\b(?:unsplash|shutterstock|istockphoto|gettyimages|stock[-_]?photo)[\w./-]*` in an asset path or `src`.
|
|
255
|
+
|
|
256
|
+
Remediation: show the real product or real people connected to it. A genuine screenshot,
|
|
257
|
+
a real customer portrait with permission, or a purpose-shot photo all beat a stock human.
|
|
258
|
+
If no real asset exists yet, a clean illustration with brand character beats a stock smile.
|
|
259
|
+
|
|
260
|
+
Primary axis: Authenticity.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## badge-spam
|
|
265
|
+
|
|
266
|
+
Rule id: `badge-spam` (catalog-only)
|
|
267
|
+
|
|
268
|
+
Decorative pills stacked to manufacture importance: "New", "Beta", "Pro", "AI-powered",
|
|
269
|
+
"v2" scattered across cards and nav with no state behind them. One badge that reflects a
|
|
270
|
+
real status orients the user. Four badges in a row are noise that the model adds because
|
|
271
|
+
badges look like product maturity. The tell is a cluster of status pills none of which
|
|
272
|
+
encode a true, current state.
|
|
273
|
+
|
|
274
|
+
Instances:
|
|
275
|
+
|
|
276
|
+
1. A card carrying "New", "Hot", and "AI-powered" badges at once.
|
|
277
|
+
2. A nav item with a permanent "Beta" pill that has shipped for a year.
|
|
278
|
+
3. Every feature tile in a grid wearing the same "Pro" badge.
|
|
279
|
+
4. An "AI-powered" badge on a feature with no AI in it.
|
|
280
|
+
5. Three or more `<Badge>` or `.badge` elements rendered in one small region.
|
|
281
|
+
|
|
282
|
+
Diagnostic regex (where applicable): a quoted label matching `\b(?:New|Beta|Pro|AI-powered|Hot|Coming soon)\b` inside a badge or chip element, flagged at three or more in one component.
|
|
283
|
+
|
|
284
|
+
Remediation: keep one badge that carries a real, current status, and delete the rest.
|
|
285
|
+
A badge should change when state changes. If a label never changes, it is decoration, not
|
|
286
|
+
status, and belongs in the copy or not at all.
|
|
287
|
+
|
|
288
|
+
Primary axis: Authenticity.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## oversized-single-word
|
|
293
|
+
|
|
294
|
+
Rule id: `oversized-single-word` (catalog-only)
|
|
295
|
+
|
|
296
|
+
A single word blown up to display size to fill a section that has no content to carry it.
|
|
297
|
+
"Build.", "Ship.", "Scale." at `text-8xl` with nothing beneath but air. Large type is a
|
|
298
|
+
strong tool for one real headline; the tell is a one-word slab standing in for a sentence
|
|
299
|
+
the writer did not write, sized for drama rather than meaning.
|
|
300
|
+
|
|
301
|
+
Instances:
|
|
302
|
+
|
|
303
|
+
1. A section whose only content is "Innovate." at `text-9xl`.
|
|
304
|
+
2. A stack of one-word slabs ("Fast." "Simple." "Powerful.") each filling a viewport.
|
|
305
|
+
3. A giant verb with no object, no supporting line, and 300px of padding around it.
|
|
306
|
+
4. Display-size text where the word count is one and the surrounding region is empty.
|
|
307
|
+
|
|
308
|
+
Diagnostic regex (where applicable): an element with `text-(?:7xl|8xl|9xl)` whose text node is a single word.
|
|
309
|
+
|
|
310
|
+
Remediation: write the sentence the word was standing in for, then size the headline to
|
|
311
|
+
serve reading. If a one-word moment is genuinely the design, give it a supporting line and
|
|
312
|
+
a reason to be that large. Density should fit the content, not inflate to fill space.
|
|
313
|
+
|
|
314
|
+
Primary axis: Density.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## motion-without-content-intent
|
|
319
|
+
|
|
320
|
+
Rule id: `motion-without-content-intent` (catalog-only)
|
|
321
|
+
|
|
322
|
+
Scroll-triggered fades, parallax layers, and entrance animations applied to every block
|
|
323
|
+
because motion reads as "modern". This is the storytelling cousin of the ambient-loop tell:
|
|
324
|
+
where `decorative-motion-without-intent` flags a forever-looping spinner, this flags motion
|
|
325
|
+
wired to scroll or load that carries no content reason. Every element fading up in sequence
|
|
326
|
+
delays the reader and signals motion chosen as decoration, not to direct attention.
|
|
327
|
+
|
|
328
|
+
Instances:
|
|
329
|
+
|
|
330
|
+
1. Every section wrapped in a `whileInView` fade-up with a staggered delay.
|
|
331
|
+
2. Parallax on three background layers that encode no depth meaning.
|
|
332
|
+
3. An entrance animation on body copy that the reader is waiting to read.
|
|
333
|
+
4. Scroll-jacking that overrides native scroll for a "cinematic" reveal.
|
|
334
|
+
|
|
335
|
+
Diagnostic regex (where applicable): `whileInView|data-aos|scroll-trigger|parallax` applied broadly across blocks without a reduced-motion guard in the same file.
|
|
336
|
+
|
|
337
|
+
Remediation: tie motion to a content reason. Animate the one element that should draw the
|
|
338
|
+
eye, let the rest render immediately, and respect `prefers-reduced-motion`. Reading content
|
|
339
|
+
should never wait on a decorative reveal. See the Motion Anti-Patterns section of
|
|
340
|
+
`reference/anti-patterns.md`.
|
|
341
|
+
|
|
342
|
+
Primary axis: Hierarchy.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## narrator-from-a-distance-UI
|
|
347
|
+
|
|
348
|
+
Rule id: `narrator-from-a-distance-UI` (catalog-only)
|
|
349
|
+
|
|
350
|
+
Copy that describes the product from the outside in a detached marketing voice instead of
|
|
351
|
+
speaking to the user doing a task. "Our platform empowers teams to achieve more" is a
|
|
352
|
+
narrator at a distance; "Invite your team and assign the first task" speaks to the person
|
|
353
|
+
in front of the screen. The tell is third-person product description where a second-person
|
|
354
|
+
instruction belongs, especially inside the product rather than on a landing page.
|
|
355
|
+
|
|
356
|
+
Instances:
|
|
357
|
+
|
|
358
|
+
1. A dashboard empty state reading "The platform helps you organize your work" instead of "Create your first board".
|
|
359
|
+
2. In-product copy in third person ("Users can export reports") rather than direct address.
|
|
360
|
+
3. A feature headline describing the feature to an investor, not to its user.
|
|
361
|
+
4. Onboarding steps narrated about the product instead of instructing the new user.
|
|
362
|
+
|
|
363
|
+
Diagnostic regex (where applicable): in-product strings matching `\b(?:our platform|the platform|users can|we help you|empowers)\b` outside marketing routes.
|
|
364
|
+
|
|
365
|
+
Remediation: write to the user in second person and name the next action. Inside the
|
|
366
|
+
product, copy should instruct and orient, not pitch. Reserve the outside-in description for
|
|
367
|
+
the marketing surface, and even there, lead with what the reader can do. See
|
|
368
|
+
`reference/copy-quality.md` for the per-surface voice contract.
|
|
369
|
+
|
|
370
|
+
Primary axis: Directness.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
230
374
|
## Notes
|
|
231
375
|
|
|
232
|
-
This is a
|
|
233
|
-
aim to fire only on the obvious cases so the WARN stays
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
376
|
+
This is a v2 catalog, growing commit by commit, not a ceiling. The hook regexes favor
|
|
377
|
+
precision over recall: they aim to fire only on the obvious cases so the WARN stays
|
|
378
|
+
trustworthy. The five v2 categories above are catalog-only until a regex earns a place in
|
|
379
|
+
the hook; they still give review a named tell and a primary axis to score against. A clean
|
|
380
|
+
pass here does not mean the design is good. It means the loudest default-AI tells are
|
|
381
|
+
absent. Real review still applies the full rubric in `reference/audit-scoring.md`, the verb
|
|
382
|
+
axes in `reference/anti-slop-rubric.md`, and the BAN / SLOP catalog in
|
|
237
383
|
`reference/anti-patterns.md`.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/manifest/scaffolder.cjs — Phase 50 (Authoring Contract v3).
|
|
4
|
+
*
|
|
5
|
+
* Pure, dependency-free generator behind the `/gdd:new-skill` scaffolder skill.
|
|
6
|
+
* The SKILL.md (source/skills/new-skill/SKILL.md) drives the interactive
|
|
7
|
+
* prompts; this module is the deterministic core it (and the test suite) call.
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* buildSkillRecord({ name, description, argumentHint, tools, userInvocable,
|
|
11
|
+
* composesWith }) -> a skills.json record object. Validates the name slug,
|
|
12
|
+
* the v3 description budget (20..1024 chars), and the comma-separated tools
|
|
13
|
+
* list. Throws on invalid input.
|
|
14
|
+
* renderSkillMd(record) -> the SKILL.md template string (frontmatter in the
|
|
15
|
+
* same canonical key order as generate-skill-frontmatter.cjs + a minimal
|
|
16
|
+
* body skeleton with the standard sections).
|
|
17
|
+
* suggestComposesWith(name, allSkills) -> heuristic composition suggestions
|
|
18
|
+
* (skills sharing a lifecycle-stage keyword with the new skill name).
|
|
19
|
+
*
|
|
20
|
+
* Dependency-free of any third party. It DOES reuse the in-repo
|
|
21
|
+
* generate-skill-frontmatter.cjs `frontmatterFromRecord` emitter so the
|
|
22
|
+
* rendered frontmatter is a byte-for-byte fixed point with the forward
|
|
23
|
+
* generator (description quoted, canonical key order, name leads).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
// Reuse the canonical frontmatter emitter so renderSkillMd stays compatible
|
|
29
|
+
// with `npm run generate:skill-frontmatter` (same quoting + key order).
|
|
30
|
+
const { frontmatterFromRecord } = require(
|
|
31
|
+
path.join(__dirname, '..', '..', 'generate-skill-frontmatter.cjs'),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Slug rule mirrors reference/skill-authoring-contract.md `## Frontmatter`:
|
|
35
|
+
// kebab-case identifier matching ^[a-z0-9][a-z0-9-._]*$.
|
|
36
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-._]*$/;
|
|
37
|
+
|
|
38
|
+
// Description budget mirrors the Phase 28.5 contract (D-02) + skills.schema.json:
|
|
39
|
+
// 20..1024 chars. The v3 "Activates for requests involving X, Y, Z." sentence
|
|
40
|
+
// is recommended (LAX by default) but not regex-enforced here.
|
|
41
|
+
const DESC_MIN = 20;
|
|
42
|
+
const DESC_MAX = 1024;
|
|
43
|
+
|
|
44
|
+
// Lifecycle-stage keyword groups used by suggestComposesWith. Skills whose
|
|
45
|
+
// name shares a group with the new skill name are plausible composition
|
|
46
|
+
// neighbours. Kept deliberately small and dependency-free; the SKILL.md treats
|
|
47
|
+
// the result as a suggestion the user confirms, never an autowire.
|
|
48
|
+
const STAGE_GROUPS = [
|
|
49
|
+
['brief', 'intake', 'start', 'new-project', 'new-cycle'],
|
|
50
|
+
['explore', 'discover', 'discuss', 'sketch', 'spike', 'map'],
|
|
51
|
+
['plan', 'planning', 'design', 'do', 'build'],
|
|
52
|
+
['verify', 'audit', 'review', 'quality-gate', 'check'],
|
|
53
|
+
['ship', 'pr', 'complete', 'closeout', 'rollout'],
|
|
54
|
+
['figma', 'extract', 'export', 'import', 'sync'],
|
|
55
|
+
['token', 'tokens', 'darkmode', 'style', 'theme'],
|
|
56
|
+
['health', 'progress', 'stats', 'report', 'timeline'],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function fail(msg) {
|
|
60
|
+
throw new Error(`scaffolder: ${msg}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Normalize a tools input (string or array) to a clean comma-list string. */
|
|
64
|
+
function normalizeTools(tools) {
|
|
65
|
+
if (tools == null) return undefined;
|
|
66
|
+
let parts;
|
|
67
|
+
if (Array.isArray(tools)) {
|
|
68
|
+
parts = tools;
|
|
69
|
+
} else if (typeof tools === 'string') {
|
|
70
|
+
parts = tools.split(',');
|
|
71
|
+
} else {
|
|
72
|
+
fail('tools must be a comma-separated string or an array of tool names');
|
|
73
|
+
}
|
|
74
|
+
const cleaned = parts.map((t) => String(t).trim()).filter(Boolean);
|
|
75
|
+
if (cleaned.length === 0) fail('tools, when provided, must name at least one tool');
|
|
76
|
+
// Each token: a Tool name or an mcp__* identifier. No commas, no empty.
|
|
77
|
+
// `\w` already includes `_`, so a single `[\w-]*` class matches plain names
|
|
78
|
+
// (Read) AND mcp__* identifiers (mcp__gdd_state__get) in linear time. The old
|
|
79
|
+
// `(__[\w-]+)*` suffix overlapped `[\w-]*` and caused exponential backtracking
|
|
80
|
+
// (CodeQL js/redos) on inputs like `A__-__-__...`; it was redundant. Removed.
|
|
81
|
+
for (const t of cleaned) {
|
|
82
|
+
if (!/^[A-Za-z][\w-]*$/.test(t)) {
|
|
83
|
+
fail(`tools entry "${t}" is not a valid tool identifier`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return cleaned.join(', ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Normalize a composes_with input (string or array) to a slug array. */
|
|
90
|
+
function normalizeComposesWith(composesWith) {
|
|
91
|
+
if (composesWith == null) return undefined;
|
|
92
|
+
let parts;
|
|
93
|
+
if (Array.isArray(composesWith)) parts = composesWith;
|
|
94
|
+
else if (typeof composesWith === 'string') parts = composesWith.split(',');
|
|
95
|
+
else fail('composesWith must be an array or comma-separated string of skill names');
|
|
96
|
+
const cleaned = parts.map((s) => String(s).trim()).filter(Boolean);
|
|
97
|
+
if (cleaned.length === 0) return undefined;
|
|
98
|
+
for (const s of cleaned) {
|
|
99
|
+
if (!NAME_RE.test(s)) fail(`composesWith entry "${s}" is not a valid skill slug`);
|
|
100
|
+
}
|
|
101
|
+
// De-dupe, preserve first-seen order.
|
|
102
|
+
return [...new Set(cleaned)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a skills.json record object from scaffolder inputs.
|
|
107
|
+
* Keys are inserted in the canonical emit order so frontmatterFromRecord
|
|
108
|
+
* produces the same byte layout generate-skill-frontmatter.cjs would.
|
|
109
|
+
* @throws on an invalid name, out-of-budget description, or malformed tools.
|
|
110
|
+
*/
|
|
111
|
+
function buildSkillRecord(input) {
|
|
112
|
+
const opts = input || {};
|
|
113
|
+
const name = typeof opts.name === 'string' ? opts.name.trim() : opts.name;
|
|
114
|
+
if (!name || typeof name !== 'string') fail('name is required (a kebab-case slug)');
|
|
115
|
+
if (!NAME_RE.test(name)) {
|
|
116
|
+
fail(`name "${name}" must match ${NAME_RE} (lower-case, starts alnum, kebab/dot/underscore)`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const description = typeof opts.description === 'string' ? opts.description.trim() : opts.description;
|
|
120
|
+
if (!description || typeof description !== 'string') fail('description is required');
|
|
121
|
+
if (description.length < DESC_MIN) {
|
|
122
|
+
fail(`description is ${description.length} chars; require >=${DESC_MIN}`);
|
|
123
|
+
}
|
|
124
|
+
if (description.length > DESC_MAX) {
|
|
125
|
+
fail(`description is ${description.length} chars; require <=${DESC_MAX}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Insertion order == canonical managed-key emit order (name leads in the
|
|
129
|
+
// emitter; the rest follow record insertion order).
|
|
130
|
+
const rec = { name, description };
|
|
131
|
+
|
|
132
|
+
const argumentHint = opts.argumentHint != null ? String(opts.argumentHint) : undefined;
|
|
133
|
+
if (argumentHint !== undefined) rec.argument_hint = argumentHint;
|
|
134
|
+
|
|
135
|
+
const tools = normalizeTools(opts.tools);
|
|
136
|
+
if (tools !== undefined) rec.tools = tools;
|
|
137
|
+
|
|
138
|
+
if (opts.userInvocable !== undefined) rec.user_invocable = Boolean(opts.userInvocable);
|
|
139
|
+
|
|
140
|
+
const composesWith = normalizeComposesWith(opts.composesWith);
|
|
141
|
+
if (composesWith !== undefined) rec.composes_with = composesWith;
|
|
142
|
+
|
|
143
|
+
return rec;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render the SKILL.md template string for a record.
|
|
148
|
+
* Frontmatter is emitted via the shared generate-skill-frontmatter emitter so
|
|
149
|
+
* it is a fixed point with the forward generator. composes_with (a Phase 50
|
|
150
|
+
* field not yet in the emitter's MANAGED set) is appended as an explicit
|
|
151
|
+
* frontmatter line; `--extract` carries it verbatim in extra_frontmatter.
|
|
152
|
+
*/
|
|
153
|
+
function renderSkillMd(record) {
|
|
154
|
+
if (!record || typeof record !== 'object') fail('renderSkillMd requires a record object');
|
|
155
|
+
// Validate/normalize defensively so renderSkillMd(buildSkillRecord(x)) and
|
|
156
|
+
// renderSkillMd(rawObject) both produce a contract-valid file.
|
|
157
|
+
const rec = buildSkillRecord({
|
|
158
|
+
name: record.name,
|
|
159
|
+
description: record.description,
|
|
160
|
+
argumentHint: record.argument_hint,
|
|
161
|
+
tools: record.tools,
|
|
162
|
+
userInvocable: record.user_invocable,
|
|
163
|
+
composesWith: record.composes_with,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Separate composes_with: the shared emitter only knows the MANAGED keys, so
|
|
167
|
+
// composes_with rides as an extra_frontmatter line (the round-trip home the
|
|
168
|
+
// forward generator already uses for non-managed keys).
|
|
169
|
+
const composesWith = rec.composes_with;
|
|
170
|
+
const emitRec = { ...rec };
|
|
171
|
+
delete emitRec.composes_with;
|
|
172
|
+
if (composesWith && composesWith.length) {
|
|
173
|
+
emitRec.extra_frontmatter = [`composes_with: [${composesWith.join(', ')}]`];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const frontmatter = frontmatterFromRecord(emitRec);
|
|
177
|
+
const prefix = '{{command_prefix}}';
|
|
178
|
+
const upper = rec.name.toUpperCase();
|
|
179
|
+
|
|
180
|
+
const body = [
|
|
181
|
+
'',
|
|
182
|
+
`# ${prefix}${rec.name}`,
|
|
183
|
+
'',
|
|
184
|
+
`**Role:** ${rec.description.split('. ')[0]}.`,
|
|
185
|
+
'',
|
|
186
|
+
'## Steps',
|
|
187
|
+
'',
|
|
188
|
+
'1. State the preconditions this skill needs (read `.design/STATE.md` if relevant).',
|
|
189
|
+
'2. Do the work. Keep each step a single concrete action.',
|
|
190
|
+
'3. Report the result and recommend the next action.',
|
|
191
|
+
'',
|
|
192
|
+
'## Output',
|
|
193
|
+
'',
|
|
194
|
+
'```',
|
|
195
|
+
`${prefix}${rec.name} result summary goes here.`,
|
|
196
|
+
'```',
|
|
197
|
+
'',
|
|
198
|
+
'## Do Not',
|
|
199
|
+
'',
|
|
200
|
+
'- Do not exceed the authoring-contract length cap (warn at 100 lines).',
|
|
201
|
+
'- Do not invent state; read it from `.design/`.',
|
|
202
|
+
'',
|
|
203
|
+
`## ${upper} COMPLETE`,
|
|
204
|
+
'',
|
|
205
|
+
].join('\n');
|
|
206
|
+
|
|
207
|
+
return `---\n${frontmatter}\n---\n${body}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Heuristic composition suggestions for a new skill.
|
|
212
|
+
* Returns skill names from `allSkills` that share a lifecycle-stage keyword
|
|
213
|
+
* with `name` (or whose name substring-matches a shared stage token). The
|
|
214
|
+
* new skill itself is never suggested. Order: stable by allSkills order.
|
|
215
|
+
* @param {string} name new skill slug
|
|
216
|
+
* @param {Array<string|{name:string}>} allSkills existing skills (names or records)
|
|
217
|
+
* @returns {string[]} suggested composition neighbours (possibly empty)
|
|
218
|
+
*/
|
|
219
|
+
function suggestComposesWith(name, allSkills) {
|
|
220
|
+
if (!name || typeof name !== 'string') return [];
|
|
221
|
+
const self = name.trim().toLowerCase();
|
|
222
|
+
const names = (Array.isArray(allSkills) ? allSkills : [])
|
|
223
|
+
.map((s) => (typeof s === 'string' ? s : s && s.name))
|
|
224
|
+
.filter((n) => typeof n === 'string' && n.trim() && n.trim().toLowerCase() !== self)
|
|
225
|
+
.map((n) => n.trim());
|
|
226
|
+
|
|
227
|
+
// Which stage groups does the new skill touch?
|
|
228
|
+
const tokensOf = (slug) => slug.toLowerCase().split(/[-._]/).filter(Boolean);
|
|
229
|
+
const selfTokens = new Set(tokensOf(self));
|
|
230
|
+
const selfGroups = new Set();
|
|
231
|
+
STAGE_GROUPS.forEach((group, idx) => {
|
|
232
|
+
if (group.some((kw) => selfTokens.has(kw) || self.includes(kw))) selfGroups.add(idx);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (selfGroups.size === 0) return [];
|
|
236
|
+
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const candidate of names) {
|
|
240
|
+
if (seen.has(candidate)) continue;
|
|
241
|
+
const cTokens = new Set(tokensOf(candidate));
|
|
242
|
+
const inGroup = [...selfGroups].some((idx) =>
|
|
243
|
+
STAGE_GROUPS[idx].some((kw) => cTokens.has(kw) || candidate.toLowerCase().includes(kw)),
|
|
244
|
+
);
|
|
245
|
+
if (inGroup) {
|
|
246
|
+
out.push(candidate);
|
|
247
|
+
seen.add(candidate);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
buildSkillRecord,
|
|
255
|
+
renderSkillMd,
|
|
256
|
+
suggestComposesWith,
|
|
257
|
+
NAME_RE,
|
|
258
|
+
DESC_MIN,
|
|
259
|
+
DESC_MAX,
|
|
260
|
+
STAGE_GROUPS,
|
|
261
|
+
};
|
|
@@ -60,6 +60,20 @@
|
|
|
60
60
|
},
|
|
61
61
|
"description": "Reserved for pin shortcuts; honored by the pin metadata catalogue."
|
|
62
62
|
},
|
|
63
|
+
"composes_with": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"items": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"description": "Phase 50 (v3) optional: skill names this skill calls as sub-orchestration. Each becomes a directed composition edge validated by scripts/validate-composition-graph.cjs (must be a DAG, no dangling refs)."
|
|
69
|
+
},
|
|
70
|
+
"next_skills": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"description": "Phase 50 (v3) optional: pipeline hint listing skill names that naturally run after this one. Each becomes a directed edge in the composition graph (DAG, no dangling refs)."
|
|
76
|
+
},
|
|
63
77
|
"extra_frontmatter": {
|
|
64
78
|
"type": "array",
|
|
65
79
|
"items": {
|