Date: 2026-05-02 Status: Design accepted — pending implementation plan Audience: Mikel (sole user; infra is personal, gitignored)
Receive a Discord message every morning at 07:00 CEST recapping the
last 24 hours of interlyse-next-paper activity: which
papers shipped, what was interesting about each, the project’s overall
state, and the current top blockers/limitations ranked by judgment.
The next-paper scheduler runs every 2h and ships ~1 paper per ~2–4h.
Without an aggregated view, knowing “what happened overnight” requires
manually walking git log,
INTERLYSE-RUN-STATUS.md, and the JSONL logs in
.scheduled-logs/. A daily push to Discord makes the work
visible without active polling.
Mirrors the existing interlyse-next-paper pattern. Three
filesystem-level pieces, all gitignored or outside the repo:
~/.config/systemd/user/
interlyse-daily-summary.timer # fires daily at 07:00 local time
interlyse-daily-summary.service # one-shot, calls the wrapper
scripts/scheduled/ # gitignored
daily-summary.sh # wrapper — runs claude, posts to Discord
daily-summary.prompt.md # the prompt Claude reads on stdin
~/.config/interlyse/
discord-webhook.url # 600 perms; already created
.scheduled-logs/ # gitignored
summary-YYYY-MM-DDTHH-MM-SS.log # claude stream-json transcript
summary-latest.json # last successful Discord payload
07:00 CEST
↓
systemd timer triggers service
↓
daily-summary.sh starts
↓
distrobox enter dev-box → claude -p < daily-summary.prompt.md
↓
claude reads:
- .scheduled-logs/*.log (last 24h, by mtime)
- INTERLYSE-RUN-STATUS.md (papers + triage)
- BACKLOG.md (blockers, cross-paper signal annotations)
- ROADMAP.md (active milestone)
- git log --since='24 hours ago'
↓
claude writes structured JSON to .scheduled-logs/summary-latest.json
↓
claude exits
↓
wrapper reads summary-latest.json, validates, POSTs to Discord webhook
↓
on success: log "posted" + exit 0
on parse failure: post "summary failed, see <log path>" + exit 1
interlyse-daily-summary.timer[Unit]
Description=Interlyse: trigger daily summary at 07:00
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
Unit=interlyse-daily-summary.service
[Install]
WantedBy=timers.target
Persistent=true ensures a missed slot (e.g. machine off
at 07:00) fires on next boot. Local time interpretation; DST handled by
systemd.
interlyse-daily-summary.service[Unit]
Description=Interlyse: daily summary post to Discord
[Service]
Type=oneshot
WorkingDirectory=/home/deck/projects/statipy
ExecStart=/home/deck/projects/statipy/scripts/scheduled/daily-summary.sh
TimeoutStartSec=20min
20 min is generous — the summary is read-only and small (probably <5 min). The hard ceiling protects against the same hang class that wedged the next-paper service.
daily-summary.shWrapper script. Same structure as run-next-paper.sh:
Set up logging to
.scheduled-logs/summary-<timestamp>.log.
Run
distrobox enter dev-box -- bash -lc 'claude -p < prompt.md > /dev/null'.
Claude writes the JSON to summary-latest.json directly
(instructed by the prompt); stdout is discarded.
After claude exits, validate JSON exists and parses.
POST it to Discord:
curl -sS -X POST "$WEBHOOK" \
-H 'Content-Type: application/json' \
--data-binary @.scheduled-logs/summary-latest.jsonPost fallback message on parse/POST failure.
No flock against the next-paper scheduler — the summary is read-only and can race with a paper run cleanly.
daily-summary.prompt.mdInstructs Claude to:
[now − 24h, now] in CEST.git log --since='24 hours ago' --grep='paper' and
.scheduled-logs/*.log for result events with
paper-shipped confirmations. Cross-reference with
INTERLYSE-RUN-STATUS.md.INTERLYSE-RUN-STATUS.md and produce a rich bullet:
.scheduled-logs/.lock (held > 90min).scheduled-logs/summary-latest.json
matching Discord’s webhook embed schema.One message with content + multiple embeds:
{
"content": "🗞 **Interlyse daily recap — Sat 2026-05-02 07:00 CEST**\ncovering 2026-05-01 07:00 → 2026-05-02 07:00 CEST",
"embeds": [
{
"title": "Papers shipped (3)",
"color": 3447003,
"fields": [
{ "name": "#26 Title — Journal Year", "value": "..." },
{ "name": "#27 ...", "value": "..." }
]
},
{
"title": "Overall state",
"description": "26 documented · 2 Full · 4 Headline-only · ...",
"color": 5763719
},
{
"title": "Top blockers (judgment-ranked)",
"description": "1. zoo::rollmean — ...\n2. lme4::lmer — ...",
"color": 15844367
},
{
"title": "Anomalies",
"description": "(none) · or list",
"color": 15548997
}
]
}Discord limits to respect: - Total payload across all embeds ≤ 6000 chars - Each embed field value ≤ 1024 chars - Max 10 embeds per message, max 25 fields per embed - The prompt instructs Claude to truncate gracefully if a paper would push past the limit (move the long detail into a “see log” note).
If 0 papers shipped in the window, the “Papers shipped” embed is replaced with a one-line “No papers shipped in the last 24h” note — the rest of the recap (state, blockers, anomalies) still goes out.
| Failure | Detection | Recovery |
|---|---|---|
| Claude session hangs | TimeoutStartSec=20min triggers |
systemd kills cgroup; no Discord post that day; wrapper exits non-zero in journal |
| Claude exits but JSON missing/malformed | Wrapper validates with jq empty |
Wrapper posts fallback “summary generation failed, see
<log path>” |
| Discord webhook returns non-2xx | curl exit code | Wrapper logs and exits non-zero; next day’s report still fires |
| Discord webhook URL deleted/rotated | curl 401/404 | Same as above; user re-creates webhook and updates
~/.config/interlyse/discord-webhook.url |
| Machine off at 07:00 | systemd timer state | Persistent=true fires on next boot |
.scheduled-logs/None. All clarifying questions resolved during brainstorming (delivery target, schedule time, window, content depth, blocker ranking method, architecture).