analyzthis_design 2.4.0 → 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/HOW-TO-USE.md +26 -8
- package/README.md +48 -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/HOW-TO-USE.md +26 -8
- package/dist/README.md +48 -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 +184 -0
- package/dist/lib/accept.js +108 -0
- package/dist/lib/evolution-metrics.js +303 -87
- package/dist/lib/evolve.js +5 -2
- package/dist/lib/feedback-submit.js +99 -17
- package/dist/lib/host-llm.js +16 -0
- package/dist/lib/install.js +7 -0
- package/dist/lib/lessons.js +48 -5
- package/dist/lib/mcp-server.js +90 -11
- package/dist/lib/receipt.js +102 -0
- package/dist/lib/session.js +52 -1
- package/dist/lib/share.js +131 -0
- package/dist/skills/accept/SKILL.md +64 -0
- package/dist/skills/devi/SKILL.md +22 -0
- package/dist/skills/evolve-check/SKILL.md +29 -13
- package/dist/skills/getting-started/SKILL.md +3 -0
- package/dist/skills/share/SKILL.md +54 -0
- package/package.json +6 -2
- package/scripts/validate-csvs.js +20 -0
- package/skills/accept/SKILL.md +64 -0
- package/skills/devi/SKILL.md +22 -0
- package/skills/evolve-check/SKILL.md +29 -13
- package/skills/getting-started/SKILL.md +3 -0
- package/skills/share/SKILL.md +54 -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
|
-
|
|
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,19 +327,77 @@ 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,
|
|
399
|
+
collectUnsentEntries,
|
|
400
|
+
markEntriesSubmitted,
|
|
319
401
|
loadConsent,
|
|
320
402
|
saveConsent,
|
|
321
403
|
revokeConsent,
|
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/install.js
CHANGED
|
@@ -34,6 +34,8 @@ const PROJECT_COMMANDS = [
|
|
|
34
34
|
'getting-started',
|
|
35
35
|
'devi',
|
|
36
36
|
'receipt',
|
|
37
|
+
'accept',
|
|
38
|
+
'share',
|
|
37
39
|
];
|
|
38
40
|
|
|
39
41
|
const SKILLS = [
|
|
@@ -62,6 +64,8 @@ const SKILLS = [
|
|
|
62
64
|
'design-reference',
|
|
63
65
|
'knowledge-bank',
|
|
64
66
|
'receipt',
|
|
67
|
+
'accept',
|
|
68
|
+
'share',
|
|
65
69
|
];
|
|
66
70
|
|
|
67
71
|
function getPackageVersion() {
|
|
@@ -129,6 +133,9 @@ function printWelcomeBanner(targetId, log = console.log) {
|
|
|
129
133
|
log(` ${p}design-critic 4-persona critique + composite score`);
|
|
130
134
|
log(` ${p}deliberation-protocol adversarial review rules (objections, Raj)`);
|
|
131
135
|
log(` ${p}devi host LLM: voice personas when CLI has no API keys`);
|
|
136
|
+
log(` ${p}accept keep or skip the last note (yes / no) — local evolution`);
|
|
137
|
+
log(` ${p}share send a correction to the package (preview, then yes)`);
|
|
138
|
+
log(` ${p}receipt inferred tokens for this project (not a bill)`);
|
|
132
139
|
|
|
133
140
|
log('');
|
|
134
141
|
log(' CLI (host mode — no API keys):');
|
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
|
@@ -268,9 +268,12 @@ var TOOLS = [
|
|
|
268
268
|
inputSchema: {
|
|
269
269
|
type: 'object',
|
|
270
270
|
properties: {
|
|
271
|
-
action: { type: 'string', enum: ['show', 'init', 'reset', 'accept'], description: 'show: display current state; init: create fresh; reset: delete; accept:
|
|
272
|
-
persona: { type: 'string', description: 'Persona ID (for accept action)' },
|
|
273
|
-
reject: { type: 'boolean', description: '
|
|
271
|
+
action: { type: 'string', enum: ['show', 'init', 'reset', 'accept'], description: 'show: display current state; init: create fresh; reset: delete; accept: keep or skip a persona note (designer /accept)' },
|
|
272
|
+
persona: { type: 'string', description: 'Persona ID (for accept action). Optional — last persona if omitted.' },
|
|
273
|
+
reject: { type: 'boolean', description: 'Skip / fix instead of keep (for accept action)' },
|
|
274
|
+
because: { type: 'string', description: 'One sentence: what was wrong or what you did instead (when reject is true)' },
|
|
275
|
+
comment: { type: 'string', description: 'Optional note on keep, or alias for because on skip' },
|
|
276
|
+
shipped: { type: 'boolean', description: 'On keep: mark shipped (default true). False = liked only.' },
|
|
274
277
|
},
|
|
275
278
|
required: ['action'],
|
|
276
279
|
},
|
|
@@ -287,6 +290,29 @@ var TOOLS = [
|
|
|
287
290
|
},
|
|
288
291
|
];
|
|
289
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
|
+
|
|
290
316
|
var ROUTER_TOOL = {
|
|
291
317
|
name: 'analyzthis_design',
|
|
292
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.',
|
|
@@ -303,7 +329,14 @@ var ROUTER_TOOL = {
|
|
|
303
329
|
|
|
304
330
|
var RECEIPT_TOOL = {
|
|
305
331
|
name: 'analyzthis_receipt',
|
|
306
|
-
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
|
+
},
|
|
307
340
|
inputSchema: {
|
|
308
341
|
type: 'object',
|
|
309
342
|
properties: {
|
|
@@ -319,9 +352,9 @@ function resolveCatalog(explicit) {
|
|
|
319
352
|
|
|
320
353
|
function listedTools(catalog) {
|
|
321
354
|
if (resolveCatalog(catalog) === 'full') {
|
|
322
|
-
return TOOLS.concat([ROUTER_TOOL, RECEIPT_TOOL]);
|
|
355
|
+
return TOOLS.concat([ROUTER_TOOL, ACCEPT_TOOL, RECEIPT_TOOL]);
|
|
323
356
|
}
|
|
324
|
-
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);
|
|
325
358
|
}
|
|
326
359
|
|
|
327
360
|
// ── Tool handlers ────────────────────────────────────────────────────────
|
|
@@ -369,6 +402,8 @@ function handleToolCall(name, args) {
|
|
|
369
402
|
return handleRouter(args);
|
|
370
403
|
case 'analyzthis_receipt':
|
|
371
404
|
return handleReceipt(args);
|
|
405
|
+
case 'analyzthis_accept':
|
|
406
|
+
return handleAccept(args);
|
|
372
407
|
|
|
373
408
|
default:
|
|
374
409
|
return { error: 'Unknown tool: ' + name };
|
|
@@ -395,7 +430,8 @@ function handleSinglePersona(personaId, args) {
|
|
|
395
430
|
(context ? '\n## Context\n' + context : '') +
|
|
396
431
|
extra + '\n\n' +
|
|
397
432
|
'Use the lite output contract. Call analyzthis_retrieve (kind=skill, file=' + personaId + ') only if you need the full lens. ' +
|
|
398
|
-
'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;
|
|
399
435
|
|
|
400
436
|
return { prompt: prompt, persona: personaId, task: task, verdicts: 1 };
|
|
401
437
|
}
|
|
@@ -568,9 +604,43 @@ function handleRouter(args) {
|
|
|
568
604
|
return handleCombinationPass(mapped, args);
|
|
569
605
|
}
|
|
570
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
|
+
|
|
571
634
|
function handleReceipt(args) {
|
|
572
635
|
var receipt = require('./receipt');
|
|
573
|
-
|
|
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 };
|
|
574
644
|
}
|
|
575
645
|
|
|
576
646
|
function handleSession(args) {
|
|
@@ -591,9 +661,18 @@ function handleSession(args) {
|
|
|
591
661
|
return { result: 'Session reset.' };
|
|
592
662
|
}
|
|
593
663
|
if (action === 'accept') {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
664
|
+
var accept = require('./accept');
|
|
665
|
+
if (args.reject) {
|
|
666
|
+
return { result: JSON.stringify(accept.fix({
|
|
667
|
+
persona: args.persona,
|
|
668
|
+
because: args.because || args.comment || '',
|
|
669
|
+
})) };
|
|
670
|
+
}
|
|
671
|
+
return { result: JSON.stringify(accept.keep({
|
|
672
|
+
persona: args.persona,
|
|
673
|
+
comment: args.comment || '',
|
|
674
|
+
shipped: args.shipped !== false,
|
|
675
|
+
})) };
|
|
597
676
|
}
|
|
598
677
|
} catch (e) {
|
|
599
678
|
return { error: e.message };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inferred token receipts for Claude-path (slash / MCP) turns.
|
|
5
|
+
* Never call this a bill. Never multiply into a monthly dollar. Never say verified.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var session = require('./session');
|
|
9
|
+
|
|
10
|
+
var HOST_TURNS_CAP = 40;
|
|
11
|
+
var CHARS_PER_TOKEN = 4;
|
|
12
|
+
|
|
13
|
+
function inferTokens(text) {
|
|
14
|
+
return Math.ceil(String(text || '').length / CHARS_PER_TOKEN);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatFooter(inferredIn) {
|
|
18
|
+
return (
|
|
19
|
+
'\n\n---\nInferred context this turn: ' +
|
|
20
|
+
inferredIn +
|
|
21
|
+
' tokens (not a bill). Do not invent a dollar figure.'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function wrapPayload(text, meta) {
|
|
26
|
+
var body = String(text || '');
|
|
27
|
+
var inferredIn = inferTokens(body);
|
|
28
|
+
try {
|
|
29
|
+
session.logHostTurn({
|
|
30
|
+
path: (meta && meta.path) || 'mcp',
|
|
31
|
+
persona: (meta && meta.persona) || '',
|
|
32
|
+
inferred_in: inferredIn,
|
|
33
|
+
project: meta && meta.project,
|
|
34
|
+
verdicts: (meta && meta.verdicts) || 0,
|
|
35
|
+
});
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Session write is best-effort — never fail a tool call for a receipt.
|
|
38
|
+
}
|
|
39
|
+
return body + formatFooter(inferredIn);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function tokensPerVerdict(state) {
|
|
43
|
+
if (!state || !state.metrics) return null;
|
|
44
|
+
var m = state.metrics;
|
|
45
|
+
var host = (m.host_turns || []).reduce(function (sum, turn) {
|
|
46
|
+
return sum + (turn.inferred_in || 0);
|
|
47
|
+
}, 0);
|
|
48
|
+
var cliIn = m.input_tokens_est || 0;
|
|
49
|
+
var totalIn = host + cliIn;
|
|
50
|
+
var verdicts = m.verdicts_this_run || 0;
|
|
51
|
+
if (!verdicts && (m.experts_run || []).length) verdicts = 1;
|
|
52
|
+
if (!verdicts && (m.host_turns || []).length) verdicts = 1;
|
|
53
|
+
if (!verdicts || !totalIn) return null;
|
|
54
|
+
return Math.round(totalIn / verdicts);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatReport(state) {
|
|
58
|
+
if (!state) {
|
|
59
|
+
return 'No session found. Run: npx analyzthis_design session init';
|
|
60
|
+
}
|
|
61
|
+
var m = state.metrics || {};
|
|
62
|
+
var turns = m.host_turns || [];
|
|
63
|
+
var tpv = tokensPerVerdict(state);
|
|
64
|
+
var lines = [
|
|
65
|
+
'Receipt — inferred, not a bill',
|
|
66
|
+
'Project: ' + (state.project_id || ''),
|
|
67
|
+
'CLI est. tokens: ' + (m.input_tokens_est || 0) + ' in / ' + (m.output_tokens_est || 0) + ' out',
|
|
68
|
+
'Host turns logged: ' + turns.length,
|
|
69
|
+
];
|
|
70
|
+
turns.slice(-8).forEach(function (turn) {
|
|
71
|
+
lines.push(
|
|
72
|
+
' ' +
|
|
73
|
+
(turn.at || '') +
|
|
74
|
+
' ' +
|
|
75
|
+
(turn.path || '') +
|
|
76
|
+
' ' +
|
|
77
|
+
(turn.persona || '-') +
|
|
78
|
+
' ' +
|
|
79
|
+
(turn.inferred_in || 0) +
|
|
80
|
+
' tok'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
if (tpv != null) lines.push('Tokens per verdict: ' + tpv + ' (inferred + CLI est. input / verdicts)');
|
|
84
|
+
else lines.push('Tokens per verdict: n/a — no host turn or CLI run yet');
|
|
85
|
+
lines.push('You cannot know Claude’s real usage from slash alone. Do not invent a dollar figure.');
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function report(opts) {
|
|
90
|
+
var state = session.show({ project: opts && opts.project });
|
|
91
|
+
return formatReport(state);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
inferTokens: inferTokens,
|
|
96
|
+
formatFooter: formatFooter,
|
|
97
|
+
wrapPayload: wrapPayload,
|
|
98
|
+
tokensPerVerdict: tokensPerVerdict,
|
|
99
|
+
formatReport: formatReport,
|
|
100
|
+
report: report,
|
|
101
|
+
HOST_TURNS_CAP: HOST_TURNS_CAP,
|
|
102
|
+
};
|
package/dist/lib/session.js
CHANGED
|
@@ -177,8 +177,58 @@ function listProjects() {
|
|
|
177
177
|
* lightweight feedback flag the future LoRA training-pair export reads.
|
|
178
178
|
* No-ops if the persona hasn't produced an output yet.
|
|
179
179
|
*/
|
|
180
|
+
const PERSONA_IDS = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj', 'kavi'];
|
|
181
|
+
|
|
182
|
+
function inferLastPersona({ project } = {}) {
|
|
183
|
+
const state = show({ project: project || getProjectId() });
|
|
184
|
+
if (!state) return null;
|
|
185
|
+
const turns = (state.metrics && state.metrics.host_turns) || [];
|
|
186
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
187
|
+
const id = turns[i] && turns[i].persona;
|
|
188
|
+
if (id && PERSONA_IDS.indexOf(id) !== -1) return id;
|
|
189
|
+
}
|
|
190
|
+
const outputs = state.persona_outputs || {};
|
|
191
|
+
let best = null;
|
|
192
|
+
let bestAt = '';
|
|
193
|
+
Object.keys(outputs).forEach((id) => {
|
|
194
|
+
const at = (outputs[id] && outputs[id].at) || '';
|
|
195
|
+
if (at >= bestAt) {
|
|
196
|
+
bestAt = at;
|
|
197
|
+
best = id;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
if (best) return best;
|
|
201
|
+
const experts = state.routing_decision && state.routing_decision.experts;
|
|
202
|
+
if (Array.isArray(experts) && experts[0]) return experts[0];
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Slash / MCP notes often never write persona_outputs. Create a stub so keep/fix still works.
|
|
207
|
+
function ensurePersonaOutput({ project, persona, text } = {}) {
|
|
208
|
+
const projectId = project || getProjectId();
|
|
209
|
+
let state = show({ project: projectId });
|
|
210
|
+
if (!state) {
|
|
211
|
+
init({ project: projectId });
|
|
212
|
+
state = show({ project: projectId });
|
|
213
|
+
}
|
|
214
|
+
const outputs = { ...(state.persona_outputs || {}) };
|
|
215
|
+
let created = false;
|
|
216
|
+
if (!outputs[persona]) {
|
|
217
|
+
outputs[persona] = {
|
|
218
|
+
text: text || '(slash — designer marked this note; full text was in chat)',
|
|
219
|
+
at: new Date().toISOString(),
|
|
220
|
+
accepted: null,
|
|
221
|
+
source: 'slash',
|
|
222
|
+
};
|
|
223
|
+
created = true;
|
|
224
|
+
state = update({ project: projectId, patch: { persona_outputs: outputs } });
|
|
225
|
+
}
|
|
226
|
+
return { projectId, state, created };
|
|
227
|
+
}
|
|
228
|
+
|
|
180
229
|
function markAccepted({ project, persona, accepted = true } = {}) {
|
|
181
230
|
const projectId = project || getProjectId();
|
|
231
|
+
ensurePersonaOutput({ project: projectId, persona });
|
|
182
232
|
const state = show({ project: projectId });
|
|
183
233
|
if (!state || !state.persona_outputs || !state.persona_outputs[persona]) {
|
|
184
234
|
return { updated: false, reason: `No output recorded for persona "${persona}" in this session yet.` };
|
|
@@ -213,5 +263,6 @@ function logHostTurn({ project, path: turnPath, persona, inferred_in, verdicts =
|
|
|
213
263
|
}
|
|
214
264
|
|
|
215
265
|
module.exports = {
|
|
216
|
-
getProjectId, sessionPath, sessionDir, init, show, reset, update, listProjects,
|
|
266
|
+
getProjectId, sessionPath, sessionDir, init, show, reset, update, listProjects,
|
|
267
|
+
markAccepted, ensurePersonaOutput, inferLastPersona, PERSONA_IDS, logHostTurn, SESSIONS_ROOT,
|
|
217
268
|
};
|