Browse documentation
Product documentation · snapshot 2026-10-28 · View source ↗

These are the product’s technical docs at this revision. Historical version names and status notes are preserved; see Releases for download qualifications.

Read as Markdown

Issues for people and automation

On this page

An issue keeps its opening event ID for its lifetime. Edits append signed full-state revisions; comments append signed discussion. Existing title-only issues remain readable. Assignment, criteria, labels, metadata and open/closed status describe work; they do not grant execution, review or merge authority.

A simple issue

In an initialized repository:

hn issue open --body "The description" "An issue title"
hn issue list
hn issue show <issue-id>
hn issue comment <full-issue-id> "Here is additional context."
hn issue help

Human creation prints a shortened ID. Run hn issue show <short-id> --json to obtain the full data.id, or create with hn issue open --json TITLE and use its full data.issue_id. Use full sha256:<64-hex> IDs for mutations. Read commands accept a unique lowercase hexadecimal prefix, with or without sha256:. Ambiguous prefixes fail. Mutation issue IDs and expected revision IDs must always be full. Flags follow the issue ID and precede free text; use -- before literal text beginning with an option spelling.

Rich state and guarded replacement

Save this complete example as issue.json:

{
  "title": "Make setup easier",
  "body": "Explain the first successful action and its expected result.",
  "status": "open",
  "criteria": [{"id": "first-run", "text": "A new contributor completes the documented example."}],
  "labels": ["documentation"],
  "assignees": ["docs-team"],
  "relations": [],
  "metadata": {"example/source": "onboarding"}
}
hn issue open --input issue.json --operation onboarding-create --json
hn issue show <full-issue-id> --json
hn issue revise <full-issue-id> --expect <current-head-id> \
  --input issue.json --operation onboarding-edit --json
hn issue close <full-issue-id> --expect <current-head-id> --operation onboarding-close --json
hn issue reopen <full-issue-id> --expect <closed-head-id> --operation onboarding-reopen --json

--input - reads the same JSON document from stdin. Input is the state object itself, not an envelope or patch. A replacement resets omitted optional fields to empty and defaults omitted status to open; copy fields you want to keep. Explicit null, unknown keys, duplicate keys, wrong-case field names, trailing JSON documents, malformed UTF-8 and unpaired surrogate escapes are rejected. A properly paired Unicode escape or intentionally supplied U+FFFD is valid. No malformed Unicode is silently repaired before signing.

Criteria have stable IDs local to the issue state and retain their listed order. Labels, assignees and relations are sets, stored in canonical sorted order; duplicates are rejected. Metadata values are strings, with keys such as example/source: a nonempty namespace and name separated by /. A namespace need not be an Internet domain. Assignees are descriptive strings, not a trust-policy actor list. Criteria record expected outcomes, not approval.

Limits

Limits are inclusive and byte lengths use UTF-8 bytes.

Surface Limit
JSON request input 256 KiB
New signed issue payload and canonical request 256 KiB each
Title 1–1024 bytes, nonblank
Body or comment text 64 KiB
Criteria 64; ID 64 bytes, text 4096 bytes
Labels / assignees 64 each; 256 bytes per value
Relations 128
Metadata 64 pairs; key 128 bytes, value 4096 bytes
Parents per signed revision 1–200
Operation key 1–128 printable non-whitespace ASCII bytes
Query page Default 50, maximum 200 records
Verified issue reader 100000 events, 100000 enumerated actor/remote ref entries
Reader object / aggregate object bytes 8 MiB / 256 MiB

The reader counts event payloads, encoded signatures and run-log attachments. It verifies all selected actor histories, including non-issue facts, because actor continuity and evidence relationships cross event kinds. Oversized legacy history receives a resource-limit diagnostic rather than being reclassified as a bad signature. Replication has its own separate selection and promotion budgets.

Reliable retries and concurrency

An optional operation key is scoped to the signing actor across issue commands. Its signed request digest binds intent, event kind, issue, expected parents and canonical requested content. Repeating exactly that operation returns the original event ID with replayed: true, even after later edits or comments. Changed content, target, expected parents or close/reopen intent with the same key returns operation_conflict. Use a new key for a new intentional action. Independent actors may use the same textual key without sharing an operation.

Replay is checked before current-head guards. Close/reopen retries recover the originally signed state, rather than deriving different content from the issue's latest state. Signed duplicate operation-key records fail explicitly; Hubnot does not choose an arbitrary replay winner.

An edit captures the actor's append position, verifies an observed repository snapshot, checks the expected issue head, then appends with actor-ref CAS. stale_revision means the supplied observation no longer satisfies the guard. concurrent_update means a read snapshot or this actor's append position changed. Read again and decide deliberately; Hubnot does not silently rebase your request.

This is snapshot preflight plus CAS on one actor history, not a global issue lock. Another actor can write after your snapshot. Both valid edits remain visible after synchronization. A mutation's head_count and conflict describe the catalog observed by that command plus its own fact, not global consensus.

Public signer pinning and operation recovery

Discover public signer information with hn identity show --json (read-only; no identity initialization or migration), whose schema is hn.identity/1 and data is {actor,name,public_key}. The issue command schema remains independently versioned as hn.issue/1.

Every issue mutation accepts optional --actor ACTOR, where ACTOR is the full 64-character lowercase public fingerprint. Omitting it preserves the existing active-identity behavior. A malformed or empty supplied actor is invalid_input. A valid pin differing from the identity actually used by the command is actor_mismatch, with zero append, including retries. Repair the caller's identity binding before retrying; do not silently select another actor.

hn issue open --actor ACTOR --operation create-example --json "An issue"
hn issue operation --actor ACTOR --operation create-example --json

issue operation requires explicit actor and operation flags and accepts no issue ID or pagination flags. It reads verified history once. A unique result has the usual envelope plus snapshot and data: {issue_id,event_id,actor,operation,intent,kind,timestamp,parents,state?,body?,request}. The state or comment body, parents, request digest and timestamp come from the original signed event, even after later revisions or conflicts. Comment results include body; rich opening/revision results include full state. No original content is summarized or truncated. Existing commands still reject blank comments.

No match is not_found; multiple distinct signed events for one actor/key are operation_conflict. Another actor's equal key is independent. Invalid history or reader budget failures remain errors and never imply absence. Lookup does not require that the queried actor be the active local identity.

For response-loss recovery, validate echoed actor/key and original semantics, then reuse the recorded parents and complete requested state for an identical mutation retry. Reconstructing a request from the latest state can produce operation_conflict. Lookup proves an observed accepted fact, not global consensus, distributed CAS, or permission to execute work. There is no new store.

Conflict inspection and resolution

An unresolved issue has several maximal revision heads. show returns state: null and conflict: true; list summaries have empty title/status. No timestamp or arrival order selects a preferred head.

hn issue heads <full-issue-id> --limit 50 --json
hn issue history <full-issue-id> --kind revisions --limit 50 --json
hn issue resolve <full-issue-id> --expect <head-a> --expect <head-b> \
  --input resolved.json --operation resolve-example --json

Inspect every attributed head, write the desired complete state, and name every observed head for ordinary resolution. Missing or non-current heads fail without an append. The successor retains all named parents and earlier history. revise, close and reopen refuse unresolved conflicts.

If there are more than 200 heads, page through heads and resolve in stages:

hn issue resolve <full-issue-id> --partial --snapshot <observed-snapshot> \
  --expect <selected-head-a> --expect <selected-head-b> \
  --input resolved.json --json

Select 2–200 current heads per partial resolution. The snapshot must still match; even a newly appended comment invalidates this observation. Unselected heads stay visible. Re-read heads/snapshot after each stage. For 201 heads, consuming 200 leaves two: the new successor and the one unconsumed original. Resolve those two explicitly to finish. An identical operation replay still returns its recorded event before checking an old snapshot.

Relationships and graph pages

A relation is {"kind":"blocks","target":"sha256:<64-hex>"}:

  • blocks: the source issue blocks the target.
  • parent: the source is a child of the target parent.
  • related: displayed from either issue, signed on its original source.

Targets must name available issues, and self-links are invalid. Local writes reject blocks/parent cycles they would introduce. Independently valid replica edits can create cycles after sync: their facts remain readable with explicit cycle diagnostics. A conflicted issue contributes links from every head with ambiguous: true and the exact revision/actor provenance.

hn issue graph <issue-id> --limit 50 --json

Graph pages contain type: "edge" rows with an edge object {source,target,kind,revision,actor,ambiguous}, and type: "cycle_member" rows with {kind,component,issue_id}. A component ID groups individual cycle members; cycle_count counts components involving the queried issue. Membership rows are paginated too, so a large cycle cannot evade the output bound.

Machine envelope and navigation

--json emits exactly one hn.issue/1 JSON envelope on stdout. Failure returns a nonzero process exit; stderr may also contain a human diagnostic. Parse stdout separately. --json accepts Go boolean spellings: true values 1/t/T/TRUE/true/True, false values 0/f/F/FALSE/false/False. The last valid actual JSON flag selects the mode; an invalid boolean reports invalid_input while preserving the previous valid choice. Option values, free text and content after -- are not JSON flags.

Success is {schema,ok:true,data,snapshot?,next_cursor?}. Failure is {schema,ok:false,error:{code,message}}. The signed event protocol remains hn/0; it is distinct from this CLI envelope version.

Command data
Mutation issue_id,event_id,replayed,conflict,head_count
Operation lookup issue_id,event_id,actor,operation,intent,kind,timestamp,parents,state?,body?,request
List items,total,conflict,cycle_count; items have id,title,status,creator,conflict,head_count
Show id,creator,state,conflict,head_ids,head_count,revision_count,comment_count
Heads/history/graph items,total,issue_id,conflict,head_count?,cycle_count
Heads item id,actor,state
History item id,actor,kind,timestamp,parents,state?,body?,intent?,operation?

All IDs in machine results are full. Empty collections are arrays. show paginates head IDs; use heads for complete head states and history for comments/revisions. History accepts --kind all|revisions|comments: revisions are topological with event-ID tie breaks; comments follow by timestamp/ID. all emits revisions before comments, not one mixed chronological stream. List and heads use stable full-ID ordering. There are currently no list filters.

issue list --details returns full current states in the same bounded page envelope. Rows are {id,creator,state,opening_metadata,conflict,head_count}; state is complete or explicitly null under conflict. opening_metadata is the complete original signed opening state metadata, or {} for legacy openings without rich state. It stays unchanged after later metadata edits and remains present under current conflicts; Hubnot does not interpret consumer metadata namespaces. Use heads for head IDs and attributed conflicting states. The default summary shape is unchanged. Detail mode is bound into continuation tokens: switching between summary and detail pages returns invalid_input. Full states are never silently truncated; use a smaller page limit when large states increase response size. One reader pass supplies each page, without an additional history read per issue.

hn issue list --limit 50 --json
hn issue list --limit 50 --cursor <next_cursor> --json
hn issue history <issue-id> --kind comments --limit 50 --json

Treat continuation tokens as opaque. Keep command, resolved issue, history kind and page size unchanged. The snapshot binds accepted actor ref names and OIDs. A changed snapshot returns stale_cursor: restart the query. Wrong-query, changed-limit, malformed and out-of-range tokens return invalid_input. total is the complete record count, not the current page length. Stop when next_cursor is absent; never infer completion from an empty selected state.

Error handling

Code Meaning / next action
invalid_input Correct request, ID, field, option or cursor; no local append.
not_found Required issue or actor/key operation is absent from accepted history.
actor_mismatch Actual signer differs from the requested pin; repair the identity binding before retrying.
ambiguous_id Use a longer unique read prefix or full ID.
stale_revision Re-read heads/snapshot and decide on a new request.
issue_conflict Inspect all heads and resolve explicitly.
operation_conflict Inspect the prior operation; use a new key for new intent.
concurrent_update A read snapshot or actor append changed; inspect/retry deliberately.
stale_cursor Restart pagination against the new observed snapshot.
resource_limit Request/reader budget exceeded; reduce scope/content or inspect history.
invalid_history Signature, chain, relationship or admission verification failed; inspect the diagnostic.
repository_error Repository or subprocess failure; preserve the original diagnostic.

Missing replication suppliers stay quarantined. Save explicit supplier actor selections and sync again; do not reinterpret an unaccepted object as issue state. See selected replication and the issue threat model.

The runnable consumer demonstrates create, show/list, guarded revision, identical retry and typed stale rejection using only this public interface. The public acceptance suites are:

go test -count=1 -run '^TestOperationalRichIssue' -v ./...

Compatibility

Optional appended event fields preserve existing signed bytes. New issue revisions require an upgraded executable; old versions may reject the new kind. Reverting a binary cannot erase already signed events. This remains an experimental protocol and does not perform a Beads migration or a Go Kitty adapter installation.

Local gh-style commands and optional issue transfer

hn gh issue create|list|view|edit|comment|close|reopen works on these same native signed issues. It does not invoke gh, contact a forge or read a token. Compatibility mutations print a full native issue ID; list/view can select the documented gh-style JSON fields, separate from the hn.issue/1 envelope above. It refuses unsupported flags/families and conflicted mutations. Use the native commands for explicit expected revisions, operation keys and conflict resolution.

hn gh issue create -t "A local issue" -b "No GitHub account is needed"
hn gh issue list --json id,title,state,conflict
hn gh issue view <native-issue-id> --json id,body,comments
hn transfer import --provider github --project OWNER/REPO --key 42 --dry-run

Actual import/export is an explicit optional adapter operation. Issue imports retain original source IDs, URLs, authors and times under descriptive forge/source/forge/observation metadata and attributed comment text; the native actor signs the imported observations. Changed source and native content are compared against verified signed baselines; competing edits, duplicate claims and unverified remappings refuse. Issue export to a new destination renders native-only criteria/relations/metadata in a descriptive appendix. Exact same-source returns keep ordinary source bodies and suppress verified source-comment echo; native-only additions without a configured lossless source-body mapping refuse rather than disappear.

Real issue export requires a provider write token and the active native identity for its canonical signed private journal; resume uses that same identity. Dry-run and native reads need no private key. Signed recovery never replaces exact positive remote reconciliation, and a negative scan after an uncertain POST never permits another creation. A nogithub build retains the local compatibility interface and all previously imported native issue content. See Optional forge interoperability for exact flags/JSON, authentication, bounds, markers, conflicts and manual recovery limits.