Heartbeat
$
npx mdskill add aaronjmars/aeon/Heartbeat> **${var}** — Area to focus on. If empty, runs all checks.
SKILL.md
.github/skills/HeartbeatView on GitHub ↗
---
name: Heartbeat
description: Proactive ambient check — surface anything worth attention
var: ""
tags: [meta]
---
> **${var}** — Area to focus on. If empty, runs all checks.
If `${var}` is set, focus checks on that specific area.
Read memory/MEMORY.md and the last 2 days of memory/logs/ for context.
## Checks (in priority order)
### P0 — Failed & stuck skills (check first)
Read `memory/cron-state.json`. This file tracks every scheduled skill's state and quality metrics:
```json
{
"skill-name": {
"last_dispatch": "2026-04-06T12:00:00Z",
"last_status": "dispatched|success|failed",
"last_success": "2026-04-06T12:05:00Z",
"last_failed": "2026-04-05T12:03:00Z",
"total_runs": 10,
"total_successes": 8,
"total_failures": 2,
"consecutive_failures": 0,
"success_rate": 0.80,
"last_quality_score": 4,
"last_error": "error signature text"
}
}
```
Flag these conditions:
- **Failed skills**: any entry with `last_status: "failed"`. Report the skill name and when it failed.
- **Stuck skills**: any entry with `last_status: "dispatched"` where `last_dispatch` is **>45 minutes ago**. The skill was dispatched but never reported back — likely hung or crashed before the state update step ran.
- **API degradation**: any skill with `consecutive_failures >= 3`. This likely indicates an external API is down or rate-limiting. Report the skill, failure count, and `last_error`. If multiple skills share similar error signatures, flag the shared dependency.
- **Chronic failures**: any skill with `success_rate < 0.5` (and `total_runs >= 5`). The skill is failing more than it succeeds.
- **Self-check**: if heartbeat's own entry shows `last_success` is **>36 hours ago** (or missing), note that heartbeat itself may be unreliable.
### P1 — Stalled PRs & urgent issues
- [ ] Any open PRs stalled > 24h? (use `gh pr list`)
- [ ] Any GitHub issues labeled urgent? (use `gh issue list`)
### P2 — Flagged memory items
- [ ] Anything flagged in memory/MEMORY.md that needs follow-up?
### P3 — Missing scheduled skills
Read `aeon.yml` for enabled skills with schedules. Cross-reference with `memory/cron-state.json`:
- If an enabled skill has **no entry at all** in the state file, it has never been dispatched by the scheduler.
- If a skill's `last_success` is **>2x its schedule interval** old (e.g., a daily skill hasn't succeeded in >48h), flag it.
Do NOT use `gh run list` for this — the state file is authoritative.
## Dedup & notification
Before sending any notification, grep memory/logs/ for the same item. If it appears in the last 48h of logs, skip it. Never notify about the same item twice.
Batch all findings into a **single notification**, grouped by priority tier:
```
🔴 FAILED: skill-a (failed 2h ago), skill-b (stuck 1h ago)
🟡 STALLED: PR #42 open 3 days
🔵 MEMORY: follow-up on X flagged 2 days ago
```
## Public status page
After the priority checks (even when everything is green — this step **always** runs), regenerate `docs/status.md` so the public GitHub Pages site reflects current fleet health.
### Data sources
- `memory/cron-state.json` — per-skill run state (authoritative)
- `memory/issues/INDEX.md` — open issue table
- `aeon.yml` — enabled skill list with schedules
- Latest `articles/token-report-*.md` (most recent by filename date) — optional; powers the Token Pulse section. Skipped silently when no file exists.
### Overall status
Compute one of three overall states from the same signals used above:
- `🔴 DEGRADED` — any P0 flag fired (failed skill, stuck skill, consecutive_failures ≥ 3, chronic failures with success_rate < 0.5, heartbeat self-check >36h stale)
- `🟡 WATCH` — any P1/P2/P3 flag fired (stalled PRs, urgent issues, flagged memory items, skills >2x their schedule interval old) or any open issue with severity `critical` or `high`
- `🟢 OK` — no flags at all
### Format
Write `docs/status.md` with Jekyll frontmatter so it renders as a gallery page:
```markdown
---
layout: default
title: "Status"
permalink: /status/
---
# Agent Status
**Overall:** 🟢 OK
**Updated:** 2026-04-24 19:06 UTC
**Open issues:** 0
**Next scheduled run:** heartbeat at 20:00 UTC
Auto-generated by the `heartbeat` skill on every run (3× daily at 08:00 / 14:00 / 20:00 UTC). If the Updated timestamp is more than ~8h stale, the agent is not running.
## Token pulse
| Token | Price | 24h | Liquidity | Volume (24h) | FDV |
|-------|-------|-----|-----------|--------------|-----|
| AEON | $0.0000032626 | -11.16% | $223.4K | $41.3K | $326.3K |
_Source: `articles/token-report-2026-04-28.md` · verdict: SLIDING_
## Skill health (last 7 days)
| Skill | Last run | Status | Success rate | Consecutive failures |
|-------|----------|--------|-------------:|---------------------:|
| token-report | 2026-04-24 12:30 UTC | ✅ success | 100% | 0 |
| fetch-tweets | 2026-04-24 06:53 UTC | ✅ success | 95% | 0 |
| … | … | … | … | … |
## Open issues
_(if INDEX.md has any open rows, render them here; otherwise: "No open issues.")_
| ID | Title | Severity | Category | Detected |
|----|-------|----------|----------|----------|
| ISS-001 | … | medium | rate-limit | 2026-04-22 |
---
*Fork this repo and your copy inherits this page automatically — [how it works](/memory/).*
```
### Rules
- Include **all** enabled skills from `aeon.yml` (not only those with recent runs). For skills with no entry in cron-state.json, show `—` for timestamp and `not yet run` in status.
- Sort the skill table by last-run timestamp descending (most recent first); skills that have never run sink to the bottom.
- Format timestamps as `YYYY-MM-DD HH:MM UTC` (strip seconds and the `Z`).
- Success rate shows `total_successes / total_runs × 100` rounded to whole percent; display `—` when `total_runs == 0`.
- Status column icons: `✅ success`, `❌ failed`, `⏳ dispatched` (if last_dispatch within 45min), `🕸 stuck` (if last_dispatch > 45min and last_status still dispatched), `—` (never run).
- For the `Next scheduled run:` line, pick the enabled skill with the soonest upcoming cron time relative to now.
- Dedup state: re-running heartbeat overwrites `docs/status.md` wholesale each time — do not append.
- Never expose values from `.env`, secrets, or anything outside cron-state.json + issues/INDEX.md + aeon.yml + articles/token-report-*.md. This file is public.
### Token pulse rules
- Pick the **latest** `articles/token-report-*.md` by filename date (sort descending, take the first match).
- **Staleness:** if the picked file's date is older than 24h relative to the heartbeat run timestamp, render `_No recent token data (latest report YYYY-MM-DD)._` in place of the table — do not lift stale figures into the table.
- **No file at all:** omit the `## Token pulse` section entirely. The status page must still render cleanly with no token row.
- **Token symbol:** read from `memory/MEMORY.md` "Tracked Token" table (first row, `Token` column). If the table is missing, render the heading as `## Token pulse` with the symbol column blank.
- **Field extraction (regex, tolerant of both old `Value | 24h Change` and new `Now | 24h Δ` table layouts):**
- **Price:** first `| Price |` row → first `$` value in the row → strip whitespace.
- **24h:** same Price row → first `±?\d+(\.\d+)?%` token in the row (typically the second cell). Render as written, preserving sign. If absent, render `—`.
- **Liquidity:** first `| Liquidity |` row → first `$` value.
- **Volume (24h):** first row whose first cell matches `Volume\b.*24h` or `24h Volume` → first `$` value.
- **FDV:** first `| FDV |` row → first `$` value.
- For any field whose row or `$` value cannot be located, render `—` for that cell only — do not skip the section.
- **Verdict line:** if the source article contains a `**Verdict:** LABEL` line, append `· verdict: LABEL` to the source line. If no Verdict line is present (older format), omit the suffix.
- **Source link:** the trailing `_Source: ..._` line names the exact article file used so a reader can verify the numbers.
The file lands on `main` through the workflow's auto-commit step — no explicit `git` commands needed in this skill.
## Output
If nothing needs attention, log "HEARTBEAT_OK" (plus the overall status page verdict, e.g. `HEARTBEAT_OK · STATUS_PAGE=OK`) and end your response.
If something needs attention:
1. Send a single concise notification via `./notify` (grouped by priority as above)
2. Log the findings and actions taken to memory/logs/${today}.md
3. Log one line with the status-page verdict, e.g. `STATUS_PAGE=DEGRADED — wrote docs/status.md`