| 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:
{
"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
P2 — Flagged memory items
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. Only flag it once its first scheduled window has clearly passed — compute the most recent cron occurrence before now from its
schedule, and flag only if that occurrence is more than 2 hours ago (the margin absorbs normal dispatcher lag). A freshly-enabled skill whose first window is still upcoming, or passed under 2h ago, is expected to have no entry — do not flag it and do not let it push the status page to 🟡 WATCH.
- If a skill's
last_success is >2x its schedule interval old (e.g., a daily skill hasn't succeeded in >48h), flag it.
- Never flag reactive-only (
schedule: reactive) or on-demand (workflow_dispatch) skills for a missing entry — they have no cron window.
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:
---
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 (daily at 08:00 UTC). If the Updated timestamp is more than ~26h 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:
- Send a single concise notification via
./notify (grouped by priority as above)
- Log the findings and actions taken to memory/logs/${today}.md
- Log one line with the status-page verdict, e.g.
STATUS_PAGE=DEGRADED — wrote docs/status.md