| name | epic-digest |
| layer | method |
| description | Generate a daily "where are we?" digest from this repo's GitHub epics โ a render-only HTML + plain-text email summarising the last 24h of activity plus a progress snapshot of every open epic. Use when asked for a "daily digest", "epic report", "status rapport", "what moved this week", or "where are we on the epics". |
| allowed-tools | mcp__github__search_issues, mcp__github__list_issues, mcp__github__issue_read, Read, Write, Bash |
Epic Digest โ the daily "where are we?" email
Turns the GitHub issue tree (FriendlyInternet/nuxt-crouton) into a skimmable digest: a
"๐งช Needs your eyes" band up top (what landed, one skimmable line per item โ title, type
badge, ๐ for visual changes, PR/preview link โ with the How-to-test steps folded into a
tap-to-expand ๐งช How to test (N) dropdown so the band stays light on mobile), then a short
"since yesterday" activity band, then one card
per open epic with a progress bar and a one-line status โ the sub-issue breakdown tucked
into a collapsible section so the overview stays clean. Built to be read at a glance, drilled
into if needed.
Interactively this is render-only. A by-hand run makes the digest (HTML +
plain text under writeups/reports/) and shows you where it landed. The
scheduled job is what delivers โ by email via Resend (see below).
When to use
- A daily/weekly "where are we?" check, a status rapport, or "what moved".
- Before a planning session, to see which epics are close to done or blocked.
- Skip if you just need one issue's detail โ open the issue directly.
What it produces
| Artifact | Path |
|---|
| HTML digest (forward / view in browser) | writeups/reports/epic-digest-<YYYYMMDD>.html |
| Plain-text digest (terminal / paste into email) | writeups/reports/epic-digest-<YYYYMMDD>.txt |
These are generated artifacts โ render them on demand; don't commit them.
Step 1 โ Pick the window
Default is the last 24h. For a Monday digest covering the weekend, or a weekly,
widen it: compute the cutoff date (e.g. 2026-06-17) and use it in the searches
below. Set windowHours in the data accordingly (24 / 72 / 168).
Step 2 โ Gather the data (GitHub MCP, read-only)
Build the JSON object described in Step 3 using these calls (all owner: pmcp,
repo: nuxt-crouton):
-
Open epics โ search_issues, query repo:FriendlyInternet/nuxt-crouton is:issue is:open label:epic.
-
Each epic's children โ issue_read with method: get_sub_issues. Count
done = children with state: "closed", total = all children. Read each
child's labels (or the get_sub_issues state) to set its status
(in-progress / blocked from the status:* label) and state.
Mark the epic blocked if it carries status:blocked or any child is blocked.
Initiatives (super-epics). Among the label:epic results, treat an issue as an
initiative if it carries label:initiative or its title starts with Initiative:.
An initiative groups other epics: parse its member epic numbers from the #NN links in
its body's ## Member epics / ## โฆconstituent epics section (that section only โ a #NN
in a prose/"consolidation" section is not a member), keep only those that are themselves
gathered open epics, and roll them up into an initiatives[] entry (epicsTotal,
activeCount, blockedCount, childrenTotal/childrenDone summed from the members, and a
members[] list of {number,title,url,done,total,status,blocked}). Remove initiatives
from epics[] so they don't render as misleading 0/0 epics. gather.mjs does all this
deterministically; the interactive flow mirrors it.
-
Activity in the window (the "since yesterday" band) โ use date-filtered search,
substituting <cutoff> (YYYY-MM-DD):
- Closed:
search_issues โ repo:FriendlyInternet/nuxt-crouton is:issue is:closed closed:>=<cutoff>
- Opened:
search_issues โ repo:FriendlyInternet/nuxt-crouton is:issue created:>=<cutoff>
- Merged PRs:
search_issues โ repo:FriendlyInternet/nuxt-crouton is:pr is:merged merged:>=<cutoff>
(note: search_issues is scoped to issues; for PRs use mcp__github__search_pull_requests
if available, else list_pull_requests filtered client-side.)
-
Write the human lines per epic โ these are the heart of the digest, the part a
busy person actually reads. We write issues as hypotheses (see the github-tasks skill),
so the digest surfaces the hypothesis and its signal. Keep them plain English, no file paths,
no jargon:
theHypothesis โ the epic's hypothesis, pulled from its "## Hypothesis" (or legacy
"## ๐ฏ The bet") / "We think thatโฆ" line: if we do X, then Y will happen โ and Y is
what we want. This is the lead line.
weWillKnowBy โ the hypothesis's signal, from the epic's "We'll know byโฆ": how we'll know
the assumption paid off (a measurable/checkable outcome).
whereWeAre โ the current status in plain words: what's done, what's next, any
blocker. This is where the last-24h movement gets folded in.
whatItIs (optional) / recentActivity (optional) โ back-compat fallbacks: theHypothesis
falls back to the legacy theBet, then to whatItIs; whereWeAre falls back to
recentActivity. Prefer the hypothesis-framed fields.
-
Loose tickets (the "no epic" band) โ open issues tracked under no epic, so
they don't vanish from the roundup. search_issues โ
repo:FriendlyInternet/nuxt-crouton is:issue is:open -label:epic, then drop anything with a
parent_issue_url (those already roll up under an epic). For each survivor keep
number, title, url, and its type:* label (as type). The renderer groups
them by type so a pile of chores reads as one block. Omit the section entirely when
there are none โ re-parent strays into epics first; this only catches the genuinely
standalone.
-
Actionables (the "๐งช Needs your eyes" band) โ the things that landed and want the
owner's eyes, each carrying the human How to test steps the author already wrote (no
LLM needed โ every closeable PR/issue is required to have a ## ๐งช How to test section):
- Merged PRs in the window โ for each, read its body, pull the
## ๐งช How to test
section into testSteps (a list of plain step strings). Set hasVisual: true if the PR
touched a UI surface (a .vue/.css file, app/components|layouts|pages/**, crouton-themes/
crouton-editor) or carries a ui-approved / ui:* label. Capture a previewUrl if the
body links a *.pmcp.dev / *.friendlyinter.net URL. label = <type> ยท merged from the
conventional-commit title (fix ยท merged).
- Epics that hit 100% but are still open (done = total, awaiting QA + close) โ one
kind: "epic" actionable whose testSteps come from the epic's ## ๐งช Verify the whole thing
rollup comment.
- Keep an item even with no steps โ the renderer shows a "author should add ๐งช How to test"
nudge rather than dropping it. Omit the whole section only when there are no actionables.
Keep the rest lean: titles, numbers, URLs, states. (Actionables are the one place we do read
PR/rollup bodies โ that's where the test steps live.)
Step 3 โ Write the data file
Write the gathered object to a temp path (e.g. writeups/reports/.epic-digest.data.json).
Shape (example.data.json next to this skill is a complete, renderable sample):
{
"generatedAt": "<ISO now>",
"windowHours": 24,
"repo": "FriendlyInternet/nuxt-crouton",
"activity": {
"opened": [{ "number": 358, "title": "...", "url": "https://github.com/...", "kind": "issue" }],
"closed": [{ "number": 351, "title": "...", "url": "...", "kind": "issue" }],
"mergedPRs": [{ "number": 352, "title": "...", "url": "...", "kind": "pr" }]
},
"actionables": [
{
"number": 391, "title": "...", "url": "https://github.com/...",
"kind": "pr",
"label": "feat ยท merged",
"hasVisual": true,
"previewUrl": "https://velo.pmcp.dev/bookings",
"testSteps": ["Open Bookingsโฆ", "Each row shows a status pill", "Filter by pending โ amber only"]
}
],
"initiatives": [
{
"number": 1632, "url": "https://github.com/...",
"title": "๐จ Visual Layout & Builder",
"epicsTotal": 5, "activeCount": 2, "blockedCount": 1, "blocked": true,
"childrenTotal": 31, "childrenDone": 14,
"members": [
{ "number": 983, "title": "...", "url": "...", "done": 7, "total": 12, "status": "blocked", "blocked": true }
]
}
],
"epics": [
{
"number": 249, "title": "...", "url": "https://github.com/...",
"status": "in-progress",
"blocked": false,
"total": 5, "done": 4,
"theHypothesis": "We think that if we do X, then Y will happen โ and Y is what we want.",
"weWillKnowBy": "The signal that tells us the hypothesis paid off.",
"whereWeAre": "Plain status: what's done, what's next, any blocker.",
"whatItIs": "(optional, back-compat) โ theHypothesis falls back to this.",
"recentActivity": "(optional, back-compat) โ whereWeAre falls back to this.",
"children": [
{ "number": 254, "title": "...", "url": "...", "state": "open", "status": "in-progress" },
{ "number": 253, "title": "...", "url": "...", "state": "closed" }
]
}
],
"loose": [
{ "number": 322, "title": "...", "url": "https://github.com/...", "type": "chore" }
]
}
Step 4 โ Render
node .claude/skills/epic-digest/render.mjs writeups/reports/.epic-digest.data.json
Dependency-free (no npm deps, no network). Default writes the .html + .txt;
--format md writes a GitHub-flavoured .md instead (used by the daily job).
Automated daily run (no LLM)
The interactive flow above gathers via GitHub MCP. The scheduled daily digest
runs entirely deterministically โ .github/workflows/epic-digest.yml (cron
0 5 * * *, ~06:00 Europe/Brussels) does:
GITHUB_TOKEN=โฆ node .claude/skills/epic-digest/gather.mjs > digest.data.json
node .claude/skills/epic-digest/render.mjs digest.data.json --out-dir .
gather.mjs parses Hypothesis (or legacy The bet) / We'll know by from each epic body and
computes whereWeAre from child counts โ no model in the loop. It also detects initiatives
(super-epics; label:initiative or an Initiative: title), rolls their member epics up into
initiatives[], and removes them from epics[] โ no model in the loop.
- Delivery is email-only, via Resend (#551). The job emails the rendered HTML
(+ text mirror) when
secrets.RESEND_API_KEY, vars.RESEND_FROM (shared with the
red-team daily) and vars.DIGEST_REPORT_EMAIL are set; unset โ the step warns and
skips so the job stays green. The old standing-issue comment rail was retired (it
produced duplicate GitHub-notification mail).
- The render is the same
render.mjs, so the hand-run and the cron stay in lockstep.
Step 5 โ Hand off
- Show the user where the files landed and a short text summary of the headline
numbers (epics, closed, PRs merged).
- HTML is for forwarding / browser viewing; text is for pasting into an email or
reading in a terminal. (Optional: render the HTML to a PNG with
.claude/skills/ui-proposal/render.mjs <html> screenshots/epic-digest.png to
preview it inline.)
Conventions & gotchas
- Actionables surface, don't author. The "Needs your eyes" steps come verbatim from each
PR/epic's required
๐งช How to test / ๐งช Verify the whole thing section. If an item lands
with no steps, that's a signal the author skipped the required section โ the nudge in the
digest is intentional, don't paper over it by inventing steps.
- Epics are the unit. Lead with epics + progress, not a flat issue list โ that's
the whole point ("focus on epics").
- Interactive = render-only. A by-hand run just writes files and shows them; the
scheduled job is what delivers (emails via Resend, see above).
<details> in email: the collapsible sub-issue section renders expanded in
mail clients that strip <details> โ that's fine, it degrades gracefully.
- The renderer sorts blocked epics first, then by % complete, so attention
lands where it's needed.