diff --git a/docs/superpowers/specs/2026-06-03-weekly-report-design.md b/docs/superpowers/specs/2026-06-03-weekly-report-design.md new file mode 100644 index 0000000..e2abbb5 --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-weekly-report-design.md @@ -0,0 +1,193 @@ +# Weekly Report — Design + +**Date:** 2026-06-03 +**Status:** Approved (pending spec review) + +## Goal + +Generate a short, standup-focused report of what the user did over the past week, +for the Wednesday standup. The report is built from the user's Claude Code session +history across all repos, distilled and summarized by Claude. Personal repos under a +configurable excluded path (default `C:\Private`) are left out. The user can author +per-day bullet notes inside ClaudeDo (via the My Day list) that are folded into the +report. + +## Decisions (from brainstorming) + +- **Data source:** all Claude Code history in `~/.claude/projects/*/*.jsonl`, both manual + sessions and ClaudeDo-run tasks, grouped by repo. +- **Exclusion:** a configurable list of path prefixes (default `["C:\\Private"]`). Any + session whose `cwd` starts with an excluded prefix is dropped. +- **Summarization:** Claude CLI summarizes. The Worker distills the logs, then runs a + single one-shot `claude -p` call via the existing `ClaudeProcess` and returns the + result markdown. No worktree, no task row, no queue. +- **Period:** default "since last Wednesday → today", computed from a configurable + standup weekday. The range is adjustable in the modal. +- **Signal fed to Claude:** user prompts (intent), assistant closing summaries, and the + user's daily notes. No git-commit scanning. +- **Placement:** a "Weekly Report" overlay modal opened from the toolbar, rendering via + the existing `MarkdownView`. +- **Output:** view-only in-app (no export). +- **Notes UI:** authored in the My Day list via a pinned non-task "Notes" pseudo-row that + repurposes the Details island into a bullet-notes editor. Per-day bullets with a day + navigator (prev/next arrows + date picker + Today). +- **Report persistence:** generated reports are stored, keyed by exact date range, and + reused. Generation is button-driven (never automatic); a Regenerate button overwrites. + +## Architecture Overview + +``` +UI (WeeklyReportModal, Details-island notes mode) + │ SignalR + ▼ +WorkerHub ── GetWeekReport / GenerateWeekReport / daily-notes CRUD + │ + ├── WeekReportService ──► ClaudeHistoryReader (scan ~/.claude/projects) + │ │ (distilled activity) + │ ├── DailyNoteRepository (notes in window) + │ ├── ClaudeProcess (one-shot summarize) + │ └── WeekReportRepository (store/reuse) + └── DailyNoteRepository (CRUD) + +Data: DailyNoteEntity, WeekReportEntity + repositories + EF migration + AppSettingsEntity: ReportExcludedPaths, StandupWeekday +``` + +## Components + +### 1. Data layer (`ClaudeDo.Data`) + +**`DailyNoteEntity`** (table `daily_notes`) +- `Id` (GUID string, init-only PK) +- `Date` (date-only; the day the bullet belongs to) +- `Text` (string, the bullet content) +- `SortOrder` (int; ordering within a day) +- `CreatedAt` (DateTime) + +**`DailyNoteRepository`** (async, CancellationToken, follows existing repo pattern) +- `ListByDayAsync(DateOnly day)` — bullets for one day, ordered by `SortOrder`. +- `ListBetweenAsync(DateOnly start, DateOnly end)` — bullets in a window (used by the report). +- `AddAsync(DateOnly day, string text)` — appends a bullet (assigns next `SortOrder`). +- `UpdateAsync(string id, string text)` +- `DeleteAsync(string id)` + +**`WeekReportEntity`** (table `week_reports`) +- `Id` (GUID string, init-only PK) +- `StartDate`, `EndDate` (date-only; the report window — unique together) +- `Markdown` (string; the generated report) +- `GeneratedAt` (DateTime) + +**`WeekReportRepository`** +- `GetByRangeAsync(DateOnly start, DateOnly end)` — stored report for an exact range, or null. +- `UpsertAsync(DateOnly start, DateOnly end, string markdown)` — insert or overwrite by range. + +**`AppSettingsEntity`** — two new columns: +- `ReportExcludedPaths` (string, JSON array of path prefixes; default `["C:\\Private"]`) +- `StandupWeekday` (int, `DayOfWeek`; default `Wednesday` = 3) + +**Migration** — one EF migration adds `daily_notes`, `week_reports`, and the two +`app_settings` columns. Entity configs in `Configuration/` (date-only and enum/JSON +conversion via `ValueConverter`, per existing convention). + +### 2. Worker (`ClaudeDo.Worker`) — new `Report/` folder + +**`ClaudeHistoryReader`** (raw → distilled) +- Input: date window + excluded path prefixes. +- Enumerates `~/.claude/projects/*/*.jsonl`. +- Parses each line as JSON; tolerant of malformed lines (skip, never throw). +- Drops a session entirely if its `cwd` starts with any excluded prefix + (case-insensitive, normalized separators). +- Keeps messages whose `timestamp` falls in `[start, end]`. +- Extracts, per repo (`cwd`) → per day: + - **user prompts**: `type == "user"` text content (string or `content[].text`). + Skip tool-result-only user turns and queue/attachment/hook noise. + - **assistant closing summaries**: the final assistant text block of each turn/session. +- Output: a structured model, e.g. + `IReadOnlyList` where `RepoActivity { RepoPath, Days: List }`. + +**`WeekReportService`** (distilled → stored summary) +- `GenerateAsync(start, end, ct)`: + 1. Read settings (excluded paths, standup weekday). + 2. `ClaudeHistoryReader` → distilled activity. + 3. `DailyNoteRepository.ListBetweenAsync` → notes grouped by day. + 4. Build one prompt: instructions for a short, standup-focused summary grouped by + repo and day, plus the distilled activity and the daily notes. Empty window → + produce a "no activity" report without calling Claude. + 5. Run `ClaudeProcess` once (`claude -p`, no worktree/agents; working dir = a neutral + dir). Read `RunResult.ResultMarkdown`. + 6. `WeekReportRepository.UpsertAsync(start, end, markdown)`; return markdown. + 7. On Claude failure, surface `RunResult.ErrorMarkdown` to the caller (do not store). +- `GetStoredAsync(start, end)` → `WeekReportRepository.GetByRangeAsync`. + +Interfaces live in `Report/Interfaces/` per the area convention. + +### 3. IPC (Hub + WorkerClient) + +**`WorkerHub`** new methods: +- `GetWeekReport(string startIso, string endIso)` → stored markdown or null. +- `GenerateWeekReport(string startIso, string endIso)` → generates, stores, returns markdown. +- `GetDailyNotes(string dayIso)` → bullets for a day. +- `AddDailyNote(string dayIso, string text)` → created bullet. +- `UpdateDailyNote(string id, string text)`. +- `DeleteDailyNote(string id)`. + +**`WorkerClient`** (UI) mirrors these, following the existing +`WorkerPrimeScheduleApi`/AppSettings method pattern. + +### 4. UI (`ClaudeDo.Ui`) + +**Weekly Report modal** (`WeeklyReportModalView` + `WeeklyReportModalViewModel`) +- Overlay modal in the `Modals/` pattern (like `WorktreesOverviewModalView`), + registered in `IslandsShellViewModel`, opened from a new toolbar button. +- Date range: two `ThemedDatePicker`s, default "since last Wednesday → today" computed + from `StandupWeekday`. +- On open and on range change: call `GetWeekReport`. + - Stored report exists → render markdown via `MarkdownView`, show `GeneratedAt`, show + a **Regenerate** button. + - None → empty state ("Not generated yet") + a **Generate** button. +- **Generate**/**Regenerate**: call `GenerateWeekReport` with a busy/spinner state; + render the returned markdown. Generation only ever runs from these buttons. +- View-only; no export. + +**Notes in My Day** +- The My Day smart list (`smart:my-day`) pins a fixed, non-task "Notes" pseudo-row at + the top, recognized by the list/selection code (not a `TaskEntity`). +- Selecting it puts the **Details island** into **notes mode** (task fields hidden, + notes editor shown). The island hosts a dedicated `NotesEditorViewModel` + small view + rather than swelling `DetailsIslandViewModel` (already ~978 lines); the bullet logic + stays isolated and testable. +- **Day navigator** in the editor header: `<` / `>` arrows to step days, a + `ThemedDatePicker` to jump to any date, and a "Today" button. Defaults to today; the + pinned row's default day rolls over at midnight (no data lost — past days remain + reachable via the navigator). +- **Bullet editing** for the selected day: list of bullets with add / inline-edit / + delete / reorder (`SortOrder`). Each operation goes through the daily-notes hub CRUD. + +### 5. Settings + +- Add the excluded-path list and the standup weekday to the existing Settings modal, + persisted via the new `app_settings` columns and the existing + `GetAppSettings`/`UpdateAppSettings` path. + +## Error Handling + +- Malformed/unreadable JSONL lines are skipped, never fatal. +- Empty window → a "no activity" report, no Claude call. +- Claude call failure → error surfaced in the modal; nothing stored. +- Date ranges normalized to date-only; the stored report key is the exact (start, end). + +## Testing + +- **`ClaudeHistoryReader`** (Worker tests, fixture `.jsonl`): date-window filtering, + excluded-prefix dropping (case/separator normalization), prompt/summary extraction, + malformed-line tolerance, repo/day grouping. +- **`WeekReportService`**: prompt-building from distilled activity + notes; empty-window + short-circuit; storage upsert; with a faked `ClaudeProcess`. +- **`DailyNoteRepository`** and **`WeekReportRepository`**: CRUD / upsert / range lookup + against real SQLite (matches existing test style). + +## Out of Scope + +- Report export (clipboard/file) — view-only for now. +- Git-commit scanning. +- Editing or summarizing full transcripts; only prompts + closing summaries are used.