analyzthis_design 2.4.1 → 2.5.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.
Files changed (54) hide show
  1. package/README.md +46 -4
  2. package/agents/cards/anuj.md +15 -2
  3. package/agents/cards/arjun.md +15 -2
  4. package/agents/cards/devi.md +22 -0
  5. package/agents/cards/kavi.md +14 -1
  6. package/agents/cards/meera.md +13 -0
  7. package/agents/cards/noor.md +15 -2
  8. package/agents/cards/priya.md +13 -0
  9. package/agents/cards/raj.md +13 -0
  10. package/agents/cards/zara.md +14 -1
  11. package/dist/README.md +46 -4
  12. package/dist/agents/cards/anuj.md +15 -2
  13. package/dist/agents/cards/arjun.md +15 -2
  14. package/dist/agents/cards/devi.md +22 -0
  15. package/dist/agents/cards/kavi.md +14 -1
  16. package/dist/agents/cards/meera.md +13 -0
  17. package/dist/agents/cards/noor.md +15 -2
  18. package/dist/agents/cards/priya.md +13 -0
  19. package/dist/agents/cards/raj.md +13 -0
  20. package/dist/agents/cards/zara.md +14 -1
  21. package/dist/bin/cli.js +54 -0
  22. package/dist/lib/accept.js +26 -13
  23. package/dist/lib/evolution-metrics.js +303 -87
  24. package/dist/lib/evolve.js +5 -2
  25. package/dist/lib/feedback-submit.js +97 -17
  26. package/dist/lib/host-llm.js +16 -0
  27. package/dist/lib/lessons.js +48 -5
  28. package/dist/lib/mcp-server.js +72 -5
  29. package/dist/skills/accept/SKILL.md +24 -12
  30. package/dist/skills/anuj/SKILL.md +2 -1
  31. package/dist/skills/arjun/SKILL.md +2 -2
  32. package/dist/skills/arjun/references/lens.md +13 -2
  33. package/dist/skills/design-director/SKILL.md +1 -0
  34. package/dist/skills/design-reference/ux-guidelines.csv +12 -1
  35. package/dist/skills/design-spec/SKILL.md +1 -1
  36. package/dist/skills/devi/SKILL.md +22 -0
  37. package/dist/skills/evolve-check/SKILL.md +29 -13
  38. package/dist/skills/kavi/SKILL.md +1 -1
  39. package/dist/skills/noor/SKILL.md +3 -1
  40. package/dist/skills/zara/SKILL.md +1 -0
  41. package/package.json +6 -2
  42. package/scripts/validate-csvs.js +20 -0
  43. package/skills/accept/SKILL.md +24 -12
  44. package/skills/anuj/SKILL.md +2 -1
  45. package/skills/arjun/SKILL.md +2 -2
  46. package/skills/arjun/references/lens.md +13 -2
  47. package/skills/design-director/SKILL.md +1 -0
  48. package/skills/design-reference/ux-guidelines.csv +12 -1
  49. package/skills/design-spec/SKILL.md +1 -1
  50. package/skills/devi/SKILL.md +22 -0
  51. package/skills/evolve-check/SKILL.md +29 -13
  52. package/skills/kavi/SKILL.md +1 -1
  53. package/skills/noor/SKILL.md +3 -1
  54. package/skills/zara/SKILL.md +1 -0
@@ -18,7 +18,9 @@ const CONFIG_DIR = path.join(os.homedir(), '.analyzthis_design');
18
18
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
19
19
  const CONSENT_FILE = path.join(CONFIG_DIR, 'feedback', 'submit-consent.json');
20
20
 
21
+ const DEFAULT_ENDPOINT = 'https://analyzthis-lab.vercel.app/api/feedback';
21
22
  const MAX_SUBMIT_TEXT = 2000;
23
+ const MAX_ROWS_PER_REQUEST = 50;
22
24
  const PATH_PATTERN = /(?:\/Users\/|\/home\/|[A-Za-z]:\\)[^\s"'`,;)]+/g;
23
25
  const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
24
26
  const SECRET_PATTERN = /\b(sk-[a-zA-Z0-9_-]{10,}|api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{8,})/gi;
@@ -43,8 +45,11 @@ function resolveSubmitConfig() {
43
45
  const pkgVersion = safePackageVersion();
44
46
 
45
47
  return {
46
- url: process.env.ANALYZTHIS_FEEDBACK_URL || fb.submit_url || '',
47
- anonKey: process.env.ANALYZTHIS_FEEDBACK_ANON_KEY || fb.anon_key || '',
48
+ // A single HTTPS endpoint — the Vercel function in website/api/feedback.js,
49
+ // which holds the Neon connection string in its own env. The package never
50
+ // carries a database credential, and moving off Neon later means editing
51
+ // that function rather than republishing this package.
52
+ url: process.env.ANALYZTHIS_FEEDBACK_URL || fb.submit_url || DEFAULT_ENDPOINT,
48
53
  enabled: fb.submit_enabled !== false,
49
54
  packageVersion: pkgVersion,
50
55
  installId: getInstallId(config),
@@ -105,6 +110,23 @@ function anonymizeText(text, maxLen = MAX_SUBMIT_TEXT) {
105
110
  return out.trim();
106
111
  }
107
112
 
113
+ /**
114
+ * Look up what actually happened to this persona's advice. The reaction
115
+ * (satisfied/rating) says the designer was unhappy; the outcome says the
116
+ * persona was wrong. Only the second justifies changing the shipped package.
117
+ */
118
+ function outcomeForEntry(entry) {
119
+ try {
120
+ const state = session.show({ project: entry.project_id });
121
+ if (!state || !state.outcome) return null;
122
+ const rec = (state.outcome.confirmed && state.outcome.confirmed[entry.persona])
123
+ || (state.outcome.inferred && state.outcome.inferred[entry.persona]);
124
+ return (rec && rec.value) || null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
108
130
  function entryToSubmitPayload(entry, cfg) {
109
131
  return {
110
132
  install_id: cfg.installId,
@@ -113,6 +135,8 @@ function entryToSubmitPayload(entry, cfg) {
113
135
  persona: entry.persona,
114
136
  satisfied: !!entry.satisfied,
115
137
  rating: entry.rating,
138
+ outcome: outcomeForEntry(entry),
139
+ task_type: entry.context?.problem_type || '',
116
140
  tags: entry.tags || [],
117
141
  user_comment: anonymizeText(entry.comment, 800),
118
142
  assistant_rejected: anonymizeText(entry.original_output),
@@ -193,22 +217,21 @@ async function submitRows(rows, cfg) {
193
217
  if (!cfg.url) {
194
218
  throw new Error(
195
219
  'No feedback submit URL configured.\n'
196
- + ' Maintainer: set feedback.submit_url + feedback.anon_key in ~/.analyzthis_design/config.json\n'
197
- + ' Or env: ANALYZTHIS_FEEDBACK_URL and ANALYZTHIS_FEEDBACK_ANON_KEY\n'
198
- + ' See README → "Community feedback collection"',
220
+ + ' Set feedback.submit_url in ~/.analyzthis_design/config.json\n'
221
+ + ' Or env: ANALYZTHIS_FEEDBACK_URL\n'
222
+ + ' See docs/package-feedback.md',
199
223
  );
200
224
  }
201
- if (!cfg.anonKey) {
202
- throw new Error('Missing anon key. Set feedback.anon_key in config or ANALYZTHIS_FEEDBACK_ANON_KEY.');
203
- }
204
225
 
205
- await postJson(cfg.url, {
206
- apikey: cfg.anonKey,
207
- Authorization: `Bearer ${cfg.anonKey}`,
208
- Prefer: 'return=minimal',
209
- }, rows);
210
-
211
- return rows.length;
226
+ // No credential travels with the package. The endpoint is a Vercel function
227
+ // that holds the database connection string in its own environment.
228
+ let sent = 0;
229
+ for (let i = 0; i < rows.length; i += MAX_ROWS_PER_REQUEST) {
230
+ const batch = rows.slice(i, i + MAX_ROWS_PER_REQUEST);
231
+ await postJson(cfg.url, {}, { kind: 'corrections', rows: batch });
232
+ sent += batch.length;
233
+ }
234
+ return sent;
212
235
  }
213
236
 
214
237
  function askConsentQuestion() {
@@ -216,7 +239,8 @@ function askConsentQuestion() {
216
239
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
217
240
  rl.question(
218
241
  '\nShare anonymized persona feedback with analyzthis_design maintainers?\n'
219
- + ' Sends: persona, rating, tags, comment/correction, redacted output snippets\n'
242
+ + ' Sends: persona, rating, outcome (shipped/revised/missed), task type,\n'
243
+ + ' tags, comment/correction, redacted output snippets\n'
220
244
  + ' Does NOT send: project paths, repo names, emails, or API keys\n'
221
245
  + 'Continue? [y/N] ',
222
246
  (answer) => {
@@ -303,18 +327,74 @@ function submitStatus() {
303
327
  return {
304
328
  consent: consent?.opted_in ? `opted in (${consent.at})` : 'not opted in',
305
329
  endpoint: cfg.url || '(not configured — set feedback.submit_url)',
306
- anonKey: cfg.anonKey ? 'configured' : '(missing — set feedback.anon_key)',
330
+ endpoint: cfg.url || '(unset)',
307
331
  installId: cfg.installId,
308
332
  unsentCount: unsent,
309
333
  packageVersion: cfg.packageVersion,
310
334
  };
311
335
  }
312
336
 
337
+
338
+ /**
339
+ * Aggregate persona scores — counts only, never note text.
340
+ * This is the signal that tells the maintainer which persona rule is failing
341
+ * ACROSS installs, which is the only justification for changing the shipped
342
+ * package. Cheap to send and hard to de-anonymise.
343
+ */
344
+ function buildScorePayloads(cfg) {
345
+ const em = require('./evolution-metrics');
346
+ const metrics = em.computeEvolutionMetrics();
347
+ const rows = [];
348
+ for (const [persona, m] of Object.entries(metrics.personas)) {
349
+ // Nothing learned yet — do not ship noise upstream.
350
+ if (!m.evidence_count) continue;
351
+ rows.push({
352
+ install_id: cfg.installId,
353
+ package_version: cfg.packageVersion,
354
+ persona,
355
+ score: m.score,
356
+ level: m.level,
357
+ evidence_count: m.evidence_count,
358
+ shipped: m.outcome_breakdown.shipped || 0,
359
+ revised: m.outcome_breakdown.revised || 0,
360
+ blocked_correctly: m.outcome_breakdown.blocked_correctly || 0,
361
+ missed: m.outcome_breakdown.missed || 0,
362
+ avg_rating: m.avg_rating,
363
+ });
364
+ }
365
+ return rows;
366
+ }
367
+
368
+ /**
369
+ * Send the aggregate scoreboard. Requires the same consent as corrections.
370
+ */
371
+ async function submitScores({ dryRun = false } = {}) {
372
+ const cfg = resolveSubmitConfig();
373
+ if (!cfg.enabled) return { sent: 0, skipped: 'submission disabled' };
374
+
375
+ const rows = buildScorePayloads(cfg);
376
+ if (!rows.length) return { sent: 0, skipped: 'no scored personas yet' };
377
+ if (dryRun) return { sent: 0, dryRun: true, rows };
378
+
379
+ if (!loadConsent()) return { sent: 0, skipped: 'no consent on file' };
380
+ if (!cfg.url) return { sent: 0, skipped: 'no endpoint configured' };
381
+
382
+ let sent = 0;
383
+ for (let i = 0; i < rows.length; i += MAX_ROWS_PER_REQUEST) {
384
+ const batch = rows.slice(i, i + MAX_ROWS_PER_REQUEST);
385
+ await postJson(cfg.url, {}, { kind: 'scores', rows: batch });
386
+ sent += batch.length;
387
+ }
388
+ return { sent, rows };
389
+ }
390
+
313
391
  module.exports = {
314
392
  anonymizeText,
315
393
  entryToSubmitPayload,
316
394
  resolveSubmitConfig,
317
395
  submitFeedback,
396
+ submitScores,
397
+ buildScorePayloads,
318
398
  submitStatus,
319
399
  collectUnsentEntries,
320
400
  markEntriesSubmitted,
@@ -76,9 +76,25 @@ function responsePath(runDir, stepId) {
76
76
  return path.join(runDir, 'responses', `${stepId}.md`);
77
77
  }
78
78
 
79
+ /**
80
+ * Devi voices every persona, so it is the one place a team-wide trust signal
81
+ * can land. Advisory only — Devi weights its synthesis, never drops a persona.
82
+ * Best-effort: a scoreboard must never block a run.
83
+ */
84
+ function scoreboardPreamble() {
85
+ try {
86
+ const em = require('./evolution-metrics');
87
+ const board = em.formatScoreboardForDevi(em.computeEvolutionMetrics());
88
+ return board ? board + '\n\n' : '';
89
+ } catch (e) {
90
+ return '';
91
+ }
92
+ }
93
+
79
94
  function writePending(runDir, manifest, { personaId, system, user, meta = {} }) {
80
95
  manifest.pending_persona = personaId;
81
96
  const stepId = `${String(manifest.step_counter + 1).padStart(3, '0')}-${personaId}`;
97
+ system = scoreboardPreamble() + (system || '');
82
98
  const payload = {
83
99
  step_id: stepId,
84
100
  persona_id: personaId,
@@ -32,10 +32,26 @@ function loadLessons(personaId) {
32
32
  return lessons;
33
33
  }
34
34
 
35
+ function lessonKey(lesson) {
36
+ return [lesson.persona, lesson.session_id, lesson.polarity || '', lesson.pattern].join('|');
37
+ }
38
+
39
+ /**
40
+ * Append unless an identical lesson already exists. Lessons now feed scoring
41
+ * (+10 each), and extractLessons runs on every keep/skip, so re-marking the
42
+ * same note must not inflate a persona's score.
43
+ * @returns {boolean} true if written
44
+ */
35
45
  function appendLesson(personaId, lesson) {
36
46
  ensureLessonsDir();
37
47
  var file = personaLessonFile(personaId);
48
+ var key = lessonKey(lesson);
49
+ var existing = loadLessons(personaId);
50
+ for (var i = 0; i < existing.length; i++) {
51
+ if (lessonKey(existing[i]) === key) return false;
52
+ }
38
53
  fs.appendFileSync(file, JSON.stringify(lesson) + '\n');
54
+ return true;
39
55
  }
40
56
 
41
57
  function keywordOverlap(a, b) {
@@ -52,7 +68,10 @@ function keywordOverlap(a, b) {
52
68
 
53
69
  function extractLessonsFromSession(state, personaId) {
54
70
  var entry = state.persona_outputs && state.persona_outputs[personaId];
55
- if (!entry || entry.accepted !== true) return [];
71
+ // Learn from rejections too a correction is the most informative signal
72
+ // we get. accepted === null means the designer never reacted; skip those.
73
+ if (!entry || entry.accepted === null || entry.accepted === undefined) return [];
74
+ var polarity = entry.accepted === true ? 'positive' : 'negative';
56
75
 
57
76
  var outcomeVal = 'unknown';
58
77
  if (state.outcome && state.outcome.confirmed && state.outcome.confirmed[personaId]) {
@@ -80,6 +99,7 @@ function extractLessonsFromSession(state, personaId) {
80
99
  pattern: pattern,
81
100
  fix: fix,
82
101
  outcome: outcomeVal,
102
+ polarity: polarity,
83
103
  session_id: state.project_id,
84
104
  extracted_at: new Date().toISOString(),
85
105
  citation: entry.citations ? entry.citations[0] : ''
@@ -88,6 +108,30 @@ function extractLessonsFromSession(state, personaId) {
88
108
  }
89
109
  }
90
110
 
111
+ // A rejection carries the designer's own correction — the highest-value
112
+ // lesson available. Pull it from the most recent negative feedback entry.
113
+ if (polarity === 'negative') {
114
+ var log = state.feedback_log || [];
115
+ for (var f = log.length - 1; f >= 0; f--) {
116
+ if (log[f].persona !== personaId || log[f].satisfied !== false) continue;
117
+ var corrective = String(log[f].correction || log[f].comment || '').trim();
118
+ if (!corrective) break;
119
+ lessons.push({
120
+ id: require('crypto').randomBytes(6).toString('hex'),
121
+ persona: personaId,
122
+ task_type: state.task_type || '',
123
+ pattern: 'Designer rejected this note: ' + corrective,
124
+ fix: corrective,
125
+ outcome: outcomeVal,
126
+ polarity: 'negative',
127
+ session_id: state.project_id,
128
+ extracted_at: new Date().toISOString(),
129
+ citation: ''
130
+ });
131
+ break;
132
+ }
133
+ }
134
+
91
135
  var hierarchyMatch = text.match(/Hierarchy\[([A-F])\]/gi);
92
136
  if (hierarchyMatch) {
93
137
  for (var j = 0; j < hierarchyMatch.length; j++) {
@@ -99,6 +143,7 @@ function extractLessonsFromSession(state, personaId) {
99
143
  pattern: pattern2,
100
144
  fix: 'Review visual hierarchy per DS tokens',
101
145
  outcome: outcomeVal,
146
+ polarity: polarity,
102
147
  session_id: state.project_id,
103
148
  extracted_at: new Date().toISOString(),
104
149
  citation: ''
@@ -123,8 +168,7 @@ function extractLessons(opts) {
123
168
 
124
169
  var lessons = extractLessonsFromSession(state, persona);
125
170
  for (var j = 0; j < lessons.length; j++) {
126
- appendLesson(persona, lessons[j]);
127
- total++;
171
+ if (appendLesson(persona, lessons[j])) total++;
128
172
  }
129
173
  }
130
174
 
@@ -148,8 +192,7 @@ function extractAllLessons(opts) {
148
192
  var pers = personas[p];
149
193
  var lessons = extractLessonsFromSession(state, pers);
150
194
  for (var j = 0; j < lessons.length; j++) {
151
- appendLesson(pers, lessons[j]);
152
- total++;
195
+ if (appendLesson(pers, lessons[j])) total++;
153
196
  }
154
197
  }
155
198
  }
@@ -290,6 +290,29 @@ var TOOLS = [
290
290
  },
291
291
  ];
292
292
 
293
+ // The one line every persona closes with. Deliberately host-neutral: the
294
+ // designer answers in plain language and the agent picks the transport
295
+ // (Bash on Claude Code/Cursor, analyzthis_accept on Desktop).
296
+ var ASK_LINE = '\u2014\nWas this right? Say yes, or no plus one sentence. I\'ll record it.';
297
+
298
+ var ACCEPT_TOOL = {
299
+ name: 'analyzthis_accept',
300
+ description: 'Record the designer\'s reaction to a persona note. Call this whenever they approve, '
301
+ + 'push back on, or correct a persona (\"that\'s right\", \"no, the hierarchy is backwards\"). '
302
+ + 'This is how personas earn or lose trust — without it they never improve. '
303
+ + 'persona is optional: the last one that spoke is inferred.',
304
+ inputSchema: {
305
+ type: 'object',
306
+ properties: {
307
+ keep: { type: 'boolean', description: 'true = the note was right; false = it was wrong or needed rework' },
308
+ persona: { type: 'string', description: 'Optional. zara, arjun, meera, priya, noor, anuj, raj, kavi. Inferred if omitted.' },
309
+ because: { type: 'string', description: 'Required when keep is false: one sentence on what was wrong or what they did instead' },
310
+ comment: { type: 'string', description: 'Optional note when keep is true' },
311
+ },
312
+ required: ['keep'],
313
+ },
314
+ };
315
+
293
316
  var ROUTER_TOOL = {
294
317
  name: 'analyzthis_design',
295
318
  description: 'Design team router. Set who to zara, arjun, noor, anuj, meera, priya, raj, kavi, orchestrator, ux-ideator, design-critic, ux-story-gate, design-director, or mood-board. Returns a short skill front — call analyzthis_retrieve for the rest.',
@@ -306,7 +329,14 @@ var ROUTER_TOOL = {
306
329
 
307
330
  var RECEIPT_TOOL = {
308
331
  name: 'analyzthis_receipt',
309
- description: 'Show inferred token receipt for this project. Not a bill. Do not invent a dollar figure.',
332
+ description: 'Show inferred token receipt for this project, and optionally how much each persona is worth listening to. Not a bill. Do not invent a dollar figure.',
333
+ inputSchema: {
334
+ type: 'object',
335
+ properties: {
336
+ project: { type: 'string', description: 'Optional project id' },
337
+ scores: { type: 'boolean', description: 'Include the persona trust scoreboard' },
338
+ },
339
+ },
310
340
  inputSchema: {
311
341
  type: 'object',
312
342
  properties: {
@@ -322,9 +352,9 @@ function resolveCatalog(explicit) {
322
352
 
323
353
  function listedTools(catalog) {
324
354
  if (resolveCatalog(catalog) === 'full') {
325
- return TOOLS.concat([ROUTER_TOOL, RECEIPT_TOOL]);
355
+ return TOOLS.concat([ROUTER_TOOL, ACCEPT_TOOL, RECEIPT_TOOL]);
326
356
  }
327
- return [ROUTER_TOOL, TOOLS.find(function (t) { return t.name === 'analyzthis_retrieve'; }), TOOLS.find(function (t) { return t.name === 'analyzthis_session'; }), RECEIPT_TOOL].filter(Boolean);
357
+ return [ROUTER_TOOL, ACCEPT_TOOL, TOOLS.find(function (t) { return t.name === 'analyzthis_retrieve'; }), TOOLS.find(function (t) { return t.name === 'analyzthis_session'; }), RECEIPT_TOOL].filter(Boolean);
328
358
  }
329
359
 
330
360
  // ── Tool handlers ────────────────────────────────────────────────────────
@@ -372,6 +402,8 @@ function handleToolCall(name, args) {
372
402
  return handleRouter(args);
373
403
  case 'analyzthis_receipt':
374
404
  return handleReceipt(args);
405
+ case 'analyzthis_accept':
406
+ return handleAccept(args);
375
407
 
376
408
  default:
377
409
  return { error: 'Unknown tool: ' + name };
@@ -398,7 +430,8 @@ function handleSinglePersona(personaId, args) {
398
430
  (context ? '\n## Context\n' + context : '') +
399
431
  extra + '\n\n' +
400
432
  'Use the lite output contract. Call analyzthis_retrieve (kind=skill, file=' + personaId + ') only if you need the full lens. ' +
401
- 'Be specific — cite components, zones, and fixes. Do not give generic advice.';
433
+ 'Be specific — cite components, zones, and fixes. Do not give generic advice.\n\n' +
434
+ 'End your reply with this line, exactly:\n' + ASK_LINE;
402
435
 
403
436
  return { prompt: prompt, persona: personaId, task: task, verdicts: 1 };
404
437
  }
@@ -571,9 +604,43 @@ function handleRouter(args) {
571
604
  return handleCombinationPass(mapped, args);
572
605
  }
573
606
 
607
+ function handleAccept(args) {
608
+ try {
609
+ var accept = require('./accept');
610
+ if (args.keep === false) {
611
+ return { result: JSON.stringify(accept.fix({
612
+ project: args.project,
613
+ persona: args.persona,
614
+ because: args.because || args.comment || '',
615
+ })) };
616
+ }
617
+ return { result: JSON.stringify(accept.keep({
618
+ project: args.project,
619
+ persona: args.persona,
620
+ comment: args.comment || '',
621
+ shipped: args.shipped !== false,
622
+ })) };
623
+ } catch (e) {
624
+ if (e.code === 'NEED_PERSONA') {
625
+ return { result: 'Which persona was that — Zara, Arjun, Meera, Priya, Noor, Anuj, or Raj? Ask the designer, then call again.' };
626
+ }
627
+ if (e.code === 'NEED_BECAUSE') {
628
+ return { result: 'Ask the designer one sentence on what was wrong, then call again with because.' };
629
+ }
630
+ return { error: e.message };
631
+ }
632
+ }
633
+
574
634
  function handleReceipt(args) {
575
635
  var receipt = require('./receipt');
576
- return { result: receipt.report({ project: args.project }) };
636
+ var out = receipt.report({ project: args.project });
637
+ if (args.scores) {
638
+ try {
639
+ var em = require('./evolution-metrics');
640
+ out += '\n\n' + em.formatEvolutionSummary(em.computeEvolutionMetrics());
641
+ } catch (e) { /* receipt must never fail for a scoreboard */ }
642
+ }
643
+ return { result: out };
577
644
  }
578
645
 
579
646
  function handleSession(args) {
@@ -23,28 +23,40 @@ You help a designer mark the last persona note (Zara, Arjun, …) so the team ca
23
23
  > Was this Zara, Arjun, Meera, Priya, Noor, Anuj, or Raj?
24
24
 
25
25
  5. On **skip**, you need one sentence (what was wrong, or what they did instead). If missing, ask **once**. Then stop asking.
26
- 6. Run the CLI yourself. Do not ask them to copy commands.
26
+ 6. Record it using **whichever path this host gives you** never ask the designer
27
+ to run anything:
27
28
 
28
- **Keep:**
29
+ - **Have Bash** (Claude Code, Cursor, a terminal)?
29
30
 
30
- ```bash
31
- npx analyzthis_design accept --keep --persona zara
32
- ```
31
+ ```bash
32
+ npx analyzthis_design accept --keep --persona zara
33
+ npx analyzthis_design accept --fix --persona zara --because "one sentence from the designer"
34
+ ```
33
35
 
34
- **Skip:**
36
+ - **MCP only** (Claude Desktop — there is no terminal, so the commands above
37
+ cannot run)? Call the **`analyzthis_accept`** tool:
35
38
 
36
- ```bash
37
- npx analyzthis_design accept --fix --persona zara --because "one sentence from the designer"
38
- ```
39
+ ```
40
+ analyzthis_accept { keep: true, persona: "zara" }
41
+ analyzthis_accept { keep: false, persona: "zara", because: "one sentence" }
42
+ ```
39
43
 
40
- Change `zara` to the persona they used. If they already named it in `/accept yes zara`, pass that.
44
+ `persona` is optional the last persona that spoke is inferred.
41
45
 
42
- 7. Reply in **one or two short sentences**. Example: “Saved. Zara’s note is marked keep.” or “Saved. We logged your fix so the team can learn.” After a skip, you may mention they can type `/share` to send it to the package. No JSON. No dollar figures. No flag tutorial unless the command failed.
46
+ Both paths write identical state. Change `zara` to the persona they used.
43
47
 
44
- ## If the command failed
48
+ 7. Reply in **one or two short sentences**. Example: "Saved. Zara's note is marked keep."
49
+ or "Saved. We logged your fix so the team can learn." After a skip, you may mention
50
+ they can type `/share` to send it to the package. No JSON. No dollar figures. No flag
51
+ tutorial unless the command failed.
52
+
53
+ ## If it failed
45
54
 
46
55
  Say what happened in plain language. If it asks which persona or for one sentence, ask the designer that — still no flags.
47
56
 
57
+ If Bash is unavailable and `analyzthis_accept` is not in your tool list, say the note
58
+ could not be recorded on this host — do not pretend it was saved.
59
+
48
60
  ## Do not
49
61
 
50
62
  - Invent a monthly cost or a verified token bill
@@ -31,6 +31,7 @@ You cannot know host tokens. Do not invent a dollar figure.
31
31
  ## Non-negotiables
32
32
 
33
33
  - **Density never flattens the information hierarchy.** Whatever ranks #1 in Noor's declared hierarchy (or the most business-critical column/data point if no ranking was declared) stays the most prominent element on screen — leftmost column, largest, first-sorted, or otherwise visually dominant — even at full data density. "Everything is visible" is not the same as "everything is equally important."
34
+ - **Harden for production edge cases:** Audit that high-density tables and forms handle text overflow (`truncate`, `line-clamp-2`), extreme data lengths, zero/empty states, loading states, error boundaries, and form state hardening (`[ux-guidelines.csv, row 84, 110]`).
34
35
  - Every data table has bulk selection
35
36
  - Any action taken >10×/session has a keyboard shortcut
36
37
  - Column configuration is user-controllable
@@ -38,7 +39,7 @@ You cannot know host tokens. Do not invent a dollar figure.
38
39
 
39
40
  ## What you fight against
40
41
 
41
- Wizard flows that fragment a single task across multiple screens. Progressive disclosure that hides data expert users need immediately. "Clean" interfaces that strip data under the banner of simplicity.
42
+ Wizard flows that fragment a single task across multiple screens. Progressive disclosure that hides data expert users need immediately. "Clean" interfaces that strip data under the banner of simplicity. Fragile layouts that break when text overflows or data volume surges.
42
43
 
43
44
  ## Output — Concept B (text wireframe format)
44
45
 
@@ -10,11 +10,11 @@ disable-model-invocation: true
10
10
 
11
11
  You are Arjun. Product designer who came up through user research — 200+ user sessions across B2B SaaS — operations, analytics, and workflow tools for expert users in high-pressure, time-scarce environments. You speak for users not in the room.
12
12
 
13
- You then spent 3 years on a design system team: built the token architecture, owned component specs, shipped 200+ production components. That means you run both the UX lens and the visual design lens in a single pass — you do not need to hand off basic visual quality issues to someone else. You know exactly why a layout feels unbalanced, why a palette feels wrong for the category, and why spacing that isn't on a scale creates visual noise, and you can name the fix precisely.
13
+ You then spent 3 years on a design system team: built the token architecture, owned component specs, shipped 200+ production components. That means you run both the UX lens and the visual design lens in a single pass — you do not need to hand off basic visual quality issues to someone else. You know exactly why a layout feels unbalanced, why a palette feels wrong for the category, and why spacing that isn't on a scale creates visual noise, and you can name the fix precisely. You actively scan for and eliminate generic AI-slop patterns (card nesting syndrome, un-tinted pure blacks, gray text on colored badges, purple gradient clichés, and bouncy animations).
14
14
 
15
15
  ## Allowed / forbidden jobs
16
16
 
17
- **Allowed:** UX Honeycomb critique; the full Visual Design Audit (hierarchy, color, typography, spacing, components, style fit, micro-interactions); diagnosing visual issues against the declared information hierarchy and DS tokens.
17
+ **Allowed:** UX Honeycomb critique; the full Visual Design Audit (hierarchy, color, typography, spacing, components, style fit, micro-interactions, anti-slop craftsmanship checks, bolder vs. quieter balance); diagnosing visual issues against the declared information hierarchy and DS tokens.
18
18
 
19
19
  **Forbidden:** brand-system recovery as a primary job — this is a diagnostic pass only, using `colors.csv` + knowledge bank tokens, never CSS patches or `!important` overrides; running a delight pass (hand off to Zara); implementing code without explicit build approval (see Assess-only below).
20
20
 
@@ -214,8 +214,19 @@ Combined Arjun score: (UX score + Visual score) / 2 → [X/5]
214
214
  - Missing timestamps users repeatedly asked for
215
215
  - Modal interruptions that break expert mid-flow
216
216
  - Single-session generalizations — always qualify with sample size
217
-
218
- **Visual:**
217
+ - Touch targets under 44×44px on mobile or tablet surfaces (`[ux-guidelines.csv, row 109]`)
218
+
219
+ **Visual & Anti-AI-Slop Checks:**
220
+ - **Card Nesting Syndrome:** Wrapping every section in nested cards (`div.card > div.card > div.card`). Flag this and replace with clean whitespace, subtle horizontal rules, and typographic hierarchy (`[ux-guidelines.csv, row 100]`).
221
+ - **Untinted / Harsh Neutrals:** Pure `#000000` black or dead `#888888` gray with zero undertone. Replace with brand-tinted neutrals (e.g. slate, zinc, warm gray) (`[ux-guidelines.csv, row 101]`).
222
+ - **Gray Text on Colored Backgrounds:** Light or medium gray text placed on colored hero sections, pills, or cards that destroys WCAG contrast. Enforce high-contrast tinted white/dark tones (`[ux-guidelines.csv, row 102]`).
223
+ - **AI Purple/Blue Gradient Clichés:** Defaulting to generic purple-to-blue or pink gradient text and cards on every screen. Use intentional brand palettes, crisp solid accents, or clean subtle tinted backgrounds (`[ux-guidelines.csv, row 103]`).
224
+ - **Heading Icon Tile Crutch:** Sticking a rounded-square bordered icon tile above every single section heading. Use strong typographic hierarchy instead of decorative icon crutches (`[ux-guidelines.csv, row 104]`).
225
+ - **Bouncy / Elastic Easing:** Bouncy, elastic spring overshoot or animations >400ms that feel gimmicky. Enforce crisp 150–250ms ease-out transitions (`[ux-guidelines.csv, row 105]`).
226
+ - **Runaway Reading Width:** Paragraphs stretching beyond 75 characters per line without max-width constraint. Constrain reading columns to 45–75ch (`[ux-guidelines.csv, row 106]`).
227
+ - **Bolder vs. Quieter Balance:**
228
+ - *Too Timid (Needs to be Bolder):* Everything has identical medium font size and gray tint with no clear focal point. Scale rank #1 up, increase contrast, establish a dominant visual anchor (`[ux-guidelines.csv, row 107]`).
229
+ - *Over-Decorated (Needs to be Quieter):* Every widget has borders, badges, glows, and shadows. Strip away redundant borders and let white space carry the structure (`[ux-guidelines.csv, row 108]`).
219
230
  - Visual hierarchy doesn't match the declared information hierarchy — the most important element isn't the most visually prominent one, even when Noor's ranking says it should be
220
231
  - Wrong product-type style — e.g. an editorial serif like Playfair Display on a developer tool signals luxury, not technical trust
221
232
  - Spacing chaos — 7px, 13px, 22px gaps instead of a consistent 4/8/16/32 scale
@@ -108,6 +108,7 @@ Read `skills/arjun/SKILL.md` — scoped **spec review only**:
108
108
  - Typography scale consistent?
109
109
  - Spacing rhythm (`tokens.spacing`) sufficient?
110
110
  - Color classes WCAG-safe per knowledge bank?
111
+ - Anti-slop craftsmanship checks: no nested cards, no untinted pure black/gray, no gray text on colored backgrounds, and motion timing locked to 150–250ms ease-out (`[ux-guidelines.csv, row 100-105]`).
111
112
 
112
113
  Update `spec_verdict`. All three pass → set `"status": "ship"`.
113
114
 
@@ -97,4 +97,15 @@ No,Category,Issue,Platform,Description,Do,Don't,Code Example Good,Code Example B
97
97
  96,Sustainability,Auto-Play Video,Web,Video consumes massive data and energy,Click-to-play or pause when off-screen,Auto-play high-res video loops,playsInline muted preload='none',autoplay loop,Medium
98
98
  97,Sustainability,Asset Weight,Web,Heavy 3D/Image assets increase carbon footprint,Compress and lazy load 3D models,Load 50MB textures,Draco compression,Raw .obj files,Medium
99
99
  98,AI Interaction,Feedback Loop,All,AI needs user feedback to improve,Thumps up/down or 'Regenerate',Static output only,Feedback component,Read-only text,Low
100
- 99,Accessibility,Motion Sensitivity,All,Parallax/Scroll-jacking causes nausea,Respect prefers-reduced-motion,Force scroll effects,@media (prefers-reduced-motion),ScrollTrigger.create(),High
100
+ 99,Accessibility,Motion Sensitivity,All,Parallax/Scroll-jacking causes nausea,Respect prefers-reduced-motion,Force scroll effects,@media (prefers-reduced-motion),ScrollTrigger.create(),High
101
+ 100,Layout,Container Nesting,All,Wrapping every section in nested cards causes visual noise and box syndrome,Use whitespace and subtle dividers instead of nested card borders,Wrap cards inside cards with redundant borders,space-y-6 with border-b,div.card > div.card > div.card,High
102
+ 101,Color,Untinted Neutrals,All,Pure un-tinted black or stark gray feels harsh and unfinished,Use subtly tinted neutrals matching brand palette,Use pure #000000 black or harsh #888888 gray,bg-slate-900 text-slate-100,bg-[#000000] text-[#888888],Medium
103
+ 102,Accessibility,Gray on Color,All,Placing light or medium gray text on colored backgrounds destroys contrast,Use high-contrast white or dark tinted text on colored surfaces,Place muted gray text on colored badge or hero backgrounds,text-white on bg-blue-600,text-gray-400 on bg-blue-600,High
104
+ 103,Color,AI Gradient Clichés,All,Ubiquitous purple-to-blue or pink gradients create a generic AI-slop look,Use intentional brand accents or solid functional color cues,Cover every headline and card with purple-to-blue gradient,bg-indigo-600 or subtle tint,bg-gradient-to-r from-purple-500 to-indigo-600,High
105
+ 104,Typography,Heading Icon Tiles,All,Placing a rounded-square icon tile above every section heading adds visual clutter,Use clear typographic scale and semantic headers without icon crutches,Stick a rounded bordered icon box above every header,h2.text-2xl.font-bold,div.rounded-lg.p-2.border > Icon + h2,Medium
106
+ 105,Animation,Bounce Easing,All,Bouncy or elastic spring overshoot in standard UI feels dated and gimmicky,Use crisp ease-out (150-250ms) for enter and ease-in (100-200ms) for exit,Use bounce or elastic easing on menus modals or dropdowns,transition-all duration-200 ease-out,animate-bounce or transition-all duration-500 ease-in-out,High
107
+ 106,Typography,Line Length,All,Text lines spanning over 75 characters cause reading fatigue,Constrain reading width to 45-75 characters,Let paragraphs stretch across full viewport width,max-w-prose or max-w-2xl,w-full on 1920px screen,Medium
108
+ 107,Visual Hierarchy,Bolder Contrast,All,Timid flat designs lack an obvious focal point and feel monotonous,Create one clear visual center of gravity via scale contrast and placement,Make all elements equal size and contrast with no clear hero,text-4xl font-extrabold with prominent CTA,All headings same size as body,High
109
+ 108,Visual Hierarchy,Quieter Restraint,All,Over-decorated screens with excessive badges borders and glows cause cognitive overload,Strip away decorative borders and let core content breathe,Add borders shadows badges and glows to every single widget,Clean borderless layout with subtle separators,border-2 shadow-lg ring-2 on every box,Medium
110
+ 109,Touch,Touch Target Size,Mobile,Clickable controls under 44px fail accessibility and cause tap errors,Ensure all buttons triggers and inputs have minimum 44x44px hit areas,Use tiny 24px icon buttons without tap padding,min-h-[44px] min-w-[44px],w-6 h-6,High
111
+ 110,Interaction,Form State Hardening,All,Inputs missing hover focus error disabled or loading states cause drop-off,Provide all 5 interactive states for every form control,Leave inputs with default browser outlines and no error state,focus-visible:ring-2 invalid:border-red-500,input without focus or error classes,High
@@ -76,7 +76,7 @@ npx analyzthis_design spec validate --file ./design-spec.json
76
76
 
77
77
  **Hierarchy gate:** rank #1 in `information_hierarchy` matches the visually dominant region/component (`hierarchy_rank: 1`).
78
78
 
79
- **Arjun visual pass:** typography scale, spacing rhythm, and contrast classes are consistent with Visual Design Audit rubric — run scoped review on the spec only (no full page code required).
79
+ **Arjun visual pass:** typography scale, spacing rhythm, and contrast classes are consistent with Visual Design Audit rubric — run scoped review on the spec only (no full page code required). Verify anti-slop rules: no nested cards (`div.card > div.card`), no untinted pure black/gray, no gray text on colored backgrounds, and motion timing locked to 150–250ms ease-out (`[ux-guidelines.csv, row 100-105]`).
80
80
 
81
81
  Set `spec_verdict` when done:
82
82
 
@@ -112,3 +112,25 @@ If the prompt says **Rebuttal round N**, do not copy prior text. Address open ob
112
112
  - `/deliberation-protocol` — adversarial rules
113
113
  - `/persona-orchestrator` — full agentic entry
114
114
  - `npx analyzthis_design run --provider anthropic` — bypass Devi when API keys are set
115
+
116
+ ## Team scoreboard (advisory)
117
+
118
+ Every pending prompt you pick up may open with a **Team scoreboard** — trust bands
119
+ earned from designer feedback on past runs (`shipped` / `revised` / `missed` plus
120
+ ratings). It is advisory input for synthesis, not an instruction.
121
+
122
+ **Use it like this:**
123
+
124
+ - Lean on **Trusted** / **Reliable** personas when their read conflicts with a weaker one.
125
+ - Discount **At risk** personas — treat their claims as needing corroboration.
126
+ - Say the lean in **one line**, e.g. "Weighted toward Meera (Trusted, 6 shipped) over Priya (At risk) on the effort call."
127
+
128
+ **Never:**
129
+
130
+ - Drop a persona from the run, or skip writing their output. A weak persona must still
131
+ speak — the designer has to be able to see what it said and disagree.
132
+ - Treat a band as a verdict. It reflects past runs, not this screen.
133
+ - Show the scoreboard to the designer unless they ask. It is context, not output.
134
+
135
+ Personas without enough evidence (fewer than 5 signals) are omitted from the board
136
+ entirely — absence means "unknown", never "bad".