Heartbeat

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.

Status commands

smine-heartbeat status queries the daemon; flags narrow or reshape it.

CommandShows
smine-heartbeat statusOne line per session (*: start unseen by this daemon), then degraded sensors.
smine-heartbeat status --format jsonStatus document (schema 1, cmd/heartbeat/schema/status-v1.json).
smine-heartbeat status --session selfCalling session (from a Claude Code Bash tool).
smine-heartbeat status --repo <path>Sessions in the repository and its worktrees.
smine-heartbeat status --worktree . --format jsonWorktree 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.
ExitMeans
3No daemon answers.
4Port's daemon serves another Claude profile.
1Anything else.

Running the daemon

ItemDetail
InstallThe 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).
Portheartbeat.port (install record), default 6101; Config page Smine tab or smine-rules settings set heartbeat.port <port>.
Platformserve 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 bodiesBoot the agent out, run bin/heartbeat serve --record /tmp/hooks.jsonl, boot it in.
Load evidencemake heartbeat-load
Address

--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.

When heartbeat is down

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.

How states are decided

Inputs: hook events, the toolpolicy denial-ledger tail, process exits, registry seeds.

EventState
SessionStartidle
UserPromptSubmitrunning
A permission prompt; a root PreToolUse of AskUserQuestion or ExitPlanMode; the notifications permission_prompt, elicitation_dialog, elicitation_url_dialog, agent_needs_inputwaiting
Stopidle; blocked if the turn's last tool outcome was a toolpolicy denial, until a later call completes.
A denial-ledger line for an idle sessionblocked
SessionEndended
StopFailure, or a process exit without SessionEnddied
A root Stop a runtime stop check blocked (Runtime enforcement)running (no turn end); under #disable hb no block, the turn ends.
RuleDetail
PidsExits count only for sessions with a pid, taken only from ~/.claude/sessions/<pid>.json or the container file.
Hook-only sessionsNo exit watch: ended or died only via SessionEnd or StopFailure, else the last state stays.
Unknown pidRegistry pid unknown to the host: a hook-seen session is died at once.
Container cellsPid 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/.
SessionStartVia 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.
SubagentsUnder 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 bodiestranscript_path outside the served Claude home: refused (409), degrades hooks; none: accepted.
SensorsDegraded inputs show in sensors (hooks, registry, process_exit, ledger, peek, action_ledger, config, and with --repos git, agentwt), never as blank rows.

Heart attack

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.

AspectRule
SparedRoot session; subagents with an allowed, matching named model.
#disable hbIgnored (heartbeat spells it); mutes every other /hook answer.
Policyheartbeat.forbidden_subagent_models: globs over model ids and Agent-call aliases; default empty (forbids nothing), set per install. Live key, via the config watch.
SightingsA denied Agent call with forbidden model input; a transcript whose first assistant line shows a forbidden model (read on tool hooks until known).
StopThen 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 itsmine-heartbeat status --session <id> --format json: each subagent's model.
Model aliasesRequested token proves nothing: haiku runs the session model on this account; hence stopping missing or mismatched models.
Limits

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.

Git state

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.

Config

RuleDetail
Fileserve --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).
Directory0700, tightened at start; also orchestrator state.
ContentsExemptions, forbidden subagent models, staging target, texts, caps, timeouts, peek_addr (from peek.port); unknown keys refused.
Reloadlive keys via the config watch; a restart key makes the apply restart the agent, losing open runs and findings.
Bad fileMalformed or unreadable: fatal at start; on reload ignored (running config kept, config sensor degraded).
Hand editsOverwritten on apply; use the Hooks tab or smine-rules settings set heartbeat.<key> <value>.
Staging targetheartbeat.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.

What a scan reports

FieldRule
ScansRef 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 statesuncommitted, no_work, removable, merged, conflicting, merge_ready, unmerged; first match wins, against the landing target's train, else the default branch's.
Landing targetbranches[].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 itemsWip 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 branchesisolation: "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.
CollisionsLevels 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 basesBound 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.
HealthSensor git: ref watch; agentwt: each repo's last scan.

Fleets

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.

CommandShows
smine-heartbeat status --fleet T1 --format jsonMembers' sessions, fleet row, member rows; once bound, only the bound repository's repos, branches, work_items, collisions (scoped like digests), trains.
smine-heartbeat status --fleetEvery fleet: id, run, target, state, members, subscriber.
smine-heartbeat status --fleet T1Text, plus member table.

Runs

StageRule
StartA 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.
JoinRefused: 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.
Removesmine-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.
WindowsNo 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.
DissolveNo 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.

Members

TopicRule
LettersJoined 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.
OperatorThe 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.
WorkersHeadless 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 fleetA done outside a fleet is no delivery (Landing).
Journalfleets[].journal: run acts the active subscriber recorded:
smine-heartbeat record --kind assigned|decided|merged|spawned|note --subject <text> [--member <letter>]… [--detail <text>]

Binding and branches

FieldRule
BindingSubscribe 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 branchfleets[].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.
Syncedfleets[].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 branchesmembers[].branch: worker: registered branch; session: its worktree's. The orchestrator sees all branches, acts on these only.
Target stateWhen
openOtherwise; 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.
integratedAt 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.
missingThe requesting scan found no such local branch.

Subscribe

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
CommandEffect
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-collisionsAlso hears collisions with non-fleet branches.
unsubscribeEnds the subscription (same session, matching token), recorded so restarts don't revive it. Joined members stay: a handover to another orchestrator.
unsubscribe --finishEnds 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.

Digests

Member changes collect while the subscriber works; on idle, one digest carries them as a new turn.

KindDetail
Member stateswaiting, idle, blocked, died, done, delivered (a joined member ran /done).
collisionAny 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 kindswork_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.

OutcomeMeans
sentDelivered.
refusedNothing sent; the subscription ends.
unconfirmedClaimed, then the write failed or the daemon died.
unclaimedLedger write failed; nothing sent.
Note

With bypassPermissions, or crossSessionInbound set to hold, a subscriber holds digests for its human's approval instead of starting a turn.

Landing

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.

CommandAnswers
smine-heartbeat doneUnjoined 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 landedHolder only: ended <branch> landed.
land --end abandonedHolder 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>
RuleDetail
TokenThe first request stores the session's messaging token; later calls must present it.
GrantA freed turn passes, same step, to the oldest waiter for that repository and target: one ledger-claimed message over its messaging socket:
Entry endslanded, 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

Entry states

FromEventTo
startrequest, turn freeholding
startrequest, turn heldqueued
queuedturn freed, grant messageholding
queuedabandoned, or session_goneend
holdinglanded or abandoned, released, or session_goneend

Read it

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.

FieldHolds
repo, branchRepository and target.
holdersession_id, title, since; absent while the turn is free.
queueOldest first, each with session_id, title, since.
releaseStaging only: exists, releasable (holds the default branch's tip: a release fast-forwards), unreleased (its commits the default branch lacks).

Runtime enforcement

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.

Tip

/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).

Runs

RuleDetail
OpenRoot 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 onlyThe declaration attaches only to hook bodies without agent id; --agent Skill calls open none.
No declarationMissing or malformed (logged) enforce.json: no run; the slash prompt still ends open runs.
NestedA root Skill call inside an open run nests.
EndRoot slash prompt, non-compact SessionStart or terminal session only.
UnwatchedRuns exist unwatched too; a watcher's own session is never enforced.

Anchors

An anchor is a named call, logged on a successful PostToolUse with its response's captures; failed calls never log.

PartRule
ToolA tool regex.
Input matches, excludesUnanchored regex on a named top-level tool_input field (non-strings as compact JSON); an absent field never matches.
agentroot (default), subagent or any.
captureRFC 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 captureAbsent on that log entry; provenance against it fails, naming the value absent.
Parsingtool_response stays raw text; parsed for anchor-matched calls only.

Checks

KindFires atFails when
provenance {on, arg, from, capture}PreToolUse of an on callInput 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 callNo after since the last since; a since that never happened owes nothing.
phase {deny, from, until}PreToolUse of a deny callA 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"}.
RuleDetail
LadderFailures 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 sessionEvery 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.
SettingsThreshold heartbeat.kill_at (default 8), text heartbeat.kill_reason; both live keys.
PassesEach 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 failureOnly the first failing check (id order, outermost run first) is denied and counted.
In-flight StopsThe 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.
CapturesRuntime 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 pointsA 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.

Watching a session

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
RuleDetail
RefusedTarget unknown, ambiguous, the watcher itself, a watcher, or already watched.
Several watchersSide by side, one per watched session; mode fixed at subscribe.
Endruntime 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.
DigestsTo an idle or blocked watcher, claim-then-send like fleet digests: one line per finding, max 50.
Subscriber guardThe watcher's SendMessage, mcp__ccd_session_mgmt__send_message and mcp__ccd_session_mgmt__stop_session calls are denied; it acts through heartbeat only.

Moves

Each finding (check_failed, judgment) unlocks one move, once, while its run is open: send or stop, ledger-claimed (synced) before going live.

MoveEffect
runtime send <finding>

Renders the finding's template (the check's recovery or the judgment's send from enforce.json; placeholders {finding}, {skill}, {version}, {check}, {reason}, {evidence}). The act agent names finding and move, never text.

Reaches the raising agent (root or subagent) at its first chance: PostToolUse additionalContext on its next successful tool call. A root, usually waiting, gets it as Stop block reason or as UserPromptSubmit additionalContext on its next prompt.

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).

Read it

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.

Fails open

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.

Action ledger

~/.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.

RecordsDetail
Fleet actsSubscriptions (repository, integration branch), digest claims, joins, member letters, deliveries and resumes, journal entries.
Run lifecyclefleet_started, left, dissolved (the last only after the run's archive file).
Landingsland_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 runsRuns predating fleet_started: UUID derived from name and first line, stable across restarts.
Never share

The ledger holds messaging tokens.

Peek

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.

RuleDetail
When it asksFor 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 subagentsRebuilt 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 peekpeek sensor degraded; subagents started while the daemon was down stay unknown until their next hook.
RetryDegraded rounds repeat every heartbeat.peek_retry (default 30s) until one succeeds, so a peek restart recovers the sensor unasked.

Fleet page and orchestrator

Page

The config server's Heartbeat section has two tabs, Fleet and Runtime.

TabShows
Runtimehttp://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.
Fleethttp://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 blockContent
Human-layer queuePermission 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 fleetHeader (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.
CollisionsThe repo's collisions, one shortened path per line, 10 shown, rest folded.
LandingsPer repository the staging target (releasable, diverged, absent) with unreleased commits; per target holder and queue. Blocks no release.
Fleet sessionsJoined members, the orchestrator, worker cells in a worker's branch.
Archived fleets/fleet/archive/<uuid> per archived run: members, subscriptions, journal.
ErrorsUnreachable or foreign daemon: a banner. Degraded sensor or failed repo scan: a collapsed card each, details listing every repo error and warning.

Members

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.

Orchestrator

  1. Start it

    From a session worktree of the repo:

    /fleet fleet: T1 target: <branch> [playbook: feature|design] [master: <plan file>] [collisions: all]
  2. Subscribe

    Subscribes as … - T1-O[WIP], never a member; switches its worktree to the run's merge branch (fleets[].merge_branch).

  3. Land members

    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.

  4. Sync the target

    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.

  5. Finish

    Removes worker branches, runs unsubscribe --finish (the run dissolves). /close in its session removes the merge branch. Landing on main stays yours.

PlaybookRuns
featureLands a delivered design member's plan first, then one implementation worker at a time; holds the sync while a landed plan awaits implementation.
designOne 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.

Worker cells

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.

Merge strategy

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.

RuleWhat it means
Land on the baseA 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 targetStarted from main or with an unknown base: nothing restacks or lands it; you pick its commits.
Staging targetUnset unless heartbeat.staging_target names one (develop below). Takes finished feature branches; you release it to main.
ReplayNever a merge commit: a branch behind its target is restacked onto the target's tip, then the target fast-forwards.
One at a timeHeartbeat grants one landing turn per repository and target.
mainNo 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 actsUnbound: no hook, no deny. The next landing handles any ref state you produce.

Branches and landing targets

Flows between refs, in order:

#FromHowTo
1Session pickerWorktree hook recreates the untouched branch from the picked branch by name.claude/NAME (session branch)
2claude/NAME/done: restack, fast-forwardfeature/x (its base)
3claude/NAME/done: restack, fast-forwarddevelop (staging target)
4claude/NAME based on mainNo target: you pick.main
5claude/NAME, claude/agent-ID (fleet member)Orchestrator: fast-forwardclaude-orch/FLEET-RUN (merge branch)
6claude-orch/FLEET-RUNSync: /doneThe fleet target
7feature/x/done branch: feature/xdevelop
8developYou: fast-forwardmain
9mainYou moved main: catch-up at the next landing.develop
RefRole
mainDefault branch. Only you move it; never a landing target.
staging targetNamed 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.

Branch states

A branch enters Working when its worktree is created and leaves from On main with /close.

StateWho actsWhat happensNext state (event)
Workingsession agentCommits 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 reviewyouAccept 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)
AcceptedheartbeatGrants 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 targetyouPick its commits onto main; nothing lands.On main (you pick every commit)
Restackingowning sessionReplays the commits the target lacks onto its tip, runs the audit.Ready (replay done, audit green); In review (restack stopped, turn abandoned)
Readyowning sessionFast-forwards the target, ends the turn.On target (target fast-forwards)
DeliveredorchestratorFast-forwards its merge branch, or asks the member to restack.On merge branch (fast-forwarded); In review (behind, restack asked)
On merge branchorchestratorSyncs the fleet's target once every member landed.On target (fleet synced through /done)
On targetyouLand 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 stagingyouFast-forward main.On main (you fast-forward main)
Partly landedowning sessionThe next /done replays only the remainder.Accepted (you type /done)
On mainyou/closeDone (/close)

Landing a session

/done in a session in no fleet. You type it; an agent's own call is admitted only after /join or inside /fleet.

  1. Clean tree

    Uncommitted work is committed first.

  2. Accept

    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.

  3. Create

    A missing staging target is created from main's tip after the grant; heartbeat grants its turn before it exists.

  4. Catch up

    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.

  5. Placed check

    smine-agentwt unplaced --onto <target> <branch> lists the commits the target lacks; none: the turn ends abandoned.

  6. Restack

    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.

  7. Fast-forward

    smine-agentwt forward --onto <target> <tip> moves the target after checking the tip holds the target's tip.

  8. End
    smine-heartbeat land --end landed
  9. Close

    /done --close then runs /close, removing the session's worktree and branch; the Desktop session stays open.

CaseRule
Every exit ends the turnStopped restack, unmoved target, nothing to land: abandoned. Fix the cause, type /done again; --close then closes nothing.
Checked-out targetChecked 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 asksDecides 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 restackOnly 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.
PlansOnly 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.
ArchivesRestack 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 turnsmine-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.

Landing a fleet

Members land on the fleet's merge branch; only the orchestrator moves it.

StepRule
DeliverA member runs /done (delivered <token>); a spawned worker counts once stopped.
At the tipA member whose branch holds the merge branch's tip lands by git merge --ff-only.
BehindAn attended member is asked once to restack onto the merge branch and /done again; the orchestrator never writes into a member's branch.
Stopped workerNo session: the orchestrator restacks its branch on a work branch, fast-forwards to it, deletes the work branch.
SyncOnce 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.

Starting behind

CaseRule
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 downSame rule on the base from smine-agentwt status; the run never stops for it.
Restack failsThe 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.

Release

RuleDetail
You releaseFast-forward main to the staging target and push; no agent session does either (record landings and picks are the operator tools above).
SignalThe 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 gateNothing reads the signal before acting for you. A release while diverged is caught up by the next staging landing.

What you may do

You doWhat happens next
Commit on mainThe next staging landing first replays its unreleased commits onto main's tip. No other target catches up.
Cherry-pick some commits of a branchPlaced, 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 handCommits placed; its /done ends the turn abandoned.
Start a session from a feature branchLands there; /fimplement and /done restack it onto that tip.
Start a session from mainNo target: you pick its commits.
Move a session to another branch/restack onto: <branch>, under the retarget guard.
Rewrite the staging targetLandings continue from the ref as it is.
Merge with /merge-resolveStill 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 branchesThe skillcue driver keeps both changelog rows; a real content conflict in the file is left to git (below).

Skill model merges

A custom git merge driver merges two branches' version bumps of one skill model file.

RuleDetail
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.
Driversmine-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).
OrderBoth 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.
FallbackAny other conflict in the file: git's text merge with markers, exit 1, one stderr line naming the reason.
Probes without a worktreeAgentwt 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-freeAgentwt'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.

Landing commands

CommandWho runs itEffect
/doneyou outside a fleet; a member or the orchestrator insideLands the session's branch on its landing target (none: nothing), or delivers a member.
/done branch: <branch>youLands a feature branch on the staging target.
/restacka session, or a skill behind its targetReplays unlanded commits onto the target's tip; lands nothing.
/closeyouRemoves the session's worktree, branch and restack branches.
smine-heartbeat donethe done skillAccepts for the landing target, answers no-target <base>, or delivers a member.
smine-heartbeat land --target <staging target>the done skillRequests the staging target's turn for a named branch.
smine-heartbeat land --end landedthe done skillEnds the holder's turn after the move.
smine-heartbeat land --end abandonedthe done skillEnds the turn without landing, or leaves the queue.
smine-heartbeat land --release <target>youEnds a stuck holder's turn.
smine-agentwt unplaced --onto <target> <branch>the restack and done skills, youLists the commits the target lacks, oldest first.
smine-heartbeat status --format jsonanyonebranches[].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.

Landing files

PartFiles
Skillsskills/git/restack, skills/orchestration/done, skills/orchestration/fleet, skills/orchestration/close, skills/feature/fimplement
Daemoncmd/heartbeat/src/land.rs; targets in cmd/heartbeat/src/branch.rs; lane prefixes in settings/claude_code/heartbeat.json
Basecmd/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 querycmd/agentwt/src/unplaced.rs, schema cmd/agentwt/schema/unplaced-v1.json
Merge drivercmd/rules/mergeskillcue.go, internal/skillgraph/merge.go
PageLandings block of internal/server/templates/_fleet_status.html