roast-my-design-system 9.2.6 → 9.3.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.
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.
|
|
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>... --notes <file.md></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>... --section "Title" <file.md></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 Claude 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
|
|
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.
|
|
3
|
+
"version": "9.3.0",
|
|
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": [
|
|
@@ -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.
|
|
4
|
+
export const VERSION = '9.3.0';
|
|
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
|
+
}
|