Wit Agent Coordination Protocol v1

March 26, 2026 · View on GitHub

Wit is a coordination daemon for multiple AI agents working on the same codebase concurrently. It prevents merge conflicts by coordinating intent before code is written. This document specifies every RPC method available on the daemon, the message envelope format, transport layer, shared types, error codes, and lifecycle state machines. A developer can implement a wit-compatible client from this document alone — no source reading required.


Table of Contents

  1. Transport
  2. Message Envelope
  3. Connection Lifecycle
  4. Symbol Path Format
  5. Methods
  6. Shared Types
  7. Error Codes
  8. Intent Lifecycle
  9. Contract Lifecycle

Transport

Primary: Unix domain socket at .wit/daemon.sock (relative to the repo root).

Framing: HTTP POST to /rpc. Set Content-Type: application/json.

The daemon starts automatically on first CLI use and runs in the background. Clients connect to the socket and send JSON-RPC requests over HTTP POST. The daemon responds synchronously.

Example connection (Unix socket via curl):

curl --unix-socket .wit/daemon.sock \
  -X POST http://localhost/rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","witVersion":"1","id":"...","method":"ping","params":{}}'

Message Envelope

All messages follow JSON-RPC 2.0 with an additional witVersion field. The id is always a UUID string (v4).

Request:

{
  "jsonrpc": "2.0",
  "witVersion": "1",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "method": "lock.acquire",
  "params": {
    "symbolPath": "src/auth.ts:validateToken",
    "sessionId": "agent-abc-123"
  }
}

Success response:

{
  "jsonrpc": "2.0",
  "witVersion": "1",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "result": {
    "symbolPath": "src/auth.ts:validateToken",
    "sessionId": "agent-abc-123",
    "acquiredAt": "2024-01-15T10:30:00.000Z",
    "expiresAt": "2024-01-15T11:00:00.000Z",
    "warnings": []
  }
}

Error response:

{
  "jsonrpc": "2.0",
  "witVersion": "1",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "error": {
    "code": -32000,
    "message": "LOCK_CONFLICT",
    "data": {
      "heldBy": "agent-xyz-456",
      "expiresAt": "2024-01-15T11:00:00.000Z"
    }
  }
}

Fields:

FieldTypeDescription
jsonrpc"2.0"Always the literal string "2.0"
witVersion"1"Protocol version. Always "1" in the current release
idstring (UUID)Correlates request to response. Use a UUID v4
methodstringRPC method name (request only)
paramsobjectMethod parameters (request only)
resultanySuccess result (success response only)
errorobjectError details (error response only)

Connection Lifecycle

A typical agent session follows this flow:

  1. Start daemon (automatic) — the wit CLI auto-starts the daemon on first use. Clients can also start it explicitly with wit daemon start.
  2. Connect — open a connection to .wit/daemon.sock.
  3. Register — call register with a unique agent name and session ID. The session ID must be stable across reconnects (e.g., derived from a UUID generated at agent startup). Registration is informational — it is not required for other methods, but omitting it means the daemon has no name for your session in query results.
  4. Coordinate — use lock, intent, and contract methods to coordinate work with other agents.
  5. Release — when done with a symbol, call lock.release. Locks also auto-expire per their TTL.

Sessions are identified by sessionId (a string you choose). There is no authentication — any client connected to the socket can use any session ID.


Symbol Path Format

Many methods take a symbolPath parameter. The format is:

<relative-file-path>:<symbol-name>

Examples:

  • src/auth.ts:validateToken
  • src/api/users.ts:createUser
  • lib/crypto.py:hash_password

The file path is relative to the repo root (the directory containing .wit/). The symbol name is the function, method, or variable name as it appears in source. The colon separator is required — methods validate that symbolPath contains a colon.

Supported file extensions: .ts, .tsx, .js, .jsx (TypeScript/JavaScript), .py (Python).


Methods

ping

Health check. Verifies the daemon is reachable.

Params: none (pass {} or omit)

Result: "pong" (string literal)

Errors: none

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "ping", "params": {}
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "result": "pong"
}

register

Register an agent with the daemon, associating a human-readable name with a session ID.

Params:

NameTypeRequiredDescription
namestringyesHuman-readable agent name (e.g., "claude-agent-1")
sessionIdstringyesStable unique identifier for this agent session

Result:

FieldTypeDescription
agentIdnumberAuto-incremented row ID assigned to this registration

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing or invalid params

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "register",
  "params": { "name": "claude-agent-1", "sessionId": "sess-abc-123" }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "result": { "agentId": 1 }
}

lock.acquire

Acquire an exclusive lock on a symbol. If the symbol is already locked by a different session and the lock has not expired, returns a LOCK_CONFLICT error. If the lock is held by the same session, the TTL is refreshed. If the lock has expired, the new session takes it over.

Acquiring a lock also parses the locked file and builds a caller-dependency graph. The response includes warnings — a list of other symbols that call the locked symbol and are currently locked by a different session. Warnings are informational and never block the acquire.

Params:

NameTypeRequiredDescription
symbolPathstringyesSymbol to lock. Must contain : (e.g., src/auth.ts:validateToken)
sessionIdstringyesCaller's session ID
ttlMsintegernoLock TTL in milliseconds. Default: 1800000 (30 minutes)

Result:

FieldTypeDescription
symbolPathstringThe locked symbol path
sessionIdstringThe session holding the lock
acquiredAtstringISO 8601 timestamp when lock was acquired
expiresAtstringISO 8601 timestamp when lock expires
warningsCallerWarning[]List of dependency warnings (may be empty)

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing/invalid params, symbolPath lacks :
-32000LOCK_CONFLICTSymbol locked by a different active session. data.heldBy and data.expiresAt provided

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "lock.acquire",
  "params": {
    "symbolPath": "src/auth.ts:validateToken",
    "sessionId": "sess-abc-123",
    "ttlMs": 3600000
  }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": {
    "symbolPath": "src/auth.ts:validateToken",
    "sessionId": "sess-abc-123",
    "acquiredAt": "2024-01-15T10:30:00.000Z",
    "expiresAt": "2024-01-15T11:30:00.000Z",
    "warnings": [
      {
        "lockedSymbol": "src/api/routes.ts:handleLogin",
        "heldBy": "sess-xyz-456",
        "chain": ["src/api/routes.ts:handleLogin", "src/auth.ts:validateToken"]
      }
    ]
  }
}

lock.release

Release a lock held by the caller's session.

Params:

NameTypeRequiredDescription
symbolPathstringyesSymbol path to release
sessionIdstringyesMust match the session that holds the lock

Result:

FieldTypeDescription
releasedbooleanAlways true on success

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing/invalid params
-32000LOCK_NOT_FOUNDNo lock exists for the given symbolPath
-32000LOCK_NOT_HELDLock exists but is held by a different session. data.heldBy provided

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "lock.release",
  "params": { "symbolPath": "src/auth.ts:validateToken", "sessionId": "sess-abc-123" }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "result": { "released": true }
}

lock.query

Query active (non-expired) locks. Optionally filter by session.

Params:

NameTypeRequiredDescription
sessionIdstringnoIf provided, return only locks held by this session

Result: Array of lock objects:

FieldTypeDescription
symbolPathstringThe locked symbol path
sessionIdstringSession holding the lock
acquiredAtstringISO 8601 timestamp
expiresAtstringISO 8601 timestamp
ttlRemainingMsnumberMilliseconds until expiry

Errors:

CodeMessageWhen
-32600INVALID_REQUESTInvalid params

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "lock.query",
  "params": {}
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": [
    {
      "symbolPath": "src/auth.ts:validateToken",
      "sessionId": "sess-abc-123",
      "acquiredAt": "2024-01-15T10:30:00.000Z",
      "expiresAt": "2024-01-15T11:00:00.000Z",
      "ttlRemainingMs": 1542000
    }
  ]
}

intent.declare

Declare that an agent intends to modify a set of files (and optionally specific symbols). The daemon stores the intent, runs conflict detection, and returns a ConflictReport. The declare always succeeds — conflicts are returned as informational warnings, not errors.

Conflict detection runs three checks:

  • INTENT_OVERLAP: another active intent touches the same file(s) and overlapping byte range
  • LOCK_INTERSECTION: a declared symbol path is locked by another session
  • DEP_CHAIN: a callee of a declared symbol is locked by another session

Params:

NameTypeRequiredDescription
sessionIdstringyesCaller's session ID
descriptionstringyesHuman-readable description of the intent
filesstring[]yesArray of file paths (at least one). Relative to repo root
symbolsstring[]noSymbol names (not paths) to associate with the intent

Result:

FieldTypeDescription
intentIdstringUUID of the created intent
conflictsConflictReportConflict analysis result

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing/invalid params, empty files array

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "intent.declare",
  "params": {
    "sessionId": "sess-abc-123",
    "description": "Refactor token validation to use RS256",
    "files": ["src/auth.ts"],
    "symbols": ["validateToken", "createToken"]
  }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": {
    "intentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "conflicts": {
      "hasConflicts": true,
      "items": [
        {
          "type": "INTENT_OVERLAP",
          "overlappingIntentId": "a1b2c3d4-...",
          "overlappingSessionId": "sess-xyz-456",
          "description": "Update auth middleware"
        }
      ]
    }
  }
}

intent.update

Update the status of an existing intent. Only the session that created the intent can update it. Status transitions are forward-only — see Intent Lifecycle.

Params:

NameTypeRequiredDescription
intentIdstringyesUUID of the intent to update
sessionIdstringyesMust match the session that declared the intent
status"active" | "resolved" | "abandoned"yesTarget status

Result:

FieldTypeDescription
intentIdstringThe updated intent's UUID
statusstringThe new status
updatedAtnumberUnix timestamp in milliseconds

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing/invalid params
-32000INTENT_NOT_FOUNDNo intent with the given ID
-32000INTENT_NOT_OWNEDIntent belongs to a different session
-32000INVALID_TRANSITIONTarget status is not a valid transition from current status

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "intent.update",
  "params": {
    "intentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "sessionId": "sess-abc-123",
    "status": "resolved"
  }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": {
    "intentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "status": "resolved",
    "updatedAt": 1705317000000
  }
}

intent.query

Query intents. Without filters, returns all declared and active intents. With an explicit status filter, returns intents with that specific status (including resolved and abandoned).

Params:

NameTypeRequiredDescription
sessionIdstringnoFilter by session
filestringnoFilter by file path (exact segment match)
statusstringnoFilter by status. When absent, defaults to declared + active

Result: Array of intent objects:

FieldTypeDescription
intentIdstringUUID
sessionIdstringOwning session
descriptionstringHuman-readable description
filesstringComma-delimited file list with leading/trailing commas
symbolsstringJSON array string of symbol names
startBytenumber | nullStart byte offset of symbol range in source
endBytenumber | nullEnd byte offset of symbol range in source
statusstringCurrent status
declaredAtnumberUnix timestamp in milliseconds
updatedAtnumberUnix timestamp in milliseconds

Errors:

CodeMessageWhen
-32600INVALID_REQUESTInvalid params

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "intent.query",
  "params": { "file": "src/auth.ts" }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": [
    {
      "intentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "sessionId": "sess-abc-123",
      "description": "Refactor token validation to use RS256",
      "files": ",src/auth.ts,",
      "symbols": "[\"validateToken\",\"createToken\"]",
      "startByte": 120,
      "endByte": 450,
      "status": "active",
      "declaredAt": 1705316000000,
      "updatedAt": 1705316500000
    }
  ]
}

contract.propose

Propose a function signature contract for a symbol. The daemon reads the symbol's current signature from disk (using tree-sitter), stores it as the contract's expected signature, and returns it. If the file does not exist or the symbol cannot be found, returns SYMBOL_NOT_FOUND.

A contract represents an agreement that a symbol's public interface will not change without coordination. After proposing, another agent must call contract.respond to accept or reject it.

Params:

NameTypeRequiredDescription
sessionIdstringyesProposing agent's session ID
symbolPathstringyesSymbol whose signature to capture. Must contain :

Result:

FieldTypeDescription
contractIdstringUUID of the created contract
symbolPathstringThe symbol path
signaturestringThe captured function signature (params + optional return type)

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing/invalid params, symbolPath lacks :
-32000SYMBOL_NOT_FOUNDFile not found, unsupported language, or symbol not in file

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "contract.propose",
  "params": {
    "sessionId": "sess-abc-123",
    "symbolPath": "src/auth.ts:validateToken"
  }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": {
    "contractId": "c1d2e3f4-1234-5678-abcd-000000000001",
    "symbolPath": "src/auth.ts:validateToken",
    "signature": "(token: string): Promise<User | null>"
  }
}

contract.respond

Accept or reject a proposed contract. A session cannot respond to its own contract (self-accept/self-reject). The contract must be in proposed status.

Params:

NameTypeRequiredDescription
contractIdstringyesUUID of the contract to respond to
sessionIdstringyesResponding agent's session ID (must differ from proposer)
acceptbooleanyestrue to accept, false to reject

Result:

FieldTypeDescription
contractIdstringUUID of the contract
status"accepted" | "rejected"New status

Errors:

CodeMessageWhen
-32600INVALID_REQUESTMissing/invalid params
-32000CONTRACT_NOT_FOUNDNo contract with the given ID
-32000CONTRACT_ALREADY_RESOLVEDContract is already accepted or rejected
-32000SELF_ACCEPT_NOT_ALLOWEDResponding session is the same as proposing session

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "contract.respond",
  "params": {
    "contractId": "c1d2e3f4-1234-5678-abcd-000000000001",
    "sessionId": "sess-xyz-456",
    "accept": true
  }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": {
    "contractId": "c1d2e3f4-1234-5678-abcd-000000000001",
    "status": "accepted"
  }
}

contract.query

Query contracts, optionally filtered by symbol path and/or status.

Params:

NameTypeRequiredDescription
symbolPathstringnoFilter by exact symbol path
statusstringnoFilter by status ("proposed", "accepted", "rejected")

Result: Array of contract objects:

FieldTypeDescription
contractIdstringUUID
proposerSessionIdstringSession that proposed the contract
symbolPathstringThe symbol path
signaturestringCaptured function signature
statusstringCurrent status
responderSessionIdstring | nullSession that responded (null if still proposed)
proposedAtnumberUnix timestamp in milliseconds
respondedAtnumber | nullUnix timestamp in milliseconds, or null

Errors:

CodeMessageWhen
-32600INVALID_REQUESTInvalid params

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "contract.query",
  "params": { "status": "accepted" }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": [
    {
      "contractId": "c1d2e3f4-1234-5678-abcd-000000000001",
      "proposerSessionId": "sess-abc-123",
      "symbolPath": "src/auth.ts:validateToken",
      "signature": "(token: string): Promise<User | null>",
      "status": "accepted",
      "responderSessionId": "sess-xyz-456",
      "proposedAt": 1705316000000,
      "respondedAt": 1705316500000
    }
  ]
}

check-contracts

Check whether staged file contents violate any accepted contracts. This method is used by the git pre-commit hook — the hook passes staged file content directly so the daemon never reads from disk.

For each accepted contract whose symbol lives in one of the provided files, the daemon parses the staged content, extracts the current signature, and compares it to the contracted signature. A violation occurs when the signatures differ or the symbol cannot be found in the staged content.

Params:

NameTypeRequiredDescription
filesFileInput[]yesArray of staged file entries

FileInput object:

FieldTypeRequiredDescription
pathstringyesRelative file path
contentstringyesFull file content as a string

Result:

FieldTypeDescription
violationsViolation[]List of contract violations. Empty array means no violations

Violation object:

FieldTypeDescription
contractIdstringUUID of the violated contract
symbolPathstringThe symbol path
expectedstringThe contracted (expected) signature
actualstringThe current signature in staged content, or "(symbol not found in staged content)"

Errors:

CodeMessageWhen
-32600INVALID_REQUESTInvalid params

Example:

// Request
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...", "method": "check-contracts",
  "params": {
    "files": [
      {
        "path": "src/auth.ts",
        "content": "export function validateToken(token: string, opts: Options): User { ... }"
      }
    ]
  }
}

// Response
{
  "jsonrpc": "2.0", "witVersion": "1",
  "id": "...",
  "result": {
    "violations": [
      {
        "contractId": "c1d2e3f4-1234-5678-abcd-000000000001",
        "symbolPath": "src/auth.ts:validateToken",
        "expected": "(token: string): Promise<User | null>",
        "actual": "(token: string, opts: Options): User"
      }
    ]
  }
}

Shared Types

CallerWarning

Returned in lock.acquire result's warnings array. Informs the acquirer that a symbol which calls the newly locked symbol is itself locked by another session. This is informational — the acquire still succeeds.

{
  lockedSymbol: string;  // Symbol path of the caller that is locked
  heldBy: string;        // Session ID holding the caller's lock
  chain: [string, string]; // [callerSymbolPath, newlyLockedSymbolPath]
}

ConflictReport

Returned in intent.declare result. Summarizes all detected conflicts.

{
  hasConflicts: boolean;  // True if items is non-empty
  items: ConflictItem[];  // Conflict details
}

ConflictItem

A union type — one of three conflict kinds:

INTENT_OVERLAP — Another active intent overlaps the same file(s) and byte range:

{
  type: "INTENT_OVERLAP";
  overlappingIntentId: string;   // UUID of the conflicting intent
  overlappingSessionId: string;  // Session that declared the conflicting intent
  description: string;           // That intent's description
}

LOCK_INTERSECTION — A symbol you declared intent for is locked by another session:

{
  type: "LOCK_INTERSECTION";
  symbolPath: string;  // The locked symbol path
  heldBy: string;      // Session holding the lock
  expiresAt: string;   // ISO 8601 expiry timestamp
}

DEP_CHAIN — A callee of a symbol you declared intent for is locked by another session:

{
  type: "DEP_CHAIN";
  intentSymbol: string;  // Your declared symbol path
  lockedCallee: string;  // The callee that is locked
  heldBy: string;        // Session holding the callee lock
}

Error Codes

CodeMessageDescription
-32600INVALID_REQUESTZod validation failure. Params are missing or have wrong types
-32601METHOD_NOT_FOUNDUnknown method name
-32000LOCK_CONFLICTAttempted to acquire a lock held by another active session
-32000LOCK_NOT_FOUNDAttempted to release a lock that doesn't exist
-32000LOCK_NOT_HELDAttempted to release a lock held by a different session
-32000INTENT_NOT_FOUNDReferenced intent ID does not exist
-32000INTENT_NOT_OWNEDIntent belongs to a different session
-32000INVALID_TRANSITIONRequested status transition is not valid from the current status
-32000SYMBOL_NOT_FOUNDSymbol not found in file (contract.propose)
-32000CONTRACT_NOT_FOUNDReferenced contract ID does not exist
-32000CONTRACT_ALREADY_RESOLVEDContract is already accepted or rejected
-32000SELF_ACCEPT_NOT_ALLOWEDProposer cannot respond to their own contract

All -32000 errors may include a data field with additional context (e.g., heldBy, expiresAt, current, requested).


Intent Lifecycle

Intents follow a forward-only state machine. Once an intent reaches a terminal state (resolved or abandoned), it cannot be updated.

              declare
                 |
                 v
           [declared]
            /       \
          active   resolved
           |     \ /
           |      X
           |     / \
           v    /   v
       [active]   [resolved]
           |
           v
       [abandoned]

Valid transitions:

FromToDescription
declaredactiveAgent has started working on the intent
declaredresolvedIntent fulfilled without ever going active
declaredabandonedIntent dropped without action
activeresolvedWork completed successfully
activeabandonedWork stopped before completion

Terminal states (resolved, abandoned) have no outgoing transitions. Attempting an invalid transition returns INVALID_TRANSITION.

The default intent.query filter returns declared and active intents only. To query terminal intents, pass an explicit status filter.


Contract Lifecycle

Contracts follow a two-state lifecycle: one agent proposes, another responds.

  propose
     |
     v
 [proposed]
   /    \
accept  reject
   |      |
   v      v
[accepted] [rejected]

Valid transitions:

FromToWhoDescription
proposedacceptedResponder (different session than proposer)Agreeing to preserve the interface
proposedrejectedResponder (different session than proposer)Declining the contract

accepted and rejected are terminal — CONTRACT_ALREADY_RESOLVED is returned if contract.respond is called on a non-proposed contract.

Enforcement: The git pre-commit hook calls check-contracts with staged content. If any accepted contract is violated (signature changed), the commit is blocked. The hook uses a 2-second timeout and fails open — if the daemon is unreachable, the commit proceeds.