lit
ProtocolAbout this document. Normative reference for the
litprogrammatic API: the operation catalog, message envelope, session lifecycle, and error model that editor integrations and other tools bind against. This is the API contract; the format it operates on (the.litrepository, objects, segmentation) isSPEC.md. Rationale and rejected alternatives live inHISTORY.md, the internal decision log, §16; when this document andHISTORY.mddisagree on a normative point, this document wins.Status: implementation in progress (first slice landed 2026-07-08).
lit serve— the JSON-RPC/stdio transport (§3.2) — carries the session lifecycle (§4) and the methodslog,show,status,diff,add, andcommit; the remaining surfaces (histories and merge beyondlog/show, the review methods,blame, checkout/tabula, maintenance, exchange, and remote) and the napi in-process embed are the ongoing build, both over the one operation layer (§11). Sections marked (open) are deliberately unresolved. The method names are the git peer register ofTERMINOLOGY.md§1.1 (HISTORY §15).
2026-07-07T21:09:30.075817Z), as stored in histories.? marks an optional field. A field
absent from a request MUST be treated as its documented default, never
as an error unless the method says the field is required.The protocol's English is its stable technical vocabulary — deliberately plain, drawn from what each practice already calls things: git verbs for version-control operations (§7; HISTORY §15), and standard copyediting / proofreading terms for the editorial surface that git has no words for. The Latin/Greek register (SPEC, TERMINOLOGY) is a parallel philological layer, recorded but not yet settled; nothing in this protocol depends on it. These English terms are fixed:
| Protocol term | Meaning | Register (deferred) |
|---|---|---|
comparison (diff) |
the differences between two versions | collatio / antibolē |
| change | one difference | — |
| move | an atom or paragraph relocated | transpositio |
| insertion | an atom present on one side only | additio |
| deletion | an atom removed | omissio |
| substitution | an atom whose content changed | substitutio |
| split | a paragraph divided | divisio |
| join | paragraphs merged | coniunctio |
| variant | two sources' competing readings of one slot | varia lectio / diaphōnia |
| reading | one source's text at a variant | lectio |
| source | the hand behind a reading or change | siglum |
| review | working through the variants | recensio |
| keep / accept / both / revise | resolve a variant (revise = write a new reading) | stet / adopt / — / emendatio |
Kind (the change kinds) is exactly
move | insertion | deletion | substitution | split | join
(plus unchanged). The editorial method names follow:
variants, resolveChange.
This protocol is the stable surface between the Literium engine and the programs that drive it — editor extensions foremost, but equally any script, service, or daemon that needs repository operations without parsing human CLI output.
In scope: every repository and remote operation the
lit CLI performs, expressed as typed request/response
methods; the session handshake that fixes the workspace and negotiates
version and capabilities; progress and cancellation for long operations;
a stable error model.
Out of scope: (a) the repository format —
object types, segmentation, hashing — which is SPEC.md and
which this protocol never re-specifies, only exposes; (b) the in-editor
text-intelligence surface — atom-boundary highlighting,
diff/blame decorations — which is an LSP-flavored layer powered
client-side by a WASM build of literium-core, specified
separately (HISTORY §16); (c) the Litorium platform HTTP APIs,
which the remote methods here consume but do not expose.
A transport carries JSON-RPC 2.0 messages. Three are defined; in-process and stdio are normative for v1, the socket form is (open).
The engine is embedded as a native Node addon (napi-rs). Each method
is an async function whose single argument is the params object and
whose resolved value is the result object; a JSON-RPC error becomes a
thrown error carrying code, message, and
data. Notifications (progress, §8) are delivered through a
callback registered at initialize. This is the path the
VSCode extension uses: no framing, no serialization boundary beyond the
napi marshaling, lowest latency.
lit serve) — normativelit serve reads requests from stdin and writes responses
to stdout, each message framed with an LSP-style header block:
Content-Length: <N>\r\n
\r\n
<N bytes of UTF-8 JSON>
stderr carries only diagnostics and is never part of the
protocol. This is the path for editors that cannot load a native addon,
for remote/sandboxed engines, and for language-agnostic tooling.
lit serve speaks exactly the §7 catalog.
A framed TCP or WebSocket transport for out-of-process or networked engines. Framing and authentication are unspecified in v1.
A session is stateful: it fixes the workspace root and holds the
negotiated capabilities. The lifecycle is initialize →
(methods…) → shutdown → exit, mirroring LSP's
shape (the one borrowing this protocol makes).
initialize
(request)MUST be the first message. Calling any other method first fails with
NotInitialized.
Params
{
"clientInfo": { "name": "lit-vscode", "version": "0.1.0" },
"workspaceRoot": "/home/writer/novel",
"capabilities": { "progress": true, "cancellation": true },
"protocolVersion": "0.1"
}workspaceRoot is the directory the session operates in;
the engine walks upward to the nearest enclosing .lit/
exactly as the CLI does (SPEC §1.3), so workspaceRoot MAY
be any path inside a repository, or an as-yet-repository-less directory
(for a first add). protocolVersion is the
highest the client supports.
Result
{
"serverInfo": { "name": "literium", "version": "1.0.0" },
"protocolVersion": "0.1",
"engine": {
"formatVersion": "1",
"segmentationVersion": "uax29-16.0.0"
},
"repository": { "found": true, "root": "/home/writer/novel" },
"capabilities": {
"methods": ["add", "commit", "status", "…"],
"remote": true,
"progress": true,
"cancellation": true
},
"account": { "signedIn": true, "email": "…", "handle": null }
}The server returns the protocolVersion it will actually
speak (≤ the client's; §10). repository.found is
false for a directory with no enclosing .lit/
— legal, since add creates one.
capabilities.remote gates the §7.6 methods.
account reflects
~/.config/lit/credentials.
initialized
(notification)The client sends this once after processing the
initialize result. The server MAY defer expensive setup
until it arrives.
shutdown (request) / exit (notification)shutdown asks the server to stop accepting new work and
returns null; exit ends the session. Over
stdio, exit terminates the process; in-process, it releases
the engine handle.
Requests, responses, and notifications are standard JSON-RPC 2.0.
Errors use the standard object
{ "code": <int>, "message": <string>, "data"?: <any> }.
Standard codes (JSON-RPC): -32700 parse
error, -32600 invalid request, -32601 method
not found, -32602 invalid params, -32603
internal error.
Literium codes occupy -33000 … -33099.
data carries a machine-readable detail object where noted.
The catalog:
| Code | Name | Meaning |
|---|---|---|
| -33000 | NotInitialized |
a method arrived before initialize |
| -33001 | NotARepository |
no enclosing .lit/ and the method needs one |
| -33002 | NoDocuments |
add found nothing to snapshot |
| -33003 | NothingPending |
add/commit with no pending change |
| -33004 | DirtyTree |
an operation would overwrite unrecorded working-tree changes;
data.paths lists them (use force) |
| -33005 | UnknownVersion |
a version hash or prefix resolves to nothing;
data.given |
| -33006 | AmbiguousVersion |
a hash prefix matches more than one; data.matches |
| -33007 | UnknownHistory |
no history by that name |
| -33008 | InvalidHistoryName |
an experiment name breaks the rules (SPEC §9.2);
data.reason |
| -33009 | (reserved) | was Conflict; a merge's variants are a structured
result (merge.apparatus), never an error — HISTORY §17 |
| -33010 | NoCoordinate |
a blame/stemma path query lacks a
coordinate |
| -33011 | SegmentationMismatch |
the repository's segmentation version differs from the engine's;
data.repo, data.engine |
| -33012 | IntegrityError |
fsck found a broken object;
data.objects |
| -33020 | NoRemote |
a pull/publish/share on a repo with no
.lit/litorium |
| -33021 | NotSignedIn |
a remote method needs an account; run
account.login |
| -33022 | RemoteRejected |
the platform refused; data.status,
data.message |
| -33023 | Diverged |
pull is not a fast-forward; push first |
| -33024 | NoHandle |
opus needs an account handle |
| -33030 | Cancelled |
the request was cancelled (§8) |
| -33031 | Unsupported |
the method is not in this server's capabilities |
An error MUST be one of these or a standard code; servers MUST NOT invent ad-hoc codes.
VersionHash = string // 64 lowercase hex
VersionRef = string // a 64-hex hash, a 6–63-hex unique prefix,
// or a history name (resolved: SPEC §8, latest)
HistoryName = string // "historia" or an experiment name
DocPath = string // repository-relative, forward slashes
Mode = "prose" | "poetry"
Coordinate = string // "gNaM" (grex N, atom M) or "aK" (flat atom K)
EditCategory = "formatting" | "structural" | "substantial" | "re-segmentation"
Timestamp = string // RFC 3339 UTC
Inscriptio = { version: VersionHash, history: HistoryName,
timestamp: Timestamp, message: string, ordinal: number }
PendingRevision = { grex: number, category: EditCategory,
summary: string } // one prepared revision on the tabula
RepoStatus = { repository: DocPath, historiaTip: VersionHash | null,
pending: PendingRevision[], manifest: ManifestChange[] }
ManifestChange = { kind: "add" | "delete" | "rename",
path: DocPath, from?: DocPath }
DocumentTrio = { path: DocPath, volumen: VersionHash,
spina: VersionHash, atomSpina: VersionHash }
Version = { hash: VersionHash, segmentation: string,
documents: DocumentTrio[], manifest: Record<DocPath, VersionHash> }
AtomLine = { coordinate: Coordinate, hash: VersionHash, text: string }
Kind = "unchanged" | "move" | "insertion" | "deletion"
| "substitution" | "split" | "join" // change kinds; §0.1, SPEC §7.4
Change = { kind: Kind, left?: Coordinate, right?: Coordinate,
hash?: VersionHash, text: string,
words?: Change[] } // a substitution may carry a
// word-level comparison (SPEC §7.4)
Reading = { source: string, text: string, hash?: VersionHash } // a source's
// reading (source = the hand)
Variant = { location: Coordinate, kind: Kind,
current: Reading, readings: Reading[] } // a substitution two
// sources contest
StemmaRecord = { version: VersionHash,
firstInscribed?: Inscriptio, lastInscribed?: Inscriptio,
path?: DocPath, coordinate?: Coordinate }
Coordinate grammar and the diff line taxonomy
(move/split/join detected exactly
by atom hash) are SPEC §7.4; edit categories are SPEC §7.2.
Each method: Params → Result, then
notable errors. Absent optional params take their default. Every method
MAY additionally raise NotInitialized,
SegmentationMismatch, or -32603 internal.
add — conscribeSnapshot the working tree onto the tabula. Creates the repository on first use.
{ mode?: Mode, force?: boolean } — mode is
required only on first add (repository creation); ignored
afterward.{ created: boolean, status: RepoStatus } —
created true when this call made the
.lit/.NoDocuments,
NothingPending, DirtyTree.commit — inscribeFinalize the tabula (or a named source version) onto a history.
{ history?: HistoryName, message?: string, revisions?: "all" | number[], fons?: VersionRef }
— history defaults to "historia";
revisions defaults to per-revision (each pending revision
inscribed separately) unless "all"; message
applies to the inscribed revision(s); fons inscribes an
existing version (adoption / re-inscription) instead of the tabula.
message is REQUIRED here (the protocol never opens an
editor; §9).{ inscriptiones: Inscriptio[] }.NothingPending,
UnknownHistory, UnknownVersion (fons).status{ history?: HistoryName }.RepoStatus.checkout /
restore — exscribeMaterialize a version into the working tree (whole version, or one document). Not a pointer move.
{ version: VersionRef, path?: DocPath, force?: boolean }.{ written: DocPath[], deleted: DocPath[] } —
deleted are documents absent from the version (document-set
fidelity, SPEC §8.9).UnknownVersion,
DirtyTree.tabulaInspect or discard the tabula.
{ discard?: boolean }.{ pending: PendingRevision[], discarded: boolean }.log — show
(history)The inscriptiones of a history, in order.
{ history?: HistoryName, path?: DocPath } —
history defaults to "historia". With
path, only the inscriptiones where that document's content
changed are returned — a document's own history; ordinals stay the
entries' positions in the full history.{ history: HistoryName, inscriptiones: Inscriptio[], path?: DocPath }.UnknownHistory.show — show
(version)A version's structure, or one document's atoms.
{ version: VersionRef, path?: DocPath, atoms?: boolean }.Version, or when
path + atoms:
{ path: DocPath, atoms: AtomLine[] }.UnknownVersion.branch —
experimentManage experiment histories.
{ action: "create" | "delete" | "list", name?: string }.list:
{ experiments: HistoryName[] }; for
create/delete:
{ name: HistoryName }.InvalidHistoryName,
UnknownHistory.merge — contexeCombining merge: weave a source into the working tree, then the
caller adds and commits.
{ source: VersionRef, target?: HistoryName, base?: VersionRef, force?: boolean }.{ variants: Variant[], status: RepoStatus } — the review
descent (SPEC §7.4, HISTORY §17) auto-resolves every mechanical kind
(moves, insertions/deletions, splits/joins); variants holds
only the substitutions two sources contested (and any move that
collided). A merge is never an error over its variants
— the working tree is a whole readable document with the changes listed
beside it; variants: [] means a clean merge. (This settles
the earlier open question: variants are a structured result, not a
Conflict error.)UnknownVersion,
DirtyTree.variants
(git peer of the CLI's conflicts)List the open variants (the changes still to review), or clear them.
{ clear?: boolean }.{ variants: Variant[], cleared: boolean }.resolveChangeResolve one variant. The four moves: keep the current reading, accept the other, both, or revise (supply a new reading).
{ location: Coordinate, action: "keep" | "accept" | "both" | "revise", text?: string }
— text required for revise.{ remaining: number } — open
variants left; at 0 the merge is an ordinary pending
version to commit.UnknownVersion (no such
location).diffCompare two versions, or the tabula / working tree against the latest.
{ left?: VersionRef, right?: VersionRef, path?: DocPath } —
zero refs: working tree vs historia tip; one ref: that vs the tree.{ documents: { path: DocPath, category: EditCategory, changes: Change[] }[] }
— EditCategory is the document-level summary (SPEC §7.2);
each Change is one entry, tagged with its kind
(§0.1, SPEC §7.4). Rendering (apparatus / leiden / marks / unified) is
the client's; the protocol carries the structured collation.UnknownVersion.blame — stemmaA version's life-path, or an atom's first/last inscription. (The peer
keeps stemma's argument shape, not git blame's —
HISTORY §15.)
{ version?: VersionRef, path?: DocPath, coordinate?: Coordinate }.StemmaRecord.NoCoordinate (path without
coordinate), UnknownVersion.gc{ dryRun?: boolean }.{ collected: { type: string, count: number }[], dryRun: boolean }.fsck{}.{ sound: boolean, problems: { object: VersionHash, type: string, detail: string }[] }.IntegrityError (only if the
walk cannot complete; a sound=false result is the normal
reporting channel).exemplarPack a version's documents (+ .exemplar metadata) for
correspondence, or repack an unpacked exemplar.
{ source?: VersionRef, out?: string } — no
source in an unpacked-exemplar directory repacks the return
trip.{ archive: string, basis: VersionHash, documents: DocPath[] }.collate — conferoIngest a returned exemplar into the working tree.
{ archive: string, force?: boolean }.{ basis: VersionHash, written: DocPath[], deleted: DocPath[], segmentationMatch: boolean }.DirtyTree.All require capabilities.remote; otherwise
Unsupported. All but
clone/account.login require a bound remote
(NoRemote) and, unless anonymous, a signed-in account
(NotSignedIn). Long ones report progress (§8).
clone{ project: string, directory?: string, remote?: string }.{ root: DocPath, versions: number, objects: number }.RemoteRejected,
Unauthorized.push{ remote?: string } —
remote only honored on the first push (project
creation).{ created: boolean, project: string, objects: number, versions: number, upToDate: boolean }.Diverged,
RemoteRejected.pull{}.{ versions: number, objects: number, upToDate: boolean }.Diverged,
NoRemote.publishContribute the historia's (or one version's) atoms to the public corpus. Permanent.
{ version?: VersionRef, confirm: boolean } —
confirm MUST be true (the protocol's stand-in
for the CLI consent prompt; §9); a false/absent confirm
fails invalid params.{ atomsUploaded: number, atomsPresent: number }.share{ version: VersionRef, expiresDays?: number } — bare
historia export is share with no version at the CLI; here
that is the separate exportHistoria result (open: fold
or split).{ url: string, token: string, expiresAt: Timestamp }.shares{}.{ shares: { token: string, url: string, expiresAt: Timestamp }[] }.opus{ slug: string, version: VersionRef }.{ url: string }.NoHandle,
RemoteRejected (slug taken).opera{}.{ opera: { handle: string, slug: string, url: string }[] }.retract{ link?: string, version?: VersionRef } — exactly one.
link revokes a share; version drops the
inscriptio (locally and, if synced, the remote historia by CAS).{ revoked?: boolean, retracted?: VersionHash }.account.login
/ account.handle / account.status /
account.logoutThe sign-in surface. account.login is progress-driven
(§9): it emits the user-code and the verification link as progress, and
returns when the link is clicked or the poll times out.
account.login Params
{ email?: string } → Result
{ email: string, handle: string | null }.account.handle Params
{ name?: string } → Result
{ handle: string | null } (with name, claims
it).account.status Params {}
→ Result
{ signedIn: boolean, email?: string, handle?: string | null, issuer?: string }.account.logout Params {}
→ Result { signedOut: boolean }.RemoteRejected (rate limit,
reserved handle), NoHandle.Methods that can take noticeable time — clone,
push, pull, publish,
account.login, merge on a large tree — MAY
emit progress and MUST be cancellable when the client advertised
progress / cancellation at
initialize.
Progress is the notification
$/progress:
{ "token": "<request-id>", "value": { "kind": "report", "message": "uploading pack 3/7", "percentage": 42 } }with kind ∈ "begin" | "report" | "end";
token is the id of the in-flight request. In-process, the
same values arrive on the initialize-registered
callback.
Cancellation is the notification
$/cancelRequest { "id": <request-id> }.
On receipt the server SHOULD stop and answer the original request with
the Cancelled error. Cancellation is best-effort: a
push already committed on the remote completes; the
response then reflects the true state.
The CLI is interactive; the protocol is not. The lowering is mechanical and normative:
add mode → add.mode. commit
messages → commit.message (REQUIRED; the protocol never
opens an editor, so per-revision editor messaging collapses to a
supplied string or, for multi-revision intent, one commit
call per revision).publish.confirm: true. Overwrite guards →
force: true, surfaced first as the DirtyTree
error carrying data.paths so a client can show the user
what would be lost before retrying.account.login emits the code and link as
$/progress begin, polls internally, and
resolves when verified or rejects with RemoteRejected on
timeout/rate-limit.protocolVersion is MAJOR.MINOR. A server
MUST speak any MINOR ≤ its own within the same
MAJOR and MUST return in initialize the
version it will actually use (the min of client and server). New methods
and new optional fields are MINOR bumps; removing a method,
renaming a field, or changing a result shape is a MAJOR
bump. A client MUST tolerate unknown fields in results (forward-compat)
and MUST consult capabilities.methods rather than assume a
method exists — a server built without the litorium feature omits §7.6
and sets capabilities.remote: false, and calling an absent
method yields Unsupported, never a silent no-op.
This document is protocol 0.1: unstable, pre-implementation. The first shipped binding freezes 1.0.
The CLI and this protocol are two front-ends over one
operation layer in literium-rs — a set of
functions that take typed inputs and return typed results (the §6–§7
shapes). The CLI adds argument parsing, prompts, and text rendering on
top; a transport binding (§3) adds marshaling on top. Neither owns the
operations. Consequences the implementation MUST preserve: a method's
result is the same data the CLI renders (so the surfaces cannot drift);
the git method names are the CLI's git peers (HISTORY §15), so the
vocabulary is shared end to end; and the engine's determinism (SPEC §5)
means a method is a pure function of repository state and params, with
the sole side effects being the documented writes to .lit/,
the working tree, and the remote.
The conformance oracle (literium-perl) scopes to the
format and the extraction/derivation objects (HISTORY §14), not to this
protocol; protocol conformance is checked against this document by the
binding's own tests.