diff --git a/README.md b/README.md index 39afd80..3ffb46f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ > A **Claude Code plugin** + an **MCP server** for Codex. With Bun, Claude Code, and Codex installed, one `npm install` wires two agents into one shared, auditable ledger. +![ContextRelay running Claude Code and Codex as one pair, with a live shared ledger](docs/assets/contextrelay-pair.gif) + +*Scripted demo using real `ctxrelay` output format — paths, IDs, and timestamps are sanitized.* + ContextRelay wires Claude Code and Codex into one repository through a loopback daemon, so they exchange live messages, hand off work, and debate decisions — with every message, handoff, note, and decision written to a shared, auditable @@ -82,6 +86,10 @@ The terminal-native management tab sits alongside those dashboard views: A typical session: Claude implements, hands the risky part to Codex for an independent review, and every move lands in the shared ledger. +![A Claude-to-Codex review handoff recorded in the ContextRelay ledger](docs/assets/contextrelay-handoff.gif) + +*Scripted from the real `ctxrelay ledger` format; the scenario is representative.* + ```text Claude → /contextrelay:handoff reason: finished the token-refresh change; want a second opinion before merge diff --git a/docs/assets/contextrelay-handoff.cast b/docs/assets/contextrelay-handoff.cast new file mode 100644 index 0000000..ff99146 --- /dev/null +++ b/docs/assets/contextrelay-handoff.cast @@ -0,0 +1,63 @@ +{"version":2,"width":90,"height":26,"timestamp":1718000000,"title":"ContextRelay — a representative Claude → Codex handoff","env":{"SHELL":"/bin/zsh","TERM":"xterm-256color"}} +[0,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[1.278,"o","\u001b[2m# Claude finished a token-refresh change — wants a second pair of eyes.\u001b[0m"] +[1.778,"o","\r\n"] +[2.878,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[2.924,"o","c"] +[2.962,"o","t"] +[3.017,"o","x"] +[3.08,"o","r"] +[3.118,"o","e"] +[3.162,"o","l"] +[3.198,"o","a"] +[3.247,"o","y"] +[3.295,"o"," "] +[3.346,"o","l"] +[3.396,"o","e"] +[3.44,"o","d"] +[3.502,"o","g"] +[3.548,"o","e"] +[3.586,"o","r"] +[3.628,"o"," "] +[3.671,"o","l"] +[3.731,"o","i"] +[3.772,"o","s"] +[3.827,"o","t"] +[4.077,"o","\r\n"] +[4.527,"o","2026-06-18T10:22:14.512Z \u001b[33mhandoff\u001b[0m claude->codex 7c1a9e02\r\n review src/auth/refresh.ts for races + token-leak paths before merge\r\n2026-06-18T10:23:48.901Z \u001b[36mmessage\u001b[0m codex->claude b2e6f4a1\r\n My independent view: refresh() is unguarded — two concurrent 401s double-refresh\r\n2026-06-18T10:24:31.770Z \u001b[36mmessage\u001b[0m codex->claude c4f0a7d5\r\n ...and the old token lingers in memory after rotation. Neither path is tested.\r\n"] +[6.527,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[6.591,"o","c"] +[6.649,"o","t"] +[6.708,"o","x"] +[6.77,"o","r"] +[6.832,"o","e"] +[6.883,"o","l"] +[6.925,"o","a"] +[6.97,"o","y"] +[7.034,"o"," "] +[7.089,"o","l"] +[7.149,"o","e"] +[7.207,"o","d"] +[7.256,"o","g"] +[7.296,"o","e"] +[7.353,"o","r"] +[7.39,"o"," "] +[7.435,"o","s"] +[7.495,"o","h"] +[7.559,"o","o"] +[7.619,"o","w"] +[7.656,"o"," "] +[7.703,"o","7"] +[7.746,"o","c"] +[7.8,"o","1"] +[7.836,"o","a"] +[7.9,"o","9"] +[7.945,"o","e"] +[7.985,"o","0"] +[8.038,"o","2"] +[8.288,"o","\r\n"] +[8.738,"o","7c1a9e02 (handoff)\r\nsession: session_8f3c2d1b\r\ntime: 2026-06-18T10:22:14.512Z\r\nsource: claude -> codex\r\n\r\nreason: finished the token-refresh change; want a second opinion before merge\r\nask: review src/auth/refresh.ts for races and token-leak paths\r\nfiles: src/auth/refresh.ts, src/auth/refresh.test.ts\r\n"] +[10.738,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[11.908,"o","\u001b[2m# Independent cross-vendor review — durable, replayable evidence.\u001b[0m"] +[12.408,"o","\r\n"] +[16.208,"o",""] diff --git a/docs/assets/contextrelay-handoff.gif b/docs/assets/contextrelay-handoff.gif new file mode 100644 index 0000000..6f28855 Binary files /dev/null and b/docs/assets/contextrelay-handoff.gif differ diff --git a/docs/assets/contextrelay-pair.cast b/docs/assets/contextrelay-pair.cast new file mode 100644 index 0000000..bfa211c --- /dev/null +++ b/docs/assets/contextrelay-pair.cast @@ -0,0 +1,47 @@ +{"version":2,"width":90,"height":26,"timestamp":1718000000,"title":"ContextRelay — two agents, one ledger","env":{"SHELL":"/bin/zsh","TERM":"xterm-256color"}} +[0,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[0.882,"o","\u001b[2m# Run Claude Code and Codex as one auditable pair\u001b[0m"] +[1.382,"o","\r\n"] +[2.382,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[2.428,"o","c"] +[2.466,"o","t"] +[2.521,"o","x"] +[2.584,"o","r"] +[2.622,"o","e"] +[2.666,"o","l"] +[2.702,"o","a"] +[2.751,"o","y"] +[2.799,"o"," "] +[2.85,"o","-"] +[2.9,"o","-"] +[2.944,"o","v"] +[3.006,"o","e"] +[3.052,"o","r"] +[3.09,"o","s"] +[3.132,"o","i"] +[3.175,"o","o"] +[3.235,"o","n"] +[3.485,"o","\r\n"] +[3.935,"o","contextrelay v3.4.0\r\n"] +[5.035,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[5.076,"o","c"] +[5.131,"o","t"] +[5.195,"o","x"] +[5.253,"o","r"] +[5.312,"o","e"] +[5.374,"o","l"] +[5.436,"o","a"] +[5.487,"o","y"] +[5.529,"o"," "] +[5.574,"o","s"] +[5.638,"o","t"] +[5.693,"o","a"] +[5.753,"o","t"] +[5.811,"o","u"] +[5.86,"o","s"] +[6.11,"o","\r\n"] +[6.56,"o","{\r\n \"instanceId\": \"ctx_9a2f10\",\r\n \"bridgeReady\": true,\r\n \"claudeState\": \"idle\",\r\n \"codexState\": \"idle\",\r\n \"claudeAttachmentStatus\": \"live_attached\",\r\n \"claudeConnected\": true,\r\n \"tuiConnected\": true,\r\n \"deliveryMode\": \"push\",\r\n \"queuedMessageCount\": 0,\r\n \"ledgerEntries\": 12,\r\n \"latestActiveHandoff\": null\r\n}\r\n"] +[8.36,"o","\u001b[1;32m~/code/acme-api\u001b[0m \u001b[36m$\u001b[0m "] +[9.602,"o","\u001b[2m# Both agents attached — every message + handoff lands in the ledger.\u001b[0m"] +[10.102,"o","\r\n"] +[13.702,"o",""] diff --git a/docs/assets/contextrelay-pair.gif b/docs/assets/contextrelay-pair.gif new file mode 100644 index 0000000..aa8774b Binary files /dev/null and b/docs/assets/contextrelay-pair.gif differ diff --git a/scripts/gen-demo-casts.mjs b/scripts/gen-demo-casts.mjs new file mode 100644 index 0000000..62bea48 --- /dev/null +++ b/scripts/gen-demo-casts.mjs @@ -0,0 +1,112 @@ +// Asciicast v2 generator for ContextRelay demos. +// Commands and the output *format* mirror real `ctxrelay` output, but values are +// trimmed and sanitized (representative paths, IDs, and timestamps) — these are +// scripted demos, not a live capture. Deterministic: same input -> identical +// .cast (seeded jitter), so the committed casts regenerate byte-for-byte. +import { writeFileSync } from "node:fs"; + +// Seeded PRNG (mulberry32) so typing jitter is reproducible — not Math.random(). +let _seed = 0x9e3779b9; +function rng() { + _seed = (_seed + 0x6d2b79f5) | 0; + let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; +} + +const GREEN = "\x1b[1;32m"; +const DIM = "\x1b[2m"; +const CYAN = "\x1b[36m"; +const YEL = "\x1b[33m"; +const RST = "\x1b[0m"; +const PROMPT = `${GREEN}~/code/acme-api${RST} ${CYAN}$${RST} `; + +function build(steps, { width = 90, height = 26, title }) { + _seed = 0x9e3779b9; // reset per cast so each regenerates identically + let t = 0.0; + const ev = []; + const out = (s) => ev.push([round(t), "o", s]); + const wait = (s) => { t += s; }; + const nl = "\r\n"; + + const type = (cmd) => { + out(PROMPT); + for (const ch of cmd) { wait(0.035 + rng() * 0.03); out(ch); } + wait(0.25); + out(nl); + }; + const comment = (text) => { + out(PROMPT); + const s = `${DIM}# ${text}${RST}`; + for (const ch of `# ${text}`) { wait(0.018); } + out(s); + wait(0.5); + out(nl); + }; + const print = (block) => { wait(0.45); out(block.replace(/\n/g, nl) + nl); }; + + const round = (x) => Math.round(x * 1000) / 1000; + + for (const step of steps) { + if (step.comment) { comment(step.comment); wait(step.pause ?? 0.9); } + else if (step.cmd) { + type(step.cmd); + if (step.out != null) print(step.out); + wait(step.pause ?? 1.4); + } else if (step.blank) { out(nl); wait(step.pause ?? 0.4); } + } + // tail hold so a looped GIF rests on the final frame + wait(2.2); out(""); + + const header = { version: 2, width, height, timestamp: 1718000000, title, + env: { SHELL: "/bin/zsh", TERM: "xterm-256color" } }; + return [JSON.stringify(header), ...ev.map((e) => JSON.stringify(e))].join("\n") + "\n"; +} + +// ---- Cast 1: the pair is live, recording to one ledger ------------------- +const pair = build([ + { comment: "Run Claude Code and Codex as one auditable pair", pause: 1.0 }, + { cmd: "ctxrelay --version", out: "contextrelay v3.4.0", pause: 1.1 }, + { cmd: "ctxrelay status", out: +`{ + "instanceId": "ctx_9a2f10", + "bridgeReady": true, + "claudeState": "idle", + "codexState": "idle", + "claudeAttachmentStatus": "live_attached", + "claudeConnected": true, + "tuiConnected": true, + "deliveryMode": "push", + "queuedMessageCount": 0, + "ledgerEntries": 12, + "latestActiveHandoff": null +}`, pause: 1.8 }, + { comment: "Both agents attached — every message + handoff lands in the ledger.", pause: 1.4 }, +], { title: "ContextRelay — two agents, one ledger" }); + +// ---- Cast 2: a representative handoff, in real ctxrelay ledger format ----- +const handoff = build([ + { comment: "Claude finished a token-refresh change — wants a second pair of eyes.", pause: 1.1 }, + { cmd: "ctxrelay ledger list", out: +`2026-06-18T10:22:14.512Z ${YEL}handoff${RST} claude->codex 7c1a9e02 + review src/auth/refresh.ts for races + token-leak paths before merge +2026-06-18T10:23:48.901Z ${CYAN}message${RST} codex->claude b2e6f4a1 + My independent view: refresh() is unguarded — two concurrent 401s double-refresh +2026-06-18T10:24:31.770Z ${CYAN}message${RST} codex->claude c4f0a7d5 + ...and the old token lingers in memory after rotation. Neither path is tested.`, pause: 2.0 }, + { cmd: "ctxrelay ledger show 7c1a9e02", out: +`7c1a9e02 (handoff) +session: session_8f3c2d1b +time: 2026-06-18T10:22:14.512Z +source: claude -> codex + +reason: finished the token-refresh change; want a second opinion before merge +ask: review src/auth/refresh.ts for races and token-leak paths +files: src/auth/refresh.ts, src/auth/refresh.test.ts`, pause: 2.0 }, + { comment: "Independent cross-vendor review — durable, replayable evidence.", pause: 1.6 }, +], { title: "ContextRelay — a representative Claude → Codex handoff" }); + +const dir = process.argv[2] || "."; +writeFileSync(`${dir}/contextrelay-pair.cast`, pair); +writeFileSync(`${dir}/contextrelay-handoff.cast`, handoff); +console.log("wrote contextrelay-pair.cast and contextrelay-handoff.cast to", dir); diff --git a/website/docs/introduction/index.md b/website/docs/introduction/index.md index 575ebb5..3ef3c72 100644 --- a/website/docs/introduction/index.md +++ b/website/docs/introduction/index.md @@ -13,6 +13,10 @@ ContextRelay is a local, provider-neutral coordination control-plane. It wires C Reach for ContextRelay when **one agent should implement while the other reviews**, when **a risky decision needs a second opinion**, or when you want an **auditable record of what the agents did before you ship**. +![ContextRelay running Claude Code and Codex as one pair, with a live shared ledger](/img/contextrelay-pair.gif) + +*Scripted demo using real `ctxrelay` output format — paths, IDs, and timestamps are sanitized.* + :::info Current version These docs describe **ContextRelay 3.4.0**, the current release on [npm](https://www.npmjs.com/package/@proofofwork-agency/contextrelay). After upgrading the package, run [`ctxrelay upgrade`](../getting-started/upgrading-contextrelay.md) to reconcile your local setup. ::: @@ -49,6 +53,12 @@ ctxrelay claude # start Claude Code wired into the daemon ctxrelay codex # start the Codex TUI wired into the daemon ``` +Once they're paired, a handoff looks like this — Claude asks Codex for an independent review, and every message lands in the shared ledger: + +![A Claude-to-Codex review handoff recorded in the ContextRelay ledger](/img/contextrelay-handoff.gif) + +*Scripted from the real `ctxrelay ledger` format; the scenario is representative.* + :::tip Three names, one CLI `contextrelay`, `ctxrelay`, and `context-relay` are the **same binary** - use whichever you prefer. This site uses `ctxrelay` for brevity. ::: diff --git a/website/static/img/contextrelay-handoff.gif b/website/static/img/contextrelay-handoff.gif new file mode 100644 index 0000000..6f28855 Binary files /dev/null and b/website/static/img/contextrelay-handoff.gif differ diff --git a/website/static/img/contextrelay-pair.gif b/website/static/img/contextrelay-pair.gif new file mode 100644 index 0000000..aa8774b Binary files /dev/null and b/website/static/img/contextrelay-pair.gif differ