Commit 17f512025467
Changed files (1)
tools
daily-plan
cmd
daily-plan
tools/daily-plan/cmd/daily-plan/main.go
@@ -88,6 +88,12 @@ func run(args []string) error {
return cmdPick(ctx, cfg)
case "weekly":
return cmdWeekly(ctx, cfg)
+ case "review":
+ period := "this week"
+ if len(args) > 1 {
+ period = strings.Join(args[1:], " ")
+ }
+ return cmdReview(ctx, cfg, period)
case "help", "-h", "--help":
printHelp()
return nil
@@ -774,6 +780,212 @@ func execCommand(name string, args ...string) (string, error) {
return stdout.String(), nil
}
+func cmdReview(ctx context.Context, cfg *config.Config, period string) error {
+ start, end, err := parsePeriod(period)
+ if err != nil {
+ return fmt.Errorf("invalid period: %w", err)
+ }
+
+ startStr := start.Format("2006-01-02")
+ endStr := end.Format("2006-01-02")
+
+ // Fetch all sources (reuse cache)
+ orgDone, _ := cache.GetOrFetch(apiCache, "org:done:"+startStr, func() ([]org.DoneItem, error) {
+ return org.FetchDoneItems(cfg.Org.Files, cfg.Org.ArchiveDir, start, end)
+ })
+ aiItems, _ := cache.GetOrFetch(apiCache, "ai:items:"+startStr, func() ([]ai.Item, error) {
+ return ai.FetchItemsSince(cfg.AI.Dir, start, end)
+ })
+ completed, _ := cache.GetOrFetch(apiCache, "jira:completed:"+startStr, func() ([]jira.Issue, error) {
+ return jira.FetchCompletedSince(ctx, cfg.Jira.User, start)
+ })
+ merged, _ := cache.GetOrFetch(apiCache, "gh:merged:"+startStr, func() ([]github.Item, error) {
+ return github.FetchMergedPRsSince(ctx, cfg.GitHub.Username, start, cfg.GitHub.Owners)
+ })
+ reviewed, _ := cache.GetOrFetch(apiCache, "gh:reviewed:"+startStr, func() ([]github.Item, error) {
+ return github.FetchReviewsGivenSince(ctx, cfg.GitHub.Username, start, cfg.GitHub.Owners)
+ })
+ issuesCreated, _ := cache.GetOrFetch(apiCache, "gh:issues-created:"+startStr, func() ([]github.Item, error) {
+ return github.FetchIssuesCreatedSince(ctx, cfg.GitHub.Username, start, cfg.GitHub.Owners)
+ })
+
+ var discussions []github.DiscussionItem
+ var comments []github.CommentItem
+ if !noDiscussions {
+ discussions, _ = cache.GetOrFetch(apiCache, "gh:discussions:"+startStr, func() ([]github.DiscussionItem, error) {
+ return github.FetchDiscussionsSince(ctx, start)
+ })
+ }
+ if !noComments {
+ comments, _ = cache.GetOrFetch(apiCache, "gh:comments:"+startStr, func() ([]github.CommentItem, error) {
+ return github.FetchCommentsSince(ctx, start)
+ })
+ }
+
+ if outputJSON {
+ return emitJSON(jsonWeekly{
+ Week: startStr,
+ NextMonday: endStr,
+ OrgDone: orgDoneToJSON(orgDone),
+ AISessions: aiToJSON(aiItems),
+ JiraCompleted: jiraToJSON(completed, cfg.Jira.BaseURL),
+ GHMerged: ghToJSON(merged),
+ GHReviewed: ghToJSON(reviewed),
+ GHIssuesCreated: ghToJSON(issuesCreated),
+ GHDiscussions: discussionsToJSON(discussions),
+ GHComments: commentsToJSON(comments),
+ })
+ }
+
+ // Markdown output
+ fmt.Printf("# Activity Review: %s to %s\n\n", startStr, endStr)
+
+ if len(orgDone) > 0 {
+ fmt.Println("## Completed Tasks (Org)")
+ // Group by section
+ bySection := make(map[string][]org.DoneItem)
+ var order []string
+ for _, item := range orgDone {
+ s := item.Section
+ if s == "" {
+ s = "(uncategorized)"
+ }
+ if _, exists := bySection[s]; !exists {
+ order = append(order, s)
+ }
+ bySection[s] = append(bySection[s], item)
+ }
+ for _, section := range order {
+ fmt.Printf("\n### %s\n\n", section)
+ for _, item := range bySection[section] {
+ fmt.Printf("- โ %s (%s)\n", item.Title, item.CompletedAt.Format("2006-01-02"))
+ }
+ }
+ fmt.Println()
+ }
+
+ if len(completed) > 0 {
+ fmt.Println("\n## Completed (Jira)")
+ for _, issue := range completed {
+ fmt.Printf("- [%s](%s/%s) %s\n", issue.Key, cfg.Jira.BaseURL, issue.Key, issue.Summary)
+ }
+ fmt.Println()
+ }
+
+ if len(merged) > 0 {
+ fmt.Println("\n## Merged PRs")
+ for _, item := range merged {
+ fmt.Printf("- [%s](%s) %s\n", item.Ref(), item.URL(), item.Title)
+ }
+ fmt.Println()
+ }
+
+ if len(reviewed) > 0 {
+ fmt.Printf("## Reviews Given (%d)\n\n", len(reviewed))
+ for _, item := range reviewed {
+ fmt.Printf("- [%s](%s) %s\n", item.Ref(), item.URL(), item.Title)
+ }
+ fmt.Println()
+ }
+
+ if len(issuesCreated) > 0 {
+ fmt.Printf("## Issues Filed (%d)\n\n", len(issuesCreated))
+ for _, item := range issuesCreated {
+ fmt.Printf("- [%s](%s) %s\n", item.Ref(), item.URL(), item.Title)
+ }
+ fmt.Println()
+ }
+
+ if !noDiscussions && len(discussions) > 0 {
+ fmt.Printf("## Discussions (%d)\n\n", len(discussions))
+ for _, d := range discussions {
+ prefix := ""
+ if d.Type == "discussion_comment" {
+ prefix = "Comment: "
+ }
+ fmt.Printf("- [%s%s](%s) (%s)\n", prefix, d.Title, d.URL, d.Repo)
+ }
+ fmt.Println()
+ }
+
+ if !noComments && len(comments) > 0 {
+ fmt.Printf("## Comments (%d)\n\n", len(comments))
+ for _, c := range comments {
+ fmt.Printf("- [%s#%d](%s) %s\n", c.Repo, c.IssueNumber, c.URL, c.IssueTitle)
+ }
+ fmt.Println()
+ }
+
+ if len(aiItems) > 0 {
+ // Filter auto-recovered
+ var filtered []ai.Item
+ for _, item := range aiItems {
+ if !strings.Contains(strings.ToLower(item.Title), "auto-recovered") {
+ filtered = append(filtered, item)
+ }
+ }
+ if len(filtered) > 0 {
+ fmt.Printf("## AI Sessions (%d)\n\n", len(filtered))
+ for _, item := range filtered {
+ project := ""
+ if item.Project != "" {
+ project = fmt.Sprintf(" [%s]", item.Project)
+ }
+ fmt.Printf("- **%s** %s%s (%s)\n", item.Type, item.Title, project, item.Date.Format("2006-01-02"))
+ }
+ fmt.Println()
+ }
+ }
+
+ return nil
+}
+
+func parsePeriod(period string) (time.Time, time.Time, error) {
+ now := time.Now()
+
+ switch strings.ToLower(strings.TrimSpace(period)) {
+ case "this week", "week", "":
+ daysFromMonday := (int(now.Weekday()) - int(time.Monday) + 7) % 7
+ monday := now.AddDate(0, 0, -daysFromMonday)
+ monday = time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, now.Location())
+ return monday, now, nil
+ case "last week":
+ daysFromMonday := (int(now.Weekday()) - int(time.Monday) + 7) % 7
+ thisMonday := now.AddDate(0, 0, -daysFromMonday)
+ lastMonday := thisMonday.AddDate(0, 0, -7)
+ lastMonday = time.Date(lastMonday.Year(), lastMonday.Month(), lastMonday.Day(), 0, 0, 0, 0, now.Location())
+ thisMondayStart := time.Date(thisMonday.Year(), thisMonday.Month(), thisMonday.Day(), 0, 0, 0, 0, now.Location())
+ return lastMonday, thisMondayStart, nil
+ case "this month", "month":
+ firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
+ return firstOfMonth, now, nil
+ case "last month":
+ firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
+ firstOfLastMonth := firstOfThisMonth.AddDate(0, -1, 0)
+ return firstOfLastMonth, firstOfThisMonth, nil
+ }
+
+ // Try YYYY-MM-DD..YYYY-MM-DD range
+ if parts := strings.SplitN(period, "..", 2); len(parts) == 2 {
+ start, err := time.Parse("2006-01-02", strings.TrimSpace(parts[0]))
+ if err != nil {
+ return time.Time{}, time.Time{}, fmt.Errorf("invalid start date: %s", parts[0])
+ }
+ end, err := time.Parse("2006-01-02", strings.TrimSpace(parts[1]))
+ if err != nil {
+ return time.Time{}, time.Time{}, fmt.Errorf("invalid end date: %s", parts[1])
+ }
+ return start, end.Add(24 * time.Hour), nil
+ }
+
+ // Try natural language via GNU date
+ start, err := parseNaturalDate(period)
+ if err != nil {
+ return time.Time{}, time.Time{}, err
+ }
+ return start, now, nil
+}
+
func printHelp() {
fmt.Println(`daily-plan โ pull from Jira/GitHub into org-mode planning
@@ -783,11 +995,12 @@ Commands:
pick Interactive pick with jayrat โ schedule
schedule KEY [DATE] Schedule a Jira/GH issue (PROJ-123 or org/repo#42)
weekly Weekly review: completed, in progress, backlog
+ review [PERIOD] Full activity review as markdown (for reports)
Flags:
--json Output as JSON (for Emacs/scripting integration)
- --no-discussions Skip GitHub discussions in weekly
- --no-comments Skip GitHub comments in weekly
+ --no-discussions Skip GitHub discussions
+ --no-comments Skip GitHub comments
Date formats:
YYYY-MM-DD Explicit date
@@ -804,5 +1017,10 @@ Examples:
daily-plan schedule SRVKP-11036 2026-03-28
daily-plan schedule tektoncd/pipeline#9149 tomorrow
daily-plan pick
- daily-plan weekly`)
+ daily-plan weekly
+ daily-plan review
+ daily-plan review "last week"
+ daily-plan review "this month"
+ daily-plan review "2026-03-01..2026-03-15"`)
+
}