this repo has no description
0
fork

Configure Feed

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

Add new project detection with ๐Ÿ†• flag for first appearances

- Add isNewProject() and getNewProjectsForDate() DB queries
- Integrate new project detection into brag summary pipeline
- Strip date prefixes from src/tries/ project names at source
- Add isNew flag to daily summary projects for UI display
- Update CLAUDE.md with monorepo path and summary quality docs

Projects in src/tries/ often have date prefixes (2025-12-15-todo-app)
which the LLM would shorten. Now handled at the source instead of
fuzzy matching in the summarizer.

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

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

alice 5f2fc2b6 acfef719

+95 -6
+10 -1
CLAUDE.md
··· 23 23 24 24 ## Gotchas 25 25 26 - - Haiku sometimes returns malformed structured output (strings instead of arrays). The code has fallback handling for this. 26 + - **Haiku + local proxy = malformed output**: The local API proxy impersonates Claude Code, so Haiku sees `<parameter>` tags and mimics them. Fix: use `mode: 'tool'` in `generateObject()` to force actual tool calling. 27 27 - Kill any stale process on port 3456 before running `bun cli serve` 28 + - **Monorepo path detection**: Claude's path encoding is lossy (`/` โ†’ `-`), so `taper-calculator-apps-web` could mean a dashed name or nested dirs. The code probes the filesystem right-to-left to find which interpretation exists, then uses git root as canonical project. 29 + 30 + ## Summary Quality 31 + 32 + Summaries should focus on **capabilities/value**, not code artifacts: 33 + - Good: "added multi-dose scheduling (backend, frontend)" 34 + - Bad: "built dose-splitter module, extended type system, created FrequencySelector" 35 + 36 + The `(backend, frontend)` scope suffix shows breadth of work without listing every file. Keep prompts aggressive about consolidation - Haiku tends toward verbosity. 28 37 29 38 ## Commands 30 39
+13 -1
src/cli/process.ts
··· 7 7 saveDailySummary, 8 8 getDatesWithoutBragSummary, 9 9 getSessionsForDate, 10 + getNewProjectsForDate, 10 11 } from '../core/db'; 11 12 12 13 interface ProcessOptions { ··· 337 338 const sessions = getSessionsForDate(date); 338 339 if (sessions.length === 0) continue; 339 340 341 + // Find which projects are new (first appearance on this date) 342 + const newProjectPaths = getNewProjectsForDate(date); 343 + const newProjectNames = new Set( 344 + sessions 345 + .filter((s) => newProjectPaths.includes(s.project_path)) 346 + .map((s) => s.project_name) 347 + ); 348 + 340 349 if (verbose) { 341 350 console.log(` Generating brag summary for ${date} (${sessions.length} sessions)`); 351 + if (newProjectNames.size > 0) { 352 + console.log(` New projects: ${[...newProjectNames].join(', ')}`); 353 + } 342 354 } 343 355 344 - const bragSummary = await generateDailyBragSummary(date, sessions); 356 + const bragSummary = await generateDailyBragSummary(date, sessions, newProjectNames); 345 357 const projectNames = [...new Set(sessions.map((s) => s.project_name))]; 346 358 347 359 saveDailySummary(date, bragSummary, projectNames, sessions.length);
+29
src/core/db.ts
··· 279 279 280 280 return rows.map((r) => r.date); 281 281 } 282 + 283 + /** 284 + * Check if a project is new (no sessions before the given date) 285 + */ 286 + export function isNewProject(projectPath: string, beforeDate: string): boolean { 287 + const database = getDb(); 288 + const row = database.query<{ count: number }, [string, string]>( 289 + 'SELECT COUNT(*) as count FROM session_summaries WHERE project_path = ? AND date < ?' 290 + ).get(projectPath, beforeDate); 291 + 292 + return (row?.count || 0) === 0; 293 + } 294 + 295 + /** 296 + * Get all new projects for a given date (first appearance) 297 + */ 298 + export function getNewProjectsForDate(date: string): string[] { 299 + const database = getDb(); 300 + 301 + // Get all projects that appear on this date 302 + const projectsOnDate = database.query<{ project_path: string }, [string]>( 303 + 'SELECT DISTINCT project_path FROM session_summaries WHERE date = ?' 304 + ).all(date); 305 + 306 + // Filter to those with no prior sessions 307 + return projectsOnDate 308 + .filter((p) => isNewProject(p.project_path, date)) 309 + .map((p) => p.project_path); 310 + }
+20 -2
src/core/session-detector.ts
··· 113 113 * -> git root: /Users/USERNAME/src/a/taper-calculator 114 114 * -> name: taper-calculator 115 115 */ 116 + /** 117 + * Strip date prefix from project names (common in src/tries/ experiments). 118 + * Pattern: "2025-12-15-todo-calendar-adhd" โ†’ "todo-calendar-adhd" 119 + */ 120 + function stripDatePrefix(name: string): string { 121 + return name.replace(/^\d{4}-\d{2}-\d{2}-/, ''); 122 + } 123 + 116 124 export function decodeProjectFolder(folderName: string): { path: string; name: string } { 117 125 // Remove leading dash 118 126 const withoutLeading = folderName.slice(1); ··· 122 130 const srcTriesMatch = withoutLeading.match(/^(Users-[^-]+-src-tries)-(.+)$/); 123 131 124 132 let decodedPath: string; 133 + let isTriesProject = false; 125 134 126 135 if (srcAMatch) { 127 136 const basePath = '/' + srcAMatch[1].replace(/-/g, '/'); ··· 131 140 const basePath = '/' + srcTriesMatch[1].replace(/-/g, '/'); 132 141 const projectPart = srcTriesMatch[2]; 133 142 decodedPath = resolveProjectPath(basePath, projectPart); 143 + isTriesProject = true; 134 144 } else { 135 145 // Fallback: just replace all dashes with slashes 136 146 decodedPath = '/' + withoutLeading.replace(/-/g, '/'); ··· 146 156 if (existsSync(decodedPath)) { 147 157 const gitRoot = findGitRoot(decodedPath); 148 158 if (gitRoot) { 149 - const projectName = gitRoot.split('/').pop() || 'unknown'; 159 + let projectName = gitRoot.split('/').pop() || 'unknown'; 160 + // Strip date prefix from tries projects 161 + if (isTriesProject) { 162 + projectName = stripDatePrefix(projectName); 163 + } 150 164 return { path: gitRoot, name: projectName }; 151 165 } 152 166 } 153 167 154 168 // No git root found - use the decoded path as-is 155 - const projectName = decodedPath.split('/').pop() || 'unknown'; 169 + let projectName = decodedPath.split('/').pop() || 'unknown'; 170 + // Strip date prefix from tries projects 171 + if (isTriesProject) { 172 + projectName = stripDatePrefix(projectName); 173 + } 156 174 return { path: decodedPath, name: projectName }; 157 175 } 158 176
+23 -2
src/core/summarizer.ts
··· 99 99 .describe('List of projects with brief outcome summaries including scope'), 100 100 }); 101 101 102 + // Extended schema with isNew flag (added post-LLM) 103 + interface DailySummaryWithNew { 104 + projects: Array<{ 105 + name: string; 106 + summary: string; 107 + isNew?: boolean; 108 + }>; 109 + } 110 + 102 111 export type DailySummary = z.infer<typeof dailySummarySchema>; 103 112 104 113 /** ··· 107 116 */ 108 117 export async function generateDailyBragSummary( 109 118 date: string, 110 - sessions: DBSessionSummary[] 119 + sessions: DBSessionSummary[], 120 + newProjectNames: Set<string> = new Set() 111 121 ): Promise<string> { 112 122 if (sessions.length === 0) { 113 123 return JSON.stringify({ projects: [] }); ··· 164 174 mode: 'tool', // Force tool use mode for reliable structured output 165 175 }); 166 176 167 - return JSON.stringify(object); 177 + // Add isNew flag to projects that are first-time appearances 178 + const isNewProject = (name: string): boolean => newProjectNames.has(name); 179 + 180 + const result: DailySummaryWithNew = { 181 + projects: object.projects.map((p) => ({ 182 + ...p, 183 + isNew: isNewProject(p.name) || undefined, 184 + })), 185 + }; 186 + 187 + return JSON.stringify(result); 168 188 } catch (error) { 169 189 console.error('Brag summary error:', (error as Error).message); 170 190 ··· 172 192 const projects = Array.from(accomplishmentsByProject.keys()).map(name => ({ 173 193 name, 174 194 summary: 'Session details unavailable', 195 + isNew: newProjectNames.has(name) || undefined, 175 196 })); 176 197 return JSON.stringify({ projects }); 177 198 }