From e7906edb74d85f3d3377d72bd0265c17cc17f1d2 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 20 Feb 2026 18:12:39 -0800 Subject: [PATCH 1/8] Count fetched MR/PR items in /check missing logic --- internal/integrations/slack/slack.go | 57 ++++++++++++++++++- .../integrations/slack/slack_logic_test.go | 15 +++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/internal/integrations/slack/slack.go b/internal/integrations/slack/slack.go index 5d878d1..b417235 100644 --- a/internal/integrations/slack/slack.go +++ b/internal/integrations/slack/slack.go @@ -867,6 +867,14 @@ func handleListMissing(api *slack.Client, db *sql.DB, cfg Config, cmd slack.Slas } monday, nextMonday := ReportWeekRange(cfg, time.Now().In(cfg.Location)) + weekItems, err := GetItemsByDateRange(db, monday, nextMonday) + if err != nil { + postEphemeral(api, cmd, fmt.Sprintf("Error loading items: %v", err)) + log.Printf("list-missing load error: %v", err) + return + } + reportedAuthors := uniqueReportedAuthors(weekItems) + reportedAuthorIDs, err := GetSlackAuthorIDsByDateRange(db, monday, nextMonday) if err != nil { postEphemeral(api, cmd, fmt.Sprintf("Error loading items: %v", err)) @@ -881,11 +889,22 @@ func handleListMissing(api *slack.Client, db *sql.DB, cfg Config, cmd slack.Slas var missing []missingMember var missingIDs []string for _, uid := range memberIDs { - if reportedAuthorIDs[uid] { + user, err := api.GetUserInfo(uid) + nameCandidates := []string{uid} + if err == nil { + if user.Profile.DisplayName != "" { + nameCandidates = append(nameCandidates, user.Profile.DisplayName) + } + if user.RealName != "" { + nameCandidates = append(nameCandidates, user.RealName) + } + if user.Name != "" { + nameCandidates = append(nameCandidates, user.Name) + } + } + if memberReportedThisWeek(uid, nameCandidates, reportedAuthorIDs, reportedAuthors) { continue } - - user, err := api.GetUserInfo(uid) if err != nil { missing = append(missing, missingMember{display: uid, userID: uid}) missingIDs = append(missingIDs, uid) @@ -963,6 +982,38 @@ func handleListMissing(api *slack.Client, db *sql.DB, cfg Config, cmd slack.Slas log.Printf("list-missing count=%d", len(missing)+len(unresolved)) } +func uniqueReportedAuthors(items []WorkItem) []string { + seen := make(map[string]bool) + var out []string + for _, item := range items { + author := strings.TrimSpace(item.Author) + if author == "" || seen[author] { + continue + } + seen[author] = true + out = append(out, author) + } + return out +} + +func memberReportedThisWeek(userID string, nameCandidates []string, reportedAuthorIDs map[string]bool, reportedAuthors []string) bool { + if reportedAuthorIDs[userID] { + return true + } + for _, candidate := range nameCandidates { + candidate = strings.TrimSpace(candidate) + if candidate == "" { + continue + } + for _, author := range reportedAuthors { + if strings.EqualFold(candidate, author) || nameMatches(candidate, author) || nameMatches(author, candidate) { + return true + } + } + } + return false +} + func postEphemeral(api *slack.Client, cmd slack.SlashCommand, text string) { postEphemeralTo(api, cmd.ChannelID, cmd.UserID, text) } diff --git a/internal/integrations/slack/slack_logic_test.go b/internal/integrations/slack/slack_logic_test.go index 1a6be14..f3a23b0 100644 --- a/internal/integrations/slack/slack_logic_test.go +++ b/internal/integrations/slack/slack_logic_test.go @@ -183,6 +183,21 @@ func TestFormatItemDescriptionForList(t *testing.T) { } } +func TestMemberReportedThisWeek(t *testing.T) { + reportedIDs := map[string]bool{"U123": true} + reportedAuthors := []string{"Alex Rivera", "Jordan Patel"} + + if !memberReportedThisWeek("U123", []string{"Alex Rivera"}, reportedIDs, reportedAuthors) { + t.Fatal("expected member to be reported when author_id is present") + } + if !memberReportedThisWeek("U999", []string{"Alex"}, map[string]bool{}, reportedAuthors) { + t.Fatal("expected fuzzy name match against fetched author to count as reported") + } + if memberReportedThisWeek("U999", []string{"Taylor"}, map[string]bool{}, reportedAuthors) { + t.Fatal("expected unmatched member name to be considered missing") + } +} + func TestDeriveBossReportFromTeamReport_FileExists(t *testing.T) { dir := t.TempDir() teamName := "TestTeam" From 922a4d5ae0e110db04d0600bea5b2784900fa35e Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 21 Feb 2026 01:57:05 -0800 Subject: [PATCH 2/8] Polish edit category label and preserve free-text statuses --- internal/integrations/slack/slack.go | 2 +- internal/report/report_builder.go | 33 +++++++++++++----- internal/report/report_builder_test.go | 48 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/internal/integrations/slack/slack.go b/internal/integrations/slack/slack.go index b417235..4781f67 100644 --- a/internal/integrations/slack/slack.go +++ b/internal/integrations/slack/slack.go @@ -1342,7 +1342,7 @@ func openEditModal(api *slack.Client, db *sql.DB, cfg Config, triggerID, channel } noChangeOpt := slack.NewOptionBlockObject( noCategoryChangeValue, - slack.NewTextBlockObject(slack.PlainTextType, "(no change)", false, false), + slack.NewTextBlockObject(slack.PlainTextType, "Auto", false, false), nil, ) catOptions := []*slack.OptionBlockObject{noChangeOpt} diff --git a/internal/report/report_builder.go b/internal/report/report_builder.go index aa1a04e..832ab94 100644 --- a/internal/report/report_builder.go +++ b/internal/report/report_builder.go @@ -403,6 +403,9 @@ func mergeIncomingItems( } func chooseNormalizedStatus(incomingStatus, llmStatus string, useLLM bool) string { + if isFreeTextStatus(incomingStatus) { + return strings.TrimSpace(incomingStatus) + } if useLLM { switch normalizeStatus(llmStatus) { case "done", "in testing", "in progress": @@ -627,10 +630,7 @@ func categoryAuthors(cat TemplateCategory) []string { } func formatTeamItem(item TemplateItem) string { - status := normalizeStatus(item.Status) - if status == "" { - status = "done" - } + status := statusForDisplay(item.Status) author := synthesizeName(item.Author) tickets := canonicalTicketIDs(item.TicketIDs) description := stripLeadingTicketPrefixIfSame(item.Description, tickets) @@ -646,10 +646,7 @@ func formatTeamItem(item TemplateItem) string { } func formatBossItem(item TemplateItem) string { - status := normalizeStatus(item.Status) - if status == "" { - status = "done" - } + status := statusForDisplay(item.Status) tickets := canonicalTicketIDs(item.TicketIDs) description := stripLeadingTicketPrefixIfSame(item.Description, tickets) description = synthesizeDescription(description) @@ -734,6 +731,26 @@ func normalizeStatus(status string) string { } } +func statusForDisplay(status string) string { + trimmed := strings.TrimSpace(status) + if trimmed == "" { + return "done" + } + if isFreeTextStatus(trimmed) { + return trimmed + } + return normalizeStatus(trimmed) +} + +func isFreeTextStatus(status string) bool { + switch strings.ToLower(strings.TrimSpace(status)) { + case "done", "in testing", "in test", "in progress": + return false + default: + return strings.TrimSpace(status) != "" + } +} + func statusBucket(status string) int { switch normalizeStatus(status) { case "done": diff --git a/internal/report/report_builder_test.go b/internal/report/report_builder_test.go index eebedca..e82e58e 100644 --- a/internal/report/report_builder_test.go +++ b/internal/report/report_builder_test.go @@ -231,6 +231,54 @@ func TestBuildReportsFromLast_LLMConfidenceAndDuplicate(t *testing.T) { } } +func TestBuildReportsFromLast_PreservesFreeTextStatus(t *testing.T) { + dir := t.TempDir() + prev := `### TEAMX 20260202 + +#### Top Focus + +- **Feature A** + - **Pat One** - Existing ongoing item (in progress) +` + if err := os.WriteFile(filepath.Join(dir, "TEAMX_20260202.md"), []byte(prev), 0644); err != nil { + t.Fatalf("write previous report: %v", err) + } + + cfg := Config{ + ReportOutputDir: dir, + TeamName: "TEAMX", + } + + orig := classifySectionsFn + classifySectionsFn = func(_ Config, items []WorkItem, _ []sectionOption, _ []existingItemContext, _ []ClassificationCorrection, _ []historicalItem) (map[int64]LLMSectionDecision, LLMUsage, error) { + out := make(map[int64]LLMSectionDecision, len(items)) + for _, item := range items { + out[item.ID] = LLMSectionDecision{ + SectionID: "S0_0", + NormalizedStatus: "in progress", + Confidence: 0.95, + } + } + return out, LLMUsage{}, nil + } + defer func() { classifySectionsFn = orig }() + + freeTextStatus := "resolved in session; root cause analysis in progress" + items := []WorkItem{ + {ID: 41, Author: "Pat Two", Description: "Investigate customer database startup issue", Status: freeTextStatus}, + } + + result, err := BuildReportsFromLast(cfg, items, mustDate(t, "20260209"), nil, nil) + if err != nil { + t.Fatalf("BuildReportsFromLast failed: %v", err) + } + + team := renderTeamMarkdown(result.Template) + if !strings.Contains(team, "Investigate customer database startup issue ("+freeTextStatus+")") { + t.Fatalf("free-text status should be preserved in team report:\n%s", team) + } +} + func TestBuildReportsFromLast_PreservesPrefixBlocks(t *testing.T) { dir := t.TempDir() prev := `### Product Alpha - 20260130 From c601b757a668044d04925d5355d312624d796531 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 21 Feb 2026 01:59:48 -0800 Subject: [PATCH 3/8] Tune default LLM batch/example settings in config --- config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index 820bdde..6afd326 100644 --- a/config.yaml +++ b/config.yaml @@ -23,10 +23,10 @@ github_repos: [] # optional: limit to specific repos, e.g. ["org/repo1", "org/r # supported values: anthropic, openai llm_provider: "anthropic" llm_model: "" # optional; uses provider default when empty -llm_batch_size: 50 +llm_batch_size: 20 llm_confidence_threshold: 0.70 -llm_example_count: 20 -llm_example_max_chars: 140 +llm_example_count: 8 +llm_example_max_chars: 100 # Optional glossary memory file for phrase/status hints llm_glossary_path: "./llm_glossary.yaml" From df2d21c2960d3a1c17e3436002ed06dc73fb014d Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 21 Feb 2026 02:08:56 -0800 Subject: [PATCH 4/8] Fix bracketed ticket ID duplication in report rendering --- internal/report/report_builder.go | 19 ++++++++++++++++++- internal/report/report_builder_test.go | 12 ++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/report/report_builder.go b/internal/report/report_builder.go index 832ab94..1737dc5 100644 --- a/internal/report/report_builder.go +++ b/internal/report/report_builder.go @@ -663,16 +663,33 @@ func canonicalTicketIDs(ticketIDs string) string { } parts := strings.Split(ticketIDs, ",") cleaned := make([]string, 0, len(parts)) + seen := make(map[string]bool, len(parts)) for _, p := range parts { - p = strings.TrimSpace(p) + p = normalizeTicketTokenForOutput(p) if p == "" { continue } + key := strings.ToLower(p) + if seen[key] { + continue + } + seen[key] = true cleaned = append(cleaned, p) } return strings.Join(cleaned, ",") } +func normalizeTicketTokenForOutput(token string) string { + t := strings.TrimSpace(token) + if t == "" { + return "" + } + t = strings.Trim(t, "[]") + t = strings.TrimSpace(t) + t = strings.TrimLeft(t, "#") + return strings.TrimSpace(t) +} + func stripLeadingTicketPrefixIfSame(description, tickets string) string { description = strings.TrimSpace(description) if description == "" || tickets == "" { diff --git a/internal/report/report_builder_test.go b/internal/report/report_builder_test.go index e82e58e..125e31e 100644 --- a/internal/report/report_builder_test.go +++ b/internal/report/report_builder_test.go @@ -392,6 +392,18 @@ func TestFormatItemDedupesLeadingTicketPrefix(t *testing.T) { if gotPlain != wantPlain { t.Fatalf("unexpected deduped plain-prefix item:\nwant: %s\ngot: %s", wantPlain, gotPlain) } + + bracketedTicketIDs := TemplateItem{ + Author: "Howard Shen", + Description: "[1201950] make up siem MVs prepare test document and assist QA on testing", + TicketIDs: "[1201950]", + Status: "in progress", + } + gotBracketed := formatTeamItem(bracketedTicketIDs) + wantBracketed := "**Howard Shen** - [1201950] Make up siem MVs prepare test document and assist QA on testing (in progress)" + if gotBracketed != wantBracketed { + t.Fatalf("unexpected bracketed ticket normalization:\nwant: %s\ngot: %s", wantBracketed, gotBracketed) + } } func TestMergeCategoryHeadingAuthors(t *testing.T) { From 603897ea9c19056a02714d49452d3e1c0acbe7ce Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 21 Feb 2026 02:12:38 -0800 Subject: [PATCH 5/8] Keep new Support Cases items at section bottom --- internal/report/report_builder.go | 38 ++++++++++++++++++++- internal/report/report_builder_test.go | 46 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/internal/report/report_builder.go b/internal/report/report_builder.go index 1737dc5..f8543d9 100644 --- a/internal/report/report_builder.go +++ b/internal/report/report_builder.go @@ -516,7 +516,12 @@ func mergeExistingItem(existing, incoming TemplateItem) TemplateItem { func reorderTemplateItems(t *ReportTemplate) { for ci := range t.Categories { for si := range t.Categories[ci].Subsections { - t.Categories[ci].Subsections[si].Items = reorderItems(t.Categories[ci].Subsections[si].Items) + sub := &t.Categories[ci].Subsections[si] + if isSupportCasesSubsection(*sub) { + sub.Items = reorderSupportCasesItems(sub.Items) + continue + } + sub.Items = reorderItems(sub.Items) } } } @@ -540,6 +545,37 @@ func reorderItems(items []TemplateItem) []TemplateItem { return sorted } +func isSupportCasesSubsection(sub TemplateSubsection) bool { + if strings.EqualFold(strings.TrimSpace(sub.Name), "Support Cases") { + return true + } + header := strings.ToLower(strings.TrimSpace(sub.HeaderLine)) + return strings.Contains(header, "support cases") +} + +func reorderSupportCasesItems(items []TemplateItem) []TemplateItem { + existing := make([]TemplateItem, 0, len(items)) + newlyAdded := make([]TemplateItem, 0, len(items)) + for _, item := range items { + if item.IsNew { + newlyAdded = append(newlyAdded, item) + continue + } + existing = append(existing, item) + } + + existing = reorderItems(existing) + sort.SliceStable(newlyAdded, func(i, j int) bool { + zi, zj := newlyAdded[i].ReportedAt.IsZero(), newlyAdded[j].ReportedAt.IsZero() + if zi != zj { + return zi + } + return newlyAdded[i].ReportedAt.Before(newlyAdded[j].ReportedAt) + }) + + return append(existing, newlyAdded...) +} + func itemIdentityKey(item TemplateItem) string { return strings.ToLower(strings.TrimSpace(item.Description)) } diff --git a/internal/report/report_builder_test.go b/internal/report/report_builder_test.go index 125e31e..ed307bc 100644 --- a/internal/report/report_builder_test.go +++ b/internal/report/report_builder_test.go @@ -593,6 +593,52 @@ func TestReorderItems_ComprehensiveSorting(t *testing.T) { } } +func TestReorderTemplateItems_SupportCasesKeepsNewItemsAtBottom(t *testing.T) { + template := &ReportTemplate{ + Categories: []TemplateCategory{ + { + Name: "Release and Support", + Subsections: []TemplateSubsection{ + { + Name: "Support Cases", + Items: []TemplateItem{ + { + Description: "Existing case in progress", + Status: "in progress", + IsNew: false, + }, + { + Description: "Newly reported resolved case", + Status: "resolved in session; root cause analysis in progress", + IsNew: true, + ReportedAt: time.Date(2026, 2, 21, 9, 0, 0, 0, time.UTC), + }, + { + Description: "Existing done case", + Status: "done", + IsNew: false, + }, + }, + }, + }, + }, + }, + } + + reorderTemplateItems(template) + items := template.Categories[0].Subsections[0].Items + + if items[0].Description != "Existing done case" { + t.Fatalf("expected existing done case first, got %q", items[0].Description) + } + if items[1].Description != "Existing case in progress" { + t.Fatalf("expected existing in-progress case second, got %q", items[1].Description) + } + if items[2].Description != "Newly reported resolved case" { + t.Fatalf("expected new support case at bottom, got %q", items[2].Description) + } +} + func mustDate(t *testing.T, ymd string) time.Time { t.Helper() d, err := time.Parse("20060102", ymd) From 86b083b0b48d7b0267fad3495f79d55eaa306966 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 10:35:28 +0000 Subject: [PATCH 6/8] Initial plan From 1b81c9c52a876c2d662b6d3a4053de22fc123892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 10:38:47 +0000 Subject: [PATCH 7/8] fix: replace per-member GetUserInfo calls with cached user map in handleListMissing Co-authored-by: WZ <719869+WZ@users.noreply.github.com> --- internal/integrations/slack/slack.go | 35 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/internal/integrations/slack/slack.go b/internal/integrations/slack/slack.go index 4781f67..af754e0 100644 --- a/internal/integrations/slack/slack.go +++ b/internal/integrations/slack/slack.go @@ -882,6 +882,17 @@ func handleListMissing(api *slack.Client, db *sql.DB, cfg Config, cmd slack.Slas return } + // Build an ID→User map from the cached users list to avoid N individual + // GetUserInfo calls (one per team member) inside the loop below. + cachedUsers, err := getCachedUsers(api) + if err != nil { + log.Printf("list-missing: getCachedUsers error: %v", err) + } + userByID := make(map[string]slack.User, len(cachedUsers)) + for _, u := range cachedUsers { + userByID[u.ID] = u + } + type missingMember struct { display string userID string @@ -889,34 +900,34 @@ func handleListMissing(api *slack.Client, db *sql.DB, cfg Config, cmd slack.Slas var missing []missingMember var missingIDs []string for _, uid := range memberIDs { - user, err := api.GetUserInfo(uid) + u, found := userByID[uid] nameCandidates := []string{uid} - if err == nil { - if user.Profile.DisplayName != "" { - nameCandidates = append(nameCandidates, user.Profile.DisplayName) + if found { + if u.Profile.DisplayName != "" { + nameCandidates = append(nameCandidates, u.Profile.DisplayName) } - if user.RealName != "" { - nameCandidates = append(nameCandidates, user.RealName) + if u.RealName != "" { + nameCandidates = append(nameCandidates, u.RealName) } - if user.Name != "" { - nameCandidates = append(nameCandidates, user.Name) + if u.Name != "" { + nameCandidates = append(nameCandidates, u.Name) } } if memberReportedThisWeek(uid, nameCandidates, reportedAuthorIDs, reportedAuthors) { continue } - if err != nil { + if !found { missing = append(missing, missingMember{display: uid, userID: uid}) missingIDs = append(missingIDs, uid) continue } - display := user.Profile.DisplayName + display := u.Profile.DisplayName if display == "" { - display = user.RealName + display = u.RealName } if display == "" { - display = user.Name + display = u.Name } if display == "" { display = uid From 3da25dd374847cd3e66b02ecb9329c5cefcf6f8c Mon Sep 17 00:00:00 2001 From: Weizhi Li Date: Mon, 23 Feb 2026 11:35:33 -0800 Subject: [PATCH 8/8] Update internal/integrations/slack/slack.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/integrations/slack/slack.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/integrations/slack/slack.go b/internal/integrations/slack/slack.go index 4781f67..7f94f6e 100644 --- a/internal/integrations/slack/slack.go +++ b/internal/integrations/slack/slack.go @@ -898,9 +898,6 @@ func handleListMissing(api *slack.Client, db *sql.DB, cfg Config, cmd slack.Slas if user.RealName != "" { nameCandidates = append(nameCandidates, user.RealName) } - if user.Name != "" { - nameCandidates = append(nameCandidates, user.Name) - } } if memberReportedThisWeek(uid, nameCandidates, reportedAuthorIDs, reportedAuthors) { continue