Skip to content

harn agents

harn agents <subcommand> [options]

The window into the coordination layer. Every agent sharing a checkout records a generation to the V3 event ledger and materializes a disposable cache at .harnery/active/<instance_id>.json. harn agents reads the authority-safe V3 projection to answer the questions that keep parallel agents out of each other’s way: who else is here, what are they holding, what is each one doing, and did anything go wrong.

Reach for it constantly: whoami and status to orient yourself, list to see peers, show / trace to dig into one agent, and health when the layer itself feels off.

For the outcome-oriented overview, see the Session state feature page. The Session state concept documents the event and replay semantics behind these fields.

harn agents whoami # this agent's name + instance_id + files claimed
harn agents list # all active agents (folds transients by default)
harn agents status # status box (name, age, files, peers, task)
harn agents status --end-turn # issue the box only after owned Git work is finalized
harn agents status --end-turn --end-session # final status, then end after this turn closes
harn agents set-task <text...> # declare what you're working on (visible to peers)
harn agents lifecycle <state> # declare active, blocked, or done task lifecycle
harn agents end # record an authoritative V3 end after final checks
harn agents reconcile # reconcile archive, idle, cascade, and host-loss signals
harn agents reconcile --watch # run the host supervisor loop
harn agents identity assume <name-or-id> # continue a durable role in this session
harn agents show <name> # deep-dive on one peer: registry state + session history
harn agents trace <name> # reconstruct one agent's lifecycle from the event ledger
harn agents watch # stream peer state changes in real time
harn agents ping <name> <msg> # message an agent by name, live or not
harn agents wait <name> # block until a peer releases its files (or exits)
harn agents release-claim <path> # drop a file claim from your own heartbeat (escape hatch)
harn agents health # one-screen coord-layer health rollup
harn agents heal-events # PIDMAP_HEAL telemetry (how often self-heal fixed drift)
harn agents heal # repair this session's disposable cache from V3

One further subtree lives under agents for advanced use: harn agents identity (the durable per-agent persona registry). Two more are recovery tools: harn agents heal forces a specific recovery action on one agent, and harn agents adapter-probe diagnoses adapter wiring. Run harn agents <sub> --help for any of them. Councils are their own top-level command; see Councils.

When an adapter session survives a WSL or coordination-state restart, let Harnery resolve the current session and rebuild its disposable cache from the authoritative V3 generation:

Terminal window
harn agents heal

Running the command with no options is the normal agent recovery path. Operators can still target a known generation with --kind cache --owner <instance-id>, plus --session-id and --adapter when the live projection cannot supply them. Harnery validates the V3 route before it writes the cache. Never edit .harnery/active/ or ledger rows by hand.

If a prepared authority transaction survives a crash and its prior state no longer exists, normal writes report pending_transaction:<txn-id>. Inspect the transaction and obtain operator approval before using the scoped quarantine mode:

Terminal window
harn agents heal \
--quarantine-transaction txn_... \
--approval-record-id <record-id> \
--yes

Harnery refuses the command when normal reconciliation is still possible, when the transaction’s event reached the ledger or event spool, or when the private producer state does not own the exact transaction. A successful recovery keeps the original transaction in .harnery/ledgers/v3/authority-recoveries/quarantine/ and writes a canonical receipt beside it. It does not add the abandoned event to the ledger. See ADR 0128 for the safety and retry contract.

Use identity assume when a new adapter session is continuing a durable role from a predecessor:

Terminal window
harn agents identity assume Yann
# or, when the shell process cannot resolve its own session anchor:
harn agents identity assume Yann --session-id <instance-id>

The target may be a persona name, an agent-Name display name, or a persona UUID. Harnery reuses the durable persona UUID when it exists and creates it when a new name is first adopted. If another local session still has a fresh heartbeat for that name but its adapter process is gone (crash, abandoned tab, healed zombie), assume reclaims that heartbeat and continues. It refuses only when another live local process still holds the name, or when cached remote presence reports it.

One command updates every authoritative layer: it appends the binding to name history, emits identity.assumed to the canonical ledger, and projects the new name and persona UUID into the heartbeat. agents whoami --json then exposes that UUID as agent_id. The web identity index is derived from the ledger and heals automatically. Repeating the same successful command is a no-op.

This is role adoption, not persona renaming. The old session keeps its historical record, while the replacement becomes the current live owner of the role. It is limited to main sessions; subagents cannot assume operator-facing identities.

One target is refused even when nobody holds the name: a persona this session’s recorded fork lineage descends from (identity_is_ancestor). A branched conversation inherits a transcript full of its parent’s name, so asking to become that parent is usually a session misled by its scrollback, not a handoff. Pass --force-ancestor when succession is genuinely the intent. See ADR 0057.

The three orientation commands.

  • whoami prints your own name, instance_id, and the files you’ve claimed. --json returns the full heartbeat record, including activity, activity_updated_at, activity_source, task_state, task_state_updated_at, and task_state_reason.
  • list shows every active agent. By default it folds kind=transient rows (short-lived helper processes) into their parent; --all shows them raw. --stale includes heartbeats older than the freshness window (agents that likely exited without cleaning up). Its structured rows keep activity and task_state separate for local and remote agents. Older heartbeats read as activity: unknown and task_state: active.
  • status renders the end-of-turn status box: your name, session age, elapsed time for the current turn, current activity, task lifecycle, declared task, context usage, files held, and a one-line peer roster. A blocked lifecycle includes its reason. --json returns the same data unformatted. --session-id <id> looks the heartbeat up by session id directly, for use from a hook whose process tree doesn’t lead back to the agent’s session pid. Add --end-turn for the required final call: Harnery checks the current session’s held paths, enclosing submodule pointers, and touched repositories before issuing the box. Dirty held work or commits missing from a configured remote make the command fail instead. Add --end-session to the closing call to durably request an approved terminal event after this exact turn closes. It requires --end-turn; the status box reports queued after this turn while the stop hook finishes the sequence.

Declares a short description of your current focus, stored on your heartbeat and shown to peers in the per-prompt snapshot and on the dashboard. Pass an empty string ('') to clear it. Like status, it accepts --session-id <id> for adapters where the process walk can’t resolve self.

First-of-session session name. The session’s first non-empty focus declaration builds a suggested_session_name (Agent <you> - <your task>), stamps it on the heartbeat, and returns it with first_of_session: true for the operator to set as their adapter’s session/tab title:

{ "task": "Auth Refactor", "first_of_session": true,
"suggested_session_name": "Agent Hollis - Auth Refactor",
"note": "Next, before any prose or another tool call, send suggested_session_name verbatim by itself in a fenced code block." }

first_of_session: true means exactly “this call produced the session name” — it is never true with a null name. A bare clear ('') as the first call does not consume the naming window; the next non-empty declaration still names the session. The --session-id path names the session like any other call. Subagents and workflow children are never named.

With hooks installed, Harnery treats the suggestion as pending until the exact copyable block has been displayed. PostToolUse asks for that block as the next assistant text. Claude Code and Codex verify ordered transcript text before allowing another tool. Cursor stamps the completed assistant text from afterAgentResponse; its preToolUse.agent_message is only the current narration, so missing the earlier block there cannot reject the tool. The block must be the first non-whitespace content and contain only suggested_session_name; the fence may carry any single-word language label, and the closing fence must be as long as the opening one. Chat UIs add a one-click Copy button, so the operator gets the exact title without hand-selecting.

Displaying the title Harnery last asked for always satisfies the latch, even when the suggestion has since changed. A title can change after the instruction goes out — a lifecycle transition re-mints it, and a rebuilt generation cache can re-mint from the current task — and an agent must not be blocked for showing exactly what it was handed.

For transcript-backed adapters, the evidence boundary is the first assistant message after the mint result. Later commentary or a user prompt does not erase an exact display. If the adapter transcript is unavailable or has not flushed yet, Harnery keeps the check pending and allows the tool without injecting another reminder. The exact PostToolUse instruction was already delivered for that mint, so repeating a generic reminder on every tool cannot create an unbounded loop. A readable missing or malformed display is still denied.

The gate always allows a single agents set-task, agents status, or agents suggest-name command. Those are the focus, recovery, and turn-closing paths, so the naming check must not block them. Routine set-task calls stay title-silent even while a display is pending. For an explicit retry, run agents suggest-name --json; it returns the exact stored pending name with session_name_retry: true and creates a fresh evidence boundary. Shell control syntax disqualifies the exemption.

The same pending comparison drives prompt reminders, including the one terminal name re-mint when lifecycle becomes done. An unminted session is reminded on every prompt until its first focus declaration. A pending session is reminded once per exact minted name, even if the current task later changes. Subagents and workflow children are skipped. Every later ordinary set-task returns first_of_session: false, session_name_retry: false, and a null name.

set-task never changes task lifecycle. If task text changes while the session is blocked or done, the command returns a warning to reopen the lifecycle explicitly. The new task text is still recorded.

The Stop-time fallback accepts only the first assistant message after the tool result that minted the name. A plain-text mention or a block printed after substantial work does not count. Claude Code can enforce that fallback; Cursor’s Stop payload has no reply text, and Codex Stop remains observe-only. PreToolUse handles the check before those adapter differences matter.

Declares whether the current task is active, blocked, or done:

Terminal window
harn agents lifecycle active
harn agents lifecycle blocked --reason "waiting for repository access"
harn agents lifecycle done --reason "implementation and verification complete"

blocked requires a reason. done requires a current task and runs the same Git finalization check as agents status --end-turn. Dirty touched paths, incomplete claim history, an uncommitted enclosing submodule pointer, an unverifiable repository, or commits missing from a configured remote make the command fail without changing lifecycle or recording an event. The gate checks repository hygiene; it does not certify tests, deployment, or the business outcome.

A successful transition records one durable coord.lifecycle_changed event and projects it into the live heartbeat. Repeating the same state and reason is a no-op, with no duplicate event. Moving away from blocked clears the old blocker reason. Subagents, transient processes, and workflow children cannot declare operator-facing lifecycle state.

Lifecycle changes do not rename the session except when it becomes done. That transition returns the original session name with [DONE] prepended:

active -> no title change
blocked -> no title change
done -> [DONE] Agent Hollis - Auth Refactor

The per-prompt reminder and display latch remain active until that terminal name is observed. Reopening a done session does not ask the agent to print or restore another title. Titles are projections for people; editing a title never changes the underlying lifecycle. Pass --session-id <id> when the calling process cannot resolve its session anchor.

harn agents end is a deliberate V3 session-finalization command. It refuses delegated children and unfinished Git claims. If the generation is already idle, it appends one approved session.ended event immediately. If the command runs inside an open turn or tool span, it durably queues the end for the stop hook. The request is bound to that turn and its existing spans; any new turn, tool request, delegation, or resume cancels it instead of terminating new work. Use --session-id or --instance-id when automatic identity resolution is unavailable.

For an agent ending itself, prefer the one-command closing ritual:

Terminal window
harn agents status --end-turn --end-session

harn agents reconcile performs one crash-safe lifecycle pass. It discovers verified adapter archives, long-idle sessions, parent and run completion, delegated-agent completion, stale sweeps, duplicate live generations, and previously submitted host-loss observations. --watch repeats the pass at coord.finalization.reconcile_interval_seconds.

Archive observations can also enter through:

Terminal window
harn agents observe-archive --adapter codex --session-id <native-id>
harn agents observe-archive --adapter codex --session-id <native-id> --unarchived

Unarchiving or subsequent activity cancels the pending archive request. A host supervisor can submit a provisional loss signal with observe-host-disappeared; it cannot bypass the configured grace period. See ADR 0077.

Reprints a session name (Agent <you> - <description>) for the operator’s tab title, read-only. This is the secondary path: set-task already suggests a name on the first focus declaration (above). Reach for suggest-name to reprint the current session’s name or re-suggest one after a topic pivot. With an explicit arg it uses that. With no arg it returns the exact stored pending name when one exists, even if the current task has changed; otherwise it derives a name from the current task:

$ harn agents suggest-name "Auth Refactor"
Agent Hollis - Auth Refactor
$ harn agents suggest-name # no arg → from current task
Agent Hollis - Auth Refactor

It prints the bare name on stdout (not a drawn box, since a box would copy its borders and labels too). --json also returns session_name_retry: true when it reprints the exact pending suggestion. PostToolUse treats that explicit response as a fresh display boundary. Like status, it accepts --session-id <id>. Mutates no state.

Two ways to dig into a single agent, addressed by name (agent-Foo or Foo) or by instance_id.

  • show is the current state view: files held, last tool, declared task, current activity, task lifecycle, plus recent session history (latest title, recent prompts, tool-usage tallies). A name is disambiguated to an instance_id by prefix match.
  • trace is the history view: it reconstructs one agent’s lifecycle from the event ledger in chronological order: session start, prompts, turns, input requests, lifecycle declarations, tools, heals, sweeps, file claims, and exit. Its header carries the latest folded activity and lifecycle, even after the live heartbeat is gone. This is the answer to “what happened to this agent / why did it vanish?” without hand-grepping the stream. --since <window> bounds it (e.g. 1h, 2d), --limit <n> caps it (default 200), and --all-tools includes the per-tool noise that’s hidden by default.

The coordination-in-motion commands.

  • watch streams peer state changes live (a file watcher on .harnery/active/), printing one line per delta: started, ended, activity, file claim, task change. --poll-ms <n> tunes the debounce (default 200).

  • ping <name> <message...> sends a message to an agent by name. It does not require the recipient to be running, which is what makes it safe to use as a single step: a live recipient is shown the message on their next prompt, and a name with no live session has the message held in a durable mailbox until a session of that name next starts. Either way the message is recorded in the recipient’s journal. The only refusal is a name no agent has ever held, and that error lists the closest known names, so a sender never has to look up who is live before sending or find another place to leave a note.

    Two ceilings keep a mailbox bounded: 4000 bytes per message and 25 pending messages per name. Past either, the send is refused and the error says to write the payload to a managed artifact and send its path instead. Delivered messages are copied to .harnery/mailbox/delivered/<name>.jsonl for audit.

    Claude Code, Codex, and Cursor receive unread messages on their next prompt. A message already waiting when a session starts may arrive with SessionStart context instead. Delivery atomically claims the pending batch, so later prompts cannot replay it. Senders need no adapter knowledge.

  • wait <name> blocks until a peer releases its files (its files_touched empties) or exits. Scope it to specific paths with --file <path> (repeatable), cap it with --timeout <dur> (suffix s/m/h/d; a bare integer means minutes; default 60m), and tune polling with --poll-secs <n> (default 5). --quiet suppresses progress lines; --json prints the terminal status at exit. Pairs naturally with ping.

release-claim <path> drops a single file claim from your own heartbeat. It’s an operator escape hatch for when an auto-release didn’t fire (e.g. a session ended mid-edit) and a peer is now blocked on a path you no longer care about.

The layer’s own diagnostics.

  • health is a one-screen rollup: heal events, council activity, zombie detection, and anomalies. Built for a daily glance or dashboard ingestion. --since <window> sets the window (default 24h); --json for structured output. When the V3 event ledger is live, an event ledger section adds read-only producer counters: open tool spans per live generation (flagging generations whose turn has closed while spans stayed open — the orphan signature that blocks a clean session end), pending finalization requests with trigger and age, intake-spool depth, diagnostics-spool counts by category (with a last-24h split), and span-count pressure against the producer-state cap. On an unavailable or invalid V3 control state the section reports that failure explicitly instead.
  • heal-events surfaces PIDMAP_HEAL telemetry: how often the pid-map self-heal had to correct drift. Sustained high counts point at an upstream process-spawn bug. --since <window> (default 7d), --limit <n> (default 20), plus --json / --csv.
Terminal window
# Orient yourself at the start of a turn
harn agents whoami
# See who else is active and what they're holding
harn agents list
# Declare your focus so peers can see it
harn agents set-task "refactoring the ingest retry loop"
# Declare an operator-facing lifecycle transition
harn agents lifecycle blocked --reason "waiting for API access"
harn agents lifecycle active
# End-of-turn status box
harn agents status
# Optional guarded form: require owned Git work to be committed and pushed
harn agents status --end-turn
# Dig into a peer's current state, then its full history
harn agents show Foo
harn agents trace Foo --since 2h
# Leave a note for a peer holding a file you need, then wait for it
harn agents ping Foo "can you release src/config.ts when you're done?"
harn agents wait Foo --file src/config.ts --timeout 15m
# Watch the layer live while agents work
harn agents watch
# Daily health glance
harn agents health --since 24h

harn agents status prints the box without inspecting Git. The guarded harn agents status --end-turn form first verifies that the paths held by this session are committed and that the repositories involved are pushed. It follows held paths into submodules and checks the enclosing gitlink too. Active edits held by another session do not block the check.

When the guarded form is required, write tools also preflight their targets. The coordination repository is authorized automatically. A host can add an exact sibling Git repository or an intentional non-Git output root through agents.finalizationRoots in project config. Harnery denies unsupported paths before the tool mutates them, and it records the selected disposition with the durable write claim. See the configuration schema for the policy shape.

Harnery does not impose the guarded form on every project. A host opts into automatic prompt and Stop enforcement with { "agents": { "requireGitFinalization": true } } in .harnery/config.jsonc; the default is false. The command-line flag remains available as a manual check either way.

The flag calls this the “final” status because it is the final status checkpoint for a turn, as opposed to a mid-turn coordination glance. It does not end the agent session. The name is compact, but “final” can also suggest session termination; the config and docs therefore describe it explicitly as a turn-ending Git guard.

The gitlink check compares the commit recorded in parent HEAD, the parent index, and the child repository’s HEAD. It does not treat unrelated modified or untracked files inside the child as a pointer change.

Committed paths stay in scope through the session’s coord.claim_changed history. The commit hook can release a clean active claim without letting an unpushed commit disappear from the final check.

An unsupported claim from an older session is reported separately from dirty Git work. The error explains how to authorize its containing repository or non-Git output root. It does not suggest committing a directory that has no Git repository.

On success, it prints the same fixed-width box as harn agents status, designed to paste straight into a reply so a human (and peers) can see your state at a glance:

┌─ agent-Foo ────────────────────────────────────────────────────────┐
│ session 48m │
│ turn 4m 12s │
│ task refactoring the ingest retry loop │
│ activity working │
│ lifecycle active │
│ context 240K / 1M (24%) │
│ files 1 held │
│ peers Bar (CC), 3 files; Baz (CC) │
│ time Tue, Jul 14, 2026, 7:40 AM CDT │
└─────────────────────────────────────────────────────────────────────┘

session is how long this session has been alive and turn is how long the turn the box closes has been running, so a long session and a short turn read differently at a glance. task is what you declared with set-task, and peers lists the other active agents with their adapter and file counts.

The turn row measures from the turn.started event the operator’s prompt recorded to the moment the box renders. A Stop-hook remediation restart is folded back into the turn it retried, because the operator experienced one turn. The row is best-effort: a session with no turn evidence in the ledger renders no turn line at all. --json carries the same measurement as turn_elapsed_secs, with turn_complete saying whether a turn.completed already closed it.

When coord.run_quality.mode is report, status also performs a non-blocking due check and shows a quality line. A fresh snapshot can be unknown, healthy, attention, or critical, followed by up to three active signal IDs. Missing, expired, truncated, or insufficient evidence is unknown. shadow mode still evaluates and records snapshots but deliberately exposes no operator-facing severity. Run quality is advisory and cannot alter the status command’s Git guard or any hook verdict.

After this session finishes a page-QA run, status carries the verdict on the runner’s clock:

│ qa passed 4m ago · 90s runner (2m queued) │
│ qa incomplete 3h ago · 34s runner │
│ qa manual 12m ago · not a pass │

The line exists so runner time is never read off session. Session age measures the agent, not the run, and a box showing session 58m beside a QA result invites the conclusion that QA took 58 minutes.

Read it as: verdict, how long ago the run completed, then how long the runner itself took. Admission-queue wait is reported separately and is never folded into runner time, so a slow-looking run that mostly sat waiting for a slot is visibly distinguishable from one that mostly worked. Hand-recorded evidence (qa-record) shows manual … · not a pass instead of a verdict and a clock: nothing re-executable ran, so there is no runner time to attribute and the contract caps such a result below a pass.

A verdict older than 24 hours renders as stale (2d) and nothing else — a day-old result says nothing about the page as it stands now. The line is absent when this session has recorded no run. Every read is best-effort: a missing, unreadable, or partial pointer renders no line rather than failing the status command.

  • Native children have their own identity. After the adapter hook observes a delegation, the child gets the exact parent-minted generation, its own heartbeat, and its own whoami / set-task authority. Child-owned tool signals land on that generation. Delegation start and stop remain parent-owned because they define the parent-child span.
  • Unbridged subprocesses still inherit the parent. A shell with no native child session ID falls back to the process tree and reports the parent. This avoids guessing when a generic subprocess has no independent identity.
  • Name vs instance_id. show, trace, ping, and wait all accept a human name (case-insensitive, with or without the agent- prefix) and resolve it to an instance_id by prefix match. Pass the instance_id directly if a name is ambiguous.