tokengolf 0.3.0 → 0.4.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/src/lib/score.js CHANGED
@@ -1,16 +1,16 @@
1
1
  export const BUDGET_TIERS = [
2
- { label: "Diamond", emoji: "💎", max: 0.1, color: "cyan" },
3
- { label: "Gold", emoji: "ðŸĨ‡", max: 0.3, color: "yellow" },
4
- { label: "Silver", emoji: "ðŸĨˆ", max: 1.0, color: "white" },
5
- { label: "Bronze", emoji: "ðŸĨ‰", max: 3.0, color: "yellow" },
6
- { label: "Reckless", emoji: "ðŸ’ļ", max: Infinity, color: "red" },
2
+ { label: 'Diamond', emoji: '💎', max: 0.1, color: 'cyan' },
3
+ { label: 'Gold', emoji: 'ðŸĨ‡', max: 0.3, color: 'yellow' },
4
+ { label: 'Silver', emoji: 'ðŸĨˆ', max: 1.0, color: 'white' },
5
+ { label: 'Bronze', emoji: 'ðŸĨ‰', max: 3.0, color: 'yellow' },
6
+ { label: 'Reckless', emoji: 'ðŸ’ļ', max: Infinity, color: 'red' },
7
7
  ];
8
8
 
9
9
  export const EFFORT_LEVELS = {
10
- low: { label: "Low", emoji: "ðŸŠķ", color: "green" },
11
- medium: { label: "Medium", emoji: "⚖ïļ", color: "white" },
12
- high: { label: "High", emoji: "ðŸ”Ĩ", color: "yellow" },
13
- max: { label: "Max", emoji: "ðŸ’Ĩ", color: "magenta", opusOnly: true },
10
+ low: { label: 'Low', emoji: 'ðŸŠķ', color: 'green' },
11
+ medium: { label: 'Medium', emoji: '⚖ïļ', color: 'white' },
12
+ high: { label: 'High', emoji: 'ðŸ”Ĩ', color: 'yellow' },
13
+ max: { label: 'Max', emoji: 'ðŸ’Ĩ', color: 'magenta', opusOnly: true },
14
14
  };
15
15
 
16
16
  export function getEffortLevel(effort) {
@@ -20,69 +20,76 @@ export function getEffortLevel(effort) {
20
20
  export const MODEL_BUDGET_TIERS = {
21
21
  haiku: { diamond: 0.15, gold: 0.4, silver: 1.0, bronze: 2.5 },
22
22
  sonnet: { diamond: 0.5, gold: 1.5, silver: 4.0, bronze: 10.0 },
23
+ opusplan: { diamond: 1.5, gold: 4.5, silver: 12.0, bronze: 30.0 },
23
24
  opus: { diamond: 2.5, gold: 7.5, silver: 20.0, bronze: 50.0 },
24
25
  };
25
26
 
26
27
  export function getModelBudgets(model) {
27
- const m = (model || "").toLowerCase();
28
- if (m.includes("haiku")) return MODEL_BUDGET_TIERS.haiku;
29
- if (m.includes("opus")) return MODEL_BUDGET_TIERS.opus;
28
+ const m = (model || '').toLowerCase();
29
+ if (m.includes('haiku')) return MODEL_BUDGET_TIERS.haiku;
30
+ if (m.includes('opusplan')) return MODEL_BUDGET_TIERS.opusplan;
31
+ if (m.includes('opus')) return MODEL_BUDGET_TIERS.opus;
30
32
  return MODEL_BUDGET_TIERS.sonnet;
31
33
  }
32
34
 
33
35
  export const MODEL_CLASSES = {
34
36
  haiku: {
35
- name: "Haiku",
36
- label: "Rogue",
37
- emoji: "ðŸđ",
38
- difficulty: "Hard",
39
- color: "red",
37
+ name: 'Haiku',
38
+ label: 'Rogue',
39
+ emoji: 'ðŸđ',
40
+ difficulty: 'Hard',
41
+ color: 'red',
40
42
  },
41
43
  sonnet: {
42
- name: "Sonnet",
43
- label: "Fighter",
44
- emoji: "⚔ïļ",
45
- difficulty: "Normal",
46
- color: "cyan",
44
+ name: 'Sonnet',
45
+ label: 'Fighter',
46
+ emoji: '⚔ïļ',
47
+ difficulty: 'Normal',
48
+ color: 'cyan',
49
+ },
50
+ opusplan: {
51
+ name: 'Paladin',
52
+ label: 'Paladin',
53
+ emoji: '⚜ïļ',
54
+ difficulty: 'Calculated',
55
+ color: 'yellow',
47
56
  },
48
57
  opus: {
49
- name: "Opus",
50
- label: "Warlock",
51
- emoji: "🧙",
52
- difficulty: "Easy",
53
- color: "magenta",
58
+ name: 'Opus',
59
+ label: 'Warlock',
60
+ emoji: '🧙',
61
+ difficulty: 'Easy',
62
+ color: 'magenta',
54
63
  },
55
64
  };
56
65
 
57
66
  export const FLOORS = [
58
- "Write the code",
59
- "Write the tests",
60
- "Fix failing tests",
61
- "Code review pass",
62
- "PR merged — BOSS 🏆",
67
+ 'Write the code',
68
+ 'Write the tests',
69
+ 'Fix failing tests',
70
+ 'Code review pass',
71
+ 'PR merged — BOSS 🏆',
63
72
  ];
64
73
 
65
74
  export function getTier(spent) {
66
- return (
67
- BUDGET_TIERS.find((t) => spent <= t.max) ||
68
- BUDGET_TIERS[BUDGET_TIERS.length - 1]
69
- );
75
+ return BUDGET_TIERS.find((t) => spent <= t.max) || BUDGET_TIERS[BUDGET_TIERS.length - 1];
70
76
  }
71
77
 
72
- export function getModelClass(model = "") {
73
- const key = Object.keys(MODEL_CLASSES).find((k) =>
74
- model.toLowerCase().includes(k),
75
- );
78
+ export function getModelClass(model = '') {
79
+ const m = model.toLowerCase();
80
+ // Check opusplan before opus to avoid substring collision
81
+ if (m.includes('opusplan')) return MODEL_CLASSES.opusplan;
82
+ const key = Object.keys(MODEL_CLASSES).find((k) => m.includes(k));
76
83
  return MODEL_CLASSES[key] || MODEL_CLASSES.sonnet;
77
84
  }
78
85
 
79
86
  export function getEfficiencyRating(spent, budget) {
80
87
  const pct = spent / budget;
81
- if (pct <= 0.25) return { label: "LEGENDARY", emoji: "🌟", color: "magenta" };
82
- if (pct <= 0.5) return { label: "EFFICIENT", emoji: "⚡", color: "cyan" };
83
- if (pct <= 0.75) return { label: "SOLID", emoji: "✓", color: "green" };
84
- if (pct <= 1.0) return { label: "CLOSE CALL", emoji: "😅", color: "yellow" };
85
- return { label: "BUSTED", emoji: "💀", color: "red" };
88
+ if (pct <= 0.25) return { label: 'LEGENDARY', emoji: '🌟', color: 'magenta' };
89
+ if (pct <= 0.5) return { label: 'EFFICIENT', emoji: '⚡', color: 'cyan' };
90
+ if (pct <= 0.75) return { label: 'SOLID', emoji: '✓', color: 'green' };
91
+ if (pct <= 1.0) return { label: 'CLOSE CALL', emoji: '😅', color: 'yellow' };
92
+ return { label: 'BUSTED', emoji: '💀', color: 'red' };
86
93
  }
87
94
 
88
95
  export function getBudgetPct(spent, budget) {
@@ -90,13 +97,13 @@ export function getBudgetPct(spent, budget) {
90
97
  }
91
98
 
92
99
  export function formatCost(amount = 0) {
93
- if (amount === 0) return "$0.00";
94
- if (amount < 0.01) return `$${(amount * 100).toFixed(3)}ÂĒ`;
100
+ if (amount === 0) return '$0.00';
101
+ if (amount < 0.01) return `$${amount.toFixed(5)}`;
95
102
  return `$${amount.toFixed(4)}`;
96
103
  }
97
104
 
98
105
  export function formatElapsed(startedAt) {
99
- if (!startedAt) return "—";
106
+ if (!startedAt) return '—';
100
107
  const ms = Date.now() - new Date(startedAt).getTime();
101
108
  const s = Math.floor(ms / 1000);
102
109
  const m = Math.floor(s / 60);
@@ -106,11 +113,21 @@ export function formatElapsed(startedAt) {
106
113
  return `${s}s`;
107
114
  }
108
115
 
116
+ // Returns opus's share of total spend as a 0–100 integer, or null (for Paladin runs)
117
+ export function getOpusPct(modelBreakdown, totalSpent) {
118
+ if (!modelBreakdown || !totalSpent) return null;
119
+ const opusCost = Object.entries(modelBreakdown)
120
+ .filter(([m]) => m.toLowerCase().includes('opus'))
121
+ .reduce((sum, [, c]) => sum + c, 0);
122
+ if (opusCost === 0) return null;
123
+ return Math.round((opusCost / totalSpent) * 100);
124
+ }
125
+
109
126
  // Returns haiku's share of total spend as a 0–100 integer, or null
110
127
  export function getHaikuPct(modelBreakdown, totalSpent) {
111
128
  if (!modelBreakdown || !totalSpent) return null;
112
129
  const haikuCost = Object.entries(modelBreakdown)
113
- .filter(([m]) => m.toLowerCase().includes("haiku"))
130
+ .filter(([m]) => m.toLowerCase().includes('haiku'))
114
131
  .reduce((sum, [, c]) => sum + c, 0);
115
132
  if (haikuCost === 0) return null;
116
133
  return Math.round((haikuCost / totalSpent) * 100);
@@ -118,90 +135,144 @@ export function getHaikuPct(modelBreakdown, totalSpent) {
118
135
 
119
136
  export function calculateAchievements(run) {
120
137
  const achievements = [];
121
- const won = run.status === "won";
138
+ const won = run.status === 'won';
122
139
  const pct = run.budget ? run.spent / run.budget : null;
123
140
  const mc = getModelClass(run.model);
124
141
 
142
+ const isPaladin = mc === MODEL_CLASSES.opusplan;
143
+
144
+ // Indecisive fires on death too (like Hubris)
145
+ if ((run.modelSwitches ?? 0) >= 3)
146
+ achievements.push({
147
+ key: 'indecisive',
148
+ label: `Indecisive — ${run.modelSwitches} model switches mid-session`,
149
+ emoji: 'ðŸŽē',
150
+ });
151
+
125
152
  // Hubris fires on death too — ultrathink and still busted
126
- if (run.thinkingInvocations > 0 && run.status === "died")
153
+ if (run.thinkingInvocations > 0 && run.status === 'died')
127
154
  achievements.push({
128
- key: "hubris",
129
- label: "Hubris — Used ultrathink, busted anyway",
130
- emoji: "ðŸĪĶ",
155
+ key: 'hubris',
156
+ label: 'Hubris — Used ultrathink, busted anyway',
157
+ emoji: 'ðŸĪĶ',
131
158
  });
132
159
 
133
- if (!won) return achievements;
160
+ // Death marks
161
+ if (!won) {
162
+ if (run.budget && run.spent / run.budget >= 2.0)
163
+ achievements.push({ key: 'blowout', label: 'Blowout — Spent 2× budget', emoji: 'ðŸ’Ĩ' });
164
+ else if (run.budget && run.spent / run.budget > 1.0 && run.spent / run.budget <= 1.1)
165
+ achievements.push({
166
+ key: 'so_close',
167
+ label: 'So Close — Died within 10% of budget',
168
+ emoji: '😭',
169
+ });
170
+ if ((run.totalToolCalls || 0) >= 30)
171
+ achievements.push({
172
+ key: 'tool_happy',
173
+ label: `Tool Happy — Died with ${run.totalToolCalls} tool calls`,
174
+ emoji: 'ðŸ”Ļ',
175
+ });
176
+ if ((run.promptCount || 0) <= 2)
177
+ achievements.push({
178
+ key: 'silent_death',
179
+ label: 'Silent Death — Died with â‰Ī2 prompts',
180
+ emoji: 'ðŸŠĶ',
181
+ });
182
+ if ((run.failedToolCalls ?? 0) >= 5)
183
+ achievements.push({
184
+ key: 'fumble',
185
+ label: `Fumble — Died with ${run.failedToolCalls} failed tool calls`,
186
+ emoji: 'ðŸĪĄ',
187
+ });
188
+ if (run.budget && run.spent / run.budget >= 0.5)
189
+ if ((run.promptCount || 0) >= 3 && run.spent / (run.promptCount || 1) >= 0.5)
190
+ achievements.push({
191
+ key: 'expensive_taste',
192
+ label: 'Expensive Taste — Over $0.50 per prompt',
193
+ emoji: '🍷',
194
+ });
195
+ return achievements;
196
+ }
134
197
 
135
198
  if (mc === MODEL_CLASSES.haiku) {
136
199
  achievements.push({
137
- key: "gold_haiku",
138
- label: "Gold — Completed with Haiku",
139
- emoji: "ðŸĨ‡",
200
+ key: 'gold_haiku',
201
+ label: 'Gold — Completed with Haiku',
202
+ emoji: 'ðŸĨ‡',
140
203
  });
141
204
  if (run.spent < 0.1)
142
205
  achievements.push({
143
- key: "diamond",
144
- label: "Diamond — Haiku under $0.10",
145
- emoji: "💎",
206
+ key: 'diamond',
207
+ label: 'Diamond — Haiku under $0.10',
208
+ emoji: '💎',
146
209
  });
147
210
  } else if (mc === MODEL_CLASSES.sonnet) {
148
211
  achievements.push({
149
- key: "silver_sonnet",
150
- label: "Silver — Completed with Sonnet",
151
- emoji: "ðŸĨˆ",
212
+ key: 'silver_sonnet',
213
+ label: 'Silver — Completed with Sonnet',
214
+ emoji: 'ðŸĨˆ',
152
215
  });
216
+ } else if (mc === MODEL_CLASSES.opusplan) {
217
+ achievements.push({
218
+ key: 'paladin',
219
+ label: 'Paladin — Completed a run as Paladin',
220
+ emoji: '⚜ïļ',
221
+ });
222
+ if (pct !== null && pct <= 0.25)
223
+ achievements.push({
224
+ key: 'grand_strategist',
225
+ label: 'Grand Strategist — LEGENDARY efficiency as Paladin',
226
+ emoji: '♟ïļ',
227
+ });
153
228
  } else if (mc === MODEL_CLASSES.opus) {
154
229
  achievements.push({
155
- key: "bronze_opus",
156
- label: "Bronze — Completed with Opus",
157
- emoji: "ðŸĨ‰",
230
+ key: 'bronze_opus',
231
+ label: 'Bronze — Completed with Opus',
232
+ emoji: 'ðŸĨ‰',
158
233
  });
159
234
  }
160
235
 
161
236
  if (pct !== null) {
162
237
  if (pct <= 0.25)
163
238
  achievements.push({
164
- key: "sniper",
165
- label: "Sniper — Under 25% of budget",
166
- emoji: "ðŸŽŊ",
239
+ key: 'sniper',
240
+ label: 'Sniper — Under 25% of budget',
241
+ emoji: 'ðŸŽŊ',
167
242
  });
168
243
  if (pct <= 0.5)
169
244
  achievements.push({
170
- key: "efficient",
171
- label: "Efficient — Under 50% of budget",
172
- emoji: "⚡",
245
+ key: 'efficient',
246
+ label: 'Efficient — Under 50% of budget',
247
+ emoji: '⚡',
173
248
  });
174
249
  }
175
250
  if (run.spent < 0.1)
176
251
  achievements.push({
177
- key: "penny",
178
- label: "Penny Pincher — Under $0.10",
179
- emoji: "🊙",
252
+ key: 'penny',
253
+ label: 'Penny Pincher — Under $0.10',
254
+ emoji: '🊙',
180
255
  });
181
256
 
182
257
  // Effort-based achievements
183
258
  if (run.effort) {
184
- if (run.effort === "low" && pct !== null && pct < 1.0)
259
+ if (run.effort === 'low' && pct !== null && pct < 1.0)
185
260
  achievements.push({
186
- key: "speedrunner",
187
- label: "Speedrunner — Low effort, completed under budget",
188
- emoji: "ðŸŽŊ",
261
+ key: 'speedrunner',
262
+ label: 'Speedrunner — Low effort, completed under budget',
263
+ emoji: '🏎ïļ',
189
264
  });
190
- if (
191
- (run.effort === "high" || run.effort === "max") &&
192
- pct !== null &&
193
- pct <= 0.25
194
- )
265
+ if ((run.effort === 'high' || run.effort === 'max') && pct !== null && pct <= 0.25)
195
266
  achievements.push({
196
- key: "tryhard",
197
- label: "Tryhard — High effort, LEGENDARY efficiency",
198
- emoji: "💊",
267
+ key: 'tryhard',
268
+ label: 'Tryhard — High effort, LEGENDARY efficiency',
269
+ emoji: '🏋ïļ',
199
270
  });
200
- if (run.effort === "max" && mc === MODEL_CLASSES.opus)
271
+ if (run.effort === 'max' && mc === MODEL_CLASSES.opus)
201
272
  achievements.push({
202
- key: "archmagus",
203
- label: "Archmagus — Opus at max effort, completed",
204
- emoji: "👑",
273
+ key: 'archmagus',
274
+ label: 'Archmagus — Opus at max effort, completed',
275
+ emoji: '👑',
205
276
  });
206
277
  }
207
278
 
@@ -209,15 +280,15 @@ export function calculateAchievements(run) {
209
280
  if (run.fastMode && mc === MODEL_CLASSES.opus) {
210
281
  if (pct !== null && pct < 1.0)
211
282
  achievements.push({
212
- key: "lightning",
213
- label: "Lightning Run — Opus fast mode, completed under budget",
214
- emoji: "⚡",
283
+ key: 'lightning',
284
+ label: 'Lightning Run — Opus fast mode, completed under budget',
285
+ emoji: '⛈ïļ',
215
286
  });
216
287
  if (pct !== null && pct <= 0.25)
217
288
  achievements.push({
218
- key: "daredevil",
219
- label: "Daredevil — Opus fast mode, LEGENDARY efficiency",
220
- emoji: "🎰",
289
+ key: 'daredevil',
290
+ label: 'Daredevil — Opus fast mode, LEGENDARY efficiency',
291
+ emoji: '🎰',
221
292
  });
222
293
  }
223
294
 
@@ -225,58 +296,54 @@ export function calculateAchievements(run) {
225
296
  const sessions = run.sessionCount || 1;
226
297
  if (sessions >= 2)
227
298
  achievements.push({
228
- key: "made_camp",
299
+ key: 'made_camp',
229
300
  label: `Made Camp — Completed across ${sessions} sessions`,
230
- emoji: "🏕ïļ",
301
+ emoji: '🏕ïļ',
231
302
  });
232
303
  if (sessions === 1)
233
304
  achievements.push({
234
- key: "no_rest",
235
- label: "No Rest for the Wicked — Completed in one session",
236
- emoji: "⚡",
305
+ key: 'no_rest',
306
+ label: 'No Rest for the Wicked — Completed in one session',
307
+ emoji: 'ðŸ”Ĩ',
237
308
  });
238
309
  if (run.fainted)
239
310
  achievements.push({
240
- key: "came_back",
241
- label: "Came Back — Fainted and finished anyway",
242
- emoji: "💊",
311
+ key: 'came_back',
312
+ label: 'Came Back — Fainted and finished anyway',
313
+ emoji: '🧟',
243
314
  });
244
315
 
245
316
  // Compaction achievements
246
317
  const compactionEvents = run.compactionEvents || [];
247
- const manualCompactions = compactionEvents.filter(
248
- (e) => e.trigger === "manual",
249
- );
250
- const autoCompactions = compactionEvents.filter((e) => e.trigger === "auto");
318
+ const manualCompactions = compactionEvents.filter((e) => e.trigger === 'manual');
319
+ const autoCompactions = compactionEvents.filter((e) => e.trigger === 'auto');
251
320
 
252
321
  if (autoCompactions.length > 0)
253
322
  achievements.push({
254
- key: "overencumbered",
255
- label: "Overencumbered — Context auto-compacted during run",
256
- emoji: "ðŸ“Ķ",
323
+ key: 'overencumbered',
324
+ label: 'Overencumbered — Context auto-compacted during run',
325
+ emoji: 'ðŸ“Ķ',
257
326
  });
258
327
 
259
328
  if (manualCompactions.length > 0) {
260
- const minPct = Math.min(
261
- ...manualCompactions.map((e) => e.contextPct ?? 100),
262
- );
329
+ const minPct = Math.min(...manualCompactions.map((e) => e.contextPct ?? 100));
263
330
  if (minPct <= 30)
264
331
  achievements.push({
265
- key: "ghost_run",
332
+ key: 'ghost_run',
266
333
  label: `Ghost Run — Manual compact at ${minPct}% context`,
267
- emoji: "ðŸĨ·",
334
+ emoji: 'ðŸĨ·',
268
335
  });
269
336
  else if (minPct <= 40)
270
337
  achievements.push({
271
- key: "ultralight",
338
+ key: 'ultralight',
272
339
  label: `Ultralight — Manual compact at ${minPct}% context`,
273
- emoji: "ðŸŠķ",
340
+ emoji: 'ðŸŠķ',
274
341
  });
275
342
  else if (minPct <= 50)
276
343
  achievements.push({
277
- key: "traveling_light",
344
+ key: 'traveling_light',
278
345
  label: `Traveling Light — Manual compact at ${minPct}% context`,
279
- emoji: "🎒",
346
+ emoji: '🎒',
280
347
  });
281
348
  }
282
349
 
@@ -284,47 +351,277 @@ export function calculateAchievements(run) {
284
351
  const ti = run.thinkingInvocations;
285
352
  if (ti > 0) {
286
353
  achievements.push({
287
- key: "spell_cast",
354
+ key: 'spell_cast',
288
355
  label: `Spell Cast — Used extended thinking (${ti}×)`,
289
- emoji: "ðŸ”Ū",
356
+ emoji: 'ðŸ”Ū',
290
357
  });
291
358
  if (pct !== null && pct <= 0.25)
292
359
  achievements.push({
293
- key: "calculated_risk",
294
- label: "Calculated Risk — Ultrathink + LEGENDARY efficiency",
295
- emoji: "🧠",
360
+ key: 'calculated_risk',
361
+ label: 'Calculated Risk — Ultrathink + LEGENDARY efficiency',
362
+ emoji: 'ðŸ§Ū',
296
363
  });
297
364
  if (ti >= 3)
298
365
  achievements.push({
299
- key: "deep_thinker",
366
+ key: 'deep_thinker',
300
367
  label: `Deep Thinker — ${ti} ultrathink invocations, completed`,
301
- emoji: "🌀",
368
+ emoji: '🌀',
302
369
  });
303
370
  }
304
371
  // Silent Run: thinking was tracked (field exists), zero invocations, SOLID or better, completed
305
372
  if (run.thinkingInvocations === 0 && pct !== null && pct <= 0.75)
306
373
  achievements.push({
307
- key: "silent_run",
308
- label: "Silent Run — No extended thinking, completed under budget",
309
- emoji: "ðŸĪŦ",
374
+ key: 'silent_run',
375
+ label: 'Silent Run — No extended thinking, completed under budget',
376
+ emoji: 'ðŸĪŦ',
310
377
  });
311
378
 
379
+ // Paladin planning-ratio achievements
380
+ if (mc === MODEL_CLASSES.opusplan) {
381
+ const opusPct = getOpusPct(run.modelBreakdown, run.spent);
382
+ if (opusPct !== null) {
383
+ if (opusPct > 60)
384
+ achievements.push({
385
+ key: 'architect',
386
+ label: `Architect — Opus handled ${opusPct}% of cost (heavy planner)`,
387
+ emoji: '🏛ïļ',
388
+ });
389
+ if (opusPct < 25)
390
+ achievements.push({
391
+ key: 'blitz',
392
+ label: `Blitz — Opus handled only ${opusPct}% of cost (light plan, fast execution)`,
393
+ emoji: 'ðŸ’Ļ',
394
+ });
395
+ if (opusPct >= 40 && opusPct <= 60)
396
+ achievements.push({
397
+ key: 'equilibrium',
398
+ label: `Equilibrium — Opus and Sonnet balanced at ${opusPct}% / ${100 - opusPct}%`,
399
+ emoji: '⚖ïļ',
400
+ });
401
+ }
402
+ }
403
+
404
+ // Model-switching achievements (skip for Paladin — multi-model is by design)
405
+ const switches = run.modelSwitches ?? 0;
406
+ const distinct = run.distinctModels ?? 0;
407
+ if (!isPaladin) {
408
+ if (distinct === 1)
409
+ achievements.push({
410
+ key: 'purist',
411
+ label: 'Purist — Single model family throughout',
412
+ emoji: '🔷',
413
+ });
414
+ if (distinct >= 2 && pct !== null && pct < 1.0)
415
+ achievements.push({
416
+ key: 'chameleon',
417
+ label: `Chameleon — ${distinct} model families used, completed under budget`,
418
+ emoji: 'ðŸĶŽ',
419
+ });
420
+ if (switches === 1 && pct !== null && pct < 1.0)
421
+ achievements.push({
422
+ key: 'tactical_switch',
423
+ label: 'Tactical Switch — Exactly 1 model switch, completed under budget',
424
+ emoji: '🔀',
425
+ });
426
+ if (switches === 0 && distinct <= 1)
427
+ achievements.push({
428
+ key: 'committed',
429
+ label: 'Committed — No model switches, one model family',
430
+ emoji: '🔒',
431
+ });
432
+
433
+ // Class Defection: declared one class but leaned heavily on another
434
+ if (run.modelBreakdown && run.spent > 0) {
435
+ const declared = (run.model || '').toLowerCase();
436
+ const isHaikuRun = declared.includes('haiku');
437
+ const isSonnetRun = declared.includes('sonnet') && !declared.includes('opus');
438
+ const opusPct2 = getOpusPct(run.modelBreakdown, run.spent) ?? 0;
439
+ const haikuPct2 = getHaikuPct(run.modelBreakdown, run.spent) ?? 0;
440
+ const nonHaikuPct = 100 - haikuPct2;
441
+ if (isHaikuRun && nonHaikuPct > 50)
442
+ achievements.push({
443
+ key: 'class_defection',
444
+ label: `Class Defection — Declared Haiku but ${nonHaikuPct}% cost on heavier models`,
445
+ emoji: '⚠ïļ',
446
+ });
447
+ else if (isSonnetRun && opusPct2 > 40)
448
+ achievements.push({
449
+ key: 'class_defection',
450
+ label: `Class Defection — Declared Sonnet but ${opusPct2}% cost on Opus`,
451
+ emoji: '⚠ïļ',
452
+ });
453
+ }
454
+ }
455
+
312
456
  // Multi-model achievements based on Haiku usage ratio
313
457
  const haikuPct = getHaikuPct(run.modelBreakdown, run.spent);
314
458
  if (haikuPct !== null) {
315
459
  if (haikuPct >= 50)
316
460
  achievements.push({
317
- key: "frugal",
461
+ key: 'frugal',
318
462
  label: `Frugal — Haiku handled ${haikuPct}% of session cost`,
319
- emoji: "ðŸđ",
463
+ emoji: 'ðŸđ',
320
464
  });
321
465
  if (haikuPct >= 75)
322
466
  achievements.push({
323
- key: "rogue_run",
467
+ key: 'rogue_run',
324
468
  label: `Rogue Run — Haiku handled ${haikuPct}% of session cost`,
325
- emoji: "ðŸŽē",
469
+ emoji: 'ðŸŽē',
326
470
  });
327
471
  }
328
472
 
473
+ // Prompting skill achievements
474
+ const promptCount = run.promptCount || 0;
475
+ const totalToolCalls = run.totalToolCalls || 0;
476
+ if (promptCount === 1)
477
+ achievements.push({
478
+ key: 'one_shot',
479
+ label: 'One Shot — Completed in a single prompt',
480
+ emoji: 'ðŸĨŠ',
481
+ });
482
+ if (promptCount >= 20)
483
+ achievements.push({
484
+ key: 'conversationalist',
485
+ label: `Conversationalist — ${promptCount} prompts`,
486
+ emoji: '💎',
487
+ });
488
+ if (promptCount <= 3 && totalToolCalls >= 10)
489
+ achievements.push({
490
+ key: 'terse',
491
+ label: `Terse — ${promptCount} prompts, ${totalToolCalls} tool calls`,
492
+ emoji: 'ðŸĪ',
493
+ });
494
+ if (promptCount >= 15 && totalToolCalls / promptCount < 1)
495
+ achievements.push({
496
+ key: 'backseat_driver',
497
+ label: 'Backseat Driver — Many prompts, few tool calls',
498
+ emoji: '🊑',
499
+ });
500
+ if (promptCount >= 2 && totalToolCalls / promptCount >= 5)
501
+ achievements.push({
502
+ key: 'high_leverage',
503
+ label: `High Leverage — ${(totalToolCalls / promptCount).toFixed(1)}× tools per prompt`,
504
+ emoji: '🏗ïļ',
505
+ });
506
+
507
+ // Tool mastery achievements
508
+ const toolCalls = run.toolCalls || {};
509
+ const editCount = toolCalls['Edit'] || 0;
510
+ const writeCount = toolCalls['Write'] || 0;
511
+ const readCount = toolCalls['Read'] || 0;
512
+ const bashCount = toolCalls['Bash'] || 0;
513
+ const distinctTools = Object.keys(toolCalls).filter((k) => toolCalls[k] > 0).length;
514
+
515
+ if (editCount === 0 && writeCount === 0 && readCount >= 1)
516
+ achievements.push({ key: 'read_only', label: 'Read Only — No edits or writes', emoji: '👁ïļ' });
517
+ if (editCount >= 10)
518
+ achievements.push({ key: 'editor', label: `Editor — ${editCount} Edit calls`, emoji: '✏ïļ' });
519
+ if (bashCount >= 10 && totalToolCalls >= 1 && bashCount / totalToolCalls >= 0.5)
520
+ achievements.push({
521
+ key: 'bash_warrior',
522
+ label: `Bash Warrior — ${bashCount} Bash calls (${Math.round((bashCount / totalToolCalls) * 100)}% of tools)`,
523
+ emoji: '🐚',
524
+ });
525
+ if (totalToolCalls >= 5 && readCount / totalToolCalls >= 0.6)
526
+ achievements.push({
527
+ key: 'scout',
528
+ label: `Scout — ${Math.round((readCount / totalToolCalls) * 100)}% Read calls`,
529
+ emoji: '🔍',
530
+ });
531
+ if (editCount >= 1 && editCount <= 3 && pct !== null && pct < 1.0)
532
+ achievements.push({
533
+ key: 'surgeon',
534
+ label: `Surgeon — Only ${editCount} Edit call${editCount > 1 ? 's' : ''}, under budget`,
535
+ emoji: '🔊',
536
+ });
537
+ if (distinctTools >= 5)
538
+ achievements.push({
539
+ key: 'toolbox',
540
+ label: `Toolbox — ${distinctTools} distinct tools used`,
541
+ emoji: '🧰',
542
+ });
543
+
544
+ // Cost per prompt
545
+ if (promptCount >= 3) {
546
+ const costPerPrompt = run.spent / promptCount;
547
+ if (costPerPrompt < 0.01)
548
+ achievements.push({
549
+ key: 'cheap_shots',
550
+ label: `Cheap Shots — $${costPerPrompt.toFixed(4)} per prompt`,
551
+ emoji: 'ðŸ’ē',
552
+ });
553
+ if (costPerPrompt >= 0.5)
554
+ achievements.push({
555
+ key: 'expensive_taste',
556
+ label: `Expensive Taste — $${costPerPrompt.toFixed(2)} per prompt`,
557
+ emoji: '🍷',
558
+ });
559
+ }
560
+
561
+ // Time-based achievements
562
+ if (run.startedAt && run.endedAt) {
563
+ const elapsedMs = new Date(run.endedAt).getTime() - new Date(run.startedAt).getTime();
564
+ const elapsedMin = elapsedMs / 60000;
565
+ if (elapsedMin < 5)
566
+ achievements.push({
567
+ key: 'speedrun',
568
+ label: `Speedrun — Completed in ${Math.round(elapsedMin * 60)}s`,
569
+ emoji: '⏱ïļ',
570
+ });
571
+ if (elapsedMin > 60 && elapsedMin <= 180)
572
+ achievements.push({
573
+ key: 'marathon',
574
+ label: `Marathon — ${Math.round(elapsedMin)}m session`,
575
+ emoji: '🏃',
576
+ });
577
+ if (elapsedMin > 180)
578
+ achievements.push({
579
+ key: 'endurance',
580
+ label: `Endurance — ${Math.round(elapsedMin / 60)}h session`,
581
+ emoji: 'ðŸŦ ',
582
+ });
583
+ }
584
+
585
+ // Phase 2: new hook fields (default 0 if not present)
586
+ const failedToolCalls = run.failedToolCalls ?? 0;
587
+ const subagentSpawns = run.subagentSpawns ?? 0;
588
+ const turnCount = run.turnCount ?? 0;
589
+
590
+ // Failed tool call achievements
591
+ if (failedToolCalls === 0 && totalToolCalls >= 5)
592
+ achievements.push({ key: 'clean_run', label: 'Clean Run — No tool failures', emoji: '✅' });
593
+ if (failedToolCalls >= 10)
594
+ achievements.push({
595
+ key: 'stubborn',
596
+ label: `Stubborn — ${failedToolCalls} failed tool calls, still won`,
597
+ emoji: '🐂',
598
+ });
599
+
600
+ // Subagent achievements
601
+ if (subagentSpawns === 0)
602
+ achievements.push({ key: 'lone_wolf', label: 'Lone Wolf — No subagents spawned', emoji: '🐚' });
603
+ if (subagentSpawns >= 5)
604
+ achievements.push({
605
+ key: 'summoner',
606
+ label: `Summoner — ${subagentSpawns} subagents spawned`,
607
+ emoji: 'ðŸ“Ą',
608
+ });
609
+ if (subagentSpawns >= 10 && pct !== null && pct < 0.5)
610
+ achievements.push({
611
+ key: 'army',
612
+ label: `Army of One — ${subagentSpawns} subagents, EFFICIENT cost`,
613
+ emoji: '🊖',
614
+ });
615
+
616
+ // Turn count achievements
617
+ if (promptCount >= 2 && turnCount >= 1 && turnCount / promptCount >= 3)
618
+ achievements.push({
619
+ key: 'agentic',
620
+ label: `Agentic — ${(turnCount / promptCount).toFixed(1)} turns per prompt`,
621
+ emoji: 'ðŸĪ–',
622
+ });
623
+ if (promptCount >= 3 && turnCount === promptCount)
624
+ achievements.push({ key: 'obedient', label: 'Obedient — One turn per prompt', emoji: '🐕' });
625
+
329
626
  return achievements;
330
627
  }