From 899ccbc9a7194d9abb461c0f069e9b47b88e9c31 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 16 Feb 2026 02:45:09 -0800 Subject: [PATCH 1/9] Use Friday date in report filenames, derive boss report from existing team report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add FridayOfWeek() helper to compute Friday from the week's Monday - Change report filenames and email subjects from Monday to Friday date - When generating boss report, skip LLM pipeline if team report already exists for the same week — read, parse, and re-render as boss mode instead - Update findLatestReportBefore to match new Friday-based filenames - Fix loadSectionOptionsForModal to use Monday from ReportWeekRange Co-Authored-By: Claude Opus 4.6 --- models.go | 5 ++++ report_builder.go | 3 ++- slack.go | 69 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/models.go b/models.go index e92f082..3356828 100644 --- a/models.go +++ b/models.go @@ -66,6 +66,11 @@ func CurrentWeekRangeAt(now time.Time) (time.Time, time.Time) { return monday, nextMonday } +// FridayOfWeek returns the Friday of the same week as the given Monday. +func FridayOfWeek(monday time.Time) time.Time { + return monday.AddDate(0, 0, 4) +} + func ReportWeekRange(cfg Config, now time.Time) (time.Time, time.Time) { hour, min, err := parseClock(cfg.MondayCutoffTime) if err != nil { diff --git a/report_builder.go b/report_builder.go index c969830..289d3c8 100644 --- a/report_builder.go +++ b/report_builder.go @@ -145,7 +145,8 @@ func findLatestReportBefore(outputDir, teamName string, reportDate time.Time) (s } prefix := teamName + "_" - currentFile := fmt.Sprintf("%s_%s.md", teamName, reportDate.Format("20060102")) + friday := FridayOfWeek(reportDate) + currentFile := fmt.Sprintf("%s_%s.md", teamName, friday.Format("20060102")) type candidate struct { path string date time.Time diff --git a/slack.go b/slack.go index 3e4e591..3079574 100644 --- a/slack.go +++ b/slack.go @@ -357,6 +357,66 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S log.Printf("generate-report mode=%s", mode) monday, nextMonday := ReportWeekRange(cfg, time.Now().In(cfg.Location)) + friday := FridayOfWeek(monday) + + // Boss mode shortcut: derive from existing team report if available. + if mode == "boss" { + teamReportFile := fmt.Sprintf("%s_%s.md", cfg.TeamName, friday.Format("20060102")) + teamReportPath := filepath.Join(cfg.ReportOutputDir, teamReportFile) + if content, readErr := os.ReadFile(teamReportPath); readErr == nil && len(content) > 0 { + log.Printf("generate-report boss: deriving from existing team report %s", teamReportPath) + template := parseTemplate(string(content)) + stripCurrentTeamTitleFromPrefix(template, cfg.TeamName) + bossReport := renderBossMarkdown(template) + filePath, err := WriteEmailDraftFile(bossReport, cfg.ReportOutputDir, friday, cfg.TeamName) + if err != nil { + log.Printf("Error writing boss report file: %v", err) + postEphemeral(api, cmd, fmt.Sprintf("Error writing report file: %v", err)) + return + } + fileTitle := fmt.Sprintf("%s report email draft", cfg.TeamName) + log.Printf("generate-report boss-from-team file=%s length=%d", filePath, len(bossReport)) + + fi, err := os.Stat(filePath) + if err != nil || fi.Size() <= 0 { + log.Printf("Error with boss report file: %v", err) + postEphemeral(api, cmd, "Error: generated boss report file is empty.") + return + } + + uploadChannel := cmd.ChannelID + if cfg.ReportPrivate { + ch, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{Users: []string{cmd.UserID}}) + if err != nil { + log.Printf("Error opening DM for private report: %v", err) + postEphemeral(api, cmd, "Error opening DM to send private report. Check bot permissions.") + return + } + uploadChannel = ch.ID + } + + _, err = api.UploadFileV2(slack.UploadFileV2Parameters{ + File: filePath, + FileSize: int(fi.Size()), + Filename: filepath.Base(filePath), + Channel: uploadChannel, + Title: fileTitle, + InitialComment: fmt.Sprintf("Generated report for week ending %s (mode: boss, derived from team report, tokens used: 0)", friday.Format("2006-01-02")), + }) + if err != nil { + log.Printf("Error uploading report file: %v", err) + postEphemeral(api, cmd, "Error uploading report file to channel. Check bot permissions.") + return + } + + msg := fmt.Sprintf("Boss report derived from existing team report (no LLM tokens used)\nSaved to: %s", filePath) + postEphemeral(api, cmd, msg) + log.Printf("generate-report done mode=boss derived-from-team") + return + } + log.Printf("generate-report boss: no existing team report found, running full pipeline") + } + items, err := GetItemsByDateRange(db, monday, nextMonday) if err != nil { postEphemeral(api, cmd, fmt.Sprintf("Error loading items: %v", err)) @@ -424,12 +484,12 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S var fileTitle string if mode == "boss" { bossReport := renderBossMarkdown(merged) - filePath, err = WriteEmailDraftFile(bossReport, cfg.ReportOutputDir, monday, cfg.TeamName) + filePath, err = WriteEmailDraftFile(bossReport, cfg.ReportOutputDir, friday, cfg.TeamName) fileTitle = fmt.Sprintf("%s report email draft", cfg.TeamName) log.Printf("generate-report boss-report-length=%d file=%s", len(bossReport), filePath) } else { teamReport := renderTeamMarkdown(merged) - filePath, err = WriteReportFile(teamReport, cfg.ReportOutputDir, monday, cfg.TeamName) + filePath, err = WriteReportFile(teamReport, cfg.ReportOutputDir, friday, cfg.TeamName) fileTitle = fmt.Sprintf("%s team report", cfg.TeamName) log.Printf("generate-report team-report-length=%d file=%s", len(teamReport), filePath) } @@ -472,7 +532,7 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S Filename: filepath.Base(filePath), Channel: uploadChannel, Title: fileTitle, - InitialComment: fmt.Sprintf("Generated report for week starting %s (mode: %s, tokens used: %s)", monday.Format("2006-01-02"), mode, tokenUsedText), + InitialComment: fmt.Sprintf("Generated report for week ending %s (mode: %s, tokens used: %s)", friday.Format("2006-01-02"), mode, tokenUsedText), }) if err != nil { log.Printf("Error uploading report file: %v", err) @@ -1557,7 +1617,8 @@ func prReportedAt(pr GitHubPR, loc *time.Location) time.Time { // --- Correction helpers --- func loadSectionOptionsForModal(cfg Config) []sectionOption { - template, _, err := loadTemplateForGeneration(cfg.ReportOutputDir, cfg.TeamName, time.Now().In(cfg.Location)) + monday, _ := ReportWeekRange(cfg, time.Now().In(cfg.Location)) + template, _, err := loadTemplateForGeneration(cfg.ReportOutputDir, cfg.TeamName, monday) if err != nil { log.Printf("edit modal load template error (non-fatal): %v", err) return nil From d7de93434108de2e9267d472eae83a50f6c30e18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:52:57 +0000 Subject: [PATCH 2/9] Initial plan From 43bc6f57903eef857737dd85201ab1f03b43a920 Mon Sep 17 00:00:00 2001 From: Weizhi Li Date: Mon, 16 Feb 2026 02:53:02 -0800 Subject: [PATCH 3/9] Update slack.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- slack.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slack.go b/slack.go index 3079574..e5ee0d2 100644 --- a/slack.go +++ b/slack.go @@ -532,7 +532,7 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S Filename: filepath.Base(filePath), Channel: uploadChannel, Title: fileTitle, - InitialComment: fmt.Sprintf("Generated report for week ending %s (mode: %s, tokens used: %s)", friday.Format("2006-01-02"), mode, tokenUsedText), + InitialComment: fmt.Sprintf("Generated report for reporting week containing %s (mode: %s, tokens used: %s)", friday.Format("2006-01-02"), mode, tokenUsedText), }) if err != nil { log.Printf("Error uploading report file: %v", err) From 528cf20de37bd0b7b6012aadb2e112b33036b4b2 Mon Sep 17 00:00:00 2001 From: Weizhi Li Date: Mon, 16 Feb 2026 02:54:20 -0800 Subject: [PATCH 4/9] Update slack.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- slack.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slack.go b/slack.go index e5ee0d2..b0a043c 100644 --- a/slack.go +++ b/slack.go @@ -401,7 +401,7 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S Filename: filepath.Base(filePath), Channel: uploadChannel, Title: fileTitle, - InitialComment: fmt.Sprintf("Generated report for week ending %s (mode: boss, derived from team report, tokens used: 0)", friday.Format("2006-01-02")), + InitialComment: fmt.Sprintf("Generated boss report (week reference date: %s, derived from team report, tokens used: 0)", friday.Format("2006-01-02")), }) if err != nil { log.Printf("Error uploading report file: %v", err) From 92bc957d83672cb80a6a7366835657d838d79f12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:54:40 +0000 Subject: [PATCH 5/9] Initial plan From ab3992884b23738cc5c468d655dd7d0ca02bf3ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:57:41 +0000 Subject: [PATCH 6/9] Add comprehensive unit tests for FridayOfWeek function Co-authored-by: WZ <719869+WZ@users.noreply.github.com> --- models_test.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/models_test.go b/models_test.go index 510b7bf..c77c067 100644 --- a/models_test.go +++ b/models_test.go @@ -21,3 +21,115 @@ func TestReportWeekRangeMondayCutoff(t *testing.T) { t.Fatalf("expected current week for Monday afternoon, got %s -> %s", from.Format("20060102"), to.Format("20060102")) } } + +func TestFridayOfWeek(t *testing.T) { + loc := time.FixedZone("UTC+0", 0) + + tests := []struct { + name string + monday time.Time + expected string + }{ + { + name: "basic monday to friday", + monday: time.Date(2026, 2, 9, 0, 0, 0, 0, loc), + expected: "20260213", // Feb 13, 2026 (Friday) + }, + { + name: "monday with time component", + monday: time.Date(2026, 2, 9, 14, 30, 45, 0, loc), + expected: "20260213", // Feb 13, 2026 (Friday) + }, + { + name: "year boundary - monday in december", + monday: time.Date(2025, 12, 29, 0, 0, 0, 0, loc), + expected: "20260102", // Jan 2, 2026 (Friday) + }, + { + name: "year boundary - monday in january", + monday: time.Date(2026, 1, 5, 0, 0, 0, 0, loc), + expected: "20260109", // Jan 9, 2026 (Friday) + }, + { + name: "month boundary", + monday: time.Date(2026, 2, 23, 0, 0, 0, 0, loc), + expected: "20260227", // Feb 27, 2026 (Friday) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + friday := FridayOfWeek(tt.monday) + got := friday.Format("20060102") + if got != tt.expected { + t.Errorf("FridayOfWeek(%s) = %s, want %s", + tt.monday.Format("20060102 15:04:05"), got, tt.expected) + } + + // Verify it's actually a Friday + if friday.Weekday() != time.Friday { + t.Errorf("FridayOfWeek(%s) returned %s (weekday: %s), expected Friday", + tt.monday.Format("20060102"), friday.Format("20060102"), friday.Weekday()) + } + + // Verify time components are preserved + if friday.Hour() != tt.monday.Hour() || friday.Minute() != tt.monday.Minute() || friday.Second() != tt.monday.Second() { + t.Errorf("FridayOfWeek(%s) time component not preserved: got %02d:%02d:%02d, want %02d:%02d:%02d", + tt.monday.Format("20060102 15:04:05"), + friday.Hour(), friday.Minute(), friday.Second(), + tt.monday.Hour(), tt.monday.Minute(), tt.monday.Second()) + } + + // Verify location is preserved + if friday.Location() != tt.monday.Location() { + t.Errorf("FridayOfWeek(%s) location not preserved: got %v, want %v", + tt.monday.Format("20060102"), friday.Location(), tt.monday.Location()) + } + }) + } +} + +func TestFridayOfWeekWithDifferentTimezones(t *testing.T) { + utc := time.UTC + pst := time.FixedZone("PST", -8*3600) + jst := time.FixedZone("JST", 9*3600) + + tests := []struct { + name string + monday time.Time + expected string + }{ + { + name: "UTC timezone", + monday: time.Date(2026, 2, 9, 10, 0, 0, 0, utc), + expected: "20260213", + }, + { + name: "PST timezone", + monday: time.Date(2026, 2, 9, 10, 0, 0, 0, pst), + expected: "20260213", + }, + { + name: "JST timezone", + monday: time.Date(2026, 2, 9, 10, 0, 0, 0, jst), + expected: "20260213", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + friday := FridayOfWeek(tt.monday) + got := friday.Format("20060102") + if got != tt.expected { + t.Errorf("FridayOfWeek(%s) = %s, want %s", + tt.monday.Format("20060102 15:04:05 MST"), got, tt.expected) + } + + // Verify location is preserved + if friday.Location() != tt.monday.Location() { + t.Errorf("FridayOfWeek(%s) location not preserved: got %v, want %v", + tt.monday.Format("20060102 15:04:05 MST"), friday.Location(), tt.monday.Location()) + } + }) + } +} From 38fc35a05b236c03651683c9f151f0eca60e81b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:58:07 +0000 Subject: [PATCH 7/9] Fix error handling in boss-mode shortcut to distinguish file-not-found from other errors Co-authored-by: WZ <719869+WZ@users.noreply.github.com> --- slack.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/slack.go b/slack.go index b0a043c..df030ce 100644 --- a/slack.go +++ b/slack.go @@ -363,7 +363,16 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S if mode == "boss" { teamReportFile := fmt.Sprintf("%s_%s.md", cfg.TeamName, friday.Format("20060102")) teamReportPath := filepath.Join(cfg.ReportOutputDir, teamReportFile) - if content, readErr := os.ReadFile(teamReportPath); readErr == nil && len(content) > 0 { + content, readErr := os.ReadFile(teamReportPath) + if readErr != nil { + if os.IsNotExist(readErr) { + log.Printf("generate-report boss: no existing team report found, running full pipeline") + } else { + log.Printf("Error reading team report file %s: %v", teamReportPath, readErr) + postEphemeral(api, cmd, fmt.Sprintf("Error reading team report file: %v", readErr)) + return + } + } else if len(content) > 0 { log.Printf("generate-report boss: deriving from existing team report %s", teamReportPath) template := parseTemplate(string(content)) stripCurrentTeamTitleFromPrefix(template, cfg.TeamName) @@ -414,7 +423,6 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S log.Printf("generate-report done mode=boss derived-from-team") return } - log.Printf("generate-report boss: no existing team report found, running full pipeline") } items, err := GetItemsByDateRange(db, monday, nextMonday) From a1b5fd62c9e6d6fb4348eb0e89d7ecb30343a45e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:59:00 +0000 Subject: [PATCH 8/9] Improve error handling clarity with better control flow and comments Co-authored-by: WZ <719869+WZ@users.noreply.github.com> --- slack.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/slack.go b/slack.go index df030ce..826a809 100644 --- a/slack.go +++ b/slack.go @@ -365,13 +365,14 @@ func handleGenerateReport(api *slack.Client, db *sql.DB, cfg Config, cmd slack.S teamReportPath := filepath.Join(cfg.ReportOutputDir, teamReportFile) content, readErr := os.ReadFile(teamReportPath) if readErr != nil { - if os.IsNotExist(readErr) { - log.Printf("generate-report boss: no existing team report found, running full pipeline") - } else { + if !os.IsNotExist(readErr) { + // Unexpected error (permission, I/O, etc.) - surface it and abort log.Printf("Error reading team report file %s: %v", teamReportPath, readErr) postEphemeral(api, cmd, fmt.Sprintf("Error reading team report file: %v", readErr)) return } + // File doesn't exist - fall through to full pipeline below + log.Printf("generate-report boss: no existing team report found, running full pipeline") } else if len(content) > 0 { log.Printf("generate-report boss: deriving from existing team report %s", teamReportPath) template := parseTemplate(string(content)) From 26440904905974f9c732a9c84ca9382830af568e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:00:03 +0000 Subject: [PATCH 9/9] Complete error handling fix for boss-mode shortcut Co-authored-by: WZ <719869+WZ@users.noreply.github.com> --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8493b6b..2db2da3 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.23.0 require ( github.com/anthropics/anthropic-sdk-go v1.22.0 github.com/mattn/go-sqlite3 v1.14.33 + github.com/robfig/cron/v3 v3.0.1 github.com/slack-go/slack v0.17.3 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/gorilla/websocket v1.5.3 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect