GH-5257: feat(briefs): daily receipts digest — per-run cost receipt lines at configurable time (default 18:00) - #5258
Conversation
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Review — REQUEST CHANGESStrong implementation overall: brief_type cross-contamination fix is correct and well-tested (both the store-level table cases and the behavioral guard in 1. BLOCKING — permanent coverage gap: runs in-flight at digest time (or created after it) are never receipted
For a cost-accountability artifact this silently undercounts spend every day — it breaks acceptance criterion 1's intent. Fix direction (pick one):
This also fixes the secondary issue that a missed digest caught up next morning currently reports the wrong day's rows. 2. Revert unrelated hunk — comment corruption in
|
|
Merge conflict detected. Auto-rebase failed and the conflict surface is not limited to go.mod/go.sum — holding for manual resolution instead of closing. Conflicted files:
|
|
🔄 Re-adopted: branch updated (new head |
|
Merge conflict detected. Auto-rebase failed and the conflict surface is not limited to go.mod/go.sum — holding for manual resolution instead of closing. Conflicted files:
|
1 similar comment
|
Merge conflict detected. Auto-rebase failed and the conflict surface is not limited to go.mod/go.sum — holding for manual resolution instead of closing. Conflicted files:
|
Re-review — APPROVE (verdict; formal review blocked on same-account)All three items from the previous round verified against commit 1e5b0d2:
Local verification (CI reported no checks while the PR was draft): Note (non-blocking, ms-wide): digest query Marked ready — autopilot may merge on green CI. |
) Adds a second, independently-scheduled Telegram-only brief listing one line per terminal execution that day (issue ref, diff size, duration, cost) plus a day total, delivered on its own schedule (default 18:00 America/New_York). Implemented as a lightweight ReceiptsScheduler rather than generalizing the existing daily Scheduler/Generator, since the flat per-execution shape doesn't fit Brief's Completed/InProgress/Blocked sections. Also fixes GetLastBriefSent, which only filtered by channel — a second brief type sharing a Telegram channel would have corrupted catch-up detection for both.
…eceipts digest (GH-5257) Fixes staticcheck QF1012 flagged by golangci-lint --new-from-rev=origin/main.
…not created_at calendar day (GH-5261) PR#5258 review: a run created before the 18:00 digest but still running at send time was excluded that day (created_at bounds it in) and excluded the next day (created_at bounds it out) — its cost never appeared in any digest. Any run created after 18:00 had the same fate. GetExecutionsForReceipts now filters/orders on completed_at, and runDigest windows [last digest SentAt, now) instead of [today 00:00, now), so every terminal execution is receipted exactly once regardless of when it started. Also reverts the SetApprovalDecision doc-comment smart-quote corruption from the same PR, and adds the optional Telegram 4096-char truncation guard for long digests.
1e5b0d2 to
a6e52de
Compare
|
🔄 Re-adopted: branch updated (new head |
Summary
Automated PR created by Pilot for task GH-5257.
Closes #5257
Changes
GitHub Issue GH-5257: feat(briefs): daily receipts digest — per-run cost receipt lines at configurable time (default 18:00)
feat(briefs): daily receipts digest — per-run cost receipt lines at configurable time (default 18:00)
📋 PLANNED 2026-08-29 — researched (briefs subsystem seam map complete), dispatching to Pilot.
Problem
Pilot sends one scheduled daily brief (14:00 local via
orchestrator.daily_brief,cron
0 8 * * *America/New_York). There is no end-of-day receipts digest:one line per completed execution — issue/PR ref, diff size, duration, dollar
cost — plus a day total. All the data already exists on
executionsrows; it'sa formatting + second-schedule feature, not new plumbing.
Blocking defect discovered during research:
GetLastBriefSent(channel)(
internal/memory/store.go:5176-5194) filtersbrief_historyby channel only,not
brief_type. Any second scheduled brief type on the same Telegram channelmakes catch-up logic read the wrong brief's last-sent timestamp (false catch-up
fires / false skips). Must be fixed as part of this task.
Design
Approach: sibling config block + lightweight fork of the scheduler idiom.
Do NOT generalize the existing
briefs.Scheduler— it hardcodesGenerateDaily()(scheduler.go:170) andBriefType: "daily"(
scheduler.go:191), and the digest content shape (flat per-execution list +total) doesn't match
briefs.Brief(Completed/InProgress/Blocked sections).A ~150-line receipts scheduler reusing the same cron + timezone + catch-up
pattern keeps the working daily brief untouched.
1. Config (
internal/config/config.go)ReceiptsDigestConfigstruct mirroringDailyBriefConfig(config.go:196-204) minus
Time(deprecated field — don't carry it over)and minus
Content/Filters(digest has no content toggles v1):Enabled bool,Schedule string,Timezone string,Channels []BriefChannelConfig(reuse existing type, config.go:206-211).ReceiptsDigest *ReceiptsDigestConfig \yaml:"receipts_digest"`toOrchestratorConfignext toDailyBrief` (config.go:168).Enabled: false,Schedule: "0 18 * * *",Timezone: "America/New_York"(match daily_briefdefault), empty channels.
configs/pilot.example.yaml: add a documentedreceipts_digest:exampleunder
orchestrator:(note:daily_brief:has no example block today —greenfield; adding a
daily_brief:example alongside is optional/welcome).2. Memory (
internal/memory/store.go)GetLastBriefSent(channel string)→ addbriefType stringparam,WHERE channel = ? AND brief_type = ?. Update the single existing call site(
internal/briefs/scheduler.go:213) to pass"daily". Existingbrief_historyrows already carrybrief_type = "daily"so no migration.GetExecutionsForReceipts(query BriefQuery): likeGetExecutionsInPeriod(store.go:2075-2125) but SELECT/Scan the fullreceipt column set — add
estimated_cost_usd, files_changed, lines_added, lines_removed, task_source_adapter, task_source_issue_id(columns exist andare populated via
internal/executor/lifecycle.go:330-332; fuller-columnScan pattern precedent:
GetQueuedTasksForProject, store.go:2375-2380).Terminal statuses only (completed + failed — failed runs still cost money;
mark them in the output). Exclude canary rows
(
COALESCE(is_canary,0)=0, same asGetBriefMetricsstore.go:2281).3. Briefs package (
internal/briefs/)New file
receipts.go(+receipts_test.go):ReceiptsScheduler: cron viarobfig/cron/v3, timezone load withUTC-fallback-and-warn (copy
scheduler.go:33-37), catch-up on start usingthe fixed
GetLastBriefSent(channel, "receipts"), records sends viaRecordBriefSentwithBriefType: "receipts".generator.go:165-168), rows fromGetExecutionsForReceipts.receipts.goorformatter_receipts.go):#5214 merged · +88 −15 · 14m · $2.75— issue ref via theestablished idiom (strip
GH-prefix from TaskID, fall back toTaskSourceIssueID, guard onTaskSourceAdapter == "github";lifecycle.go:427-438), fall back to task title when no issue number.Failed runs marked (e.g.
✗ failedinstead of status).N runs · +ΣA −ΣD · $Σ.ΣΣ.formatDuration(formatter.go:100-112),escapeTelegramMarkdown(delivery.go:334-344) on all dynamic strings,and the parse-entity-error plain-text retry pattern
(
delivery.go:246-255, 349-354).DeliveryService/TelegramSenderseam (delivery.go:19-21)or accept the sender directly — whichever needs less surface; no new
adapter code (
telegramBriefAdapter,cmd/pilot/adapters.go:26-41, wrapsSendBriefMessagealready).4. Wiring (
cmd/pilot/main.go)DailyBriefblock (main.go:3775-3836): readcfg.Orchestrator.ReceiptsDigest, construct +Start(ctx)the receiptsscheduler. Store nil-check same as main.go:3805. Extracting a shared
startBriefScheduler-style helper to cut the ~60-line duplication iswelcome but optional — do not let it grow the diff into a refactor.
Acceptance criteria
orchestrator.receipts_digest.enabled: truewith default schedule sends aTelegram digest at 18:00 configured-timezone: one line per terminal
execution that day (issue ref, +adds −dels, duration, $cost) + total line.
GetLastBriefSentfilters bybrief_type; daily-brief catch-up andreceipts catch-up cannot cross-contaminate on a shared channel (test
covers: both types recorded on same channel, each reads its own).
code paths untouched except the one
GetLastBriefSentcall-site arg).totals),
GetExecutionsForReceipts(column completeness, canary exclusion,period boundaries),
GetLastBriefSenttype filter.configs/pilot.example.yamldocumentsreceipts_digest.make lint && make testgreen.Non-goals (v1)
supports adding later).
owner/repodisplay —Executionhas onlyProjectPath, no repo-namefield; single-project deployments don't need it. Do not add columns.
briefs.Schedulerinto a multi-brief engine.Refs
embedded above with file:line anchors).