From claude-code-hermit
Executes heartbeat checklist from HEARTBEAT.md to detect stale sessions, waiting timeouts, and evaluate items for issues in Claude Code Hermit. Supports run/start/stop/status/edit subcommands.
npx claudepluginhub gtapps/claude-code-hermit --plugin claude-code-homeassistant-hermitThis skill uses the workspace's default tool permissions.
Background health checker that periodically evaluates a checklist and surfaces anything that needs operator attention. Inspired by OpenClaw's heartbeat pattern.
Runs agent heartbeat for periodic checks: memory consolidation from logs, dream review, custom HEARTBEAT.md tasks, file organization. Auto-executes every 30min during active hours or on manual trigger.
View and modify Hermit agent configuration for projects including model, channels, budget prompts, morning brief, heartbeat, routines, idle behavior, compaction thresholds, Docker packages, and unattended mode.
Displays WoterClip status: schedule, last heartbeat with persona/issue/outcome, recent Linear issue changes (completed/in-progress/blocked/new), todo queue by persona/priority, blocked items. Supports --history flag.
Share bugs, ideas, or general feedback.
Background health checker that periodically evaluates a checklist and surfaces anything that needs operator attention. Inspired by OpenClaw's heartbeat pattern.
/claude-code-hermit:heartbeat run — execute one tick immediately
/claude-code-hermit:heartbeat start — start the recurring loop
/claude-code-hermit:heartbeat stop — stop the recurring loop
/claude-code-hermit:heartbeat status — show last result and loop state
/claude-code-hermit:heartbeat edit — modify the checklist
Execute one heartbeat tick immediately. Useful for testing the checklist.
.claude-code-hermit/HEARTBEAT.md.claude-code-hermit/config.json for heartbeat settingsactive_hours — if the current time is outside the configured window:
session_state from state/runtime.json (the single source of lifecycle truth — never parse SHELL.md Status: for decisions). If session_state is waiting: skip this check entirely (agent is intentionally blocked). If session_state is in_progress:
## Progress Log entry timestamp from SHELL.md. If no progress entry exists, use the session start time from SHELL.md header.heartbeat.stale_threshold from config (default: "2h").stale-session and text "No progress for {elapsed}. Process may have died, be stuck in a rate limit, or desynced after context compaction. Reply resume to unstick via /claude-code-hermit:session-start, or idle to drop the session."session_state is waiting (from runtime.json) and heartbeat.waiting_timeout is set (not null):
waiting (from last Progress Log entry or status change).waiting_timeout with no channel activity: update runtime.json session_state to idle, update SHELL.md Status to idle (cosmetic), notify the operator: "No operator response for {timeout}. Transitioning to idle."## Monitoring: [HH:MM] Heartbeat: resumed (was inactive)..claude-code-hermit/sessions/SHELL.md for current task contextSemantic key taxonomy for built-in checks:
stale-sessionchecklist:<first-8-chars-of-item-normalized>proposal-pending:<PROP-NNN>waiting-timeoutmicro-proposal-pending:<id>custom:<first-100-chars-normalized> — fallback onlyBefore appending any alert to SHELL.md Monitoring, run this procedure:
.claude-code-hermit/state/alert-state.json. If missing, initialize: {"alerts": {}, "last_digest_date": null, "self_eval": {}}.alerts:
count: 1, consecutive_clean: 0, suppressed: false, first_seen: today, last_seen: today, text: <alert text>. Append to Monitoring normally.consecutive_clean to 0, update last_seen. Append to Monitoring normally.suppressed: true, reset consecutive_clean to 0. Append once: [HH:MM] Heartbeat: above alert suppressed after 5 fires (first: {first_seen}). Daily digest only. Notify the operator.consecutive_clean to 0, update last_seen. Do NOT append to Monitoring.consecutive_clean. If consecutive_clean >= 2: resolve the alert and remove entry from state.
[HH:MM] Heartbeat: resolved — {text}. Remove entry.last_digest_date is not today: if any suppressed alerts exist, notify the operator: Suppressed alert digest: {list with counts and ages}. Set last_digest_date to today..claude-code-hermit/state/micro-proposals.json → pending. For each entry in the array where status is "pending" and tier is 1: append to Monitoring: [HH:MM] Heartbeat: micro-proposal '{id}' awaiting operator input — {question}. Use semantic key micro-proposal-pending:<id> for dedup.state/alert-state.json.If nothing actionable:
total_ticks in alert-state.json)heartbeat.show_ok:
true: notify the operator with "Heartbeat OK"false (default): no channel message — silent acknowledgmentIf something found:
## Monitoring section with timestamp (subject to alert dedup above)Heartbeat alert:
- Step 3 may be unblocked: test environment is now reachable
- PROP-018 has been open for 3 sessions without review
Important: Do NOT implement fixes — only report. The operator or the next session decides what to do. If a finding suggests a reusable improvement, create a proposal via /claude-code-hermit:proposal-create — proposals are documentation, not action.
After evaluating the checklist (always, regardless of findings):
total_ticks in state/alert-state.jsontotal_ticks % 20 == 0: run self-evaluation (see below)Triggered every 20 ticks. The tick counter persists in state/alert-state.json so it survives session boundaries.
Steps:
Read state/alert-state.json.
For each HEARTBEAT.md item: count alert lines referencing that item in the evaluation window (last 20 ticks). If zero alerts → increment clean_ticks in self_eval entry and update sessions_seen / last_session_id. If alert fired → reset clean_ticks to 0.
sessions_seen definition: Number of distinct session IDs (read from SHELL.md **ID:** field) during which this item was evaluated. Incremented only when the current session ID differs from last_session_id.
Self-eval entry fields: text (string), clean_ticks (int, reset to 0 on alert), noise_ticks (int, see below), sessions_seen (int), last_session_id (string), first_observed (date string), proposed (bool).
2b. Proposals scan (dismissal cleanup + noise tracking): Scan proposals/ once for all PROP-NNN files that have self_eval_key present and non-null in frontmatter. For each matched proposal, look up the self_eval entry by self_eval_key, then apply both rules in a single pass:
Dismissal cleanup (runs for all items, every self-eval pass):
source is auto-detected, status is dismissed, and proposed: true → reset proposed: false and clear clean_ticks to 0. Heartbeat owns its own state cleanup — no dependency on reflect or proposal-act timing.Noise tracking (runs only for items where alerts fired in the last 20 ticks):
status is accepted or resolved → reset noise_ticks to 0 (the alert led somewhere useful).status is dismissed → increment noise_ticks by 1 (alert fired, fix was dismissed — noisy signal).noise_ticks.Checklist weight: Count items in HEARTBEAT.md. If > 10: track in self_eval with text "Checklist weight: {N} items".
Proposal threshold check: For each self_eval entry where proposed: false:
clean_ticks >= 20 AND sessions_seen >= 3 → create proposal via /claude-code-hermit:proposal-create with category capability, source: auto-detected, self_eval_key: <item_key>, evidence: "Item '{text}' has been clean for {clean_ticks} consecutive ticks across {sessions_seen} sessions. Consider removing it from HEARTBEAT.md to reduce token cost."noise_ticks >= 20 AND sessions_seen >= 3 → create proposal via /claude-code-hermit:proposal-create with category capability, source: auto-detected, self_eval_key: <item_key>, evidence: "Item '{text}' has fired {noise_ticks} consecutive noisy alerts (alerts tied to dismissed proposals) across {sessions_seen} sessions. Consider retuning or removing it from HEARTBEAT.md."proposed: true after creating either type of proposal.No channel message. No SHELL.md append. Output only flows through the proposal pipeline.
Write state/alert-state.json.
Start a recurring heartbeat loop using /loop.
heartbeat.every (default: "2h")/loop <interval> /claude-code-hermit:heartbeat runrun which checks active hours and evaluates the checklistIf a heartbeat loop is already running, cancel it first before creating a new one, then start fresh. This is safe to call from a routine — it resets the 3-day /loop expiry without losing any state.
Stop the recurring heartbeat loop.
/loop[HH:MM] Heartbeat: stoppedReport current heartbeat state:
Open .claude-code-hermit/HEARTBEAT.md for the operator to modify.
/claude-code-hermit:hermit-settings routines to add the items as routines, then remove them from HEARTBEAT.mdIf the operator asks about idle tasks (add, remove, manage idle work):
.claude-code-hermit/IDLE-TASKS.md instead of HEARTBEAT.mdIDLE-TASKS.md.template in state-templates/idle_behavior is not "discover" in config, warn: "idle_behavior is set to 'wait' — these tasks won't be picked up automatically"After evaluating the checklist, if SHELL.md status is idle:
NEXT-TASK.md pickup (active for both wait and discover idle behavior):
Check for sessions/NEXT-TASK.md. If found, respect the configured escalation level:
waiting. Set waiting_reason to "conservative_pickup" in runtime.json./claude-code-hermit:session-startThe following only activate when idle_behavior is "discover" in config:
If no NEXT-TASK.md and no pending channel messages:
Idle task pickup: Read .claude-code-hermit/IDLE-TASKS.md. If the file exists and has unchecked items (- [ ]):
state/runtime.json to set:
"idle_task": {
"text": "<exact checkbox text>",
"line": <1-based line number>,
"picked_at": "<ISO 8601>"
}
/claude-code-hermit:session-start --task '<item text>' with cost capped at idle_budget from config (default: "$0.50").escalation: conservative regardless of the global setting — idle tasks should never do anything risky without asking.session-mgr during the idle transition, so it stays atomic with the archive.Priority alignment: Read OPERATOR.md for context. Think about whether current work aligns with the operator's stated priorities and constraints. If deadlines or budgets need attention, cross-reference with .claude/cost-log.jsonl and alert.
All time comparisons use the timezone from config.json.
Morning/evening routines are handled by /claude-code-hermit:hermit-routines — each routine is a per-session CronCreate registered by hermit-start.py on launch.
Manage routines with /claude-code-hermit:hermit-settings routines. See the brief skill for --morning and --evening flag behavior.