Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
status
| status |
|---|
| living |
Coauthoring Sidecar — the Cross-Rung Contract (v1.0)
| Author(s) | Ben Stull (with Claude) |
| Status | living — this is a standing contract, not a one-shot Solution Design |
| Contract version | v1.0 · schemaVersion: 1 |
| Machine-checkable half | vscode-cowriting-plugin/schemas/coauthoring-sidecar.schema.json (+ scripts/validate-sidecar.mjs) |
| Reference implementation | vscode-cowriting-plugin/src/model.ts (serializer), src/mergeArtifacts.ts (merge), scripts/crossrung-reply.mjs (conforming foreign writer) |
| Source artifacts | Solution Design coauthoring-cross-rung-format.md (F5, Feature benstull/vscode-cowriting-plugin#14) · supersedes the format sections (§6.3) of coauthoring-inner-loop.md, coauthoring-attribution.md, coauthoring-propose-accept.md as the format reference |
This document is canonical (INV-14). Format changes land here first, then in the JSON Schema, then in code — documentation-leads-automation applied to the format. The Schema and every implementation conform to this prose; on disagreement, this document wins and the others are fixed.
Changelog
| Date | Contract | schemaVersion | Change |
|---|---|---|---|
| 2026-06-10 | v1.0 | 1 | Initial publication. Ratifies the shipped F2–F4 format (anchors/threads/attributions/proposals) and adds the additive cross-rung identity fields (Provenance.email, agent.onBehalfOf) plus the compatibility (INV-15/16) and merge (INV-17) rules. |
1. Scope
The coauthoring sidecar is the git-native record of a document's human+machine coauthoring state: anchored discussion threads, authorship attribution, and pending machine proposals. It is the canonical cross-rung carrier — any rung of the ladder (the VS Code editor extension, a Gitea-rung surface, rfc-app) reads and writes the same file per this contract; git push/pull is the transport. There is no server, no sync daemon, and no migration between rungs ("git is truth; the surfaces are clients" — rfc-app#46).
- Location:
.threads/<repo-relative-document-path>.json, in the same repository as the document it describes (e.g.docs/spec.md→.threads/docs/spec.md.json). The document path is the sidecar key; file rename/move tracking is out of scope for v1. - One sidecar per document. Each sidecar is a single JSON object, the Artifact (§3.1).
- Multiple writers are expected. The editor extension is a writer, not the writer. Any process conforming to §5 may append to or modify the record.
2. Serialization rules (INV-2)
A conforming writer emits the artifact deterministically, so diffs stay minimal and any two conforming writers produce byte-identical output for the same logical content:
- Pretty JSON, 2-space indent, UTF-8, exactly one trailing newline, no BOM.
- Root key order:
schemaVersion,document,anchors,threads,attributions,proposals— then unknown root keys (rule 5). anchorskeys sorted lexicographically.- Known keys of every object in the canonical order given by the field
tables in §3 (the order shown is normative). Optional known fields that are
absent are omitted entirely (never
null). - Unknown keys serialize after known keys, sorted lexicographically (INV-15's deterministic placement — see §4).
3. Data model
3.1 Artifact (root)
| Key | Type | Req | Semantics |
|---|---|---|---|
schemaVersion |
integer ≥ 1 | ✓ | The format major. v1.0 of this contract = 1. See §4. |
document |
object | ✓ | { "path": string } — repo-relative path of the described document; the sidecar key. |
anchors |
object | ✓ | Map of anchor id → Anchor (§3.2). The shared anchoring primitive: threads, attributions, and proposals all reference entries here by id. Anchors referenced by no record are pruned by writers that own the rewrite. |
threads |
array | ✓ | Thread[] (§3.4). |
attributions |
array | ✓ | AttributionRecord[] (§3.5). State, not history — the current authorship map, not an edit log. |
proposals |
array | ✓ | Proposal[] (§3.6). Pending-only — accepted/rejected proposals are removed, never archived here. |
All four sections are required even when empty ({} / []).
3.2 Anchor & Fingerprint
An Anchor is { "fingerprint": Fingerprint }.
A Fingerprint locates a text region by content, not position:
| Key | Type | Req | Semantics |
|---|---|---|---|
text |
string | ✓ | The anchored text itself. The text is the truth: a region resolves only where this text is found (with its context). For proposals, text is also the exact replace target. |
before |
string | ✓ | ≤ ~3 lines / 120 chars of leading context. |
after |
string | ✓ | ≤ ~3 lines / 120 chars of trailing context. |
lineHint |
integer ≥ 0 | ✓ | 0-based line of the anchor start at write time. A tie-breaker among multiple text matches, never truth: a reader must not resolve an anchor by line number alone. |
Resolution semantics: resolve by exact text match disambiguated by
before/after, tie-broken by proximity to lineHint; if the text cannot be
found, the record is orphaned — surfaced to the user, never silently moved
or guessed (the editor's INV-1).
3.3 Provenance
The reusable author field, carried by messages, attributions, and proposals.
Identity is git's own model: email is the cross-rung join key (forges
already map email → account); id is the display/short name. There are no
signatures in v1 — this is provenance, not authentication; the git history is
the integrity layer.
| Key | Type | Req | Semantics |
|---|---|---|---|
kind |
"human" | "agent" |
✓ | Who kind of actor authored this. |
id |
string | ✓ | Short/display name (e.g. a username, "claude"). Not forge-resolvable on its own. |
email |
string | — | The actor's email — the cross-rung identity join key. Omitted when unknown (e.g. no git config). |
agent |
object | ✓ when kind:"agent" |
Machine details, below. Absent on humans. |
agent payload:
| Key | Type | Req | Semantics |
|---|---|---|---|
sdk |
string | ✓ | The driving SDK (e.g. "@cline/sdk"). |
model |
string | ✓ | Model name/alias used for the turn. |
sessionId |
string | ✓ | The SDK session/run id (may be "" when unavailable). |
onBehalfOf |
object | — | { "id": string, "email"?: string } — the human operator the machine acted for (rfc-app#46 §6.5's On-behalf-of: made data). Omitted when not established. |
PII note: emails in a repo-committed file are exactly what git commits
already record; adopters should know the sidecar carries the same class of
data as git log.
3.4 Thread & Message
Thread — an anchored discussion:
| Key | Type | Req | Semantics |
|---|---|---|---|
id |
string | ✓ | Unique id (reference writers use t_<uuid>). |
anchorId |
string | ✓ | Key into anchors. |
status |
"open" | "resolved" |
✓ | Discussion state. |
messages |
array | ✓ | Message[], chronological. |
Message:
| Key | Type | Req | Semantics |
|---|---|---|---|
id |
string | ✓ | Unique id (m_<uuid>). |
author |
Provenance | ✓ | §3.3. |
body |
string | ✓ | Markdown message body. |
createdAt |
string | ✓ | ISO-8601 UTC timestamp (YYYY-MM-DDTHH:mm:ss.sssZ). |
3.5 AttributionRecord
The current authorship map: an anchored, author-attributed span. State, not history — records are coalesced/split as the document evolves.
| Key | Type | Req | Semantics |
|---|---|---|---|
id |
string | ✓ | Unique id (at_<uuid>). |
anchorId |
string | ✓ | Key into anchors. |
author |
Provenance | ✓ | Who authored the span's text. |
createdAt |
string | ✓ | ISO-8601 UTC. |
updatedAt |
string | ✓ | ISO-8601 UTC; bumped when the span's extent/fingerprint changes. Drives merge resolution (§6). |
turnId |
string | — | Groups all spans applied by one machine turn. |
3.6 Proposal
A pending machine edit awaiting a human decision. Decisions are human-only; a proposal never mutates the document until accepted.
| Key | Type | Req | Semantics |
|---|---|---|---|
id |
string | ✓ | Unique id (p_<uuid>). |
anchorId |
string | ✓ | Key into anchors; the anchor's fingerprint.text is the exact target text and is immutable for the proposal's life. |
replacement |
string | ✓ | The full proposed text for the anchored range. |
author |
Provenance | ✓ | The proposing machine (with onBehalfOf where known). |
createdAt |
string | ✓ | ISO-8601 UTC. |
turnId |
string | — | Groups N proposals born of one turn. |
instruction |
string | — | What the human asked for (review context). |
4. Compatibility & versioning (INV-15, INV-16)
schemaVersion is the major. The rules:
- Within a major, changes are additive-only: new optional fields or new root sections. Existing documents remain valid unmodified; the major does not bump.
- Readers ignore unknown optional fields they don't implement (but see rule 3).
- Writers preserve unknown fields — INV-15 (round-trip preservation). A conforming writer rewriting a sidecar MUST carry every field it does not recognize, at every object level (root, anchor, fingerprint, thread, message, provenance, attribution, proposal, …), serialized after known keys, sorted (§2 rule 5). No rung's writer may destroy another rung's data.
- A newer major is read-only — fail-safe. A reader encountering
schemaVersiongreater than the major it implements MUST NOT write the sidecar: render what it understands, surface a warning naming the version, and skip every write path. Recoverable by upgrading the older rung. - Breaking changes bump the major and are recorded in this changelog.
The JSON Schema deliberately allows unknown fields everywhere; it validates known-field structure only. Validation of a v-next document against the v1 schema therefore still passes for the v1 subset — by design.
5. Conforming-writer checklist
Any process writing a sidecar (a forge surface, a CLI, an editor) MUST:
- Validate before writing (schema + the §3 semantics it touches); refuse to operate on an invalid record rather than "fixing" it silently.
- Refuse newer majors (§4 rule 4).
- Preserve unknown fields (§4 rule 3).
- Mint fresh uuid-based ids for new records (
m_/t_/at_/p_+ UUID); never reuse or guess ids. - Stamp ISO-8601 UTC timestamps (
new Date().toISOString()shape). - Carry identity: set
author.emailwhen the writer knows it; agents setonBehalfOfwhen the operator is established. - Serialize per §2 (byte-determinism) and validate again after the rewrite.
Executable example: vscode-cowriting-plugin/scripts/crossrung-reply.mjs — a
self-contained foreign writer that appends a thread reply per this checklist.
6. Merge rules (INV-17)
Two writers on two clones can diverge; git will surface the sidecar as a
textual conflict. The semantic resolution is a deterministic 2-way merge,
merge(ours, theirs):
- Refuse differing majors (error; a rung never merges what it can't fully read).
anchors: union by key. Same key, divergent value → deterministic tie-break (below), key reported.threads/attributions/proposals: union byid. Records present on one side only pass through; identical records dedupe. Same-id divergence resolves:- Thread: the shell (all fields except
messages) and the messages merge independently. Divergent shells → deterministic tie-break, thread id reported. (A consequence: divergentstatusresolves to"resolved"over"open", since the tie-break is lexicographic.)messagesunion byid, ordered by (createdAt,id); divergent same-id messages → tie-break, message id reported. The common forge case — one side appends a reply while the other edits elsewhere — merges cleanly by construction (fresh uuids, disjoint sections). - Attribution: newer
updatedAtwins; equalupdatedAt→ tie-break. Id reported either way. - Proposal / Message / Anchor (no
updatedAt): tie-break. Id reported.
- Thread: the shell (all fields except
- Unknown root keys: union; same key divergent → tie-break, key reported.
document: must agree; divergence → tie-break +"document"reported (should not happen — sidecars are per-path).
The deterministic tie-break: the lexicographically larger
stable-stringify wins, where stable-stringify is JSON with all object keys
sorted and no whitespace. Symmetric: merge(a,b) and merge(b,a) pick the
same winner.
Merge honesty (INV-17): every rule-resolved divergence is surfaced to the caller (the list of resolved ids/keys) — never silently guessed. Humans remain the final arbiter: the file is plain pretty JSON and stays human-resolvable; these rules give tools and humans the same answer.
Reference implementation:
vscode-cowriting-plugin/src/mergeArtifacts.ts (mergeArtifacts(ours, theirs): { merged, conflicts }). Wiring it as a .gitattributes merge driver
is a deployment convenience, not part of this contract.
7. Out of scope (v1)
File rename/move tracking · signed provenance / authentication ·
ratifiedBy / multi-party provenance · rejected-proposal history trails ·
any transport beyond git itself.