@hegemonart/get-design-done 1.48.0 → 1.50.0

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 (70) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +8 -2
  3. package/CHANGELOG.md +93 -0
  4. package/README.md +4 -0
  5. package/SKILL.md +2 -1
  6. package/agents/design-auditor.md +37 -4
  7. package/agents/design-context-builder.md +2 -0
  8. package/agents/design-debt-crawler.md +36 -5
  9. package/agents/design-executor.md +2 -0
  10. package/agents/design-fixer.md +4 -1
  11. package/agents/design-planner.md +2 -0
  12. package/agents/design-reflector.md +2 -0
  13. package/agents/design-research-synthesizer.md +2 -0
  14. package/agents/design-verifier.md +7 -15
  15. package/dist/claude-code/.claude/skills/audit/SKILL.md +1 -1
  16. package/dist/claude-code/.claude/skills/brief/SKILL.md +1 -1
  17. package/dist/claude-code/.claude/skills/compare/SKILL.md +1 -1
  18. package/dist/claude-code/.claude/skills/connections/SKILL.md +1 -1
  19. package/dist/claude-code/.claude/skills/darkmode/SKILL.md +1 -1
  20. package/dist/claude-code/.claude/skills/design/SKILL.md +1 -1
  21. package/dist/claude-code/.claude/skills/discover/SKILL.md +1 -1
  22. package/dist/claude-code/.claude/skills/do/SKILL.md +1 -1
  23. package/dist/claude-code/.claude/skills/explore/SKILL.md +1 -1
  24. package/dist/claude-code/.claude/skills/fast/SKILL.md +1 -1
  25. package/dist/claude-code/.claude/skills/health/SKILL.md +2 -2
  26. package/dist/claude-code/.claude/skills/live/SKILL.md +1 -1
  27. package/dist/claude-code/.claude/skills/new-skill/SKILL.md +90 -0
  28. package/dist/claude-code/.claude/skills/plan/SKILL.md +1 -1
  29. package/dist/claude-code/.claude/skills/progress/SKILL.md +9 -1
  30. package/dist/claude-code/.claude/skills/quick/SKILL.md +1 -1
  31. package/dist/claude-code/.claude/skills/scan/SKILL.md +1 -1
  32. package/dist/claude-code/.claude/skills/ship/SKILL.md +1 -1
  33. package/dist/claude-code/.claude/skills/verify/SKILL.md +1 -1
  34. package/hooks/gdd-design-quality-check.js +340 -0
  35. package/hooks/hooks.json +9 -0
  36. package/package.json +12 -2
  37. package/reference/anti-slop-rubric.md +173 -0
  38. package/reference/audit-scoring.md +4 -0
  39. package/reference/debt-categories.md +20 -1
  40. package/reference/registry.json +28 -0
  41. package/reference/reviewer-confidence-gate.md +108 -0
  42. package/reference/skill-authoring-contract.md +97 -15
  43. package/reference/skill-graph.md +118 -0
  44. package/reference/visual-tells.md +383 -0
  45. package/scripts/lib/confidence-route.cjs +60 -0
  46. package/scripts/lib/manifest/scaffolder.cjs +261 -0
  47. package/scripts/lib/manifest/schemas/skills.schema.json +14 -0
  48. package/scripts/lib/manifest/skills.json +26 -18
  49. package/scripts/lib/worktree-resolve.cjs +221 -0
  50. package/sdk/mcp/gdd-state/server.js +37 -4
  51. package/sdk/mcp/gdd-state/tools/shared.ts +61 -0
  52. package/skills/audit/SKILL.md +1 -1
  53. package/skills/brief/SKILL.md +1 -1
  54. package/skills/compare/SKILL.md +1 -1
  55. package/skills/connections/SKILL.md +1 -1
  56. package/skills/darkmode/SKILL.md +1 -1
  57. package/skills/design/SKILL.md +1 -1
  58. package/skills/discover/SKILL.md +1 -1
  59. package/skills/do/SKILL.md +1 -1
  60. package/skills/explore/SKILL.md +1 -1
  61. package/skills/fast/SKILL.md +1 -1
  62. package/skills/health/SKILL.md +2 -2
  63. package/skills/live/SKILL.md +1 -1
  64. package/skills/new-skill/SKILL.md +90 -0
  65. package/skills/plan/SKILL.md +1 -1
  66. package/skills/progress/SKILL.md +9 -1
  67. package/skills/quick/SKILL.md +1 -1
  68. package/skills/scan/SKILL.md +1 -1
  69. package/skills/ship/SKILL.md +1 -1
  70. package/skills/verify/SKILL.md +1 -1
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: gdd-new-skill
3
+ description: "Scaffolds a new Phase-28.5 + Phase-50-compliant skill: gathers a name, a multi-paragraph v3 description, a lifecycle stage, an allowed-tools list, and optional composes_with neighbours, then writes source/skills/<name>/SKILL.md from the pure generator. Use when adding a brand-new gdd skill and you want the frontmatter, length cap, and v3 description form correct from the first commit. Activates for requests involving authoring a skill, scaffolding a command, creating a new SKILL.md, or adding a slash command."
4
+ argument-hint: "<skill-name>"
5
+ tools: Read, Write, Bash, AskUserQuestion
6
+ user-invocable: true
7
+ ---
8
+
9
+ # /gdd:new-skill
10
+
11
+ **Role:** Interactively scaffold a contract-compliant skill. Gather the fields, then write `source/skills/<name>/SKILL.md` from the pure generator at `scripts/lib/manifest/scaffolder.cjs`. This skill writes ONE source file. It does NOT touch `scripts/lib/manifest/skills.json` and does NOT run the build; it prints the exact follow-up commands instead.
12
+
13
+ Read `reference/skill-authoring-contract.md` first for the length cap, the frontmatter required fields, and the v3 description form.
14
+
15
+ ## Prompt strategy (clack with a fallback)
16
+
17
+ Mirror the installer pattern in `scripts/install.cjs`: try `@clack/prompts` lazily, and degrade to `AskUserQuestion` (or plain text) when it is absent. Use this probe at the start:
18
+
19
+ ```bash
20
+ node -e "try { require.resolve('@clack/prompts'); console.log('clack'); } catch { console.log('fallback'); }"
21
+ ```
22
+
23
+ - `clack`: drive `clack.text`, `clack.select`, and `clack.confirm` from a short Node script.
24
+ - `fallback`: ask the same questions with `AskUserQuestion` (one prompt per field), or read plain answers.
25
+
26
+ Either way the answers feed the same record builder. Never block waiting on a TTY in a non-interactive run; fall back to `AskUserQuestion`.
27
+
28
+ ## Step 1 - Gather the fields
29
+
30
+ 1. **name**: the slug from `$ARGUMENTS` if present, else ask. Must match `^[a-z0-9][a-z0-9-._]*$`. Reject `source/skills/<name>/` collisions.
31
+ 2. **description**: a multi-paragraph v3 form. Sentence one is what the skill does. Sentence two is `Use when <triggers>`. Sentence three is `Activates for requests involving <kw1>, <kw2>, <kw3>.` Keep it 20 to 1024 chars.
32
+ 3. **lifecycle stage**: pick one of brief / explore / plan / verify / ship / figma / token / report (free text allowed). This seeds the composition suggestions.
33
+ 4. **tools**: a comma list (for example `Read, Write, Bash`). Empty means inherit-all.
34
+ 5. **composes_with**: auto-suggest neighbours, then confirm. Compute the suggestion list:
35
+
36
+ ```bash
37
+ node -e "
38
+ const s = require('./scripts/lib/manifest/scaffolder.cjs');
39
+ const m = require('./scripts/lib/manifest/skills.json');
40
+ console.log(JSON.stringify(s.suggestComposesWith(process.argv[1], m.skills)));
41
+ " "<name>"
42
+ ```
43
+
44
+ Present the suggestions; let the user accept, edit, or clear them.
45
+
46
+ ## Step 2 - Length pre-check
47
+
48
+ Before writing, estimate the body length. If the planned body would exceed about 100 lines, warn the user (the authoring contract warns at 100 and blocks at 250) and suggest extracting domain content into a co-located `source/skills/<name>/<topic>.md` reference. The generated skeleton is small; the warning is for the prose the user will add next.
49
+
50
+ ## Step 3 - Write the file
51
+
52
+ Build the record and render the file with the pure generator, then write it with the Write tool (not shell redirection):
53
+
54
+ ```bash
55
+ node -e "
56
+ const s = require('./scripts/lib/manifest/scaffolder.cjs');
57
+ const rec = s.buildSkillRecord({
58
+ name: process.env.SK_NAME,
59
+ description: process.env.SK_DESC,
60
+ argumentHint: process.env.SK_HINT || undefined,
61
+ tools: process.env.SK_TOOLS || undefined,
62
+ userInvocable: process.env.SK_UI === 'true',
63
+ composesWith: (process.env.SK_COMPOSES || '').split(',').map(x => x.trim()).filter(Boolean),
64
+ });
65
+ process.stdout.write(s.renderSkillMd(rec));
66
+ "
67
+ ```
68
+
69
+ `buildSkillRecord` throws on an invalid name, an out-of-budget description, or a malformed tools list. Surface the thrown message to the user and re-prompt the offending field. Capture stdout and write it verbatim to `source/skills/<name>/SKILL.md` via the Write tool.
70
+
71
+ ## Step 4 - Tell the user the follow-up
72
+
73
+ The skill stops here by contract. Print the next two commands for the user to run after they add the manifest record:
74
+
75
+ ```
76
+ 1. Add a record for "<name>" to scripts/lib/manifest/skills.json
77
+ (description, argument_hint, tools, user_invocable; put composes_with in extra_frontmatter).
78
+ 2. npm run generate:skill-frontmatter && npm run build:skills
79
+ ```
80
+
81
+ Note that `npm run generate:skill-frontmatter:check` and the skills-coverage test stay red until the manifest record exists; that is expected and is the maintainer's step.
82
+
83
+ ## Do Not
84
+
85
+ - Do not edit `scripts/lib/manifest/skills.json`, `package.json`, or any reference doc.
86
+ - Do not run `build:skills` or `generate:skill-frontmatter` for the user; print the commands.
87
+ - Do not write the SKILL.md with shell redirection; use the Write tool so the content is exact.
88
+ - Do not invent a description; require the v3 three-sentence form from the user.
89
+
90
+ ## NEW-SKILL COMPLETE
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plan
3
- description: "Stage 3 of 5 orchestrator that reads DESIGN-CONTEXT.md, runs optional research (phase-researcher / pattern-mapper / assumptions-analyzer / synthesizer), spawns design-planner + design-plan-checker, and writes DESIGN-PLAN.md. Use when DESIGN-CONTEXT.md is locked and you need a wave-ordered execution plan."
3
+ description: "Stage 3 of 5 orchestrator that reads DESIGN-CONTEXT.md, runs optional research (phase-researcher / pattern-mapper / assumptions-analyzer / synthesizer), spawns design-planner + design-plan-checker, and writes DESIGN-PLAN.md. Use when DESIGN-CONTEXT.md is locked and you need a wave-ordered execution plan. Activates for requests involving breaking design work into steps, sequencing implementation, or planning a build."
4
4
  argument-hint: "[--auto] [--parallel]"
5
5
  user-invocable: true
6
6
  tools: Read, Write, Bash, Glob, Task, AskUserQuestion, ToolSearch, mcp__gdd_state__get, mcp__gdd_state__transition_stage, mcp__gdd_state__add_decision, mcp__gdd_state__add_must_have, mcp__gdd_state__update_progress, mcp__gdd_state__set_status, mcp__gdd_state__add_blocker, mcp__gdd_state__checkpoint, mcp__gdd_state__probe_connections
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gdd-progress
3
- description: "Shows current pipeline position and routes to next action. --forensic runs 6-check integrity audit."
3
+ description: "Shows current pipeline position and routes to next action. --forensic runs 6-check integrity audit. Activates for requests involving showing current project state, routing to the next action, or a status check."
4
4
  argument-hint: "[--forensic]"
5
5
  tools: Read, Bash, Grep, Glob, mcp__gdd_state__get, mcp__gdd_status, mcp__gdd_phase_current
6
6
  ---
@@ -78,6 +78,14 @@ Seeds ready: 0
78
78
  ━━━━━━━━━━━━━━━━━━━━━━
79
79
  ```
80
80
 
81
+ ## Step 3.5 - Composition-graph readiness
82
+
83
+ After the pipeline state (and forensic audit if run), surface a one-line composition-graph hint from `scripts/lib/manifest/skills.json`. Count skill records declaring `composes_with` or `next_skills`, then probe for structural problems (cycles in the directed graph, or edges pointing at a skill name that has no record). Print one line:
84
+
85
+ - `Composition graph: <edges> edges, <skills-with-edges> skills wired | cycles: <n> | dangling: <n>`
86
+
87
+ Run `scripts/validate-composition-graph.cjs` for the authoritative cycle and dangling-edge counts when that validator is present; until then report `0` and note the graph is not yet wired. This is a readiness hint, not a gate.
88
+
81
89
  ## Step 4 - Update notice (safe-window surface)
82
90
 
83
91
  After printing the pipeline state, emit the plugin-update banner if one is present. This file is written by `hooks/update-check.sh` subject to the state-machine guard (mid-pipeline stages suppress it) and per-version dismissal.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gdd-quick
3
- description: "Run the pipeline with optional agents skipped for speed. Skips: phase-researcher, design-assumptions-analyzer, design-integration-checker. Keeps: planner, executor, verifier, auditor."
3
+ description: "Run the pipeline with optional agents skipped for speed. Skips: phase-researcher, design-assumptions-analyzer, design-integration-checker. Keeps: planner, executor, verifier, auditor. Activates for requests involving a lightweight design pass, a fast iteration, or a quick low-ceremony change."
4
4
  argument-hint: "[--skip <agent-name>] [stage]"
5
5
  tools: Read, Task
6
6
  disable-model-invocation: true
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scan
3
- description: "Pre-pipeline initializer that maps an existing repo's design system (colors, typography, spacing, components, tokens), runs the anti-pattern audit, scores the 7 weighted categories, and writes DESIGN.md + .design/DESIGN-DEBT.md. Use when starting work in any new or existing repo before /gdd:discover."
3
+ description: "Pre-pipeline initializer that maps an existing repo's design system (colors, typography, spacing, components, tokens), runs the anti-pattern audit, scores the 7 weighted categories, and writes DESIGN.md + .design/DESIGN-DEBT.md. Use when starting work in any new or existing repo before /gdd:discover. Activates for requests involving a fast read-only anti-pattern sweep, a quick design lint, or spotting slop without a full audit."
4
4
  argument-hint: "[--quick] [--full]"
5
5
  user-invocable: true
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gdd-ship
3
- description: "Post-verify PR flow - creates a clean PR branch, invokes code review check, and prepares the PR for merge."
3
+ description: "Post-verify PR flow - creates a clean PR branch, invokes code review check, and prepares the PR for merge. Activates for requests involving finishing a cycle, packaging design output, or moving work to a pull request."
4
4
  argument-hint: "[--title <PR title>] [--draft]"
5
5
  tools: Read, Write, Bash, AskUserQuestion, Task
6
6
  disable-model-invocation: true
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: verify
3
- description: "Stage 5 of 5 orchestrator that spawns design-auditor, design-verifier, and design-integration-checker in sequence, interprets pass/gap result, and drives the gap-response loop (inline fix, save-and-exit, or accept-as-is). Use when implementation is complete and ready for final pre-ship verification."
3
+ description: "Stage 5 of 5 orchestrator that spawns design-auditor, design-verifier, and design-integration-checker in sequence, interprets pass/gap result, and drives the gap-response loop (inline fix, save-and-exit, or accept-as-is). Use when implementation is complete and ready for final pre-ship verification. Activates for requests involving checking finished UI against the design system, running a pre-ship review, or final verification."
4
4
  argument-hint: "[--auto] [--post-handoff]"
5
5
  user-invocable: true
6
6
  tools: mcp__gdd_state__get, mcp__gdd_state__transition_stage, mcp__gdd_state__add_must_have, mcp__gdd_state__add_blocker, mcp__gdd_state__resolve_blocker, mcp__gdd_state__update_progress, mcp__gdd_state__set_status, mcp__gdd_state__checkpoint, mcp__gdd_state__probe_connections
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * hooks/gdd-design-quality-check.js — advisory PostToolUse hook for the
5
+ * default-AI-aesthetic regex floor (Phase 49, Quick Anti-Slop Floor).
6
+ *
7
+ * The cheapest possible anti-slop pass: on every front-end file write, scan the
8
+ * written content for the visual tells that mark a UI as "an AI generated this"
9
+ * (gradient spam, the purple/violet default palette, glassmorphism stacks, the
10
+ * Inter default, centered-everything heroes, undraw/isometric clip art, filler
11
+ * CTA copy, decorative motion with no loading intent). Each match is a non
12
+ * blocking WARN. The catalog the rules come from lives at
13
+ * reference/visual-tells.md (8 named categories, 1:1 with the 8 rules here).
14
+ *
15
+ * Contract (mirrors hooks/gdd-a11y-gate.js):
16
+ * - Read stdin JSON (the PostToolUse payload: {tool_name, tool_input,
17
+ * tool_response, cwd, ...}).
18
+ * - Only act on Write/Edit/MultiEdit tools targeting a .tsx/.vue/.svelte/.astro
19
+ * file. Everything else is a bare {continue:true}.
20
+ * - Scan the written content against 8 regex rules; collect matches as warnings.
21
+ * - Emit one `design_quality_warn` event through scripts/lib/event-chain.cjs
22
+ * (baseDir injected from cwd; the emit is best-effort and never fatal).
23
+ * - Print a concise advisory to stdout and ALWAYS write {continue:true}, exit 0.
24
+ * This hook is WARN-only. It never blocks a write.
25
+ *
26
+ * Dependency-free: core fs/path plus the in-repo event-chain helper. No npm deps.
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+
32
+ /** Front-end source extensions this hook scans. */
33
+ const FRONTEND_EXT = ['.tsx', '.vue', '.svelte', '.astro'];
34
+
35
+ /**
36
+ * The 8 v1 rules. Each `category` matches a heading in reference/visual-tells.md.
37
+ * `test(content)` returns an array of { line, match } hits (possibly empty).
38
+ * Regexes are tuned for precision (low false-positive) over recall.
39
+ */
40
+
41
+ /** Find the 1-based line number for a character offset in `content`. */
42
+ function lineOf(content, index) {
43
+ let line = 1;
44
+ for (let i = 0; i < index && i < content.length; i++) {
45
+ if (content[i] === '\n') line += 1;
46
+ }
47
+ return line;
48
+ }
49
+
50
+ /** Collect up to `cap` global-regex matches as {line, match}. */
51
+ function collect(content, re, cap = 5) {
52
+ const hits = [];
53
+ let m;
54
+ re.lastIndex = 0;
55
+ while ((m = re.exec(content)) !== null) {
56
+ hits.push({ line: lineOf(content, m.index), match: m[0] });
57
+ if (m.index === re.lastIndex) re.lastIndex += 1; // zero-width guard
58
+ if (hits.length >= cap) break;
59
+ }
60
+ return hits;
61
+ }
62
+
63
+ /** Count global-regex matches without allocating the match list. */
64
+ function countMatches(content, re) {
65
+ let n = 0;
66
+ let m;
67
+ re.lastIndex = 0;
68
+ while ((m = re.exec(content)) !== null) {
69
+ n += 1;
70
+ if (m.index === re.lastIndex) re.lastIndex += 1;
71
+ }
72
+ return n;
73
+ }
74
+
75
+ const RULES = [
76
+ {
77
+ rule: 'gradient-spam',
78
+ category: 'gradient-spam',
79
+ // >=3 Tailwind gradient-direction utilities in one file.
80
+ run(content) {
81
+ const re = /\bbg-gradient-to-(?:r|br|tr|b|bl|l|tl|t)\b/g;
82
+ const count = countMatches(content, re);
83
+ if (count < 3) return [];
84
+ const hits = collect(content, re, 5);
85
+ // Tag the first hit with the aggregate count for the advisory.
86
+ if (hits.length) hits[0].match = `${hits[0].match} (x${count})`;
87
+ return hits;
88
+ },
89
+ },
90
+ {
91
+ rule: 'generic-cta',
92
+ category: 'default-AI-hero',
93
+ // Filler hero / CTA copy. Word-boundaried, case-insensitive.
94
+ run(content) {
95
+ const re = /\b(?:Get Started|Welcome to|Lorem ipsum|Learn More)\b/gi;
96
+ return collect(content, re, 5);
97
+ },
98
+ },
99
+ {
100
+ rule: 'centered-everything-syndrome',
101
+ category: 'centered-everything-syndrome',
102
+ // mx-auto AND text-center co-occurring inside one className string.
103
+ run(content) {
104
+ // Match a quoted class string that contains both utilities, in either order.
105
+ const re =
106
+ /(["'`])(?=[^"'`]*\bmx-auto\b)(?=[^"'`]*\btext-center\b)[^"'`]*\1/g;
107
+ return collect(content, re, 5);
108
+ },
109
+ },
110
+ {
111
+ rule: 'inter-everything',
112
+ category: 'inter-everything',
113
+ // font-inter utility OR a font-family: Inter declaration, when no other
114
+ // custom font token (font-<name>, --font-*, or a second font-family) is near.
115
+ run(content) {
116
+ const interRe = /\bfont-inter\b|font-family:\s*['"]?Inter\b/gi;
117
+ const interCount = countMatches(content, interRe);
118
+ if (interCount === 0) return [];
119
+ // A sibling custom-font signal suppresses the warning (a deliberate stack).
120
+ const siblingFont =
121
+ /\bfont-(?!inter\b|sans\b|serif\b|mono\b|medium\b|semibold\b|bold\b|light\b|normal\b|thin\b|black\b|extrabold\b|extralight\b)[a-z]/i.test(
122
+ content,
123
+ ) ||
124
+ /--font-[a-z]/i.test(content) ||
125
+ /font-family:\s*['"]?(?!Inter\b)[A-Za-z]/i.test(content);
126
+ if (siblingFont) return [];
127
+ return collect(content, interRe, 5);
128
+ },
129
+ },
130
+ {
131
+ rule: 'purple-violet-default',
132
+ category: 'purple-violet-default',
133
+ // The default-AI palette bg-(purple|violet)-(500|600|700) with no theme
134
+ // token class (bg-primary / bg-brand / bg-accent / a CSS var) nearby.
135
+ run(content) {
136
+ const re = /\bbg-(?:purple|violet)-(?:500|600|700)\b/g;
137
+ if (countMatches(content, re) === 0) return [];
138
+ const themeToken =
139
+ /\bbg-(?:primary|brand|accent|surface|foreground|background|muted)\b/i.test(
140
+ content,
141
+ ) || /bg-\[(?:var\(--|hsl|oklch|rgb)/i.test(content);
142
+ if (themeToken) return [];
143
+ return collect(content, re, 5);
144
+ },
145
+ },
146
+ {
147
+ rule: 'glassmorphism-spam',
148
+ category: 'glassmorphism-spam',
149
+ // >=3 of backdrop-blur* / bg-white/(10|20|30) in one file.
150
+ run(content) {
151
+ const re = /\bbackdrop-blur(?:-\w+)?\b|\bbg-white\/(?:10|20|30)\b/g;
152
+ const count = countMatches(content, re);
153
+ if (count < 3) return [];
154
+ const hits = collect(content, re, 5);
155
+ if (hits.length) hits[0].match = `${hits[0].match} (x${count})`;
156
+ return hits;
157
+ },
158
+ },
159
+ {
160
+ rule: 'isometric-illustration-fallback',
161
+ category: 'isometric-illustration-fallback',
162
+ // undraw / isometric markers in an asset path or src attribute.
163
+ run(content) {
164
+ const re = /\b(?:undraw|isometric)[\w./-]*/gi;
165
+ return collect(content, re, 5);
166
+ },
167
+ },
168
+ {
169
+ rule: 'decorative-motion-without-intent',
170
+ category: 'decorative-motion-without-intent',
171
+ // animate-(pulse|bounce|spin) on a non-loading, non-icon element.
172
+ // Conservative: only flag a className that has the animate utility but no
173
+ // loading/skeleton/spinner/icon signal on the same class string.
174
+ run(content) {
175
+ const re =
176
+ /(["'`])(?=[^"'`]*\banimate-(?:pulse|bounce|spin)\b)(?![^"'`]*(?:\b(?:animate-(?:pulse|bounce|spin)\s+)?(?:loading|loader|spinner|skeleton|icon|i-)\b|sr-only))[^"'`]*\1/g;
177
+ return collect(content, re, 5);
178
+ },
179
+ },
180
+ ];
181
+
182
+ /**
183
+ * Resolve the written content from a PostToolUse payload, tolerating Write
184
+ * (tool_input.content), Edit (new_string), and MultiEdit (edits[].new_string),
185
+ * and falling back to a tool_response filePath/content when present.
186
+ *
187
+ * Returns { filename, content } or null when there is nothing front-end to scan.
188
+ */
189
+ function extractWrite(payload) {
190
+ if (!payload || typeof payload !== 'object') return null;
191
+ const tool = payload.tool_name || payload.toolName;
192
+ if (tool !== 'Write' && tool !== 'Edit' && tool !== 'MultiEdit') return null;
193
+
194
+ const input = payload.tool_input || payload.toolInput || {};
195
+ const filename =
196
+ input.file_path ||
197
+ input.filePath ||
198
+ input.path ||
199
+ (payload.tool_response &&
200
+ (payload.tool_response.filePath || payload.tool_response.file_path)) ||
201
+ '';
202
+ if (!filename) return null;
203
+
204
+ const ext = path.extname(String(filename)).toLowerCase();
205
+ if (!FRONTEND_EXT.includes(ext)) return null;
206
+
207
+ const parts = [];
208
+ if (typeof input.content === 'string') parts.push(input.content);
209
+ if (typeof input.new_string === 'string') parts.push(input.new_string);
210
+ if (Array.isArray(input.edits)) {
211
+ for (const e of input.edits) {
212
+ if (e && typeof e.new_string === 'string') parts.push(e.new_string);
213
+ }
214
+ }
215
+ // Fall back to a post-write file content echo if the input carried none.
216
+ if (parts.length === 0 && payload.tool_response) {
217
+ const tr = payload.tool_response;
218
+ if (typeof tr.content === 'string') parts.push(tr.content);
219
+ }
220
+
221
+ const content = parts.join('\n');
222
+ if (!content) return null;
223
+ return { filename: String(filename), content };
224
+ }
225
+
226
+ /**
227
+ * Pure evaluator: scan `content` (with `filename` for category context) against
228
+ * the 8 rules. Exported for unit testing without a process.
229
+ *
230
+ * @returns {{ warnings: Array<{rule, category, line, match}>, count: number }}
231
+ */
232
+ function evaluate(content, filename) {
233
+ const warnings = [];
234
+ if (typeof content !== 'string' || content.length === 0) {
235
+ return { warnings, count: 0 };
236
+ }
237
+ for (const r of RULES) {
238
+ let hits = [];
239
+ try {
240
+ hits = r.run(content) || [];
241
+ } catch {
242
+ hits = []; // a misbehaving rule must never break the advisory
243
+ }
244
+ for (const h of hits) {
245
+ warnings.push({
246
+ rule: r.rule,
247
+ category: r.category,
248
+ line: h.line,
249
+ match: h.match,
250
+ });
251
+ }
252
+ }
253
+ return { warnings, count: warnings.length };
254
+ }
255
+
256
+ /** Best-effort event emit through the in-repo event-chain helper. Never throws. */
257
+ function emitEvent(cwd, filename, result) {
258
+ try {
259
+ const { appendChainEvent } = require('../scripts/lib/event-chain.cjs');
260
+ appendChainEvent({
261
+ agent: 'gdd-design-quality-check',
262
+ outcome: 'warn',
263
+ event: 'design_quality_warn',
264
+ file: filename,
265
+ warning_count: result.count,
266
+ categories: [...new Set(result.warnings.map((w) => w.category))],
267
+ warnings: result.warnings.slice(0, 20),
268
+ baseDir: cwd,
269
+ });
270
+ } catch {
271
+ /* observability is best-effort — swallow */
272
+ }
273
+ }
274
+
275
+ /** Build the concise stdout advisory string for a non-empty result. */
276
+ function advisoryNote(filename, result) {
277
+ const cats = [...new Set(result.warnings.map((w) => w.category))];
278
+ const base = path.basename(filename);
279
+ const lines = [
280
+ `gdd-design-quality-check: ${result.count} visual-tell ` +
281
+ `warning${result.count === 1 ? '' : 's'} in ${base} ` +
282
+ `across ${cats.length} categor${cats.length === 1 ? 'y' : 'ies'} ` +
283
+ `(${cats.join(', ')}).`,
284
+ ];
285
+ for (const w of result.warnings.slice(0, 8)) {
286
+ lines.push(` - [${w.rule}] line ${w.line}: ${w.match}`);
287
+ }
288
+ lines.push(' See reference/visual-tells.md for remediation patterns. (advisory, non-blocking)');
289
+ return lines.join('\n');
290
+ }
291
+
292
+ /**
293
+ * Core hook entry. Accepts a parsed payload, returns the decision object to
294
+ * write to stdout. Always returns { continue: true } (advisory only).
295
+ * Exported for unit testing without spawning a process.
296
+ */
297
+ function main(payload, opts = {}) {
298
+ const cwd = (payload && payload.cwd) || opts.cwd || process.cwd();
299
+ const write = extractWrite(payload);
300
+ if (!write) return { continue: true };
301
+
302
+ const result = evaluate(write.content, write.filename);
303
+ if (result.count === 0) return { continue: true };
304
+
305
+ emitEvent(cwd, write.filename, result);
306
+ const note = advisoryNote(write.filename, result);
307
+ return { continue: true, systemMessage: note };
308
+ }
309
+
310
+ async function run(stdin = process.stdin, stdout = process.stdout) {
311
+ let buf = '';
312
+ for await (const chunk of stdin) buf += chunk;
313
+ let payload;
314
+ try {
315
+ payload = JSON.parse(buf || '{}');
316
+ } catch {
317
+ stdout.write(JSON.stringify({ continue: true }));
318
+ return;
319
+ }
320
+ const decision = main(payload);
321
+ if (decision.systemMessage) {
322
+ // Surface the advisory on stderr too so it is visible in plain hook logs.
323
+ try {
324
+ process.stderr.write(decision.systemMessage + '\n');
325
+ } catch {
326
+ /* swallow */
327
+ }
328
+ }
329
+ stdout.write(JSON.stringify(decision));
330
+ }
331
+
332
+ // Run as a CLI only when invoked directly; tests require() this module and call
333
+ // evaluate()/main() against mock payloads without triggering stdin reads.
334
+ if (require.main === module) {
335
+ run().catch(() => {
336
+ process.stdout.write(JSON.stringify({ continue: true }));
337
+ });
338
+ }
339
+
340
+ module.exports = { main, evaluate, extractWrite, RULES, FRONTEND_EXT };
package/hooks/hooks.json CHANGED
@@ -124,6 +124,15 @@
124
124
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-a11y-gate.js\""
125
125
  }
126
126
  ]
127
+ },
128
+ {
129
+ "matcher": "Write|Edit|MultiEdit",
130
+ "hooks": [
131
+ {
132
+ "type": "command",
133
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-design-quality-check.js\""
134
+ }
135
+ ]
127
136
  }
128
137
  ],
129
138
  "Stop": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -73,6 +73,10 @@
73
73
  "verify:harness": "node scripts/verify-harness.cjs",
74
74
  "check:domain-links": "node scripts/check-domain-cross-links.cjs",
75
75
  "check:no-duplication": "node scripts/check-no-duplication.cjs",
76
+ "validate:composition-graph": "node scripts/validate-composition-graph.cjs",
77
+ "validate:skill-frontmatter": "node scripts/validate-skill-frontmatter.cjs",
78
+ "build:skill-graph": "node scripts/generate-skill-graph.cjs",
79
+ "build:skill-graph:check": "node scripts/generate-skill-graph.cjs --check",
76
80
  "sync:rule-catalogue": "node scripts/sync-rule-catalogue.cjs --check",
77
81
  "validate:manifest": "node scripts/validate-manifest.cjs --check",
78
82
  "validate:schemas": "node --experimental-strip-types scripts/validate-schemas.ts",
@@ -115,7 +119,13 @@
115
119
  "agent-sdk",
116
120
  "figma",
117
121
  "extractor",
118
- "design-system-sync"
122
+ "design-system-sync",
123
+ "worktree-safe",
124
+ "anti-slop",
125
+ "confidence-gate",
126
+ "anti-slop-rubric",
127
+ "skill-composition",
128
+ "skill-graph"
119
129
  ],
120
130
  "skills": [
121
131
  "SKILL.md"