A background daemon per Claude profile tracking each session's state: running, waiting, blocked, idle, ended or died.
You and orchestrator agents read session state here, not from transcripts. Landing flow: Merge strategy; stateless gate rules: Permission System.
smine-heartbeat status queries the daemon; flags narrow or reshape it.
| Command | Shows |
|---|---|
| smine-heartbeat status | One line per session (*: start unseen by this daemon), then degraded sensors. |
| smine-heartbeat status --format json | Status document (schema 1, cmd/heartbeat/schema/status-v1.json). |
| smine-heartbeat status --session self | Calling session (from a Claude Code Bash tool). |
| smine-heartbeat status --repo <path> | Sessions in the repository and its worktrees. |
| smine-heartbeat status --worktree . --format json | Worktree holding the path (.: cwd's, resolved to its canonical toplevel) and its sessions. branches: its row, if a registered repository's last scan holds it; branches[0].target: its landing target. |
| Exit | Means |
|---|---|
| 3 | No daemon answers. |
| 4 | Port's daemon serves another Claude profile. |
| 1 | Anything else. |
| Item | Detail |
|---|---|
| Install | The installer: on macOS the LaunchAgent com.smine.heartbeat (KeepAlive) and ~/go/bin/smine-heartbeat; on Windows the logon task \smine\com.smine.heartbeat (restarts on failure) running the checkout's bin\heartbeat.exe, with %LOCALAPPDATA%\smine\bin\smine-heartbeat.exe on PATH. |
| Logs | ~/Library/Logs/smine-heartbeat.{out,err}.log; on Windows %LOCALAPPDATA%\claude-routine\logs\heartbeat.log, written through --log-file (Task Scheduler captures nothing). |
| Port | heartbeat.port (install record), default 6101; Config page Smine tab or smine-rules settings set heartbeat.port <port>. |
| Platform | serve runs on macOS and Windows. macOS watches process exits through kqueue; elsewhere the daemon polls pid liveness every heartbeat.exits_poll (default 1s), so an exit is seen up to one interval late. Fleet messaging sockets are unix-only: on Windows subscribe is refused and the daemon logs it once at start (Fleets). |
| Capture hook bodies | Boot the agent out, run bin/heartbeat serve --record /tmp/hooks.jsonl, boot it in. |
| Load evidence | make heartbeat-load |
--addr / HEARTBEAT_ADDR binds any address; only the LaunchAgent's or logon task's 127.0.0.1:<heartbeat.port> is loopback. No route checks the caller.
Sessions keep working, with a hook error notice per event; no heart attack runs.
A restart rebuilds only sessions with a ~/.claude/sessions/ file and a transcript at <claude home>/projects/*/<id>.jsonl. Hook-only sessions stay unknown until their next hook, then show since_restart; old ledger denials do not re-block them.
Inputs: hook events, the toolpolicy denial-ledger tail, process exits, registry seeds.
| Event | State |
|---|---|
SessionStart | idle |
UserPromptSubmit | running |
A permission prompt; a root PreToolUse of AskUserQuestion or ExitPlanMode; the notifications permission_prompt, elicitation_dialog, elicitation_url_dialog, agent_needs_input | waiting |
Stop | idle; blocked if the turn's last tool outcome was a toolpolicy denial, until a later call completes. |
| A denial-ledger line for an idle session | blocked |
SessionEnd | ended |
StopFailure, or a process exit without SessionEnd | died |
A root Stop a runtime stop check blocked (Runtime enforcement) | running (no turn end); under #disable hb no block, the turn ends. |
| Rule | Detail |
|---|---|
| Pids | Exits count only for sessions with a pid, taken only from ~/.claude/sessions/<pid>.json or the container file. |
| Hook-only sessions | No exit watch: ended or died only via SessionEnd or StopFailure, else the last state stays. |
| Unknown pid | Registry pid unknown to the host: a hook-seen session is died at once. |
| Container cells | Pid file <claude home>/smine/container-sessions/<pid>.json, written by the routine runner once the session id is known: the host pid of the cell's docker run client, so exit watch and died work as for sessions/. |
| SessionStart | Via the command hook binary heartbeatfeed (~/.claude/hooks/bin), which posts the body to HEARTBEAT_ADDR and discards the answer; every other event is an HTTP hook. |
| Subagents | Under their root, never change its state; own their permission requests and notifications. A tool hook whose agent_id no SubagentStart registered counts as the root's only in an open turn (--agent session); after the root's Stop it is a background helper, the root idle until its next prompt. |
| Foreign bodies | transcript_path outside the served Claude home: refused (409), degrades hooks; none: accepted. |
| Sensors | Degraded inputs show in sensors (hooks, registry, process_exit, ledger, peek, action_ledger, config, and with --repos git, agentwt), never as blank rows. |
The heart attack is heartbeat's own kill: heartbeat alone, no orchestrator or subscription, is allowed to end a session's agent automatically. Its one application today is the model policy: it stops any subagent on a forbidden model, spawned without a model, or whose transcript model lacks the named one.
| Aspect | Rule |
|---|---|
| Spared | Root session; subagents with an allowed, matching named model. |
#disable hb | Ignored (heartbeat spells it); mutes every other /hook answer. |
| Policy | heartbeat.forbidden_subagent_models: globs over model ids and Agent-call aliases; default empty (forbids nothing), set per install. Live key, via the config watch. |
| Sightings | A denied Agent call with forbidden model input; a transcript whose first assistant line shows a forbidden model (read on tool hooks until known). |
| Stop | Then every PreToolUse, PostToolUse, PostToolUseFailure of the subagent gets continue: false, reason as stopReason and systemMessage; it ends at its next tool hook. An inherited forbidden model gets one tool call before it is read. |
| See it | smine-heartbeat status --session <id> --format json: each subagent's model. |
| Model aliases | Requested token proves nothing: haiku runs the session model on this account; hence stopping missing or mismatched models. |
Long tool call: the subagent stops when it returns. Started before a heartbeat restart: unregistered, never stopped. Hook answered past Claude Code's 1 s timeout: no stop; the next carries it.
serve --repos <claude home>/smine/repos.json enables git state. The file is read at start and watched: a repo registered on the Repos page is scanned and watched at once, a removed one leaves the rows, no restart. A file not written yet (no repo registered) is an empty registry; a malformed one degrades the git sensor and keeps the running set.
| Rule | Detail |
|---|---|
| File | serve --config <claude home>/heartbeat/config.json (default under --claude-home), rendered by the install's heartbeat component from catalog defaults plus record overrides (Settings § heartbeat). |
| Directory | 0700, tightened at start; also orchestrator state. |
| Contents | Exemptions, forbidden subagent models, staging target, texts, caps, timeouts, peek_addr (from peek.port); unknown keys refused. |
| Reload | live keys via the config watch; a restart key makes the apply restart the agent, losing open runs and findings. |
| Bad file | Malformed or unreadable: fatal at start; on reload ignored (running config kept, config sensor degraded). |
| Hand edits | Overwritten on apply; use the Hooks tab or smine-rules settings set heartbeat.<key> <value>. |
| Staging target | heartbeat.staging_target, where /done branch: lands. Unset by default: no staging landing row or train base, /done branch: refuses naming the setting. Set: top-level staging_target in status. |
| Field | Rule |
|---|---|
| Scans | Ref change in any worktree, git-changing Bash call or turn end in an agent worktree: one in-process agentwt scan; per repo one running, one queued; a 250 ms settle merges one command's triggers. Count heartbeat.scans_completed; last-scan failures repos[].scan_error, repos[].warnings. |
| Branch states | uncommitted, no_work, removable, merged, conflicting, merge_ready, unmerged; first match wins, against the landing target's train, else the default branch's. |
| Landing target | branches[].target: a fleet member's merge branch; else the base, if a local branch neither agent-owned nor default; else absent. agentwt reads the base off the reflog creation line the worktree hook writes when recreating the untouched session branch from the picked branch's name. |
| Work items | Wip plan dirs with a new or changed design/*.md in a worktree's feature folders; fleet targets and merge branches; the fork point. A merged plan is no branch's pending work. |
| Subagent branches | isolation: "worktree" subagents run on claude/agent-<agent_id>, in the row's agents (session, agent id); no fleet members. sessions: root sessions with cwd in the worktree. |
| Collisions | Levels planned_file, planned_symbol, touched, confirmed; a row per branch pair and level. Never counted: exempt_paths, version-only plan hunks and edits. Lanes: branches under a collision_prefixes prefix (default and deployed ["claude/"]; empty: none). Routine, review, restack, fleet merge branches and detached worktrees never collide, so a member's merge-branch conflict shows only as its branch state. |
| Train bases | Bound fleets add trains for target (subscribe --target feature/heartbeat) and merge branch (claude-orch/T1-<run uuid>), also a containment candidate: a member landed there reads merged, as agentwt status --candidate <merge branch> --train-base <merge branch> --train-base feature/heartbeat shows. Also: a configured staging target in each repo whose last scan saw it; every non-default local base the last scan's rows resolved. Old train_bases key: refused as unknown. |
| Health | Sensor git: ref watch; agentwt: each repo's last scan. |
A fleet: joined sessions, each subscriber, its registered worker branches. /join fleet: T1 runs smine-heartbeat join --fleet T1. Heartbeat never reads or writes titles for membership; the session-title hook adds the member token after a join.
| Command | Shows |
|---|---|
| smine-heartbeat status --fleet T1 --format json | Members' sessions, fleet row, member rows; once bound, only the bound repository's repos, branches, work_items, collisions (scoped like digests), trains. |
| smine-heartbeat status --fleet | Every fleet: id, run, target, state, members, subscriber. |
| smine-heartbeat status --fleet T1 | Text, plus member table. |
| Stage | Rule |
|---|---|
| Start | A name's first admitted join or subscribe starts a run (UUID v7: fleets[].instance, fleets[].started_at) owning letters, journal, roster; one live run per name. |
| Join | Refused: unknown or ended session; fleet bound to a repository other than the cwd's. Rejoin keeps the letter; joining another fleet leaves this one. |
| Leave | /close runs smine-heartbeat leave (token-checked against the join). Automatic when the session ends, dies, or is gone after a heartbeat restart (checked after the registry scan). A joined subscriber leaves at subscribe. |
| Remove | smine-heartbeat remove --fleet T1 --member A: any lettered member, workers too; a human act, no session, also the fleet page's Remove button. A new join restores the member. |
| Windows | No fleet subscriptions: digests travel over the subscriber's messaging socket, which the daemon reaches only on unix, so subscribe is refused (messaging sockets are not supported on windows, logged once at start). join, done, fleet and runtime deploy; their subscribe step reports the refusal. |
| Dissolve | No joined member, no active subscription: archived in full to ~/.claude/heartbeat/archive/<uuid>.json (members with token, branch, delivery, join/leave time; subscriptions; journal; 0600 in a 0700 dir; schema cmd/heartbeat/schema/archive-v1.json), then dropped from status. Name and letters free up: the next /join fleet: T1 starts at T1-MA. |
| Topic | Rule |
|---|---|
| Letters | Joined sessions and registered workers get letters in join or registration order (A … Z, AA, …), never reused in a run. members[].token = <fleet>-M<letter> (T1-MA): printed by join; used in titles, digests, journal, page. |
| Operator | The subscriber: no letter, token <fleet>-O. |
| Member guard | /done is a member's one channel to the orchestrator: its and its subagents' SendMessage calls are denied; each prompt gets a token-and-role reminder. |
| Workers | Headless cells; an ended session leaves its fleet, so a cell cannot stay a member. The active subscriber registers the branch via smine-heartbeat worker --branch claude/<name> --state running (token-checked like record), printing the token. Row: via: worker, branch, subscriber's session id. Registrations replay from the action ledger at start; member_assigned lines with an agent_id (from subagent membership) replay to nothing. |
| Delivered | /done (smine-heartbeat done, token-checked) sets members[].delivered until the member's next prompt. Workers: once the subscriber ran smine-heartbeat worker --branch claude/<name> --state delivered; --state running reopens. Landed only when delivered and its branch holds the merge branch's tip. |
| No fleet | A done outside a fleet is no delivery (Landing). |
| Journal | fleets[].journal: run acts the active subscriber recorded: |
smine-heartbeat record --kind assigned|decided|merged|spawned|note --subject <text> [--member <letter>]… [--detail <text>]
| Field | Rule |
|---|---|
| Binding | Subscribe binds the fleet to the subscriber's repository (the registered one holding its cwd) and target branch (fleets[].repo, fleets[].target); earlier joiners from other repositories are no members, listed in fleets[].foreign. |
| Merge branch | fleets[].merge_branch: claude-orch/<fleet>-<run uuid> once bound (the uuid keeps a reused name off an old run's branch). Members are judged against it and fast-forward onto it. |
| Synced | fleets[].synced: target holds the merge branch, so true from the cut, before any landing; "all work is on the target" only with target_state: integrated. |
| Member branches | members[].branch: worker: registered branch; session: its worktree's. The orchestrator sees all branches, acts on these only. |
| Target state | When |
|---|---|
| open | Otherwise; also before the first scan requesting target and merge branch, and after a daemon restart for a merged, cleaned-up fleet until a worker branch reappears. |
| integrated | At least one worker, all delivered, every worker branch still merged or removable, at least one seen in a scan since subscribing (this daemon run). Removed or never-created worker branches and joined workers in the main checkout count as done; subscriber, merge and target branches do not count. |
| missing | The requesting scan found no such local branch. |
An orchestrator subscribes from its Bash tool, handing the daemon its messaging socket and token (CLAUDE_CODE_MESSAGING_SOCKET, CLAUDE_CODE_MESSAGING_TOKEN).
smine-heartbeat subscribe --fleet T1 --target feature/heartbeat
| Command | Effect |
|---|---|
| subscribe --fleet T1 --target <branch> | Binds the fleet. Refused: unknown or ended session, socket not the registered one, cwd in no registered repository, T1 bound to another repository or branch. |
| subscribe --outside-collisions | Also hears collisions with non-fleet branches. |
| unsubscribe | Ends the subscription (same session, matching token), recorded so restarts don't revive it. Joined members stay: a handover to another orchestrator. |
| unsubscribe --finish | Ends the run: members leave, the run dissolves into its archive. |
A restarted session has a new socket and must subscribe again; the old subscription ends itself.
Member changes collect while the subscriber works; on idle, one digest carries them as a new turn.
| Kind | Detail |
|---|---|
| Member states | waiting, idle, blocked, died, done, delivered (a joined member ran /done). |
collision | Any level (Git state). Member branch versus member, never the subscriber's own; the merge branch only if collision_prefixes lists its prefix; any lane with subscribe --outside-collisions. Once per branch pair and level until gone; 8 paths, then +N more (paths may shift; status shows all current). |
| Other git kinds | work_item_changed, merge_ready, fleet_integrated, all of the bound repository. |
Latest member state wins; stale changes and fleet lines drop (fleet_integrated after a reopen). Joins, restarts and the first scan after start announce nothing. Member lines lead with the token:
idle(T1-MA CC,FD - Design - T1-MA[WIP]) done(T1-MC general-purpose a1)
Digests are ledger-claimed before sending, never sent twice.
| Outcome | Means |
|---|---|
| sent | Delivered. |
| refused | Nothing sent; the subscription ends. |
| unconfirmed | Claimed, then the write failed or the daemon died. |
| unclaimed | Ledger write failed; nothing sent. |
With bypassPermissions, or crossSessionInbound set to hold, a subscriber holds digests for its human's approval instead of starting a turn.
One landing turn per registered repository and target; a branch's landing target is its branches row's target, the staging target a setting (Git state § Config). Only the done skill creates or moves targets. Who lands what, when: Merge strategy.
| Command | Answers |
|---|---|
| smine-heartbeat done | Unjoined session, fleet subscriber included: requests its branch's landing-target turn in its cwd's repository. accepted <target> granted or accepted <target> queued <n> (place, from 1). No target: nothing requested, no-target <base> (row's base or unknown), exit 0. A repeat re-answers the current state. |
| smine-heartbeat land --target <branch> | Turn without acceptance: granted <branch> or queued <branch> <n>. Refused: <branch> is the default branch, <branch> is an agent branch, <branch> is no local branch, <repo> is not scanned yet. A configured staging target is accepted before it exists; the done skill creates it in the granted turn. |
| land --end landed | Holder only: ended <branch> landed. |
| land --end abandoned | Holder or queued session: ended <branch> abandoned. |
| land --release <branch> | Your override, in the repository, no session: released <branch> <session id>. |
Refusals exit 1, reason on stderr: unknown, ended or died session; cwd unknown or in no registered repo; or below (no branch row: done from a worktree no scan listed yet).
no branch row for <cwd> the session already has a landing entry for <target> in <repo> token does not match the landing request not the turn holder no landing turn is held for <branch>
| Rule | Detail |
|---|---|
| Token | The first request stores the session's messaging token; later calls must present it. |
| Grant | A freed turn passes, same step, to the oldest waiter for that repository and target: one ledger-claimed message over its messaging socket: |
| Entry ends | landed, abandoned, released, session_gone (ended, died, or unknown after the registry edge's startup scan); never a prompt or clock. At most one entry per session across repositories and targets. |
heartbeat: landing turn granted for <target> in <repo>. Continue the landing steps, then run: smine-heartbeat land --end landed
| From | Event | To |
|---|---|---|
| start | request, turn free | holding |
| start | request, turn held | queued |
| queued | turn freed, grant message | holding |
| queued | abandoned, or session_gone | end |
| holding | landed or abandoned, released, or session_gone | end |
Status landings: a row per selected repository and target with holder or queue, plus a staging-target row per selected repository while one is configured. --fleet selects the bound repository only, none before binding.
| Field | Holds |
|---|---|
| repo, branch | Repository and target. |
| holder | session_id, title, since; absent while the turn is free. |
| queue | Oldest first, each with session_id, title, since. |
| release | Staging only: exists, releasable (holds the default branch's tip: a release fast-forwards), unreleased (its commits the default branch lacks). |
Skills declare enforcement in the enforce block of <leaf>.cue; deploy renders enforce.json beside the deployed SKILL.md only if the install holds both gate and heartbeat components. No file: no enforced run, no gate rule.
Patterns carry install-scoped $SKILL_… variables, filled with this install's program, regex-quoted (^smine-rules base ). An unset variable drops its anchor plus the checks and judgments naming it. The model check refuses repo-scoped variables in anchors (Skillcue § Interfaces and environment).
Stateless gate rules: the tool-policy gate (Permissions). Heartbeat enforces anchors and checks in every session, reporting failed checks and judgment points to the session's one watcher, its bound /runtime agent.
/heartbeat-demo scenario:<name> shows each layer acting, one scenario per rule, check and ladder step. Only tests cover the rootDenied gate rule and the runtime stop move (cmd/toolpolicy/enforce_test.go, cmd/heartbeat/tests/scenarios/runtime.rs).
| Rule | Detail |
|---|---|
| Open | Root slash-command prompt or root Skill call with a deployed enforce.json, read by the hook edge then: a run keeps its starting declaration. |
| Root only | The declaration attaches only to hook bodies without agent id; --agent Skill calls open none. |
| No declaration | Missing or malformed (logged) enforce.json: no run; the slash prompt still ends open runs. |
| Nested | A root Skill call inside an open run nests. |
| End | Root slash prompt, non-compact SessionStart or terminal session only. |
| Unwatched | Runs exist unwatched too; a watcher's own session is never enforced. |
An anchor is a named call, logged on a successful PostToolUse with its response's captures; failed calls never log.
| Part | Rule |
|---|---|
| Tool | A tool regex. |
| Input matches, excludes | Unanchored regex on a named top-level tool_input field (non-strings as compact JSON); an absent field never matches. |
agent | root (default), subagent or any. |
capture | RFC 6901 pointers into the result. Bash: the JSON document closing stdout (suffix from the first line start where the rest parses as one value; earlier log lines skipped, later output voids it). Other tools: the response object. |
| Voided capture | Absent on that log entry; provenance against it fails, naming the value absent. |
| Parsing | tool_response stays raw text; parsed for anchor-matched calls only. |
| Kind | Fires at | Fails when |
|---|---|---|
| provenance {on, arg, from, capture} | PreToolUse of an on call | Input at arg differs from capture capture of the last from call, or that call yielded none, or no from exists. |
| requires {on, after, since?} | PreToolUse of an on call | No after since the last since; a since that never happened owes nothing. |
| phase {deny, from, until} | PreToolUse of a deny call | A from succeeded and no until followed. |
| stop {require, after} | Root Stop without background_tasks (one awaiting a background Workflow, agent or shell is not finishing). | after succeeded, no require followed; answered {"decision": "block"}. |
| Rule | Detail |
|---|---|
| Ladder | Failures before a run's eighth deny the call (or block the Stop) with the check's reason, raising a check_failed finding with its recovery template. The eighth kills the run: its call gets continue: false, kill reason as stopReason and systemMessage, shown to the user at once. |
| Killed session | Every agent of the session gets that answer until the human's next root prompt, which resets open runs (killed false, failures emptied, passes and findings kept). A prompt lifting no kill resets nothing. |
| Settings | Threshold heartbeat.kill_at (default 8), text heartbeat.kill_reason; both live keys. |
| Passes | Each hook judges every guard-matching check, not only up to the first failure: provenance and requires on on, phase on deny, stop on a judged root Stop after after succeeded. Runs count holds per check, also on calls another check denied: row passes ({check, count} by check id, empty if none), kept after a kill. |
| First failure | Only the first failing check (id order, outermost run first) is denied and counted. |
| In-flight Stops | The Stop body's background_tasks decides: an in-flight entry defers stop checks to the next root Stop with none (a launch turn by its own tasks); nothing else exempts. A pending runtime send still reaches a deferred Stop as block reason. |
| Captures | Runtime document anchors[].captures: each anchored call's output. Provenance reads the producing (last from) call's; judgment evidence the newest value per capture name. |
| Judgment points | A successful anchor call named by a judgment entry raises a judgment finding with evidence (capture values, arg:<pointer> inputs) while the session has a watcher. |
A watch is one-to-one: /runtime session: <id or title> [mode: act] runs the subscribe below, binding the watcher to exactly one session; only its findings reach the watcher or can be answered.
smine-heartbeat runtime subscribe --target <id or exact title> --mode report|act
| Rule | Detail |
|---|---|
| Refused | Target unknown, ambiguous, the watcher itself, a watcher, or already watched. |
| Several watchers | Side by side, one per watched session; mode fixed at subscribe. |
| End | runtime unsubscribe or either session ending; when the watched one ends, a last digest (undelivered findings, watch ended — …) makes the watcher stop instead of waiting. A restart rebinds from the ledger. |
| Digests | To an idle or blocked watcher, claim-then-send like fleet digests: one line per finding, max 50. |
| Subscriber guard | The watcher's SendMessage, mcp__ccd_session_mgmt__send_message and mcp__ccd_session_mgmt__stop_session calls are denied; it acts through heartbeat only. |
Each finding (check_failed, judgment) unlocks one move, once, while its run is open: send or stop, ledger-claimed (synced) before going live.
| Move | Effect |
|---|---|
| runtime send <finding> |
Renders the finding's template (the check's Reaches the raising agent (root or subagent) at its first chance: At most once: lost if Claude Code drops the answer past its 1 s timeout; void if pending at run end. |
| runtime stop <finding> | Arms (session, agent): that agent's tool hooks get continue: false until the root's next UserPromptSubmit (root) or its SubagentStop (subagent). Text heartbeat.runtime_stop_reason (kill: heartbeat.kill_reason), carried twice in every stop answer, as stopReason and systemMessage. |
Verdict chain per hook answer: heart attack → member guard → runtime (subscriber guard, the hook's check decision incl. a killed session, armed stop, hand-over).
smine-heartbeat runtime status [--format json] or GET /runtime: the runtime document (schema 1, cmd/heartbeat/schema/runtime-v1.json) with watches (subscriptions, each with target), runs (open, ended, killed, each with anchors, failures, passes) and every session's findings; no token.
Heartbeat checks lapse on HTTP hook failures (non-blocking in Claude Code), answers past the 1 s hook timeout, runs opened before a daemon restart (runs and findings are memory-only), --agent sessions and #disable hb sessions. Gate rules hold while the deployed enforce.json exists (gate and heartbeat installed); on an unparseable one the gate fails closed and heartbeat opens no run. SubagentStop runs no stop check.
~/.claude/heartbeat/actions.jsonl: append-only, 0600, in the 0700 heartbeat dir; replayed at start, so runs, letters, delivered members, journals and landing turns survive restarts.
| Records | Detail |
|---|---|
| Fleet acts | Subscriptions (repository, integration branch), digest claims, joins, member letters, deliveries and resumes, journal entries. |
| Run lifecycle | fleet_started, left, dissolved (the last only after the run's archive file). |
| Landings | land_requested, land_granted, land_ended, carrying the turn's target; turns re-derive from requested and ended. Records from before targets hold develop and replay unchanged. |
| Old runs | Runs predating fleet_started: UUID derived from name and first line, stable across restarts. |
The ledger holds messaging tokens.
The daemon uses the config server's central peek on 127.0.0.1:<peek.port> when its /healthz serves the same Claude home; it never starts one. peek_addr renders from the peek.port setting, default 4242.
| Rule | Detail |
|---|---|
| When it asks | For a subscriber's spawn tree, after a restart and after each subscriber turn. Its subagents feed runtime checks and branch attribution, never fleet membership. |
| Finished subagents | Rebuilt subagents finish on a foreground result, background agents on peek's task_completed event (peek 1.2.9 and later); an older peek keeps background agents finished while the daemon was down listed as running. |
| Without peek | peek sensor degraded; subagents started while the daemon was down stay unknown until their next hook. |
| Retry | Degraded rounds repeat every heartbeat.peek_retry (default 30s) until one succeeds, so a peek restart recovers the sensor unasked. |
The config server's Heartbeat section has two tabs, Fleet and Runtime.
| Tab | Shows |
|---|---|
| Runtime | http://127.0.0.1:6001/runtime: the runtime document, read-only (watches; runs with session, skill, version, state, anchors, failures, findings; findings), refreshed every 5 s. Send and stop stay with the /runtime agent. |
| Fleet | http://127.0.0.1:6001/fleet, filtered by ?fleet=T1; runs bin/heartbeat status --format json (plus --fleet <id> under ?fleet=) every 5 s. Tables wrap, so refreshes keep the scroll position. |
| Fleet tab block | Content |
|---|---|
| Human-layer queue | Permission candidates, waiting members, merge-ready members without /done, fleets ready to sync or whose sync git refused, targets ready for main or missing. Heartbeat state only, nothing agent-filed. |
| Per fleet | Header (run, target, merge branch, state, synced, collision scope), train-step chips, member table (state first, token and name with Remove button, role operator/member/worker, latest skill, work item, branch, since, last active), journal newest first. |
| Collisions | The repo's collisions, one shortened path per line, 10 shown, rest folded. |
| Landings | Per repository the staging target (releasable, diverged, absent) with unreleased commits; per target holder and queue. Blocks no release. |
| Fleet sessions | Joined members, the orchestrator, worker cells in a worker's branch. |
| Archived fleets | /fleet/archive/<uuid> per archived run: members, subscriptions, journal. |
| Errors | Unreachable or foreign daemon: a banner. Degraded sensor or failed repo scan: a collapsed card each, details listing every repo error and warning. |
Attended sessions run /join fleet: T1 once, /done when work is committed and finished. Titles: … - T1-MA[WIP], [READY] after a committing turn, then [DONE]; /close leaves the fleet, ending at [CLOSED]. Outside a fleet, /done lands the session's own branch on its landing target; one based on the default branch lands nothing.
From a session worktree of the repo:
/fleet fleet: T1 target: <branch> [playbook: feature|design] [master: <plan file>] [collisions: all]
Subscribes as … - T1-O[WIP], never a member; switches its worktree to the run's merge branch (fleets[].merge_branch).
Judges collisions, fast-forwards delivered members onto the merge branch (git merge --ff-only), journals every act; never checks out the target. Members behind the merge branch are asked to restack and /done again; delivered worker branches behind it the orchestrator restacks on a work branch. Never a merge commit.
Once integrated, syncs the target via /done in its landing turn: restack if it moved, then an ancestry-checked fast-forward (smine-agentwt forward). A target checked out elsewhere moves in that worktree, refused only while it has uncommitted changes to tracked files (queue: sync blocked until you commit). A target that is no landing target answers no-target; the orchestrator stops.
Removes worker branches, runs unsubscribe --finish (the run dissolves). /close in its session removes the merge branch. Landing on main stays yours.
| Playbook | Runs |
|---|---|
| feature | Lands a delivered design member's plan first, then one implementation worker at a time; holds the sync while a landed plan awaits implementation. |
| design | One unattended designer per master-plan split row, two at a time in Depends-on order; ends with every derived plan through its code gate, implements nothing. |
Each worker starts as a background command: a claude -p cell in .claude/worktrees/<name> on claude/<name>.
smine-routine worker -base <merge branch> -name <name> -skill <leaf> -args <text>
The launcher commits leftovers, removes the worktree, prints one JSON result line (status done, failed or stopped); its exit wakes the orchestrator. A cell is a root session, so its subagents work in its own worktree; a subagent's subagents were fenced to the orchestrator's.
-keep-stopped keeps a stopped cell's worktree; -resume <session id> continues it there with -args as next prompt, how the feature playbook hands a stopped implementer its refined plan.
A session's branch lands on the branch it started from, by replay and fast-forward, never by a merge commit. Heartbeat grants the turns (Landing); this section is who lands what, when.
| Rule | What it means |
|---|---|
| Land on the base | A session lands on its base, the branch it started from, if that is a local branch neither agent-owned nor default; a fleet member on its fleet's merge branch; any other branch on none. This is its heartbeat row's target. |
| No target | Started from main or with an unknown base: nothing restacks or lands it; you pick its commits. |
| Staging target | Unset unless heartbeat.staging_target names one (develop below). Takes finished feature branches; you release it to main. |
| Replay | Never a merge commit: a branch behind its target is restacked onto the target's tip, then the target fast-forwards. |
| One at a time | Heartbeat grants one landing turn per repository and target. |
main | No agent session pushes or moves it. Two operator tools write it: the routine runner lands the nightly's session records (with auto-merge on, also a judge-accepted night's ready changes), never pushing; the Proposals page's Pick uses smine-agentwt pick. |
| Your git acts | Unbound: no hook, no deny. The next landing handles any ref state you produce. |
Flows between refs, in order:
| # | From | How | To |
|---|---|---|---|
| 1 | Session picker | Worktree hook recreates the untouched branch from the picked branch by name. | claude/NAME (session branch) |
| 2 | claude/NAME | /done: restack, fast-forward | feature/x (its base) |
| 3 | claude/NAME | /done: restack, fast-forward | develop (staging target) |
| 4 | claude/NAME based on main | No target: you pick. | main |
| 5 | claude/NAME, claude/agent-ID (fleet member) | Orchestrator: fast-forward | claude-orch/FLEET-RUN (merge branch) |
| 6 | claude-orch/FLEET-RUN | Sync: /done | The fleet target |
| 7 | feature/x | /done branch: feature/x | develop |
| 8 | develop | You: fast-forward | main |
| 9 | main | You moved main: catch-up at the next landing. | develop |
| Ref | Role |
|---|---|
| main | Default branch. Only you move it; never a landing target. |
| staging target | Named by heartbeat.staging_target, unset by default. Unset, /done branch: refuses with no staging target configured — set heartbeat.staging_target and heartbeat shows no staging row. Local, created from main by its first landing. The one ref a landing moves non-forward, only in a catch-up. |
| feature/<x> | Any other local branch a session starts from: that session's landing target. Nothing creates or catches it up. |
| claude/<name> | A session's branch; keeps its name through every restack. |
| claude-orch/<fleet>-<run uuid> | A fleet's merge branch, checked out by its orchestrator; its members' landing target. |
| claude-restack/<slug>-<short> | A running restack's work branch; with -orig, the archive of the series it replaced. <slug>: branch name lower-cased, each run of characters outside a-z and 0-9 one dash. <short>: first 8 hex characters of the target's tip. |
| claude-routines/<group>-<date>[-n] | A routine's dated run branch. The nightly mints a pair: …-proposals holds its change commits, each picked, rejected, or dropped by the runner once the night ages out of the window or smine-agentwt probe --onto main answers conflict for it; …-batch holds its session records, landed the same night. A short-lived …-landing scratch branch exists while auto-merge lands. |
| refs/smine/rejected/<sha> | A change commit refused on the Proposals page: settled, kept reachable. The user's reason sits in the backlog's archive.rejected row whose commit names the sha, read there by the miners. |
| refs/smine/dropped/<sha> | A change commit the runner aged out or found stale: kept reachable, listed in History, never suppresses re-mining. |
A branch enters Working when its worktree is created and leaves from On main with /close.
| State | Who acts | What happens | Next state (event) |
|---|---|---|---|
| Working | session agent | Commits on its claude/<name> branch. | In review (turn ends, audit green); Working (/fimplement starts behind its target: re-anchor); Partly landed (you pick some commits) |
| In review | you | Accept with /done, or ask for a fix. | Working (fix asked); Accepted (you type /done, has a target); No target (you type /done, based on main); Delivered (fleet member runs /done); Partly landed (you pick some commits); On main (you pick every commit) |
| Accepted | heartbeat | Grants the target's landing turn or queues the session. | Restacking (granted, behind the target); Ready (granted, at the target's tip); On target (every commit already placed) |
| No target | you | Pick its commits onto main; nothing lands. | On main (you pick every commit) |
| Restacking | owning session | Replays the commits the target lacks onto its tip, runs the audit. | Ready (replay done, audit green); In review (restack stopped, turn abandoned) |
| Ready | owning session | Fast-forwards the target, ends the turn. | On target (target fast-forwards) |
| Delivered | orchestrator | Fast-forwards its merge branch, or asks the member to restack. | On merge branch (fast-forwarded); In review (behind, restack asked) |
| On merge branch | orchestrator | Syncs the fleet's target once every member landed. | On target (fleet synced through /done) |
| On target | you | Land a feature target with /done branch: <target>, or fast-forward main from the staging target. | On staging (feature target landed with /done); On main (staging target, you fast-forward main) |
| On staging | you | Fast-forward main. | On main (you fast-forward main) |
| Partly landed | owning session | The next /done replays only the remainder. | Accepted (you type /done) |
| On main | you | /close | Done (/close) |
/done in a session in no fleet. You type it; an agent's own call is admitted only after /join or inside /fleet.
Uncommitted work is committed first.
smine-heartbeat done resolves the landing target and requests its turn: accepted <target> granted or queued <n>. Queued: the session ends its turn; heartbeat's message when the turn frees resumes it at step 3. No target: no-target <base>, title to DONE, nothing lands.
A missing staging target is created from main's tip after the grant; heartbeat grants its turn before it exists.
If main holds commits the staging target lacks, the staging target's unreleased commits replay onto main's tip and it moves; the old tip stays as claude-restack/<slug>-<short>-orig.
smine-agentwt unplaced --onto <target> <branch> lists the commits the target lacks; none: the turn ends abandoned.
They replay onto the target's tip on a work branch (conflicts resolved by both sides' intent, audit run), unless the target's tip is already in the branch. The checked-out branch keeps its name; its old series becomes the -orig archive, removed through the safety-gated remove once adoption is green (kept if the removal is skipped). Any other branch stays on its work branch for the caller to adopt, with no archive.
smine-agentwt forward --onto <target> <tip> moves the target after checking the tip holds the target's tip.
smine-heartbeat land --end landed
/done --close then runs /close, removing the session's worktree and branch; the Desktop session stays open.
| Case | Rule |
|---|---|
| Every exit ends the turn | Stopped restack, unmoved target, nothing to land: abandoned. Fix the cause, type /done again; --close then closes nothing. |
| Checked-out target | Checked out in another worktree (your main checkout on the feature branch): fast-forwarded there, index and files following. Refused while that worktree has uncommitted changes to tracked files: commit there, type /done again. |
| Restack never asks | Decides every conflict, contradicting changes included, naming each call in its report. Where nothing decides between two intents, it keeps the target's behavior and fits the replayed commit to it. |
| Stopped restack | Only a red audit or an unattributable file stops it: branch untouched, work branch kept; a rerun onto the same tip resumes. |
| Fleet member | /done --close delivers and closes nothing: the orchestrator lands the branch first. |
| Feature branch | /done branch: <local branch> lands a branch without a live session, always on the staging target; refused for the staging target itself, main and agent branches. Turn via smine-heartbeat land --target <staging target>; no rename. |
| Plans | Only a feature-branch landing archives plans. Between restack and fast-forward, each feature folder under plans/ it brings goes through /plan-archive on the tip: wip plan dirs are copied to the plans archive as done-<tip hash>, committed there, then removed by a commit landing with the feature. The staging target and main never hold a wip plan. A session's own /done leaves plans on the feature branch for later sessions. A stopped archive ends the turn abandoned; the next landing replaces the archived copy. |
| Other target | /done takes no target; only /restack onto: <branch> moves a session, making that branch its base. |
| Archives | Restack removes its own -orig archive after adoption. /close removes the session's claude-restack/<slug>-… branches (slug from the session branch) through the same safety-gated remove, keeping and reporting any whose commits nothing else holds. |
| Stuck turn | smine-heartbeat land --release <target>, in the repository, ends the holder's turn. A turn also ends when its session ends or dies; never by clock or prompt. |
Members land on the fleet's merge branch; only the orchestrator moves it.
| Step | Rule |
|---|---|
| Deliver | A member runs /done (delivered <token>); a spawned worker counts once stopped. |
| At the tip | A member whose branch holds the merge branch's tip lands by git merge --ff-only. |
| Behind | An attended member is asked once to restack onto the merge branch and /done again; the orchestrator never writes into a member's branch. |
| Stopped worker | No session: the orchestrator restacks its branch on a work branch, fast-forwards to it, deletes the work branch. |
| Sync | Once every worker landed, the orchestrator calls /done. The merge branch was cut from the fleet's target, its landing target, so /done lands it in that target's turn, staging create and catch-up included. A target that is no landing target: no-target, the fleet stops. |
| Case | Rule |
|---|---|
| Re-anchor | /fimplement first resolves its landing target (heartbeat row target) and restacks before reading the tree if the target's tip is not in the branch. No target: no re-anchor. |
| Heartbeat down | Same rule on the base from smine-agentwt status; the run never stops for it. |
| Restack fails | The run never stops: it implements on the current base, naming the restack's reason in its report. |
| Retarget guard | /restack onto: <branch> other than the base stops if <branch> lacks base commits the branch carries; bring <branch> up to the base first. |
| Rule | Detail |
|---|---|
| You release | Fast-forward main to the staging target and push; no agent session does either (record landings and picks are the operator tools above). |
| Signal | The fleet page's landings block: per repository the staging target as releasable (main's tip in it), diverged (main holds commits it lacks) or absent, with the unreleased commit count; every target's turn holder and queue. |
| Never a gate | Nothing reads the signal before acting for you. A release while diverged is caught up by the next staging landing. |
| You do | What happens next |
|---|---|
Commit on main | The next staging landing first replays its unreleased commits onto main's tip. No other target catches up. |
| Cherry-pick some commits of a branch | Placed, conflict-adapted picks included; /done lands the rest. |
| Pick every commit of a branch | /done finds nothing and ends its turn. |
| Land an accepted branch by hand | Commits placed; its /done ends the turn abandoned. |
| Start a session from a feature branch | Lands there; /fimplement and /done restack it onto that tip. |
Start a session from main | No target: you pick its commits. |
| Move a session to another branch | /restack onto: <branch>, under the retarget guard. |
| Rewrite the staging target | Landings continue from the ref as it is. |
Merge with /merge-resolve | Still there for merges outside this strategy; a later restack replays the branch's own commits without the merge commit. |
| Bump a skill's version on two branches | The skillcue driver keeps both changelog rows; a real content conflict in the file is left to git (below). |
A custom git merge driver merges two branches' version bumps of one skill model file.
| Rule | Detail |
|---|---|
| Binding | .gitattributes binds every <leaf>.cue with skills/*/*.cue merge=skillcue and skills/*/*/*.cue merge=skillcue; root files interfaces.cue and vocabulary.cue keep git's text merge. Git calls the driver in merge, cherry-pick and merge-tree. |
| Driver | smine-rules merge-skillcue %O %A %B %P %X %Y, registered as merge.skillcue.driver by the installer (git config --global) and the routine cell image (git config --system). |
| Order | Both sides' new changelog rows stay. The side with the later committer time (from git's %X/%Y conflict labels) goes on top, a tie to the incoming side; row dates decide when a label resolves to no commit. Rows renumber upward from the base version, each by its bump kind; the frontmatter version follows the top row. |
| Fallback | Any other conflict in the file: git's text merge with markers, exit 1, one stderr line naming the reason. |
| Probes without a worktree | Agentwt runs merge-tree with GIT_ATTR_SOURCE=<onto tip>, so the binding applies in a container cell with no .gitattributes on disk. |
| Placement checks stay driver-free | Agentwt's in-memory gix merges (status, unplaced, the fleet order, heartbeat's git edge) only ask whether a change is already on a candidate, which no driver changes. Agentwt drops every merge.<name> config section on opening the repository: gix turns a driver's conflict exit into a hard error. |
| Command | Who runs it | Effect |
|---|---|---|
| /done | you outside a fleet; a member or the orchestrator inside | Lands the session's branch on its landing target (none: nothing), or delivers a member. |
| /done branch: <branch> | you | Lands a feature branch on the staging target. |
| /restack | a session, or a skill behind its target | Replays unlanded commits onto the target's tip; lands nothing. |
| /close | you | Removes the session's worktree, branch and restack branches. |
| smine-heartbeat done | the done skill | Accepts for the landing target, answers no-target <base>, or delivers a member. |
| smine-heartbeat land --target <staging target> | the done skill | Requests the staging target's turn for a named branch. |
| smine-heartbeat land --end landed | the done skill | Ends the holder's turn after the move. |
| smine-heartbeat land --end abandoned | the done skill | Ends the turn without landing, or leaves the queue. |
| smine-heartbeat land --release <target> | you | Ends a stuck holder's turn. |
| smine-agentwt unplaced --onto <target> <branch> | the restack and done skills, you | Lists the commits the target lacks, oldest first. |
| smine-heartbeat status --format json | anyone | branches[].target per branch; staging_target; landings: holders and queues per target, the staging target's release state. |
| smine-rules settings set heartbeat.staging_target <branch> | you (or the Config page's Smine tab) | Names the staging target (unset by default); restart-class, so the apply restarts heartbeat. |
| Part | Files |
|---|---|
| Skills | skills/git/restack, skills/orchestration/done, skills/orchestration/fleet, skills/orchestration/close, skills/feature/fimplement |
| Daemon | cmd/heartbeat/src/land.rs; targets in cmd/heartbeat/src/branch.rs; lane prefixes in settings/claude_code/heartbeat.json |
| Base | cmd/worktreecreate/main.go recreates the untouched session branch from the picked branch's name at SessionStart, so its reflog's one creation line names the base; cmd/agentwt/src/gitstate.rs reads that line, unchanged. |
| Placement query | cmd/agentwt/src/unplaced.rs, schema cmd/agentwt/schema/unplaced-v1.json |
| Merge driver | cmd/rules/mergeskillcue.go, internal/skillgraph/merge.go |
| Page | Landings block of internal/server/templates/_fleet_status.html |