This skill should be used when the user asks to "run a heartbeat", "run the agent loop", "process GitHub issues", "check for work", or runs the /heartbeat command. Executes the WoterClip heartbeat — picks up GitHub issues, resolves personas, does work, and reports back.
Scanned 8/30/2026
Install to Claude Code
npx -y skills add wotai-dev/woterclip --skill heartbeat --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Heartbeat?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/wotai-dev-heartbeat)More formats (shields.io, HTML) on the badges page.
---
name: heartbeat
description: This skill should be used when the user asks to "run a heartbeat", "run the agent loop", "process GitHub issues", "check for work", or runs the /heartbeat command. Executes the WoterClip heartbeat — picks up GitHub issues, resolves personas, does work, and reports back.
version: 0.1.0
---
# WoterClip Heartbeat
Execute the WoterClip heartbeat cycle: pick up assigned GitHub issues, resolve the right persona, do the work, and report back with structured comments. All GitHub operations go through the `gh` CLI — no MCP server required.
**Arguments:**
- `--dry-run` — Show what would be picked up without doing work
- `--persona <name>` — Only pick issues matching a specific persona
**Reference files** (consult as needed during execution):
- `${CLAUDE_PLUGIN_ROOT}/references/comment-format.md` — Comment templates and rules
- `${CLAUDE_PLUGIN_ROOT}/references/label-conventions.md` — Label lifecycle and atomic label operations
- `${CLAUDE_PLUGIN_ROOT}/references/status-mapping.md` — GitHub state model, sort order, inbox filtering
- `${CLAUDE_PLUGIN_ROOT}/references/sub-issues.md` — Canonical create/attach/verify procedure for sub-issues
- `${CLAUDE_PLUGIN_ROOT}/references/persona-dispatch.md` — Subagent dispatch, outcome contract, and fallback rules
- `${CLAUDE_PLUGIN_ROOT}/references/beat-economics.md` — Clock capture, time ceiling, stop reasons, log fields
All `gh issue` / `gh api` calls below target the repo from config `github.repo` (pass `--repo <owner/name>` explicitly — never rely on the working directory's default remote).
## Step 1: Load Config & Lock
1. Read `.woterclip/config.yaml`. If missing, stop and instruct the user to run `/woterclip-init`. Nothing is created yet, so this exit records nothing.
2. Check for lockfile at `.woterclip/.heartbeat-lock`.
- Exists and **less than** `stale_lock_hours` old → stop: "Previous heartbeat still active. Skipping." **This beat never took the lock — do not delete it and do not record a beat line.**
- Exists and **older than** `stale_lock_hours` → delete it, log: "Cleaned stale lockfile."
- No lockfile → proceed.
3. Take the lock. This one command creates it **and prints it**, so the beat observes its own id and start epoch:
```bash
printf '{"beat_id":"%s","started_at":"%s","started_epoch":%s}\n' \
"$(date -u +%s)-$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(date -u +%s)" \
| tee .woterclip/.heartbeat-lock
```
Carry the printed `beat_id` and `started_epoch` forward — they are the beat's identity and its clock.
4. **Ownership rule, applied at every exit from here on.** Re-read `.woterclip/.heartbeat-lock`. Delete it **only if it still carries this beat's `beat_id`**; if it is missing or carries another id, a later beat cleaned and re-took it — leave it alone. Deleting a lock this beat does not own hands two beats the same repo.
5. **Every exit from here on** records one beat line (step 9 format) naming that exit's stop reason, then applies the ownership rule. `--dry-run` and a superseded beat record none. The exit-to-reason map is in `${CLAUDE_PLUGIN_ROOT}/references/beat-economics.md`. Beat elapsed is `$(date -u +%s)` minus `started_epoch`.
Check quiet hours: if `quiet_hours.enabled` and current time is within the quiet window:
- `behavior: "skip"` → record a `quiet_hours` beat line, apply the ownership rule, exit: "Quiet hours active. Skipping."
- `behavior: "triage-only"` → proceed but only load Orchestrator persona (skip worker personas in step 3).
## Step 2: Check Inbox
1. Fetch open issues assigned to the authenticated user:
```bash
gh issue list --repo <owner/name> --assignee @me --state open \
--json number,title,labels,createdAt --limit 100
```
2. Filter and sort client-side per `${CLAUDE_PLUGIN_ROOT}/references/status-mapping.md` § Filter Rules and § Sort Order (labels come from the JSON above).
3. Detect stale `agent-working` labels: if an issue has `agent-working` but no heartbeat comment within `stale_lock_hours`, clean the stale label (`gh issue edit N --remove-label agent-working`, post cleanup comment).
## Step 3: Pick Issue
1. If `--persona <name>` flag is set, filter to only issues matching that persona's label.
2. Pick the first issue from the sorted inbox.
3. If `--dry-run`, report what would be picked, then apply the ownership rule and exit. No beat occurred, so record **no** beat line:
```
Dry run — would pick:
#12 [backend] "Issue title" (in-progress, priority:high)
Queue:
#15 [frontend] "Other issue" (todo)
```
4. If no issues match → record a beat line (`queue_empty` when zero issues were worked this beat, otherwise `complete` with the running count), apply the ownership rule, exit: "No issues in queue. Heartbeat complete."
## Step 4: Resolve Persona
1. Read the issue's labels. Find the persona label by matching against the `personas` map in config.
2. No persona label found → load the persona with `is_default: true` (typically Orchestrator).
3. Load persona files from `.woterclip/<persona.path>/`:
- `SOUL.md` → inject into context as identity instructions
- `TOOLS.md` → inject into context as tool guidance
- `config.yaml` → read runtime settings
The persona's `config.yaml` supplies `model`, `thinking_effort`, `max_turns`, and `enable_chrome`; how each feeds the step 8 dispatch is defined in `${CLAUDE_PLUGIN_ROOT}/references/persona-dispatch.md`.
## Step 5: Validate Tools
Read `required_tools` from persona config. Verify each entry by its kind:
- `gh` → verify with `gh auth status` (exit 0) — this proves both the CLI and authentication
- `mcp__*` entries (e.g., `mcp__neon`) → these are MCP tool prefixes, not executables: verify by checking whether any tool starting with that prefix is available in the current session. Never run `command -v` on an `mcp__*` name.
- Other executables (e.g., `docker`) → verify with `command -v <tool>`
**If `gh` itself is unavailable or unauthenticated:** no GitHub mutation is possible — do not attempt the blocked-comment path (it needs gh). Record a `blocked_exit` beat line, apply the ownership rule, and exit with an error message telling the user to run `gh auth login`. (Same rule as the mid-work failure in step 8.)
**If any other required tool is unavailable** → stop work on this issue immediately:
- Post a blocked comment naming the missing tool
- Apply `agent-blocked` label, remove `agent-working` if present:
`gh issue edit N --add-label agent-blocked --remove-label agent-working`
- Proceed to step 11 (next issue). **This is not a beat exit** — record no beat line and do not touch the lockfile; the beat's stop reason still comes from step 11.
## Step 6: Lock Issue
1. Read the issue's current labels: `gh issue view N --json labels --jq '.labels[].name'`.
2. If `agent-working` is already present (from a previous heartbeat on same issue), proceed.
3. Otherwise: `gh issue edit N --add-label agent-working` (atomic — no need to rewrite the label set).
## Step 7: Understand Context
1. Run `date -u +%s` and carry the printed value as this issue's start epoch — it covers the dispatch. Separate from the beat clock in the lockfile.
2. Read issue title, body, and all comments: `gh issue view N --json title,body,comments`.
3. If the issue is a sub-issue, read its parent for broader context: follow the `Parent: #N` reference in the issue body (guaranteed by the sub-issue creation procedure in `${CLAUDE_PLUGIN_ROOT}/references/sub-issues.md`), then `gh issue view <parent> --json title,body,comments`.
4. Identify new comments since the last heartbeat (look for comments after the last WoterClip-formatted comment).
5. Parse heartbeat counter: find the last comment matching `Heartbeat #N` pattern. Next comment will be `#N+1`. If none found, start at `#1`.
## Step 8: Do Work
Dispatch the work to a persona subagent per `${CLAUDE_PLUGIN_ROOT}/references/persona-dispatch.md` — prompt composition, the model parameter, fallback, error handling, and the outcome contract are all defined there. The branch shape:
1. Dispatch on the persona's `model`; the subagent returns a structured outcome that steps 9–10 consume.
2. Refused at invocation — no subagent primitive, or override rejected before execution began → work inline on the session model; record the fallback for the step 9 report (never block the issue for this reason alone).
3. Failure after execution may have begun, or unparseable outcome → treat as a **blocked** outcome (step 10 blocked path); when indeterminate, prefer blocked — re-running inline could duplicate work.
The work itself — wherever it runs — follows the persona's SOUL.md instructions and varies by persona:
**Orchestrator persona:** Triage the issue – apply persona labels, create sub-issues, or escalate. Never write code.
**CEO persona:** Make strategic decisions – prioritization, scope, architecture, coordination. Never write code.
**Worker personas (backend, frontend, etc.):**
- Use repo tools (Read, Write, Edit, Bash, Grep, Glob) to implement changes
- For large scope: decompose into GitHub sub-issues following `${CLAUDE_PLUGIN_ROOT}/references/sub-issues.md` (create with `--assignee @me` + persona label + `Parent: #N` body reference, attach by issue ID with `-F sub_issue_id=`, verify the attach, summarize in your returned outcome)
- For small scope: work directly, use internal tasks to track progress
- Commit changes with descriptive conventional commit messages
- Respect `max_turns` from persona config as a work budget
**If `gh` auth expires or GitHub API errors persist mid-work:** Stop immediately. Leave `agent-working` in place (cleaned as stale by a later beat). Record a `blocked_exit` beat line, apply the ownership rule, exit.
## Step 9: Report
Before posting, re-read the lockfile and confirm it still carries this beat's `beat_id`, and that the issue still carries `agent-working`. If a later beat superseded this one, exit **silently** — no comment, no beat line, no lock deletion; all three are the successor's. A beat line here would close its in-flight group. If the report post itself fails with persistent gh errors, follow the step 8 mid-work rule.
Post a structured comment on the GitHub issue: `gh issue comment N --repo <owner/name> --body "..."`.
Follow the comment format from `${CLAUDE_PLUGIN_ROOT}/references/comment-format.md`:
- Include `Heartbeat #N` counter (incremented from step 7)
- Include timestamp and duration
- Include the `**Model:**` line naming the model that performed the work — the loop's dispatch parameter (session model on fallback, naming both configured and actual); the subagent's self-reported model is advisory only
- Include persona name in footer
- List commits with SHAs, sub-issues created, and next steps
- For blocked status: @-mention who needs to act (Board user's login from `github.board_user`) **and include the required `**Clears when:**` line** naming what will unblock the issue
Append an **issue line** to `.woterclip/heartbeat-log.jsonl` — one per issue worked. `duration_sec` is `date -u +%s` now, minus the start epoch printed at step 7 — seconds on this issue, not the beat's cost:
```json
{"heartbeat": N, "timestamp": "ISO", "issue": "#12", "persona": "name", "duration_sec": N, "status": "in_progress|completed|blocked|triaged|decomposed", "actions": ["description"]}
```
At each beat exit that still owns its lock — step 3, 5, 8, 9, or 11 — append exactly one **beat line**. Field, exception, and stop-reason definitions are in `${CLAUDE_PLUGIN_ROOT}/references/beat-economics.md`:
```json
{"type": "beat", "started_at": "ISO", "ended_at": "ISO", "beat_elapsed_sec": N, "issues_worked": N, "stop_reason": "time_ceiling"}
```
## Step 10: Update State
Read the issue's current labels (`gh issue view N --json labels`), then update based on outcome:
Transitions are defined in `${CLAUDE_PLUGIN_ROOT}/references/status-mapping.md` § WoterClip Outcomes. Two ordering rules the loop must honor:
- **Completed** — remove `agent-working` and `in-progress` *before* `gh issue close` (close does not touch labels, and stale-label cleanup only scans open issues). If a PR opened instead, swap to `in-review` and leave the issue open.
- **Blocked** — one combined edit: `gh issue edit N --remove-label agent-working --add-label agent-blocked`.
If any `--add-label` fails because the label doesn't exist on the repo, create it and retry (see `${CLAUDE_PLUGIN_ROOT}/references/label-conventions.md` § Label Operations) — do not skip the transition.
For blocked issues: @-mention the Board user's GitHub login in the comment text (e.g., "**@board-login** — please review"). Note: GitHub does not notify a user of their own comments — if `github.board_user` is the same account gh is authenticated as, the mention will NOT produce a notification; the Board must watch the repo or check `/woterclip-status` for blocked items.
## Step 11: Next Issue or Exit
"Eligible issues remain" means the sorted inbox from step 2 minus issues worked this beat — do not re-query to answer these tests. The ceiling (1200s default) and stop reasons are defined in `${CLAUDE_PLUGIN_ROOT}/references/beat-economics.md`.
1. Elapsed ≥ ceiling with issues remaining → stop intake, reason `time_ceiling`.
2. Else issues worked ≥ `max_issues_per_heartbeat` with issues remaining → stop intake, reason `issue_budget`.
3. Else issues remain → return to **Step 2** for the next issue.
4. Else the queue is exhausted → reason `complete`.
Before exiting, add to the report: suggest pausing the schedule if 0 todo issues remain, and suggest Board attention rather than more heartbeats if 3+ issues are blocked. Then exit per the step 1 invariant.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!