witness
<div align="center">
<a href="https://witness.zalize.com"><img src="docs/og.png" alt="Witness — You say it. Witness proves it." width="760"></a>
<br><br>
<a href="https://github.com/wookat/witness/stargazers"><img src="https://img.shields.io/github/stars/wookat/witness?style=flat-square&color=ff4f00" alt="GitHub stars"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-17181c?style=flat-square" alt="MIT license"></a>
<img src="https://img.shields.io/badge/node-%E2%89%A518-17181c?style=flat-square" alt="Node ≥ 18">
<img src="https://img.shields.io/badge/MCP-12%20tools-17181c?style=flat-square" alt="12 MCP tools">
<br>
<b>English</b> · <a href="README.zh-CN.md">中文</a> · <a href="https://witness.zalize.com">witness.zalize.com</a> · <a href="VISION.md">VISION.md</a>
</div>
<br>
**Voice-anchored causal evidence recorder for AI coding agents.**
You experience the product and talk — *“this button does nothing.”*
Witness welds every word to the causal evidence of that exact second: clicks, requests,
console errors, DOM mutations, screenshots, full rrweb replay — then hands a structured
evidence package to your AI coding agent.
<p align="center"><img src="docs/demo.gif" alt="Recording a session with wit" width="820"></p>
## Why
AI agents can't see what you see. Describing a visual bug costs ten minutes of
screenshots and prompt crafting — and still loses the *why*. Voice is the cheapest
annotation channel there is; it just never had a recorder.
Three streams run in parallel the moment `wit start` opens a managed browser:
1. **Your voice** — continuous audio + live transcription. One take, many issues.
2. **Operations + causal bursts** — every click captures its ~1.8s aftermath
(requests, responses, console errors, DOM changes). A click with *no* aftermath
is itself recorded as evidence.
3. **Page evolution** — rrweb full-DOM replay + event-driven screenshots + periodic
DOM snapshots.
## Install
```bash
npm i -g witness-recorder # needs Node 18+ and system Edge/Chrome
wit start http://localhost:5173 # managed browser opens — talk & click
```
`rec` is an alias for `wit` — same binary, shorter to type.
No downloads, no API keys. System Edge/Chrome is preferred; bundled Chromium is the fallback.
## During recording
| Gesture | Effect |
|---|---|
| Browse + talk | Full audio; clicks/scrolls/inputs/navigation auto-logged; live transcript preview under the toolbar |
| `Ctrl+click` | Mark an element — selector, outerHTML, component chain, red-frame screenshot |
| `M` | Inspect mode: hover-highlight, click to mark; **drag to crop a region** |
| `N` | Explicit boundary marker — `⏺ ISSUE #n` divider; a strong grouping hint for the agent |
| `P` | Pause/resume — **all events and audio freeze** (privacy) |
| `Esc` | Exit inspect mode |
| `■` / `Alt+Shift+R` | Stop and finalize the evidence package |
`events.jsonl` streams to disk as you go — kill the process and you lose only the last
few hundred milliseconds. `wit recover` rebuilds everything.
## What the agent reads
Each spoken fragment lands on a timeline as a `SAY` block carrying the causal burst of
that moment — dead clicks, mutations, errors — with linked shots and DOM snapshots.
<p align="center"><img src="docs/evidence.png" alt="session.md — each SAY anchors its causal burst" width="820"></p>
## CLI
```bash
wit start <url> [--lang en-US] [--duration 60] [--no-rr] [--associate]
[--shot-format jpeg|webp] [--dsf 1|2] [--keyframes]
wit list # sessions with url/duration/say count
wit open latest # open review.html
wit transcribe <dir> # re-run cloud ASR
wit correct <dir> # re-run LLM text polish
wit associate <dir> # LLM picks relevant evidence per SAY (ZHIPU_API_KEY)
wit recover <dir> # rebuild artifacts after a crash
wit segments <dir> # regenerate speech-anchor index + bundles
wit keyframes <dir> # render rrweb → per-segment keyframe jpgs
wit mcp [dir] # stdio MCP server over the evidence
wit config asr <engine> # live ASR: auto|doubao-web|doubao-ime|sr|off
```
## The evidence package — `.witness/sessions/<ts>_<host>/`
Agent-facing artifacts (PROMPT.md / SEGMENTS.md / session.md) are always generated in English — the most reliable corpus for LLMs; your words stay verbatim, never translated.
```
SEGMENTS.md # speech anchors: verbatim + suspects + shot/dom refs (agents read first)
segments/*.md # per-segment bundle: verbatim frags, dead clicks, errors, linked shots
keyframes/*.jpg # rrweb-rendered replay frames per segment (wit keyframes)
KEYFRAMES.md # keyframe index
segments.json # machine-readable segment index (what MCP serves)
session.md # timeline archive: each SAY block carries its causal burst
PROMPT.md # paste-ready handoff instructions + page vocabulary
review.html # human view: speech cards + audio seek + filmstrip + embedded rrweb replay
events.jsonl # complete raw stream, crash-safe
rrweb.jsonl # full DOM event stream (--no-rr to disable)
dom/*.html # DOM snapshots on nav/mark/30s
assoc.json # optional LLM evidence-association suggestions
shots/*.jpg # click/mark crops + periodic + spoken-segment frames (--shot-format webp for ~35% smaller)
bodies/* # response bodies ≤64KB (.json for JSON, .txt otherwise)
session.json # meta: url, duration, asr engine, counts
audio.webm / transcript.json
```
## MCP — evidence as an API
`wit mcp` serves the session over stdio. Point any MCP-capable agent
(Claude Code, Cursor, Devin) at it and it pulls exactly what it needs:
`list_segments` · `segment_detail(n)` · `session_overview` · `list_says` · `session_md` · `events_around(t_ms, radius, kinds)` ·
`get_errors` · `get_shot` · `get_body` · `get_dom_state(t)` · `get_asr` · `set_asr`
## Principles
- **Evidence is never rewritten** — speech lands verbatim; merging/correction/association are display-layer suggestions.
- **Evidence ≠ intent** — behavior without a spoken anchor is never presented as a bug.
- **Local-first** — deterministic recording; live SAY subtitles stream through free cloud ASR
(`auto` = doubao-web → doubao-ime → browser SR fallback — pick via `wit config asr`, `--asr`, `WIT_ASR`, or the
`get_asr`/`set_asr` MCP tools); final transcript falls back to bcut (bilibili editor ASR, no key needed)
when no OPENAI key is set. LLM layers are optional.
- **Privacy engineered in** — password fields masked across four channels, credential fields redacted in POST bodies,
`WIT_NO_BODIES=1` disables body capture, `P` freezes everything, `.witness/` is auto-gitignored.
## Development
```bash
npm install && npm run build && npm link
npm test # build + node:test suite (align / speech-run / bursts / prompts / review html)
```
<br>
<div align="center">
<img src="docs/logo.svg" width="38" alt="Witness">
<br>
<sub>Built by <b>zalize</b> (<a href="https://github.com/wookat">@wookat</a>) · <a href="LICENSE">MIT</a></sub>
<br>
<sub>Questions welcome — <a href="mailto:wookat@qq.com">wookat@qq.com</a> · <a href="https://github.com/wookat/witness/issues">issues</a></sub>
</div>
TDQS
Scored across 12 tools
Each tool maps to a distinct evidence artifact (segments, says, errors, shots, DOM, bodies, raw events), and descriptions explicitly route the agent ("READ THIS FIRST" on list_segments, cross-references between list_says and segment_detail). Minor overlap exists between get_errors and events_around(kinds=error), and between session_overview and session_md, but the boundaries are stated clearly enough to avoid misselection.
A readable pattern holds: get_* for fetching artifacts (get_shot, get_dom_state, get_body, get_errors), list_* for indexes, session_* for global metadata. Deviations exist (segment_detail, events_around, session_md) which break the verb_noun convention, but the set is still predictable.
12 tools is squarely in the well-scoped range, with each tool covering a distinct layer of the evidence pipeline (overview, segmentation, raw events, artifacts, config). Nothing feels redundant or padded.
The surface covers the full investigation lifecycle: session metadata, speech anchors, per-segment evidence bundles, timeline rendering, raw events, errors, and artifact retrieval plus ASR get/set. Minor gaps remain—no session-wide search or filtering across all suspects/dead clicks, and no direct listing of available shots/bodies beyond the file list in session_overview.