analyzthis_design 2.4.1 → 2.5.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 +35 -4
- package/agents/cards/anuj.md +13 -0
- package/agents/cards/arjun.md +13 -0
- package/agents/cards/devi.md +22 -0
- package/agents/cards/kavi.md +13 -0
- package/agents/cards/meera.md +13 -0
- package/agents/cards/noor.md +13 -0
- package/agents/cards/priya.md +13 -0
- package/agents/cards/raj.md +13 -0
- package/agents/cards/zara.md +13 -0
- package/dist/README.md +35 -4
- package/dist/agents/cards/anuj.md +13 -0
- package/dist/agents/cards/arjun.md +13 -0
- package/dist/agents/cards/devi.md +22 -0
- package/dist/agents/cards/kavi.md +13 -0
- package/dist/agents/cards/meera.md +13 -0
- package/dist/agents/cards/noor.md +13 -0
- package/dist/agents/cards/priya.md +13 -0
- package/dist/agents/cards/raj.md +13 -0
- package/dist/agents/cards/zara.md +13 -0
- package/dist/bin/cli.js +54 -0
- package/dist/lib/accept.js +26 -13
- package/dist/lib/evolution-metrics.js +303 -87
- package/dist/lib/evolve.js +5 -2
- package/dist/lib/feedback-submit.js +97 -17
- package/dist/lib/host-llm.js +16 -0
- package/dist/lib/lessons.js +48 -5
- package/dist/lib/mcp-server.js +72 -5
- package/dist/skills/accept/SKILL.md +24 -12
- package/dist/skills/devi/SKILL.md +22 -0
- package/dist/skills/evolve-check/SKILL.md +29 -13
- package/package.json +6 -2
- package/scripts/validate-csvs.js +20 -0
- package/skills/accept/SKILL.md +24 -12
- package/skills/devi/SKILL.md +22 -0
- package/skills/evolve-check/SKILL.md +29 -13
|
@@ -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
|
-
|
|
47
|
-
|
|
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
|
-
+ '
|
|
197
|
-
+ ' Or env: ANALYZTHIS_FEEDBACK_URL
|
|
198
|
-
+ ' See
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
package/dist/lib/host-llm.js
CHANGED
|
@@ -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,
|
package/dist/lib/lessons.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
26
|
+
6. Record it using **whichever path this host gives you** — never ask the designer
|
|
27
|
+
to run anything:
|
|
27
28
|
|
|
28
|
-
**
|
|
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
|
-
**
|
|
36
|
+
- **MCP only** (Claude Desktop — there is no terminal, so the commands above
|
|
37
|
+
cannot run)? Call the **`analyzthis_accept`** tool:
|
|
35
38
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
```
|
|
40
|
+
analyzthis_accept { keep: true, persona: "zara" }
|
|
41
|
+
analyzthis_accept { keep: false, persona: "zara", because: "one sentence" }
|
|
42
|
+
```
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
`persona` is optional — the last persona that spoke is inferred.
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
Both paths write identical state. Change `zara` to the persona they used.
|
|
43
47
|
|
|
44
|
-
|
|
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
|
|
@@ -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".
|
|
@@ -72,21 +72,35 @@ Tell the user what's needed:
|
|
|
72
72
|
|
|
73
73
|
## Evolution metrics
|
|
74
74
|
|
|
75
|
-
The dashboard shows per-persona
|
|
75
|
+
The dashboard shows per-persona trust scores (0-100). A persona **starts at 50**
|
|
76
|
+
and moves in both directions, so a rejection genuinely costs it.
|
|
76
77
|
|
|
77
78
|
| Score | Level | Meaning |
|
|
78
79
|
|-------|-------|---------|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| 40-59 |
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
| 80-100 | Trusted | Consistently shipped; weight heavily |
|
|
81
|
+
| 60-79 | Reliable | More hits than misses |
|
|
82
|
+
| 40-59 | Baseline | Neutral, or not enough evidence yet |
|
|
83
|
+
| 20-39 | Developing | More rework than wins |
|
|
84
|
+
| 0-19 | At risk | Repeatedly wrong or missed |
|
|
85
|
+
|
|
86
|
+
Signed contributions:
|
|
87
|
+
|
|
88
|
+
| Signal | Points |
|
|
89
|
+
|---|---|
|
|
90
|
+
| outcome `shipped` | **+15** |
|
|
91
|
+
| outcome `blocked_correctly` | **+10** |
|
|
92
|
+
| outcome `revised` | **-5** |
|
|
93
|
+
| outcome `missed` | **-15** |
|
|
94
|
+
| each rating | `(rating - 3) x 4` → 5* = +8, 1* = -8 |
|
|
95
|
+
| each positive lesson | +10 |
|
|
96
|
+
| patch proposed / applied | +20 / +25 |
|
|
97
|
+
|
|
98
|
+
**Evidence gating:** below 5 signals a persona is reported as
|
|
99
|
+
`Baseline (insufficient evidence)` regardless of score — one bad note must not
|
|
100
|
+
brand a persona. Scores are derived on read, so changing weights re-scores history
|
|
101
|
+
with no migration.
|
|
102
|
+
|
|
103
|
+
Scope is **global per persona** by default. Pass `--project` to scope down.
|
|
90
104
|
|
|
91
105
|
## CLI reference
|
|
92
106
|
|
|
@@ -94,8 +108,10 @@ Scoring:
|
|
|
94
108
|
# Check readiness + dashboard
|
|
95
109
|
npx analyzthis_design evolve --ready
|
|
96
110
|
|
|
97
|
-
# Just the dashboard
|
|
111
|
+
# Just the dashboard (or the shorter alias)
|
|
98
112
|
npx analyzthis_design evolve --metrics
|
|
113
|
+
npx analyzthis_design scores
|
|
114
|
+
npx analyzthis_design scores --persona arjun
|
|
99
115
|
|
|
100
116
|
# Extract patches (dry-run by default)
|
|
101
117
|
npx analyzthis_design evolve --extract --dry-run
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "analyzthis_design",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "8 AI design personas
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "8 AI design personas \u2014 v2.2 project-scoped knowledge bank (no cross-project vault entanglement), v2.0 chunked execution with frontier planner + free/cheap chunk models, adversarial deliberation loops, opt-in community feedback, Kavi knowledge collection, DesignSpec producer path, wireframe skills, UX critique, Agent Skills for Cursor, Claude, Codex, Grok, Windsurf. Plain source \u2014 no obfuscation, no auto-install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
7
7
|
"cursor-skill",
|
|
@@ -46,5 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=16"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@neon/config": "^1.2.0",
|
|
52
|
+
"@neon/env": "^1.2.0"
|
|
49
53
|
}
|
|
50
54
|
}
|
package/scripts/validate-csvs.js
CHANGED
|
@@ -180,6 +180,26 @@ for (var fileKey in schema.files) {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ── website/system-prompt.txt drift guard ───────────────────────────────────
|
|
184
|
+
// The site deploys from website/ independently of `npm publish`, so nothing else
|
|
185
|
+
// would catch a stale prompt. Browser-only tools have no other way in, and a
|
|
186
|
+
// silently outdated prompt is worse than a missing one.
|
|
187
|
+
(function checkSystemPrompt() {
|
|
188
|
+
var promptPath = path.join(__dirname, '..', 'website', 'system-prompt.txt');
|
|
189
|
+
if (!fs.existsSync(promptPath)) {
|
|
190
|
+
errors.push('website/system-prompt.txt is missing — run: npm run build');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
var expected = require(path.join(__dirname, '..', 'lib', 'system-prompt.js')).buildPrompt({ mode: 'both' });
|
|
195
|
+
if (fs.readFileSync(promptPath, 'utf8') !== expected) {
|
|
196
|
+
errors.push('website/system-prompt.txt is out of date — run: npm run build');
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
errors.push('could not verify website/system-prompt.txt: ' + e.message);
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
|
|
183
203
|
// Report
|
|
184
204
|
if (warnings.length) {
|
|
185
205
|
console.log('\n── Warnings ──');
|