A multiplayer, role-based coordination protocol for human-AI collaborative development.
Check out pvp.codes!
PVP is NOT a chatbot. It's a coordination layer where multiple humans and AI agents collaborate in real-time to:
- Shape prompts collaboratively
- Observe AI reasoning
- Gate actions before execution
- Record auditable streams of decisions
The code is a side effect; the real artifact is the recorded, auditable stream of human decisions mediated by AI execution.
- ✅ Real-time multiplayer collaboration
- ✅ Role-based access control (driver, navigator, adversary, observer, approver, admin)
- ✅ Approval gates for high-risk operations
- ✅ Live streaming of AI thinking and responses
- ✅ Context sharing and management
- ✅ Session forking and merging
- ✅ Interrupt mechanisms for human intervention
- ✅ WebSocket transport with reconnection
- ✅ Terminal UI (TUI) client
- ✅ Structured logging and monitoring
- ✅ Decision tracking - Git-based audit trail with automatic commit metadata
- ✅ Session tasks - Goal and task tracking with session persistence
- ✅ Strict mode - Validate agent actions against session tasks/goals
npm install github:agrathwohl/pvpInstall globally to use the CLI binaries from anywhere:
# From GitHub Package Registry
npm i -g @agrathwohl/pvp
# Verify installation
pvp --versionPrerequisites: The agent requires Bun runtime. Install it first:
curl -fsSL https://bun.sh/install | bashAfter global installation, four binaries are available:
| Binary | Runtime | Description |
|---|---|---|
pvp |
Node.js | Umbrella CLI with server, tui, agent, bridge, and install-hooks subcommands |
pvp-server |
Node.js | WebSocket server for session coordination |
pvp-tui |
Node.js | Terminal UI client for human participants |
pvp-agent |
Bun | Claude AI agent with tool execution |
Start the PVP WebSocket server:
# Default (port 3000)
pvp-server
# Custom port and host
pvp-server --port 8080 --host 0.0.0.0The server persists every session's messages (including session.create) to a JSONL transcript at ~/.pvp/sessions/{sessionId}.jsonl (override with PVP_SESSION_LOG_DIR). If a message arrives for a session the server doesn't know (e.g., after a restart), the session is restored from its transcript: name and config come from the logged session.create, and the full message history is replayed to joiners.
Connect to a session with the Terminal UI:
# Create a new session
pvp-tui --url ws://localhost:3000 --name "Alice"
# Join existing session
pvp-tui --url ws://localhost:3000 --session <session-id> --name "Bob"
# Connect to remote server
pvp-tui --url wss://ws.pvp.codes --session <session-id> --name "Alice"Connect a Claude AI agent to a session:
# Basic usage (requires ANTHROPIC_API_KEY env var)
pvp-agent --server ws://localhost:3000 --session <session-id>
# With local working directory (agent executes commands in your local folder)
pvp-agent --server wss://ws.pvp.codes --session <session-id> --local
# Specify working directory path
pvp-agent --server wss://ws.pvp.codes --session <session-id> --local /path/to/project
# Full options
pvp-agent \
--server ws://localhost:3000 \
--session <session-id> \
--name "Claude Assistant" \
--model claude-sonnet-4-5-20250929 \
--api-key sk-ant-... \
--local
# OpenAI-compatible endpoint (requires OPENAI_API_KEY or --api-key)
pvp-agent \
--server ws://localhost:3000 \
--session <session-id> \
--provider openai \
--base-url http://localhost:11434/v1 \
--model llama3.1pvp-agent options:
| Option | Description | Default |
|---|---|---|
-s, --server <url> |
WebSocket server URL | (required) |
--session <id> |
Session ID to join | (required) |
-n, --name <name> |
Agent display name | Claude Assistant |
-m, --model <model> |
Model identifier | claude-sonnet-4-5-20250929 |
-k, --api-key <key> |
API key | $ANTHROPIC_API_KEY ($OPENAI_API_KEY when --provider openai) |
--provider <name> |
LLM provider: anthropic or openai (OpenAI-compatible endpoints) |
anthropic |
--base-url <url> |
Base URL for OpenAI-compatible endpoints (e.g., ollama, vllm) | https://api.openai.com/v1 |
-l, --local [path] |
Use local working directory | (disabled) |
--mcp-config <file> |
MCP servers config file | auto-discovers ./.pvp/mcp.json, then ~/.pvp/mcp.json |
Important: pvp-agent uses Bun runtime. If you see Cannot find package "bun", ensure Bun is installed and in your PATH.
# Clone and install dependencies
git clone https://github.com/your-username/pvp.git
cd pvp
npm install
# Build the project
npm run buildimport { startServer, startTUI } from "@agrathwohl/pvp";
// Start server
const server = await startServer({ port: 3000 });
// Start TUI client
await startTUI({
url: "ws://localhost:3000",
name: "Alice",
role: "driver",
});// Agent requires Bun runtime - import from submodule
import { startAgent } from "@agrathwohl/pvp/agent";
const agent = await startAgent({
url: "ws://localhost:3000",
session: "session-id",
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Strict mode is not available on startAgent - use the ClaudeAgent class directly
import { ClaudeAgent } from "@agrathwohl/pvp/agent";
const strictAgent = new ClaudeAgent({
serverUrl: "ws://localhost:3000",
sessionId: "session-id",
apiKey: process.env.ANTHROPIC_API_KEY,
strictMode: true, // Validate all actions against session tasks
});
await strictAgent.connect();See API Documentation for complete reference.
This project uses different runtimes per component for optimal performance and security:
| Component | Runtime | Command | Notes |
|---|---|---|---|
| Server | Node.js | npm run server |
WebSocket server, session management |
| TUI | Node.js | npm run tui |
Terminal user interface client |
| Agent | Bun | npm run agent |
Claude AI agent with shell execution |
The agent component requires Bun runtime because its shell executor (src/agent/tools/shell-executor.ts) uses Bun.spawn for secure command execution:
- Security: Array-based arguments prevent shell injection attacks
- Performance: Native subprocess streaming without external dependencies
- Safety: Built-in timeout and buffer limits
Important: Do NOT run the agent with tsx or Node.js. It will fail with module not found errors. Always use npm run agent.
npm run server
# or
npm run server -- --port 3000 --host 0.0.0.0# Create a new session
npm run tui -- --server ws://localhost:3000 --name "Alice" --role driver
# Join an existing session
npm run tui -- --server ws://localhost:3000 --session <session-id> --name "Bob" --role navigator# Join an existing session with Claude AI agent
npm run agent -- --server ws://localhost:3000 --session <session-id>
# With custom settings
npm run agent -- \
--server ws://localhost:3000 \
--session <session-id> \
--name "Claude" \
--model claude-sonnet-4-5-20250929 \
--api-key sk-ant-...
# Note: Requires ANTHROPIC_API_KEY environment variable or --api-key flag
# Get your API key at: https://console.anthropic.com/Agent Capabilities:
- Pluggable LLM providers: Anthropic (default) or any OpenAI-compatible endpoint (
--provider openai --base-url ...) - Native Anthropic tool use API for shell command execution
- Command safety categorization (safe/low/medium/high/critical)
- Approval gates for risky operations
- Real-time streaming output
- Multi-turn conversation with tool results
- Session-level task and goal tracking
- Strict mode for task-aligned execution
- Monitoring tool for files/commands/URLs with generated reports — a custom
analysis_promptleads the insights in every report surface - Nushell integration - structured data output when
nuis available
When Nushell is installed, the PVP agent gains a second shell tool (execute_nushell_command) that it prefers for commands where structured output is valuable. Instead of parsing raw text, the agent receives typed JSON — tables, records, and lists — directly from command output.
Why this matters for LLMs:
- No output parsing errors.
lsreturns[{name, type, size, modified}]not text to regex. - Queryable data.
ls | where size > 10kb | sort-by sizeworks natively. - File format awareness.
open data.csvauto-parses CSV into records. Same for JSON, TOML, YAML, XML. - Plugin ecosystem. Nushell plugins extend what the agent can do without writing new PVP tools — parquet files, SQL queries, git status as structured data, and more.
- Structured HTTP.
http getauto-parses JSON API responses into queryable records.
The agent automatically detects nu on PATH (or via PVP_NUSHELL_PATH / NUSHELL_PATH env vars) and registers the nushell tool alongside the standard shell tool. No configuration required.
# Install nushell (https://www.nushell.sh/book/installation.html)
# Arch/NixOS/Homebrew/Cargo - see nushell docs for your platform
# Verify it works
nu -c 'version | select version build_os'
# The PVP agent will detect it automatically on next startEnvironment variables:
| Variable | Description | Default |
|---|---|---|
PVP_NUSHELL_PATH |
Explicit path to nu binary |
(auto-detect) |
PVP_NUSHELL_NO_CONFIG |
Set to true to skip loading user nu config |
false |
When nushell is available, the agent's system prompt tells it when to prefer execute_nushell_command vs execute_shell_command. The POSIX shell tool remains available for long-running processes, interactive commands, and POSIX-specific features.
Safety categorization notes:
- Output redirect operators (
out>,o>>,err>,e>>,o+e>) elevate a command to thewritecategory (approval required), regardless of spacing around the target file —ls out> fileis treated as a write. Bare>comparisons (e.g.,where size > 10mb) do not match. - Blocked command patterns (e.g.,
shutdown,mkfs, raw disk writes) take precedence overschema_onlyprobes: a blocked command is rejected even when only its output schema is requested, since the original command would still run as the pipeline head.
The agent includes a built-in tasks tool for managing session goals and task lists:
# Example: Agent can manage tasks during a session
# Tasks are persisted to the session and survive agent reconnections
# Operations available:
# - set_goal: Set the primary session objective
# - get_goal: Retrieve current goal
# - add: Create a new task
# - update: Modify task properties
# - complete: Mark task as done
# - remove: Delete a task
# - list: Show all tasks
# - clear: Remove all tasksTask Properties:
| Property | Type | Description |
|---|---|---|
id |
string | Unique task identifier |
title |
string | Task name |
description |
string | Optional details |
status |
pending | in_progress | completed |
Current state |
priority |
low | medium | high |
Task priority |
created_at |
ISO timestamp | Creation time |
updated_at |
ISO timestamp | Last modification |
completed_at |
ISO timestamp | Completion time |
Strict mode ensures all agent actions align with session tasks. There is no CLI flag for it; it is currently reachable only programmatically via the ClaudeAgent class:
import { ClaudeAgent } from "@agrathwohl/pvp/agent";
const agent = new ClaudeAgent({
serverUrl: "ws://localhost:3000",
sessionId: "<session-id>",
strictMode: true,
});
await agent.connect();When strict mode is enabled:
- Agent validates prompts against session tasks before responding
- Tool executions are checked for task alignment
- Agent receives task context at the start of each interaction
- Misaligned requests are flagged for review
Use Cases:
- Focused coding sessions with defined objectives
- Project management with explicit deliverables
- Supervised agent execution with guardrails
┌─────────────────┐ ┌─────────────────┐
│ TUI Client │─────▶│ PVP Server │
│ (Ink/React) │ │ (WebSocket) │
└─────────────────┘ └─────────────────┘
│
┌────────┴────────┐
│ Protocol │
│ - 40+ msgs │
│ - Gates │
│ - Context │
│ - Forks │
└─────────────────┘
PVP includes an integrated git decision tracking system that automatically captures session context and embeds it into git commits.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ PVP Server │────▶│ Bridge Service │────▶│ Git Hooks │
│ │ │ (port 9847) │ │ │
│ • Messages │ │ • State mgmt │ │ • Trailers │
│ • Approvals │ │ • HTTP API │ │ • Git notes │
│ • Tool exec │ │ • Webhooks │ │ • Validation │
└─────────────────┘ └─────────────────┘ └─────────────────┘
-
Bridge Service - Local daemon that maintains session state
- Starts automatically with the PVP server (or run standalone with
pvp bridge) - HTTP API at
http://localhost:9847 - Unix socket at
/tmp/pvp-git-bridge.sock
- Starts automatically with the PVP server (or run standalone with
-
TUI Integration - Real-time decision tracking display
- Shows messages, prompts, approvals since last commit
- Displays tool execution summary
- Polls bridge service every 5 seconds
-
Git Hooks - Automatic commit metadata injection
prepare-commit-msg- Injects PVP trailerspost-commit- Stores extended metadata in git-notespre-push- Validates PVP metadata coverage
When connected, the TUI shows a decision tracking panel:
┌─────────────────────────────────────────────────────────────────┐
│ 📊 Decision Tracking | Msgs: 15 | Prompts: 3 | Approvals: 2 │
│ Tools: shell_execute, file_write | Last: a1b2c3d │
└─────────────────────────────────────────────────────────────────┘
Two distinct mechanisms write PVP metadata into commits:
1. Agent git_commit tool — when the agent commits through its git_commit tool, the commit message is built with these trailers (src/agent/tools/git-commit-tool.ts):
feat(auth): implement JWT validation [pvp:msg-01]
Added JWT token validation middleware.
PVP-Session: ses_01HX7K9P4QZCVD3N8MYW6R5T2B
PVP-Messages: msg-01,msg-03,msg-05
PVP-Confidence: 0.85
PVP-Decision-Type: implementation
Decision-By: human:alice,ai:claude
Here PVP-Messages is a comma-separated list of the message IDs the commit references.
2. prepare-commit-msg hook — for any commit made while a session is active, the installed hook appends a separate metadata block sourced from the bridge service:
---
PVP-META: true
PVP-SESSION: ses_01HX7K9P4QZCVD3N8MYW6R5T2B
PVP-PARTICIPANTS: alice,claude
PVP-MESSAGES: 15
PVP-PROMPTS: 3
PVP-APPROVALS: 2
PVP-TOOLS: shell_execute,file_write
Note: in the hook's block, PVP-MESSAGES is a count of messages since the last commit, not a list of message IDs.
| Endpoint | Method | Description |
|---|---|---|
/commit-context |
GET | Current session metrics |
/extended-metadata |
GET | Full session metadata |
/status |
GET | Bridge service status |
/commits |
GET | Recent tracked commits (?limit=N, max 100) |
- Architecture Guide - Full system design
- Git Commit Protocol - Commit format specification
- Git Hooks README - Hook installation and usage
- Human: driver, navigator, adversary, observer, approver, admin
- Agent: AI assistants that execute actions
- Session: create, join, leave, end, config_update
- Participant: announce, role_change
- Heartbeat: ping, pong
- Presence: active, idle, away, disconnected
- Context: add, update, remove (files, references, structured data)
- Secrets: share, revoke (API keys, credentials)
- Prompts: draft, submit, amend
- Thinking: start, chunk, end (AI reasoning streams)
- Response: start, chunk, end (AI output streams)
- Tools: propose, approve, reject, execute, output, result
- Gates: request, approve, reject, timeout (approval workflows)
- Interrupts: raise, acknowledge (human intervention)
- Forks: create, switch (parallel exploration)
- Merge: propose, execute (combine forks)
Gates require human approval before executing high-risk operations:
// Configure which operations require approval
const config: SessionConfig = {
require_approval_for: ["file_write", "shell_execute", "deploy"],
default_gate_quorum: { type: "any", count: 2 }, // Require 2 approvals
gate_timeout_seconds: 300, // Optional: gate expiry (default 300)
// ...
};Gate expiry: the server arms a timer for each gate (gate_timeout_seconds, default 300). When it expires without quorum, the gate is removed and a gate.timeout message (sender system, resolution rejected, with approvals_received/approvals_required) is broadcast. The agent treats a timed-out gate like a rejection: the tool is not executed and its result reports that the approval gate timed out. Values ≤ 0 disable expiry.
Quorum Rules:
any: N approvals from anyoneall: All approvers must approverole: N approvals from specific rolespecific: Specific participants must approvemajority: >50% of approvers
Share context between participants:
const contextMsg = createMessage("context.add", sessionId, participantId, {
key: "requirements",
content_type: "file",
content: fileContents,
visible_to: ["agent_01"], // Optional: restrict visibility
});Reserved Context Keys:
| Key | Content Type | Description |
|---|---|---|
session:tasks |
structured |
Tasks and goals state (managed by tasks tool) |
The session:tasks context is automatically managed by the agent's tasks tool. Frontend applications should subscribe to context.add messages with this key to display task state. See Tasks Frontend Integration for implementation details.
import { WebSocketClient } from "./src/transports/websocket.js";
import { createMessage } from "./src/protocol/messages.js";
import { ulid } from "ulid";
const participantId = ulid();
const client = new WebSocketClient("ws://localhost:3000", participantId);
client.on("connected", () => {
const createMsg = createMessage("session.create", ulid(), participantId, {
name: "My Session",
config: {
/* ... */
},
});
client.send(createMsg);
});
client.connect();See examples/ directory for complete examples:
basic-session.ts- Single participant sessionmulti-participant.ts- Multiple humans collaborating
| Key | Mode | Action |
|---|---|---|
p |
stream | Start composing prompt |
Enter |
compose | Submit prompt |
Esc |
compose | Cancel composition |
a |
gate | Approve gate |
r |
gate | Reject gate |
t |
stream | Toggle thinking panel |
d |
stream | Toggle debug panel |
g |
stream | Toggle tasks panel |
h |
stream | Show transcript for last git commit |
H |
stream | Toggle transcript panel |
Ctrl+C |
any | Exit |
Every message follows this structure:
{
v: 1, // Protocol version
id: string, // Message ID (ULID)
ts: string, // ISO timestamp
session: string, // Session ID
sender: string, // Participant ID
type: string, // Message type
ref?: string, // Reference to another message
seq?: number, // Sequence number (total ordering)
causal_refs?: string[], // Causal dependencies
fork?: string, // Fork ID
payload: object // Type-specific payload
}{
require_approval_for: ToolCategory[],
default_gate_quorum: QuorumRule,
allow_forks: boolean,
max_participants: number,
ordering_mode: "causal" | "total",
on_participant_timeout: "wait" | "skip" | "pause_session",
heartbeat_interval_seconds: number,
idle_timeout_seconds: number,
away_timeout_seconds: number,
gate_timeout_seconds?: number // Gate expiry in seconds (default 300; ≤0 disables)
}At runtime, session.config_update may be sent by participants holding the driver or admin role — drivers control session tolerance settings like require_approval_for and gate_timeout_seconds. The update broadcast excludes agent-type participants; agents are not made aware of config changes.
Real-time bidirectional communication for TUI clients.
Server: src/transports/websocket.ts
Client: Automatic reconnection with exponential backoff
# Watch mode (server)
npm run dev
# Type checking
npm run build
# Run tests
npm testimport { createMessage } from "./src/protocol/messages.js";
const message = createMessage(
"prompt.submit", // type
sessionId, // session
participantId, // sender
{
// payload
content: "Build a login form",
target_agent: "claude_01",
contributors: [participantId],
context_keys: ["requirements"],
},
);import { ParticipantManager } from "./src/server/participant.js";
const pm = new ParticipantManager();
if (pm.canPrompt(participant)) {
// Submit prompt
}
if (pm.canApprove(participant)) {
// Approve gate
}import { GateManager } from "./src/server/gates.js";
const gm = new GateManager();
const gate = gm.createGate(gateRequest);
gm.addApproval(gate, approverId);
const { met, reason } = gm.evaluateQuorum(gate, participants);LOG_LEVEL=info # Logging level (debug, info, warn, error)
NODE_ENV=production # Environment# Build
npm run build
# Run server
NODE_ENV=production npm run server -- --port 3000
# Or with PM2
pm2 start dist/server/index.js --name pvp-server- Unit tests (protocol messages, session/gates, tool executors incl. nushell, LLM provider conversion, MCP config/risk, decision tracking, git bridge, commit message builder, npm tool, monitor tool, TUI structured output)
- Integration tests (MCP server integration)
- Decision tracking system (git-based audit trail)
- Session tasks and goals with persistence
- Strict mode for task-aligned agent execution
- Nushell integration (structured data output, plugin support)
- LLM providers (Anthropic, OpenAI-compatible endpoints via
--provider/--base-url) - Persistent session recovery (restore from JSONL transcripts on join)
- Gate expiry enforcement (
gate_timeout_seconds,gate.timeoutbroadcast) - MCP transport support
- Message replay functionality
- Web UI client
- Enhanced fork/merge workflows
- Decision tree visualization
This is an implementation of the PVP specification. Contributions should:
- Follow the protocol specification precisely
- Maintain type safety (TypeScript strict mode)
- Include tests for new features
- Use structured logging (pino)
- API Reference - Complete programmatic API documentation
- Changelog - Version history and release notes
- Decision Tracking Architecture - Full system design
- Git Commit Protocol - Commit format specification
- Git Hooks Guide - Hook installation and usage
- Testing Guide - Test patterns and coverage
- Tasks Frontend Integration - Frontend implementation guide for tasks/goals
MIT
Built following the Pair Vibecoding Protocol specification.