name: h2a version: 0.66.0 description: Coordinate with other CLI agents (Claude Code, Codex, Gemini) via the h2a protocol — open a live session, list peers, exchange messages, drive a signed negotiation. Use when the user wants the current CLI to interact with another agent through a shared workspace.
/h2a
When invoked, parse the arguments and dispatch to the matching subcommand below. If no subcommand is given, default to status.
/h2a → status (alias)
/h2a connect [root] → bootstrap a live session in this conversation
/h2a status → show current session state and health summary
/h2a discover [scope] → list live peer agents
/h2a conductor [workspace] → who owns/conducts a workspace right now (null = none yet)
/h2a conductor claim → claim conductor for a workspace (earliest live claimant wins)
/h2a conductor release → release conductor claim (frees it for the next claimant)
/h2a conductor-launch-check [workspace] → DRY-RUN: polls track; recommends launching a conductor if stalled + none live
/h2a conductor-launch [workspace] → EMIT a launch REQUEST to remote when stalled+no conductor; capped 1/30min, requires --confirm; h2a never spawns
/h2a send <peer> "<text>" → put a message envelope in a peer's inbox
/h2a receive → read this agent's inbox and react to new envelopes
/h2a negotiate <verb> ... → drive a negotiation lifecycle (open|offer|sign|stabilize)
/h2a model "<situation>" → propose a tailored h2a model for an org/situation
/h2a disconnect → cleanly close the current session
/h2a help → print this command map
The user's raw arguments arrive appended below the prompt; treat anything after /h2a as the routing token.
Routing
/h2a connect [root]
Bootstrap a live session so this agent can cooperate with other CLI agents.
Steps:
- Verify the
h2abinary is on PATH (h2a --help). If absent, instruct the user to runnpm i -g @sentropic/h2a-cli@latestand stop. - Decide the shared root. If
[root]was passed, use it; otherwise prefer<this-workspace>/.h2a, else~/h2a-workspace/.h2a. Confirm with the user only if ambiguous. - Run
h2a init --root <root>(idempotent). - Resolve YOUR perennial identity — do NOT invent it. Run
h2a connect --host <h> --root <root>; it returns the perennial instance idhost:slug(label):uuid12(DEC-114/116), bound per(host, workspace)and reclaimed on reconnect — the same workspace always gets the same id (never minthost:workspace-leafby hand; that barehost:labelform is the channel alias, see "Identity & addressing"). Use the returnedinstance. - Call the MCP tool
h2a_session_openwith{ instance, host, interests: { scopes: ["scope:default"], negotiations: [] } }(instance = the resolved perennial id from step 4). - Print a short summary: instance id, session id, peers currently live, the four notification topics this session is subscribed to.
End with the cue: "Connected. Try /h2a discover to see who else is around, or /h2a send <peer> \"hi\" to message someone."
/h2a status
Show the current connectivity state. Steps:
- If no session has been opened yet in this conversation, say so and suggest
/h2a connect. - Otherwise call
h2a_discover_sessionsand find the session whosesessionIdmatches the one returned byh2a_session_open. - Print: instance id, session id, state (
opening|live|draining|closed|expired),heartbeatAt, count of peer sessions, count of unread inbox envelopes (callh2a_inboxwithaction: "read"and report length). - If anything looks stale (no recent heartbeat, no peers when peers were expected), suggest
/h2a connectto re-open.
/h2a discover [scope]
List currently-live peer agents.
Peer resolution rule — always use h2a_discover_sessions, never grep the registry:
To reach a peer by PURPOSE/role, RESOLVE it via h2a_discover_sessions filtering on scope (the canonical purpose key) or name (case-insensitive substring match on the agent's display name) — e.g. { scope: "scope:chat" } or { name: "radar" }. NEVER grep the registry for a free-text name: instance ids are workspace-derived (host:slug(workspace):uuid), so a name grep misses. If several sessions match, list them and let the user pick (or pick the single live one); if none match, the agent has not advertised that scope/name yet.
Steps:
- Call
h2a_discover_sessionswith{ scope }(omit if no argument); add{ name: "<substring>" }when the user asks to find a peer by friendly name. - Filter out the current agent's own session (compare against
h2a_session_open'ssessionIdif known). - For each remaining session, print:
instance,host,name(if set),interests.scopes(comma-joined), a relative heartbeat age ("12s ago"), andsessionId. - Sort by host then instance.
- If empty: say so and suggest the user check that the other CLIs ran
/h2a connectagainst the same root.
End with: "To message one of these: /h2a send <instance> \"<text>\"."
/h2a conductor [workspace]
Resolve who owns/conducts a workspace right now. Call h2a_conductor (MCP) or h2a conductor --workspace <id|path> (CLI).
conductor= the earliest live active-claimant (WP-G1b, claim-based election), or a live in-workspace agent registered with roleCONDUCTOR(back-compat), ornullif none. Null is the normal/expected state today — agents auto-register asAGENTS, notCONDUCTOR. Do NOT treatnullas an error; it means "no designated owner yet."claimedBy= the instance that won via claim-based election, ornullif resolved by role or absent.candidates= all in-workspace live agents (anyone who has a fresh presence record for that workspace), regardless of role.live=trueif at least one candidate is present.workspaceargument: aws:<uuid>workspace id or a filesystem path (resolved via the same derivation presence uses). Defaults to cwd.
When to use it: call h2a_conductor before acting in a workspace on behalf of another agent, to check whether a designated conductor is already live. If conductor is non-null, coordinate with them first rather than acting unilaterally.
/h2a conductor claim [--instance <self>] [workspace]
Claim the conductor role for a workspace (WP-G1b — additive, reversible). Appends a claim event; returns the post-claim conductorFor result. The caller should be itself if it won (earliest live claimant wins; a later claimant is listed in candidates but does not displace the earlier one as long as it is still live).
CLI: h2a conductor claim --instance <self> [--workspace <id|path>] [--root <path>]
MCP: h2a_conductor_claim { instance, workspaceId|workspacePath }
Exit 1 if --instance is missing.
/h2a conductor release [--instance <self>] [workspace]
Release the conductor claim for a workspace (WP-G1b — additive, reversible). Appends a release event; returns the post-release conductorFor result (conductor is the next live claimant, or null if no other claimant is live).
CLI: h2a conductor release --instance <self> [--workspace <id|path>] [--root <path>]
MCP: h2a_conductor_release { instance, workspaceId|workspacePath }
Exit 1 if --instance is missing.
/h2a conductor-launch-check [workspace] [--idle-ms <ms>]
DRY-RUN (D3). Polls track workspace-activity and returns a recommendation to launch a conductor if work is durably stalled and no conductor is live.
h2a does NOT spawn anything. A recommendation === "launch" result is purely advisory. The actual launch is parked pending a spawn-policy decision and remote-trigger support.
CLI: h2a conductor-launch-check [--workspace <id|path>] [--root <path>] [--idle-ms <ms>]
MCP: h2a_conductor_launch_check { workspaceId|workspacePath, idleMs? }
Return shape: { workspaceId, trackAvailable, conductor, conductorLive, pending, stalled, recommendation, reason, suggestedHosts? }
recommendationis"launch"when: no conductor is live AND track reports at least one stalled item.suggestedHosts=["claude", "codex", "agy"](user preference order) whenrecommendation === "launch".trackAvailable: falsewhentrackis not installed or errors — graceful,recommendationstays"none"(no false positives).--idle-msis the stall threshold passed totrack workspace-activity(default 86400000 = 24h).
A one-liner stderr note is emitted when recommendation === "launch" reminding callers this is a DRY-RUN.
/h2a conductor-launch [workspace] [--idle-ms <ms>] [--confirm] [--remote <instance>] [--instance <self>]
D3 EMISSION. When conductorLaunchCheck returns recommendation="launch", emits a conductor-launch-request envelope to a live remote agent.
h2a NEVER spawns a process. It only puts a request envelope in the remote's inbox; the remote agent executes the actual spawn.
Gates (both must pass to emit):
- Human confirmation: without
--confirm, only preview the request (action: "would-emit"). With--confirm, emit + record the marker. - Cap/cooldown: at most 1 emission per 30 minutes per workspace (
action: "cooldown"if too recent).
--instance <self> is required with --confirm (identifies the sender/signer).
Host preference in the request: ["claude", "codex", "agy"].
CLI: h2a conductor-launch [--workspace <id|path>] [--root <path>] [--idle-ms <ms>] [--confirm] [--remote <instance>] [--instance <self>]
MCP: h2a_conductor_launch { workspaceId|workspacePath, idleMs?, confirm?, remote?, instance? }
Return shape (all cases exit 0 except user errors):
{ action: "none", reason, ...check }— recommendation is not "launch".{ action: "cooldown", reason, lastSpawnAt }— cooldown not elapsed.{ action: "would-emit", request, note }— preview (no --confirm).{ action: "no-remote", reason }— no live remote agent found (--confirm but no remote).{ action: "emitted", to, request }— emitted + marker recorded.
/h2a send <peer> "<text>"
Compose and route an envelope to a named peer.
Resolve-before-send rule (0.59.0+): Always resolve the peer to its LIVE full id (host:label:uuid12) via h2a_discover_sessions BEFORE sending. A bare host:label sent to an ambiguous target (>1 live agent sharing that alias) or a phantom target (0 live, 0 registered) is now REFUSED by h2a_inbox put / h2a CLI with exit 1. Never invent a sub-label. The safe pattern is: discover → pick the exact live host:label:uuid12 → send to that. A bare alias is only acceptable for a dormant/wake-drop or exactly 1 live match (the tool surfaces the live candidate in liveCandidate).
Steps:
Resolve the target + check liveness FIRST (
h2a_discover_sessions). Pick the addressing form (see "Identity & addressing"):- target is a specific live agent → use its full perennial id
host:slug:uuid12from discover (NOT the barehost:label, which is ambiguous when several agents share a workspace); - target is a role/channel or a known-dormant peer → use the channel form
host:label. - If the user names a peer by PURPOSE/role, call
h2a_discover_sessionswith{ scope: "<purpose>" }. If the user names a peer by FRIENDLY NAME, callh2a_discover_sessionswith{ name: "<substring>" }. NEVER grep the registry for a name: instance ids are workspace-derived (host:slug(workspace):uuid) so a text search misses. If several match, list and ask; if none, the agent hasn't advertised that scope/name. - If
<peer>is missing, list discover and ask the user to pick.
- target is a specific live agent → use its full perennial id
If
"<text>"is missing, prompt the user for the content.Compose an
H2AEnvelopeJSON:{ "protocol": "sentropic.h2a", "version": "0.1", "id": "env:<epoch-ms>:<4hex>", "type": "event", "actor": { "instance": "<this-agent-instance>", "role": "<this-agent-role-or-AGENTS>", "scope": "<shared-scope-or-default>" }, "body": { "kind": "message", "text": "<text>" }, "createdAt": "<ISO-8601-now>" }Hard rule: the recipient MUST be host-qualified (
<host>:<label>, e.g.claude:radar-immobilier). A bare label (e.g.radar-immobilier) is rejected — the same label can live on several hosts (claude, codex, …), so a bare label is ambiguous and routes to an orphan inbox nobody reads.Call
h2a_inboxwith{ action: "put", instance: "<peer>", envelope }.Conversation threading (optional, lightweight): To continue an existing back-and-forth as a thread, add two top-level fields to the envelope JSON before putting it:
"threadId": "<id>"— reuse thethreadIdfrom the peer's previous envelope, or mint a fresh one asthr:<epoch-ms>:<4hex>to start a new thread."replyTo": "<prev-envelope-id>"— theidof the envelope you are replying to.
To reconstruct the ordered fil of a thread (for supervision or before opening a formal negotiation):
h2a thread --id <threadId> --instance <self-instance> --root <root>This returns the envelopes (from your inbox + outbox) that share that
threadId, sorted ascending bycreatedAt, deduped. No new store — storage is derived on the fly.Report honestly per the target's liveness (h2a writes the inbox unconditionally — it does NOT yet error on a dead target, so YOU must say which it was):
- target was live in discover → "Delivered to
<peer>(live) — push fires if subscribed." - target was NOT live → "
<peer>is dormant — envelope deposited for its wake (no live session; it will only see this once woken)." Never claim "Delivered" to a dormant/unknown peer.
- target was live in discover → "Delivered to
For richer payloads (file pointer, deliverable, status update), set body.kind to a category and add the relevant fields; ask the user if ambiguous.
/h2a receive
Read this agent's inbox and react to whatever is new.
Steps:
- Call
h2a_inboxwith{ action: "read", instance: "<this-agent-instance>" }. - For each envelope, print: sender (
actor.instance),body.kind, a short summary of the content, and the envelope id. - Ask the user what to do with each (reply via
/h2a send, mark read by callingh2a_inboxwithaction: "pop", or ignore). - If a
notifications/h2amessage withtopic: inbox.envelope_arrivedwas just observed in the JSON-RPC stream, react immediately rather than waiting for the user to type/h2a receive.
Pop after processing (hygiene rule): Once you have acted on an envelope, call h2a_inbox with { action: "pop", instance: "<this-agent-instance>", envelopeId: "<id>" } to remove it from the inbox. read is non-destructive — an un-popped envelope resurfaces on every subsequent poll and masks genuinely new arrivals. Pop as soon as the envelope is handled, not just when explicitly instructed.
/h2a negotiate <verb> ...
Drive a step of the h2a negotiation lifecycle. The subverb is one of:
/h2a negotiate open <id> <scope> <parties> "<subject>"— callh2a_open_negotiationwith a freshH2ANegotiationRecord./h2a negotiate offer <id> <artifact-json>— callh2a_offer./h2a negotiate counter <id> <artifact-json>— callh2a_counteroffer./h2a negotiate sign <id> <artifact-json> [--key <pem-path>]— callh2a_sign(the private key PEM path defaults to<root>/keys/<instance>.key.pemfromh2a keys generate)./h2a negotiate stabilize <id>— callh2a_stabilizeonce the quorum is signed./h2a negotiate journal <id>— print the journal entries.
For each verb, validate the required arguments, surface any tool error verbatim, and print a one-line confirmation with the latest journal entry id.
/h2a model "<situation>"
Propose a tailored h2a model for a free-form organization or situation (e.g. "a hospital coordinating with insurers and a regulator", "a 3-team scaled-agile delivery with AI agents"). This is an advisory mapping, not a protocol action — it emits a design, it does not write to the store.
Method (follow it faithfully — do not invent vocabulary):
- Ground in the canonical model first. Read
VOCABULARY.mdfor the frozen roles (PRINCIPAL,EXECUTIF,CONDUCTOR,AGENTS,CONTROL,MANDATAIRE) and substrate (SCOPE,CONTRACT,POLICY,ENGAGEMENT,MANDATE,SLOT,BINDING,NEGOTIATION,AUTHORITY), and skimevaluations/README.md(the common grid) plus the closest existing use-case inevaluations/(A enterprise, B ecosystem, C government, D 15-conductors, E agentic squad). Reuse their patterns; do not coin new roles or artifacts. - Map the situation against the common grid: actors→roles, scopes, authority/mandate/signature, CONTRACT vs ENGAGEMENT vs POLICY, obligations/rights/clauses, controls, escalations, audit, deadlocks/precedence, gaps.
- Respect the invariants (these are load-bearing):
- A
SCOPEnever signs; a mandated INSTANCE signs for a party/scope. ENGAGEMENThas a scope — it is not the scope. Name both.- Anything that must be owned needs a
PRINCIPAL(e.g. architecture ownership) —CONTROLonly audits/vetoes, it owns nothing. POLICYis a durable rule (standalone or a clause of a CONTRACT); prefer expressing rules as engagement clauses unless they are cross-cutting/imposed.- Delegated AI agents are
AGENTS(orSUBAGENTS, parent-addressed) viaMANDATE+BINDING, default execution-only / non-signing. MANDATAIREpresents/records, never judges; escalation targets the scope's competent authority.
- A
- Output, mirroring the
evaluations/*.mdstructure:- a Mermaid diagram of roles/scopes/contract flows;
- a mapping table (real element → h2a construct → note);
- contracts vs policies, a multi-actor case, gaps, and a one-paragraph compatibility hypothesis;
- the nearest built-in profile id (
A_ENTERPRISE/B_ECOSYSTEM/C_GOVERNMENT_CITIZEN/D_SAFE) and how the situation differs from it.
- Offer to persist: ask whether to save the proposal as a new
evaluations/<slug>.md(same format) for review. Only write the file if the user agrees.
Keep it a proposal: surface assumptions and the genuine design forks (e.g. one scope with co-principals vs sub-scopes) rather than silently deciding them.
/h2a disconnect
Cleanly close the current session.
Steps:
- Call
h2a_session_closewith{ sessionId, state: "closed" }. - Confirm.
This is rarely needed manually — the session auto-closes when this CLI process exits, via the mcp-serve shutdown hook (DEC-051). Use it if the user wants to release the slot before exiting.
/h2a help
Print the command map at the top of this file. Concise, no extra prose.
Keepalive (optional, launcher-side)
When the launcher or remote infrastructure runs h2a keepalive [--root <path>] [--interval <ms>], it acts as an external presence prober: it lists the live tmux panes via tmux list-panes -aF '#{pane_id}' and refreshes the heartbeatAt of every presence file whose launchContext.tmux.pane is still alive. This keeps pane-alive agents visible on the bus even if the host suspends the MCP child process's heartbeat interval. The --once flag does a single pass (useful for testing and scripting); without it, the prober loops on an unref'd interval. h2a works without it — keepalive is purely additive.
Identity & addressing (DEC-114/116 — read before sending)
There are two addressable forms; pick deliberately:
- Agent (perennial, specific):
host:slug(label):uuid12— e.g.claude:sentropic:cbf32fe0800b. This is YOUR identity (resolved byh2a connect, reclaimed per(host, workspace)) and how you address one specific live agent. Get a peer's full id from/h2a discover— never guess the uuid. - Channel (role / alias):
host:label— e.g.claude:sentropic-scale. The barehost:labelform is a named mailbox/alias, not a specific agent; use it for a role, a known-dormant peer you want to leave a message for, or a wake drop. A perennial agent also reads its ownhost:labelalias inbox (dedup), so channel messages reach whichever agent adopts that label.
Resolution rule — discover, never grep: To reach a peer by PURPOSE/role, filter h2a_discover_sessions on scope (the canonical purpose key). To reach a peer by FRIENDLY NAME, filter on name (case-insensitive substring match on session.name). NEVER grep the registry file for a free-text name: instance ids are workspace-derived (host:slug(workspace):uuid), so a name grep misses. If several sessions match, list them and pick/ask; if none match, the agent hasn't advertised that scope/name yet.
Consequences to respect:
- Addressing is case-insensitive; the label is slugified (0.40.0+). The handle
is canonicalized as
lower(host):slugify(label)[:lower(uuid)], soclaude:matchID≡claude:matchid— both reach the one inbox. The canonical channel for a label is thereforehost:slugify(label)(lowercased,[a-z0-9._-]), not the raw display case. Don't tell a peer to address you by a mixed-case label; it resolves the same either way. (Pre-0.40.0 deposits in a raw mis-cased dir are still read.) - One perennial id per
(host, workspace)— several concurrent sessions of the same agent share that id and its inbox (they are distinguishable in/h2a discoverbysessionId, but inbox delivery is per-agent, not per-session). To reach "whoever is working in project X", address the agent id; there is no per-session inbox today. - Liveness is not enforced on send —
h2a_inbox putwrites even to a dead target. Always/h2a discoverfirst and report truthfully (see/h2a send): delivered (live) vs deposited for wake (dormant). recipientLive:false/ "dormant" ≠ "not working" — liveness means only "has a freshmcp-servepresence session", i.e. reachable by a push now. An agent doing real work headless (e.g.codex exec, a one-shot review) runs with no presence session and so shows as not-live while it is busy. Never relaunch, reassign, or duplicate a task just because a peer reads as not-live. Before assuming a peer is idle: check itsworkStatus(working|blocked|…in discover/h2a status, if it sets one), look for a recent delivery/report from it, or ask the human — do not infer "free" from "not-live". Liveness gates delivery, not work in progress.
Defaults and conventions
Auto-connect (recommended): register the MCP server with
mcp-serve --auto-open --host <h>so a session opens at host startup (EVO-6/DEC-105);/h2a disconnectleaves early./h2a connectstays available for manual/explicit connect.Shared root: same
<root>/.h2a/for every cooperating CLI. If the user has not declared one, look for<cwd>/.h2a/, then~/h2a-workspace/.h2a, ask if neither exists.Instance id: the perennial
host:slug(label):uuid12resolved byh2a connect(reclaimed per(host, workspace)) — do NOT hand-minthost:workspace-leaf; that bare form is the channel alias.Subscriptions: when opening a session, subscribe to all four canonical notification topics (
presence.peer_joined,presence.peer_left,inbox.envelope_arrived,negotiation.event_appended) unless the user narrows the scope.JSON-RPC notifications:
notifications/h2amessages arrive on stdout interleaved with tool responses. They have noidfield and usemethod: "notifications/h2a". React to them in real time rather than polling.
Failure modes
- Tool returns
{ error: ... }→ surface the error verbatim, stop the current subcommand. h2abinary missing → install instruction + stop.- Root not initialized → suggest
/h2a connect. - Session not open → suggest
/h2a connect(do not silently bypass). - Peer not in
h2a_discover_sessions→ name it, suggest checking the spelling or that the peer ran/h2a connectagainst the same root.
Related commands shipped by @sentropic/h2a-cli
These can be invoked directly from the shell at any time, outside the slash-command flow:
h2a doctor [--root <path>]— quick health probe.h2a sessions [--root <path>]— same listing as/h2a discoverbut from the shell.h2a status [--root <path>] [--scope <s>] [--instance <i>]— inventory of connected sessions split direct (local mcp-serve heartbeat) vs indirect (mirrored in from a remote/sidecar), with counts.h2a keys generate --instance <id>— produce an ed25519 PEM keypair.h2a install-skills --host <claude|codex|gemini|agy>— re-install or update this skill on another host. (For agy/Antigravity the skill is written to the shared~/.gemini/commands/h2a.toml; the command then prints animportHint— runagy plugin import gemini(thenagy plugin enable h2a) to pull it into agy. DEC-096/101.)
Operational surfaces (also shell-invocable; matching h2a_* MCP tools where noted):
h2a nhi report|inventory|attest|offboard|export— Non-Human-Identity posture / inventory / signed attestation / coordinated offboard / SPIFFE-bundle export (h2a_nhi_*, DEC-087..090/094).h2a blockage raise|list|resolve— the peer blockage feedback loop, distinct from the drumbeat and from escalation (h2a_blockage_*, DEC-092). Subscribed sessions getpeer.blocked/peer.unblockedpushes.h2a drumbeat record|scan|clear|escalations|watch— anti-stall relance daemon + escalation-to-PRINCIPAL (DEC-086/091/095). Gov D2/D4 (0.64.0): a claimed conductor owns its workspace's relances — peers defer on fresh stalls (relanceCount=0); a failsafe lets a peer step in if the conductor leaves an agent stuck (relanceCount>=1, preventing workspace freeze). Cross-workspace relances emit a CoI advisory (warn, not blocked). Both behaviors are opt-in and default-allow: pass--instance <self>toh2a drumbeat watchto activate them.h2a sysml verify --json <env> --public-key <pem>— verify a SysML-v2 ref embedded in a signed envelope (commit-trust + content-integrity, DEC-099).h2a host setup|status|plugin --host <codex|claude|gemini|agy>— render the per-host MCP config / stop-hook glue (DEC-093/096).