roast-my-design-system 9.2.6 → 9.3.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/README.md CHANGED
@@ -17,7 +17,7 @@ npx roast-my-design-system@latest
17
17
  Run it at the root of a UI repo. One second later, a report opens.
18
18
  No account. No network. No telemetry. Nothing in your repo changes.
19
19
 
20
- **Current release: 9.2.** Version history in [CHANGELOG.md](CHANGELOG.md).
20
+ **Current release: 9.3.** Version history in [CHANGELOG.md](CHANGELOG.md).
21
21
 
22
22
  ## The idea
23
23
 
@@ -206,6 +206,7 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
206
206
  | <code>...&nbsp;--notes&nbsp;&lt;file.md&gt;</code> | The agent's read of this scan, embedded in the report as **"What the repo teaches the agent"**: which findings matter, which good numbers are accidents, what to fix first. Labelled as written by AI and kept apart from the measured numbers. The Claude Code skill writes and passes it automatically; the flag is here so any agent can |
207
207
  | <code>...&nbsp;--section&nbsp;"Title"&nbsp;&lt;file.md&gt;</code> | A further agent-written chapter after the notes, same styling, same label, sub-headings allowed. Repeatable |
208
208
  | `... --hook` | The check the Claude Code plugin runs after every edit, for hand-installed setups: reads the hook event on stdin, checks the file that changed, prints only the findings the edit added as JSON. Always exits 0 |
209
+ | `... --stop-hook` | The review the Claude Code plugin runs when the agent finishes its turn, for hand-installed setups: reads the Stop event on stdin and, when the files the session changed carry new problems, answers with a block that sends the agent back once. `--session-start` notes the work that was already uncommitted. `ROAST_STOP_REVIEW=off` turns it off. Always exits 0 |
209
210
  | `... --mcp` | The scan as a local MCP server: 5 tools your agent calls while writing UI, plus the `roast-fix` prompt that serves the top fix from a fresh scan. See [Live answers over MCP](#live-answers-over-mcp) |
210
211
  | <code>/roast-my-design-system</code> (in&nbsp;Claude&nbsp;Code) | The full experience: the roast in chat *and* embedded in the report as "What the repo teaches the agent", the rules offer, and the fix loop with Claude on your own numbers |
211
212
 
@@ -300,7 +301,7 @@ npx roast-my-design-system@latest
300
301
  /plugin install roast-my-design-system@roast-my-design-system
301
302
  ```
302
303
 
303
- What it installs: two skills, one local MCP server and one edit hook, nothing else. **roast** (`/roast-my-design-system`, or "roast my design system") scans the whole repo, writes the report with Claude's read of the numbers inside it, then walks the fixes with you. **review** (`/roast-my-design-system:review`, or "review my UI changes") checks only what changed, in about a second. The hook runs after every file the agent edits and hands back the findings that edit added; a file with nothing new gets no message.
304
+ What it installs: two skills, one local MCP server and two checks that run on their own, nothing else. **roast** (`/roast-my-design-system`, or "roast my design system") scans the whole repo, writes the report with Claude's read of the numbers inside it, then walks the fixes with you. **review** (`/roast-my-design-system:review`, or "review my UI changes") checks only what changed, in about a second. The edit check runs after every file the agent edits and hands back the findings that edit added; a file with nothing new gets no message. The end-of-turn review runs when the agent says it is done: if the files this session changed still carry problems it added, the agent is sent back once to fix them before it finishes. Advice-only warnings never send it back, a problem is sent back once a session, and work that was uncommitted before the session started is left alone. To turn the review off, set `ROAST_STOP_REVIEW=off` in your Claude Code settings (`"env": { "ROAST_STOP_REVIEW": "off" }`).
304
305
 
305
306
  If those commands error, your Claude Code is older than the plugin marketplace. Update it, or use the manual route:
306
307
 
package/cli/roast.mjs CHANGED
@@ -92,6 +92,10 @@ Run it yourself
92
92
  number of files it removed, so a scoped scan says so
93
93
  --json print the scan summary as JSON on stdout (implies --no-open)
94
94
  --check check the working tree's changed files (git diff + untracked)
95
+ --stop-hook Claude Code end-of-turn review: reads the Stop event on stdin
96
+ and sends the agent back once when the files this session
97
+ changed carry new violations. ROAST_STOP_REVIEW=off turns it
98
+ off. --session-start notes the work that was already there
95
99
  --hook Claude Code edit hook: reads the PostToolUse event on stdin,
96
100
  checks the one file that changed, answers with findings
97
101
  against the design system and print the findings; exits 1
@@ -141,6 +145,24 @@ if (argv.includes('--mcp')) {
141
145
  console.error(`roast-my-design-system --hook: ${e.message}`);
142
146
  }
143
147
  process.exit(0);
148
+ } else if (argv.includes('--stop-hook') || argv.includes('--session-start')) {
149
+ // Claude Code's Stop and SessionStart hooks, same rules as --hook: read the
150
+ // event on stdin, exit 0 whatever happens. Stop answers with a block and the
151
+ // findings when the agent should go back and fix what it added.
152
+ let raw = '';
153
+ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
154
+ try {
155
+ const { stopResult, sessionStart } = await import(pathToFileURL(join(SCRIPTS, 'mcp/hook.mjs')).href);
156
+ const event = raw.trim() ? JSON.parse(raw) : {};
157
+ if (argv.includes('--session-start')) sessionStart(event);
158
+ else {
159
+ const r = stopResult(event);
160
+ if (r) console.log(JSON.stringify({ decision: 'block', reason: r.text }));
161
+ }
162
+ } catch (e) {
163
+ console.error(`roast-my-design-system ${argv.includes('--session-start') ? '--session-start' : '--stop-hook'}: ${e.message}`);
164
+ }
165
+ process.exit(0);
144
166
  } else if (argv.includes('--check')) {
145
167
  argv.splice(argv.indexOf('--check'), 1);
146
168
  const root = resolve(argv.find((a) => !a.startsWith('--')) || process.cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "9.2.6",
3
+ "version": "9.3.1",
4
4
  "mcpName": "io.github.gregkozakiewicz/roast-my-design-system",
5
5
  "description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner scores your design system 0-100 against 112 public repos, reads React and web components (Stencil, Lit), hands you a copy-paste fix prompt for each top finding, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
6
6
  "keywords": [
@@ -50,7 +50,12 @@ const DOOR_TYPO_RE = /(?<![\w-])(?:[\w-]+:)*(?:font-(?:thin|extralight|light|nor
50
50
  export const blankComments = (s) => s
51
51
  .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
52
52
  .replace(/(^|[^:'"`\\])(\/\/[^\n]*)/g, (m, pre, c) => pre + ' '.repeat(c.length));
53
- export const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|templates?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
53
+ // Not templates: in a product a templates folder is usually a screen
54
+ // (teable's admin templates, documenso's and formbricks' template pickers),
55
+ // and hiding it hid their colours from the report and the live checks. A
56
+ // project generator's templates are left out by the walk instead, and email
57
+ // templates by the email rule (2026-09-30).
58
+ export const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demos?|playground|fixtures?|__tests__|__mocks__|e2e|cypress)\//i;
54
59
 
55
60
  /**
56
61
  * @param root repo root
@@ -80,12 +80,13 @@ const LEGACY_DIR_RE = /^(?:old|legacy|deprecated|archive|archived)$|[-_](?:old|l
80
80
 
81
81
  // Scaffolding: the code a project generator copies into someone else's new
82
82
  // repo, not the product. A starters/ folder, or a templates folder kept by a
83
- // generator (create-app, a cli), a script or a dev tool: backstage's
84
- // create-app/templates, the dashboard starter's scripts/cleanup-templates,
85
- // BuilderIO's starters (133 duplicate copies). The name alone is not enough:
86
- // backstage's scaffolder form templates, OpenCTI's case templates and
87
- // nocobase's block templates are screens of the product (2026-09-30).
88
- const SCAFFOLD_DIR_RE = /(^|\/)starters$|(^|\/)(?:create-[\w-]+|cli|scripts|dev|generators?)\/(?:[^/]+\/)*[\w-]*templates?$/i;
83
+ // generator (create-app, a cli, a command-line tool's commands), a script or
84
+ // a dev tool: backstage's create-app/templates, the dashboard starter's
85
+ // scripts/cleanup-templates, BuilderIO's starters (133 duplicate copies),
86
+ // novu's commands/init/templates. The name alone is not enough: backstage's
87
+ // scaffolder form templates, OpenCTI's case templates and nocobase's block
88
+ // templates are screens of the product (2026-09-30).
89
+ const SCAFFOLD_DIR_RE = /(^|\/)starters$|(^|\/)(?:create-[\w-]+|cli|commands|scripts|dev|generators?)\/(?:[^/]+\/)*[\w-]*templates?$/i;
89
90
 
90
91
  export function walkRepo(root, maxDepth = 14, exclusions = null, readAnyway = new Set()) {
91
92
  const files = { code: [], styles: [], other: [] };
@@ -1,7 +1,7 @@
1
1
  // Single version constant for the engine — imported by diagnose (report
2
2
  // footer) and rules (generated-by line). This is the bump spot that used to
3
3
  // live as a const inside diagnose/index.mjs.
4
- export const VERSION = '9.2.6';
4
+ export const VERSION = '9.3.1';
5
5
  // The shape of harvest.json and summary.json. Bumped only when a field is
6
6
  // renamed, removed or changes meaning; a new field is not a new schema. A
7
7
  // tool comparing two scans compares like with like by this number, not by VERSION.
@@ -83,6 +83,12 @@ export function hookResult(payload) {
83
83
  // (five repeats in one Dub rehearsal, 2026-09-25). Violations repeat
84
84
  // until they are fixed.
85
85
  const warned = new Set(ledger['\u0000warned'] ?? []);
86
+ // the files this session wrote, for the review at the end of the turn: a
87
+ // file named by an edit, or a file a shell command changed after the
88
+ // session began (uncommitted work already there is the person's, not the
89
+ // agent's)
90
+ const touched = new Set(ledger['\u0000touched'] ?? []);
91
+ const base = ledger['\u0000base'] ?? {};
86
92
  const blocks = [];
87
93
  let total = 0, knowledge = null;
88
94
  for (const abs of candidates) {
@@ -91,6 +97,7 @@ export function hookResult(payload) {
91
97
  const mtime = statSync(real).mtimeMs;
92
98
  if (!fromEditor && ledger[real] === mtime) continue;
93
99
  ledger[real] = mtime;
100
+ if (fromEditor || base[real] !== mtime) touched.add(real);
94
101
  // The repo is the session's working directory when the file lives under
95
102
  // it (the same root the MCP server would be pointed at); a file edited
96
103
  // elsewhere is judged against its own repository.
@@ -102,6 +109,7 @@ export function hookResult(payload) {
102
109
  if (r) { blocks.push(r.text); total += r.total; }
103
110
  }
104
111
  ledger['\u0000warned'] = [...warned];
112
+ ledger['\u0000touched'] = [...touched];
105
113
  writeLedger(session, ledger);
106
114
  if (!blocks.length) return null;
107
115
  blocks.push('Fix these now, in the repo\'s own vocabulary as each fix says (a token, a spacing step, a theme path), not by deleting the code. A value that is deliberate stays, with a one-line comment saying why.');
@@ -109,7 +117,7 @@ export function hookResult(payload) {
109
117
  }
110
118
 
111
119
  /** One file against the system: only what the working copy added since HEAD. */
112
- function judge(real, root, k, warned = new Set()) {
120
+ function judge(real, root, k, warned = new Set(), { violationsOnly = false } = {}) {
113
121
  const rel = relative(realpathSync.native(root), real).split('\\').join('/');
114
122
  const text = readFileSync(real, 'utf8');
115
123
  const before = beforeOf(k, rel);
@@ -124,11 +132,89 @@ function judge(real, root, k, warned = new Set()) {
124
132
  const fresh = typeof before === 'string' ? newSince(all, validateContent({ text: before, file: rel }, k).findings) : all;
125
133
  const findings = fresh.filter((f) => {
126
134
  if (f.severity !== 'warning') return true;
135
+ if (violationsOnly) return false;
127
136
  const key = `${rel}|${f.rule}|${f.message}`;
128
137
  if (warned.has(key)) return false;
129
138
  warned.add(key);
130
139
  return true;
131
140
  });
132
141
  if (!findings.length) return null;
142
+ if (violationsOnly) return { rel, findings, exempt, total: findings.length };
133
143
  return { text: `Design-system check of ${rel} (roast-my-design-system, the same engine as roast_validate): ${findingLines(findings, k, exempt).join('\n')}`, total: findings.length };
134
144
  }
145
+
146
+ /**
147
+ * SessionStart: note the uncommitted UI files that were already there, so the
148
+ * review at the end of a turn never hands the agent someone else's work.
149
+ * Kept for the whole session: a resumed or compacted session keeps its start.
150
+ */
151
+ export function sessionStart(payload) {
152
+ const session = payload?.session_id;
153
+ if (!session) return;
154
+ const ledger = readLedger(session);
155
+ if (ledger['\u0000base']) return;
156
+ const cwd = typeof payload?.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
157
+ const base = {};
158
+ for (const abs of changedFiles(cwd)) {
159
+ try { const real = realpathSync.native(abs); base[real] = statSync(real).mtimeMs; } catch { /* gone */ }
160
+ }
161
+ ledger['\u0000base'] = base;
162
+ writeLedger(session, ledger);
163
+ }
164
+
165
+ // Anything but these leaves the review on: it exists so that nobody has to
166
+ // remember to ask for it.
167
+ const OFF = /^(off|0|false|no)$/i;
168
+
169
+ /**
170
+ * Stop: the review at the end of the agent's turn. The edit hook tells the
171
+ * agent about drift after each edit, and the agent may carry on regardless
172
+ * (Haiku still shipped it in 1 of 5 runs with the edit hook on, September
173
+ * 2026). This judges every UI file the session changed, keeps the violations
174
+ * that are new since HEAD, and when there are any sends the agent back once to
175
+ * fix them. Warnings never send it back. A second stop in a row always goes
176
+ * through: one extra round, never a loop.
177
+ * @param payload the Stop event Claude Code writes to stdin
178
+ * @returns { text, total } when the agent should go back, else null
179
+ */
180
+ export function stopResult(payload, env = process.env) {
181
+ if (OFF.test(String(env.ROAST_STOP_REVIEW ?? ''))) return null;
182
+ if (payload?.stop_hook_active) return null;
183
+ const session = payload?.session_id;
184
+ if (!session) return null;
185
+ const ledger = readLedger(session);
186
+ const touched = ledger['\u0000touched'] ?? [];
187
+ if (!touched.length) return null;
188
+ const cwd = typeof payload?.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
189
+ // A problem is sent back once a session. What the agent kept after that
190
+ // (a deliberate value, commented) is its call, not a loop across turns.
191
+ const sent = new Set(ledger['\u0000sentback'] ?? []);
192
+ const results = [];
193
+ let total = 0, knowledge = null;
194
+ for (const real of touched) {
195
+ if (!existsSync(real)) continue;
196
+ const under = (root) => root && real.startsWith(realpathSync.native(root) + '/');
197
+ const root = under(cwd) ? cwd : gitTop(dirname(real));
198
+ if (!root) continue;
199
+ const k = knowledge?.root === root ? knowledge : (knowledge = loadKnowledge(root));
200
+ const r = judge(real, root, k, new Set(), { violationsOnly: true });
201
+ if (!r) continue;
202
+ const findings = r.findings.filter((f) => !sent.has(`${r.rel}|${f.rule}|${f.message}`));
203
+ if (!findings.length) continue;
204
+ for (const f of findings) sent.add(`${r.rel}|${f.rule}|${f.message}`);
205
+ results.push({ ...r, findings, total: findings.length });
206
+ total += findings.length;
207
+ }
208
+ if (!total) return null;
209
+ ledger['\u0000sentback'] = [...sent];
210
+ writeLedger(session, ledger);
211
+ const L = [`Before you finish: the design-system review (roast-my-design-system) found ${total} problem${total === 1 ? '' : 's'} in what this session added to ${results.length} file${results.length === 1 ? '' : 's'}.`];
212
+ for (const r of results.slice(0, 10)) {
213
+ L.push(`${r.rel}:`);
214
+ for (const f of r.findings.slice(0, 8)) L.push(` ✕ L${f.line} ${f.message}${f.fix ? `\n Fix: ${f.fix}` : ''}`);
215
+ if (r.findings.length > 8) L.push(` (+${r.findings.length - 8} more in this file)`);
216
+ }
217
+ if (results.length > 10) L.push(`(+${results.length - 10} more files)`);
218
+ L.push('Fix these in the repo\'s own vocabulary, as each fix says (a token, a spacing step, a theme path, the existing component), then finish. A value that is deliberate stays, with a one-line comment saying why; say so in your reply.');
219
+ return { text: L.join('\n'), total };
220
+ }