Compact guidance for agents working in this repo. Every line here is something you'd likely get wrong without help.
When using duet, produce HTML artifacts — not markdown plans, not text descriptions. The entire point is that plans, dashboards, wireframes, architecture diagrams, and code diffs should be rendered as HTML so humans can visually review and annotate them. Markdown plans are invisible feedback opportunities. HTML artifacts are actionable.
If you are an AI agent integrating with duet: your output for visual review must be an HTML file, opened via duet open. Text-only output bypasses the review layer entirely.
# Run all tests
pytest
# Run a single test
pytest tests/test_core.py -k test_name
# Run the CLI
python -m duet.cli open path/to/artifact.html
python -m duet.cli export path/to/artifact.html
python -m duet.cli poll path/to/artifact.html
# Get artifact-type guidance (playbooks for landing pages, dashboards, etc.)
python -m duet.cli playbook
python -m duet.cli playbook landing-page
# Get design system guidance + CDN snippets
python -m duet.cli design
# Run the server standalone
python -m duet.server_main --port 4387
# Install agent hooks
duet setup hooksNo pip install -e . needed for dev — just run directly. The duet script entry point requires package install.
duet/cli.py spawns a detached background server (duet/server_main.py) if one isn't running. The CLI talks to the server via httpx on localhost:4387. State persists to ~/.duet/state.json.
Do not import server modules from CLI code. The boundary is HTTP, not Python imports.
DuetServer._register_routes() on the FastAPI app. Key endpoints:
GET /api/poll?file=...— long-poll, blocks until feedback arrives (used by AI agents)GET /events/{key}— SSE for browser clients (reload, chat sync, agent presence)GET /artifact/{key}/— serves HTML with annotation SDK injected
JSON-file store with asyncio.Lock. Sessions keyed by sha256(filepath)[:16]. All mutations must be inside async with self._lock. The take_feedback method atomically drains prompts + warnings.
Inlines local CSS/JS/images as data URIs or <style>/<script> blocks. Remote references left as-is. Caps: 10MB per asset, 25MB per bundle. SDK script stripped from exports.
All config via env vars — no config files:
DUET_PORT(default 4387)DUET_HOST(default127.0.0.1)DUET_STATE_DIR(default~/.duet)DUET_HTML_APP_API_URL,DUET_HTML_APP_TOKEN(publishing)DUET_IDLE_TIMEOUT_MS,DUET_EXPORT_MAX_ASSET_BYTES,DUET_EXPORT_MAX_BUNDLE_BYTES
The intended workflow for an AI agent using duet:
duet open artifact.html— starts session, opens browser- Human annotates, sends feedback
duet poll artifact.html— blocks until feedback arrives (structured: element + prompt + DOM snapshot)- Agent makes changes to the HTML file
- File watcher triggers live reload in browser
- Repeat from step 2
- Server must be running before CLI commands that talk to it — CLI auto-starts it but there's a brief startup race
~/.duet/whiteboards/<key>/<index>.json— sidecar files for Excalidraw scenes- JS/CSS assets in
duet/assets/are vendored plain files, no build step. SDK assembled at runtime fromartifact-sdk.js+mermaid-node.js - Playbooks source is
duet/assets/playbooks.json(serialized from upstream JS module) - Tests are in
tests/—test_core.py,test_e2e.py,test_whiteboard_channel.py,test_e2e_browser.py(requires Kimi WebBridge daemon) duet playbookreturns artifact-type guidance (landing pages, dashboards, etc.) — use it to shape what you generateduet designreturns design system guidance with DaisyUI CDN snippets — use it for styling
- Python 3.10+ required, compiles to 3.10 syntax
httpxfor all HTTP, neverrequestsasyncio.Eventfor long-poll blocking, not sleep loopshtml.parser.HTMLParserfor HTML rewriting in exports (not lxml for that part)