this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(wins): add tiny wins tracking system (M4)

Add wins table and tools for tracking small accomplishments:
- record_tiny_win: Save wins with category/magnitude, get celebratory feedback
- get_wins_summary: View win history, streaks, and patterns

Helps users with ADHD celebrate small victories and build momentum.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice d42dfb94 ab0e5dbb

+480 -4
+4 -4
.beads/issues.jsonl
··· 9 9 {"id":"assistant-97y.2","title":"record_deviation tool (src/tools/context.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:07.642333Z","updated_at":"2025-12-11T13:45:07.642333Z","dependencies":[{"issue_id":"assistant-97y.2","depends_on_id":"assistant-97y","type":"parent-child","created_at":"2025-12-11T13:45:07.642793Z","created_by":"daemon"}]} 10 10 {"id":"assistant-97y.3","title":"Deviations table","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:09.220873Z","updated_at":"2025-12-11T13:45:09.220873Z","dependencies":[{"issue_id":"assistant-97y.3","depends_on_id":"assistant-97y","type":"parent-child","created_at":"2025-12-11T13:45:09.223162Z","created_by":"daemon"}]} 11 11 {"id":"assistant-97y.4","title":"What was I doing? context recall","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:18.160344Z","updated_at":"2025-12-11T13:45:18.160344Z","dependencies":[{"issue_id":"assistant-97y.4","depends_on_id":"assistant-97y","type":"parent-child","created_at":"2025-12-11T13:45:18.161411Z","created_by":"daemon"}]} 12 - {"id":"assistant-m8g","title":"M4: Tiny Wins","description":"","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-11T13:43:38.033993Z","updated_at":"2025-12-11T13:43:38.033993Z","dependencies":[{"issue_id":"assistant-m8g","depends_on_id":"assistant-nno","type":"blocks","created_at":"2025-12-11T13:43:53.082473Z","created_by":"daemon"}]} 13 - {"id":"assistant-m8g.1","title":"record_tiny_win tool (src/tools/wins.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:03.571473Z","updated_at":"2025-12-11T13:45:03.571473Z","dependencies":[{"issue_id":"assistant-m8g.1","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:03.571956Z","created_by":"daemon"}]} 14 - {"id":"assistant-m8g.2","title":"get_wins_summary tool (src/tools/wins.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:04.336122Z","updated_at":"2025-12-11T13:45:04.336122Z","dependencies":[{"issue_id":"assistant-m8g.2","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:04.336616Z","created_by":"daemon"}]} 15 - {"id":"assistant-m8g.3","title":"Wins database table","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:05.347711Z","updated_at":"2025-12-11T13:45:05.347711Z","dependencies":[{"issue_id":"assistant-m8g.3","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:05.348177Z","created_by":"daemon"}]} 12 + {"id":"assistant-m8g","title":"M4: Tiny Wins","description":"","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-11T13:43:38.033993Z","updated_at":"2025-12-12T13:18:50.071071Z","closed_at":"2025-12-12T13:18:50.071071Z","dependencies":[{"issue_id":"assistant-m8g","depends_on_id":"assistant-nno","type":"blocks","created_at":"2025-12-11T13:43:53.082473Z","created_by":"daemon"}]} 13 + {"id":"assistant-m8g.1","title":"record_tiny_win tool (src/tools/wins.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:03.571473Z","updated_at":"2025-12-12T13:18:44.514136Z","closed_at":"2025-12-12T13:18:44.514136Z","dependencies":[{"issue_id":"assistant-m8g.1","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:03.571956Z","created_by":"daemon"}]} 14 + {"id":"assistant-m8g.2","title":"get_wins_summary tool (src/tools/wins.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:04.336122Z","updated_at":"2025-12-12T13:18:44.515174Z","closed_at":"2025-12-12T13:18:44.515174Z","dependencies":[{"issue_id":"assistant-m8g.2","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:04.336616Z","created_by":"daemon"}]} 15 + {"id":"assistant-m8g.3","title":"Wins database table","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:05.347711Z","updated_at":"2025-12-12T13:16:41.997335Z","closed_at":"2025-12-12T13:16:41.997335Z","dependencies":[{"issue_id":"assistant-m8g.3","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:05.348177Z","created_by":"daemon"}]} 16 16 {"id":"assistant-nno","title":"M2: Tools + Items","description":"","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-12-11T13:43:35.019588Z","updated_at":"2025-12-11T16:40:05.645575Z","closed_at":"2025-12-11T16:40:05.645575Z","dependencies":[{"issue_id":"assistant-nno","depends_on_id":"assistant-pqh","type":"blocks","created_at":"2025-12-11T13:43:50.878442Z","created_by":"daemon"}]} 17 17 {"id":"assistant-nno.1","title":"Database schema + Drizzle setup (src/db/)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:37.136609Z","updated_at":"2025-12-11T16:27:05.155748Z","closed_at":"2025-12-11T16:27:05.155748Z","dependencies":[{"issue_id":"assistant-nno.1","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:37.137113Z","created_by":"daemon"}]} 18 18 {"id":"assistant-nno.2","title":"Tool dispatcher (src/tools/dispatcher.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:38.375255Z","updated_at":"2025-12-11T16:27:27.073341Z","closed_at":"2025-12-11T16:27:27.073341Z","dependencies":[{"issue_id":"assistant-nno.2","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:38.375743Z","created_by":"daemon"}]}
+8
src/db/migrations/0001_add_wins_table.sql
··· 1 + CREATE TABLE `wins` ( 2 + `id` text PRIMARY KEY NOT NULL, 3 + `user_id` integer NOT NULL, 4 + `content` text NOT NULL, 5 + `category` text DEFAULT 'other' NOT NULL, 6 + `magnitude` text DEFAULT 'tiny' NOT NULL, 7 + `created_at` integer DEFAULT (unixepoch()) NOT NULL 8 + );
+150
src/db/migrations/meta/0001_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "a8bb92b2-e828-403c-a571-37641eae863d", 5 + "prevId": "5d1f65f7-f7cd-4508-b4c2-e3675a81d1ed", 6 + "tables": { 7 + "items": { 8 + "name": "items", 9 + "columns": { 10 + "id": { 11 + "name": "id", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "user_id": { 18 + "name": "user_id", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "type": { 25 + "name": "type", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "content": { 32 + "name": "content", 33 + "type": "text", 34 + "primaryKey": false, 35 + "notNull": true, 36 + "autoincrement": false 37 + }, 38 + "status": { 39 + "name": "status", 40 + "type": "text", 41 + "primaryKey": false, 42 + "notNull": true, 43 + "autoincrement": false, 44 + "default": "'open'" 45 + }, 46 + "priority": { 47 + "name": "priority", 48 + "type": "integer", 49 + "primaryKey": false, 50 + "notNull": true, 51 + "autoincrement": false, 52 + "default": 2 53 + }, 54 + "parent_id": { 55 + "name": "parent_id", 56 + "type": "text", 57 + "primaryKey": false, 58 + "notNull": false, 59 + "autoincrement": false 60 + }, 61 + "created_at": { 62 + "name": "created_at", 63 + "type": "integer", 64 + "primaryKey": false, 65 + "notNull": true, 66 + "autoincrement": false, 67 + "default": "(unixepoch())" 68 + }, 69 + "updated_at": { 70 + "name": "updated_at", 71 + "type": "integer", 72 + "primaryKey": false, 73 + "notNull": true, 74 + "autoincrement": false, 75 + "default": "(unixepoch())" 76 + } 77 + }, 78 + "indexes": {}, 79 + "foreignKeys": {}, 80 + "compositePrimaryKeys": {}, 81 + "uniqueConstraints": {}, 82 + "checkConstraints": {} 83 + }, 84 + "wins": { 85 + "name": "wins", 86 + "columns": { 87 + "id": { 88 + "name": "id", 89 + "type": "text", 90 + "primaryKey": true, 91 + "notNull": true, 92 + "autoincrement": false 93 + }, 94 + "user_id": { 95 + "name": "user_id", 96 + "type": "integer", 97 + "primaryKey": false, 98 + "notNull": true, 99 + "autoincrement": false 100 + }, 101 + "content": { 102 + "name": "content", 103 + "type": "text", 104 + "primaryKey": false, 105 + "notNull": true, 106 + "autoincrement": false 107 + }, 108 + "category": { 109 + "name": "category", 110 + "type": "text", 111 + "primaryKey": false, 112 + "notNull": true, 113 + "autoincrement": false, 114 + "default": "'other'" 115 + }, 116 + "magnitude": { 117 + "name": "magnitude", 118 + "type": "text", 119 + "primaryKey": false, 120 + "notNull": true, 121 + "autoincrement": false, 122 + "default": "'tiny'" 123 + }, 124 + "created_at": { 125 + "name": "created_at", 126 + "type": "integer", 127 + "primaryKey": false, 128 + "notNull": true, 129 + "autoincrement": false, 130 + "default": "(unixepoch())" 131 + } 132 + }, 133 + "indexes": {}, 134 + "foreignKeys": {}, 135 + "compositePrimaryKeys": {}, 136 + "uniqueConstraints": {}, 137 + "checkConstraints": {} 138 + } 139 + }, 140 + "views": {}, 141 + "enums": {}, 142 + "_meta": { 143 + "schemas": {}, 144 + "tables": {}, 145 + "columns": {} 146 + }, 147 + "internal": { 148 + "indexes": {} 149 + } 150 + }
+7
src/db/migrations/meta/_journal.json
··· 8 8 "when": 1765470257411, 9 9 "tag": "0000_high_zemo", 10 10 "breakpoints": true 11 + }, 12 + { 13 + "idx": 1, 14 + "version": "6", 15 + "when": 1765545390738, 16 + "tag": "0001_add_wins_table", 17 + "breakpoints": true 11 18 } 12 19 ] 13 20 }
+31
src/db/schema.ts
··· 39 39 * Type for selecting items from the database 40 40 */ 41 41 export type SelectItem = typeof items.$inferSelect; 42 + 43 + /** 44 + * Wins table - stores tiny wins for positive reinforcement 45 + * 46 + * Tracks small accomplishments to build momentum and combat ADHD-related 47 + * feelings of underachievement. 48 + */ 49 + export const wins = sqliteTable('wins', { 50 + id: text('id').primaryKey().notNull(), 51 + userId: integer('user_id').notNull(), // Telegram user ID 52 + content: text('content').notNull(), // What the user accomplished 53 + category: text('category', { enum: ['task', 'habit', 'self_care', 'social', 'work', 'creative', 'other'] }) 54 + .notNull() 55 + .default('other'), 56 + magnitude: text('magnitude', { enum: ['tiny', 'small', 'medium', 'big'] }) 57 + .notNull() 58 + .default('tiny'), // How significant the win is 59 + createdAt: integer('created_at', { mode: 'timestamp' }) 60 + .notNull() 61 + .default(sql`(unixepoch())`), 62 + }); 63 + 64 + /** 65 + * Type for inserting new wins 66 + */ 67 + export type InsertWin = typeof wins.$inferInsert; 68 + 69 + /** 70 + * Type for selecting wins from the database 71 + */ 72 + export type SelectWin = typeof wins.$inferSelect;
+1
src/tools/index.ts
··· 158 158 import './capture'; 159 159 import './context'; 160 160 import './breakdown'; 161 + import './wins';
+279
src/tools/wins.ts
··· 1 + /** 2 + * Wins tools for Letta agents 3 + * 4 + * Provides tools for recording and summarizing tiny wins: 5 + * - record_tiny_win: Record a small accomplishment 6 + * - get_wins_summary: Get a summary of recent wins 7 + */ 8 + 9 + import { and, desc, eq, gte, sql } from 'drizzle-orm'; 10 + import { db, schema } from '../db'; 11 + import { registerTool, type ToolDefinition } from './dispatcher'; 12 + 13 + /** 14 + * Win categories 15 + */ 16 + export type WinCategory = 'task' | 'habit' | 'self_care' | 'social' | 'work' | 'creative' | 'other'; 17 + 18 + /** 19 + * Win magnitude levels 20 + */ 21 + export type WinMagnitude = 'tiny' | 'small' | 'medium' | 'big'; 22 + 23 + /** 24 + * Arguments for record_tiny_win tool 25 + */ 26 + export interface RecordTinyWinArgs { 27 + /** What the user accomplished */ 28 + content: string; 29 + /** Category of the win */ 30 + category?: WinCategory; 31 + /** How significant the win is */ 32 + magnitude?: WinMagnitude; 33 + } 34 + 35 + /** 36 + * Result from record_tiny_win tool 37 + */ 38 + export interface RecordTinyWinResult { 39 + /** ID of saved win */ 40 + id: string; 41 + /** Celebratory message */ 42 + message: string; 43 + /** Running total of wins today */ 44 + todayCount: number; 45 + } 46 + 47 + /** 48 + * Arguments for get_wins_summary tool 49 + */ 50 + export interface GetWinsSummaryArgs { 51 + /** Number of days to look back (default 7) */ 52 + days?: number; 53 + /** Filter by category (optional) */ 54 + category?: WinCategory; 55 + /** Maximum number of wins to return */ 56 + limit?: number; 57 + } 58 + 59 + /** 60 + * Result from get_wins_summary tool 61 + */ 62 + export interface GetWinsSummaryResult { 63 + /** Total wins in period */ 64 + totalWins: number; 65 + /** Wins grouped by category */ 66 + byCategory: Record<string, number>; 67 + /** Wins grouped by magnitude */ 68 + byMagnitude: Record<string, number>; 69 + /** Recent wins list */ 70 + recentWins: { 71 + id: string; 72 + content: string; 73 + category: string; 74 + magnitude: string; 75 + createdAt: string; 76 + }[]; 77 + /** Streak info */ 78 + streak: { 79 + currentDays: number; 80 + message: string; 81 + }; 82 + } 83 + 84 + /** 85 + * record_tiny_win tool - Record a small accomplishment 86 + */ 87 + export const recordTinyWinTool: ToolDefinition<RecordTinyWinArgs, RecordTinyWinResult> = registerTool({ 88 + name: 'record_tiny_win', 89 + description: 90 + 'Record a tiny win or small accomplishment. Use this to celebrate even the smallest victories - getting out of bed, sending an email, drinking water. Every win counts!', 91 + parameters: { 92 + type: 'object', 93 + properties: { 94 + content: { 95 + type: 'string', 96 + description: 'What the user accomplished (e.g., "Got out of bed", "Replied to that email", "Ate breakfast")', 97 + }, 98 + category: { 99 + type: 'string', 100 + enum: ['task', 'habit', 'self_care', 'social', 'work', 'creative', 'other'], 101 + description: 102 + 'Category of the win: task (completed something), habit (daily routine), self_care (health/wellness), social (people interaction), work (job related), creative (making things), other', 103 + }, 104 + magnitude: { 105 + type: 'string', 106 + enum: ['tiny', 'small', 'medium', 'big'], 107 + description: 108 + 'How significant: tiny (just did it), small (took some effort), medium (meaningful achievement), big (major milestone)', 109 + }, 110 + }, 111 + required: ['content'], 112 + }, 113 + handler: async (args, context) => { 114 + const id = crypto.randomUUID(); 115 + 116 + // Insert the win 117 + await db.insert(schema.wins).values({ 118 + id, 119 + userId: context.userId, 120 + content: args.content, 121 + category: args.category ?? 'other', 122 + magnitude: args.magnitude ?? 'tiny', 123 + }); 124 + 125 + // Count wins today 126 + const todayStart = new Date(); 127 + todayStart.setHours(0, 0, 0, 0); 128 + 129 + const todayWins = await db 130 + .select({ count: sql<number>`count(*)` }) 131 + .from(schema.wins) 132 + .where(and(eq(schema.wins.userId, context.userId), gte(schema.wins.createdAt, todayStart))); 133 + 134 + const todayCount = todayWins[0]?.count ?? 1; 135 + 136 + // Generate celebratory message based on magnitude and count 137 + const messages = { 138 + tiny: ['Nice! Every little step counts!', "That's a win!", 'You did it!'], 139 + small: ['Good job! Keep that momentum going!', "Well done! You're on a roll!"], 140 + medium: ['Impressive! That took real effort!', 'Amazing work! You should feel proud!'], 141 + big: ['WOW! That is a major accomplishment!', "Incredible! You're crushing it!"], 142 + }; 143 + 144 + const magnitude = args.magnitude ?? 'tiny'; 145 + const messageList = messages[magnitude]; 146 + const baseMessage = messageList[Math.floor(Math.random() * messageList.length)] ?? 'Great job!'; 147 + 148 + // Add streak bonus message if they have multiple wins today 149 + let streakBonus = ''; 150 + if (todayCount >= 5) { 151 + streakBonus = ` You've logged ${String(todayCount)} wins today - you're on fire!`; 152 + } else if (todayCount >= 3) { 153 + streakBonus = ` ${String(todayCount)} wins today - great momentum!`; 154 + } 155 + 156 + return { 157 + id, 158 + message: baseMessage + streakBonus, 159 + todayCount, 160 + }; 161 + }, 162 + }); 163 + 164 + /** 165 + * get_wins_summary tool - Get a summary of recent wins 166 + */ 167 + export const getWinsSummaryTool: ToolDefinition<GetWinsSummaryArgs, GetWinsSummaryResult> = registerTool({ 168 + name: 'get_wins_summary', 169 + description: 170 + 'Get a summary of recent wins to see progress and patterns. Use this to remind the user of their accomplishments when they feel down or need motivation.', 171 + parameters: { 172 + type: 'object', 173 + properties: { 174 + days: { 175 + type: 'integer', 176 + minimum: 1, 177 + maximum: 30, 178 + description: 'Number of days to look back (default 7, max 30)', 179 + }, 180 + category: { 181 + type: 'string', 182 + enum: ['task', 'habit', 'self_care', 'social', 'work', 'creative', 'other'], 183 + description: 'Filter by category (optional)', 184 + }, 185 + limit: { 186 + type: 'integer', 187 + minimum: 1, 188 + maximum: 50, 189 + description: 'Maximum number of wins to return in the list (default 10, max 50)', 190 + }, 191 + }, 192 + required: [], 193 + }, 194 + handler: async (args, context) => { 195 + const days = args.days ?? 7; 196 + const limit = args.limit ?? 10; 197 + 198 + // Calculate date range 199 + const startDate = new Date(); 200 + startDate.setDate(startDate.getDate() - days); 201 + startDate.setHours(0, 0, 0, 0); 202 + 203 + // Build where conditions 204 + const conditions = [eq(schema.wins.userId, context.userId), gte(schema.wins.createdAt, startDate)]; 205 + 206 + if (args.category) { 207 + conditions.push(eq(schema.wins.category, args.category)); 208 + } 209 + 210 + // Get all wins in period 211 + const wins = await db 212 + .select() 213 + .from(schema.wins) 214 + .where(and(...conditions)) 215 + .orderBy(desc(schema.wins.createdAt)); 216 + 217 + // Calculate by category 218 + const byCategory: Record<string, number> = {}; 219 + const byMagnitude: Record<string, number> = {}; 220 + 221 + for (const win of wins) { 222 + byCategory[win.category] = (byCategory[win.category] ?? 0) + 1; 223 + byMagnitude[win.magnitude] = (byMagnitude[win.magnitude] ?? 0) + 1; 224 + } 225 + 226 + // Calculate streak (consecutive days with at least one win) 227 + const daysWithWins = new Set<string>(); 228 + for (const win of wins) { 229 + const dateStr = win.createdAt.toISOString().split('T')[0]; 230 + if (dateStr !== undefined && dateStr !== '') { 231 + daysWithWins.add(dateStr); 232 + } 233 + } 234 + 235 + let currentStreak = 0; 236 + const today = new Date(); 237 + for (let i = 0; i < days; i++) { 238 + const checkDate = new Date(today); 239 + checkDate.setDate(today.getDate() - i); 240 + const dateStr = checkDate.toISOString().split('T')[0]; 241 + if (dateStr !== undefined && dateStr !== '' && daysWithWins.has(dateStr)) { 242 + currentStreak++; 243 + } else if (i > 0) { 244 + // Allow today to not have wins yet (they might be adding one now) 245 + break; 246 + } 247 + } 248 + 249 + // Generate streak message 250 + let streakMessage = 'Start your streak by logging wins each day!'; 251 + if (currentStreak >= 7) { 252 + streakMessage = `Amazing! ${String(currentStreak)}-day streak! You're building great habits!`; 253 + } else if (currentStreak >= 3) { 254 + streakMessage = `Nice ${String(currentStreak)}-day streak going! Keep it up!`; 255 + } else if (currentStreak >= 1) { 256 + streakMessage = `${String(currentStreak)} day${currentStreak > 1 ? 's' : ''} with wins - building momentum!`; 257 + } 258 + 259 + // Format recent wins 260 + const recentWins = wins.slice(0, limit).map((win) => ({ 261 + id: win.id, 262 + content: win.content, 263 + category: win.category, 264 + magnitude: win.magnitude, 265 + createdAt: win.createdAt.toISOString(), 266 + })); 267 + 268 + return { 269 + totalWins: wins.length, 270 + byCategory, 271 + byMagnitude, 272 + recentWins, 273 + streak: { 274 + currentDays: currentStreak, 275 + message: streakMessage, 276 + }, 277 + }; 278 + }, 279 + });