@hegemonart/get-design-done 1.48.0 → 1.49.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.
@@ -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.49.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",
@@ -115,7 +115,10 @@
115
115
  "agent-sdk",
116
116
  "figma",
117
117
  "extractor",
118
- "design-system-sync"
118
+ "design-system-sync",
119
+ "worktree-safe",
120
+ "anti-slop",
121
+ "confidence-gate"
119
122
  ],
120
123
  "skills": [
121
124
  "SKILL.md"
@@ -1121,6 +1121,20 @@
1121
1121
  "type": "output-contract",
1122
1122
  "phase": 48,
1123
1123
  "description": "Phase 48 brief-quality rubric: 5 anti-patterns (vague verbs, missing audience, immeasurable success criteria, scope creep, missing anti-goals) the brief-auditor surfaces."
1124
+ },
1125
+ {
1126
+ "name": "visual-tells",
1127
+ "path": "reference/visual-tells.md",
1128
+ "type": "heuristic",
1129
+ "phase": 49,
1130
+ "description": "Phase 49 visual-tells catalog: 8 default-AI-aesthetic categories (default-AI-hero, gradient-spam, isometric-illustration-fallback, centered-everything-syndrome, inter-everything, purple-violet-default, glassmorphism-spam, decorative-motion-without-intent) with diagnostic regex + remediation; backs the gdd-design-quality-check hook."
1131
+ },
1132
+ {
1133
+ "name": "reviewer-confidence-gate",
1134
+ "path": "reference/reviewer-confidence-gate.md",
1135
+ "type": "meta-rules",
1136
+ "phase": 49,
1137
+ "description": "Phase 49 reviewer confidence gate: 4-question Pre-Report Gate + confidence 0.0-1.0 field; HIGH/CRITICAL require >=0.8 + cited proof, <0.5 stays Tentative and never reaches design-fixer."
1124
1138
  }
1125
1139
  ]
1126
1140
  }
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: reviewer-confidence-gate
3
+ type: meta-rules
4
+ version: 1.0.0
5
+ phase: 49
6
+ tags: [review, confidence, audit, verify, gap, routing, anti-slop]
7
+ last_updated: 2026-06-03
8
+ ---
9
+
10
+ # Reviewer Confidence Gate
11
+
12
+ Audit and verify findings can inflate severity without proof. A grep hit gets reported as a BLOCKER; a single line read out of context becomes a MAJOR. This contract adds a confidence discipline so review agents (`design-auditor`, `design-verifier`, `design-debt-crawler`) earn the severity they assign, and so `design-fixer` only auto-applies fixes that are backed by evidence.
13
+
14
+ Every emitting agent runs the Pre-Report Gate before writing a finding, stamps each finding with a `confidence` score, and parks weak findings in a `## Tentative` section that the fixer never reads. The routing helper `scripts/lib/confidence-route.cjs` encodes the same rule in code.
15
+
16
+ ## Pre-Report Gate
17
+
18
+ Before you emit any finding or gap, answer these four questions. If you cannot answer all four with a clear yes, the finding is not ready to ship at its stated severity.
19
+
20
+ - **a. Can I cite `file:line`?** Point at the exact location. A finding with no concrete location is a hunch, not a defect.
21
+ - **b. Can I state the failure mode in one sentence?** Name what breaks for the user or the build. If the sentence needs an "and" plus a "maybe", the finding is two findings or none.
22
+ - **c. Did I read context beyond the modified file?** Confirm the call site, the token definition, or the parent component. A value that looks wrong in isolation is often correct once you read what feeds it.
23
+ - **d. Is the severity defensible?** A BLOCKER blocks shipping. A MAJOR is a real deviation from intent. If you would not defend the label to the author, lower it.
24
+
25
+ ## The `confidence` field
26
+
27
+ Every finding carries a `confidence: 0.0-1.0` field. It records how sure you are that the finding is real and correctly classified, not how bad the issue is. Severity and confidence are independent axes: a cosmetic issue can be high confidence, and a suspected BLOCKER can be low confidence.
28
+
29
+ | Range | Meaning | Where it goes |
30
+ |-------|---------|---------------|
31
+ | `>= 0.8` | Cited `file:line`, one-sentence failure mode, context read. | Reported at full severity; eligible for auto-fix. |
32
+ | `0.5 - 0.8` | Real signal, but evidence is partial or context is incomplete. | Reported, routed to user review, never auto-fixed. |
33
+ | `< 0.5` | A hunch, a guess, or a pattern match you could not confirm. | Moved to `## Tentative`; never reaches `design-fixer`. |
34
+
35
+ ## Routing rule
36
+
37
+ The gate controls what reaches the fixer. The rule is:
38
+
39
+ - A HIGH severity finding (BLOCKER or MAJOR) requires `confidence >= 0.8` **and** a `file:line` citation **and** a one-sentence failure mode. Below `0.8`, a HIGH finding is surfaced for user review instead of auto-fix.
40
+ - A finding with `confidence < 0.5` stays in the `## Tentative` section and never reaches `design-fixer`.
41
+ - A finding with `confidence` in the `0.5 - 0.8` band is surfaced in the report but routed to user review, not auto-fix.
42
+
43
+ `scripts/lib/confidence-route.cjs` exports `route({ severity, confidence, tentative })` and returns `'fix'`, `'user-review'`, or `'drop'`. Agents and the fixer share this single decision so the matrix stays consistent.
44
+
45
+ ### Routing matrix
46
+
47
+ The full decision table the helper encodes:
48
+
49
+ | Severity | `tentative` | confidence | Destination |
50
+ |----------|-------------|------------|-------------|
51
+ | any | `true` | any | `drop` (never reaches fixer) |
52
+ | any | `false` | `< 0.5` | `drop` (stays tentative) |
53
+ | BLOCKER or MAJOR | `false` | `0.5 - 0.8` | `user-review` |
54
+ | BLOCKER or MAJOR | `false` | `>= 0.8` | `fix` |
55
+ | MINOR or COSMETIC | `false` | `0.5 - 0.8` | `user-review` |
56
+ | MINOR or COSMETIC | `false` | `>= 0.8` | `fix` |
57
+
58
+ Read the table as: tentative wins first, then the `0.5` floor, then the severity-specific `0.8` auto-fix gate.
59
+
60
+ ## How to emit a finding
61
+
62
+ After the Pre-Report Gate passes, write the finding with the `confidence` field on its own line inside the existing locked format. For `design-verifier` gaps this sits alongside the other gap fields:
63
+
64
+ ```text
65
+ ### BLOCKER G-01: raw error object rendered on payment failure
66
+ - Phase: 2
67
+ - Description: Checkout.tsx renders the error object directly
68
+ - Expected: a human-readable failure message
69
+ - Actual: users see "[object Object]"
70
+ - Location: src/Checkout.tsx:88
71
+ - Suggested fix: render error.message with a fallback string
72
+ - confidence: 0.85
73
+ ```
74
+
75
+ A finding that scores `< 0.5` is not written in the gap list at all. It goes under a `## Tentative` heading in the same report, in plain prose, so a human can promote it later if context proves it real.
76
+
77
+ ## Paired examples
78
+
79
+ Each pair shows a raw finding (before the gate) and the same finding after the gate corrects it.
80
+
81
+ ### Example 1: severity inflated, no context read
82
+
83
+ **Before:** `BLOCKER: hardcoded color #1a73e8 in Button.tsx breaks theming.`
84
+
85
+ **After:** `MINOR G-04: raw #1a73e8 instead of a semantic token. confidence: 0.9`. Reading context (question c) showed `Button.tsx:42` is the token definition file, so theming is not broken; the issue is a style-coherence nit, not a shipping blocker. High confidence, low severity.
86
+
87
+ ### Example 2: a grep guess that could not be confirmed
88
+
89
+ **Before:** `MAJOR: missing reduced-motion guard, animations will trigger vestibular issues.`
90
+
91
+ **After:** moved to `## Tentative` with `confidence: 0.4`. The grep matched `framer-motion` but question a failed: no single `file:line` proves the guard is absent app-wide, and a root `MotionConfig` may cover it. Parked as tentative; the fixer never sees it.
92
+
93
+ ### Example 3: real defect, evidence complete
94
+
95
+ **Before:** `error states look weak somewhere in the checkout flow.`
96
+
97
+ **After:** `BLOCKER G-01: Checkout.tsx:88 renders the raw error object, so users see "[object Object]" on a failed payment. confidence: 0.85`. All four questions pass: cited location, one-sentence failure mode, call site read, severity defensible. Auto-fix eligible.
98
+
99
+ ### Example 4: partial evidence, honest mid-band score
100
+
101
+ **Before:** `MAJOR: empty state copy is generic across the app.`
102
+
103
+ **After:** `MINOR G-06: Inbox.tsx:30 empty state reads "No data". confidence: 0.65`. One real instance is cited, but question c is only half done: the "across the app" claim was not verified. Scored mid-band, surfaced for user review rather than auto-fixed, and the severity was lowered to match the single confirmed instance.
104
+
105
+ ## Agent integration
106
+
107
+ - `design-auditor`, `design-verifier`, and `design-debt-crawler` run the Pre-Report Gate, stamp each finding with `confidence`, and route sub-0.5 findings to `## Tentative`.
108
+ - `design-fixer` skips every gap in `## Tentative` and skips BLOCKER or MAJOR gaps whose `confidence < 0.8`, routing those to user review instead of auto-fix.