Facet is where you and your coding agent look at the same plan. The agent writes Markdown; Facet renders it in a browser window on your machine, and carries your click back to the agent as typed data. It is one binary reading files on disk, so there is no account, no workspace, and nothing to migrate into.
examples/rollout-plan.md, 102 lines of plain Markdown, rendered.Agents write good staged plans: stages, a risk table, a couple of approaches with trade-offs, and several points where they need a decision before carrying on. In a terminal that arrives as four hundred lines of scrollback, and the answer goes back as prose the agent has to interpret, so an approval meant for one stage gets taken for the whole plan.
Facet renders the plan so it can be scanned, and turns each decision into a gate whose answer comes back typed: a chart instead of a column of numbers, a board instead of a bulleted backlog, a click instead of a paragraph. The plan above is 949 tokens of Markdown, measured below.
sequenceDiagram
actor Human
participant Facet as Facet
participant Agent
Agent->>Agent: writes plan.md
Agent->>Facet: facet plan.md
Facet-->>Human: rendered page: banner, tables, chart, gates
Agent->>Facet: facet focus risk-table
Facet-->>Human: ghost-gaze glows on that block
Agent->>Facet: facet await --kind approve,reject
Facet-->>Human: cue "blocked", pending count on the window title
Human->>Facet: clicks Approve
Facet->>Facet: appends to plan.md.facet-tour.jsonl
Facet-->>Agent: gesture: kind, target, anchor, payload
Agent->>Facet: edits plan.md
Facet-->>Human: page live-reloads in place
Edit the file and the open page updates in place, so a revision does not need a re-paste and the current version is never buried in scrollback.
Each verb is limited in what it can do to your attention. An agent working through a long plan calls them often, and only one of them can interrupt you.
| Verb | Direction | Does | Can it take focus? |
|---|---|---|---|
facet <doc> |
agent -> human | render, and raise the window | yes, and it is the only one that can |
facet focus |
agent -> human | glow a ghost-gaze on one block | no, it is ambient |
facet cue |
agent -> human | publish thinking / review / blocked / done to the /board home |
no, it is state rather than motion |
facet await |
human -> agent | block until the next gesture, return it as data | n/a |
focus is the one for pointing. When a chat reply cites a specific stage, the
agent glows that block in the page in the same turn, so "the risk in stage 3"
refers to something visible rather than to a position in scrollback. Between
them the verbs make a long session feel closer to pairing than to reading a
transcript: the agent puts a document on screen, points at what it is talking
about, and waits when it needs an answer.
A plan with a real fork in it puts the fork inline, next to the evidence for it:
:::interact{kind=select target=backfill-pacing options="conservative,balanced,aggressive"}
Which backfill pacing should S3 run at?
:::
The agent blocks on the answer:
facet await --session examples/rollout-plan.md --kind approve,rejectand gets one JSON line back. This is real output from the screenshot below:
{"kind":"approve","target":"plan-approval","anchor":"b2qgw40","gate":"acd04bc908685fa2","payload":{"decision":"approve"},"human":"operator","session":"examples/rollout-plan.md"}There are three kinds: approve (Approve and Reject buttons), select (one
button per option), and annotate (free text pinned to a block). The answer
names its own target, so a plan can carry several open gates at once without the
replies getting mixed up.
A decision is durable, and it is bound to the question that was asked, because a gate's identity is a fingerprint of its own content.
stateDiagram-v2
direction LR
[*] --> Pending: agent writes a gate
Pending --> Decided: human clicks
Decided --> Decided: re-decide, last write wins
Decided --> Pending: agent edits the question
Decided --> [*]: settled, replaced by the outcome
A decided gate still renders decided after a reload, a reconnect, or a daemon restart, because the journal of gestures is replayed. Change the wording or the options and the gate re-opens with the previous answer kept as history, which is how you re-ask when stage 4 turns up something that invalidates a call made at stage 2.
Gestures are data, never instructions. A gesture carries a kind, a target, an anchor, a payload, and a session. Nothing in the page can author content or steer the agent, and a click never modifies the document. Clicks land in a sibling
*.facet-tour.jsonljournal next to the file.
A staged plan is the hard case. It is too long to hold in your head, and the parts that need you are scattered through it. The page is built around finding those parts and acting on them without re-reading the whole thing.
| Feature | What it does |
|---|---|
| Fold and jump-nav | Three or more ## sections become collapsible, and a table of contents in the left column jumps to any of them |
| Attention drawer | One panel holding every open decision and every unread agent reply, under a single count that also rides the window title |
| Decision navigator | Steps through the open gates with prev and next, scrolling each into view and focusing its control. The count shrinks as you answer |
| Keyboard | j and k for next and previous decision, a approve, r reject, 1 to 9 pick an option, z fold a section, ? for help |
| Select to comment | Select any text in the document and a Comment chip opens a composer. The quoted span and its block anchor go back to the agent as a gesture |
| Comment threads | Each thread keeps its quote, a jump back to the block it came from, author-tagged replies, and a reply box. The agent's replies stream in live |
| Ghost-gaze | facet focus glows the block the agent is talking about, and can point inside a rendered visual at one Mermaid node or one chart series |
| Pan and zoom | Wheel to zoom, drag to pan, and fullscreen on any chart or diagram, so a wide flowchart can use the whole display |
| Cross-doc links | A relative .md link re-renders in place with a breadcrumb and a Back button. Traversal is bounded to the served root, and http(s) links open in your system browser |
| Station switcher | Every other session the daemon is hosting, which of them are waiting on you, and one click to jump |
![]() Select to comment: a selection raises the chip, and the composer quotes the span it is about. |
![]() One drawer: the open decision with prev/next, and the thread with the agent's reply in place. |
Comments are conversation, not content. They live in the drawer and never enter
the .md, and their bodies render as literal text rather than through the
markdown pipeline, so a comment carrying a :::interact gate or a <script>
tag shows up inert.
None of the above asks you to change how the agent works. Point Facet at a plan it wrote last year, a README, a meeting note, and it renders. There is no front matter to add, no config file, and no build step.
Compatibility runs both ways:
- A document that knows nothing about Facet renders fine. Facet handles
GitHub Flavored Markdown, and it picks up on a handful of conventions that
agent-written documents tend to use anyway (a
## TLDRheading, a leading status emoji in a table cell, a> **Note:**blockquote) to add some colour and hierarchy. A document with none of those still renders; you just get plain output. - A document that uses Facet's additions stays ordinary Markdown. The
additions are fenced code blocks with an info string, so a chart fence falls
back to a syntax-highlighted code block on GitHub or in any other viewer. The
directive syntax (
:::interact) shows up as its literal source line. Nothing needs a converter, and nothing is lost if you stop using Facet.
The plan also stays a file in the repository, so it reviews in a pull request and greps like anything else.
There is no platform under any of this. Facet is a binary that reads files on your disk and serves them to a local browser window. No account, no hosted workspace, no proprietary format, no sync, no egress. Any program that can write a file and run a command can drive it, so the Claude Code skill and the MCP shim are conveniences rather than requirements. Stop using it and you still have the Markdown, the Git history, and a plain-text journal of every decision.
Download the binary for your platform from the
latest release, put
it on your PATH, then:
facet path/to/plan.mdThe first call starts the daemon and opens the window for you; the command itself returns straight away. On Windows that is a native WebView2 window, which ships with Windows 10 and 11. Elsewhere, and on a Windows image without WebView2, it is a Chrome app-window, so a Chromium-family browser (Chrome, Chromium, or Edge) is the fallback requirement.
There is one window. Repeat invocations re-point it and register each document
as a named session on the same local daemon. Each release carries SHA256SUMS
and THIRD-PARTY-NOTICES.md beside the binaries.
go installis not a supported install path. The binary embeds a web bundle that is built by the release pipeline rather than committed to the tree, sogo installhas nothing to embed. Build from source withtask buildinstead (see below).
facet <artifact.md> open a rendered document
facet new <genre> scaffold plan | decision | review-findings | perf-report | ...
facet lint <doc...> validate the added syntax, file:line diagnostics
facet await ... block for the next human gesture, returned as data
facet focus ... point the ghost-gaze at a block
facet export <doc> bake a document and its decisions into one offline HTML file
facet mcp run the MCP shim over stdio
Facet works on its own as a viewer. The loop above only closes when the program writing your Markdown knows the verbs. Both pieces below are optional.
facet installThat writes the /facet skill to ~/.claude/skills/facet/SKILL.md, which
teaches Claude Code the authoring conventions, the workflow verbs, and when a
gate is worth opening. Re-run it after upgrading the binary; it overwrites in
place.
For the MCP tools (facet_open, facet_sessions, facet_focus, facet_cue,
facet_await, facet_reply), register the shim with your client:
claude mcp add --scope user facet -- facet mcpor per project, in .mcp.json:
{ "mcpServers": { "facet": { "command": "facet", "args": ["mcp"] } } }Facet never edits your client config for you; ~/.claude.json and .mcp.json
stay yours.
If you use a different agent, or you just want to hand one a single page to read, point it at docs/for-agents.md. It covers what Facet is, how to install it, what Markdown to write, and how the click comes back, and it is written to be read cold.
Two layers. The first needs nothing from you; the second is a small amount of extra syntax.
| You write | Facet renders |
|---|---|
## TLDR: ... |
a tinted banner at the top of the document |
a leading ✅ in a table cell |
a toned verdict pill |
> **⚠ SECURITY:** ... |
an amber callout block |
## HIGH and a leading 🔴 on a finding |
a severity rail down the findings |
```python |
offline syntax highlighting |
three or more ## sections |
fold-by-heading and a jump-nav table of contents |
[plan](../active/x.md) |
in-Facet navigation with a breadcrumb and a back button |
Markers are read leading-only, so an emoji in the middle of a sentence is ignored and ordinary prose never gets decorated by accident. The last row is what makes a plan, its ADR and its runbook walk as one set.
```bar
title: Upload latency by stage (ms), inline vs enqueued
x: stage
y: ms
series: inline, enqueued
validate, 40, 38
decode, 1180, 0
store, 210, 205
```
| Fence | Renders |
|---|---|
bar line scatter dist |
themed, interactive Vega-Lite charts from a comma-split grammar |
board |
a triage board: col: headers, then id text !cue cards |
flame |
an interactive icicle graph from folded stacks |
diff |
a unified diff with hunk line numbers and add/delete stats |
mermaid |
themed diagrams (flowchart, sequence, state, gantt) |
vega-lite |
a raw v6 spec, for charts the terse grammar can't express |
math |
KaTeX, rendered offline |
There is also a small directive vocabulary for components Markdown has no word
for: kpi, card, status, callout, timeline, steps, compare,
callouts (line-pinned code annotations), and interact.
facet lint checks all of it and reports file:line diagnostics, so the
author gets strictness while the reader never gets a broken page. A malformed
chart shows an inline error, an unknown fence falls back to a code block, and a
failure in the render pipeline falls back to raw <pre>. Readable Markdown is
the floor.
![]() Review findings: severity rails, toned finding rows, offline syntax highlighting. |
![]() Diagrams: Mermaid, themed to match and rendered offline. |
The agent pays for every plan it writes, so the added syntax is priced against
the alternative. Measured with the cl100k_base tokenizer, against the closest
thing that produces the same rendered component:
| Component | Facet | Equivalent | Ratio |
|---|---|---|---|
| Grouped bar chart | 75 | 360 (Vega-Lite JSON) | 4.8x |
| Four-column triage board | 69 | 234 (inline HTML) | 3.4x |
| Five directive components | 126 | 713 (inline HTML) | 5.7x |
A select decision gate |
36 | no equivalent | n/a |
The whole of examples/rollout-plan.md comes to 949 tokens, and that includes
a TLDR banner, a status table, a triage board, a grouped bar chart, a stepper,
a comparison table, two decision gates, two callouts, and a flame graph. The
cost is close to what the same document would have cost as prose.
Per-directive breakdown: docs/authoring.md.
Five worked examples ship in examples/: a rollout plan, an event
pipeline decision record, a storage decision, a cutover runbook, and a code
review. Open any of them with facet examples/<file>.md.
Facet binds 127.0.0.1 only, rejects foreign Host headers, and requires a
random per-daemon token for every mutation. Rendered HTML passes through
rehype-sanitize with an allowlisted schema and is served under a restrictive
CSP. The page makes no external requests, and a test enforces that. app
fences run inside sandboxed iframes with no egress.
⚠ Before you publish a repository that has used Facet: Markdown artifacts and committed tour journals can carry sensitive material. Review the working tree and the Git history. See SECURITY.md for the full trust boundaries and the private reporting process.
Facet is beta software at 0.x. It gets used daily on real work and the invariants below are covered by tests, but the surface is still moving and the rough edges are real. Bug reports, questions, and disagreements about the design are all welcome in issues; CONTRIBUTING.md covers sending a change.
The direction is a shared working surface rather than a viewer: the agent drafts, you decide in the page, and the record of each decision sits next to the thing it decided.
Facet is versioned with SemVer, so 0.x releases may break things. Rather than leave that as a blanket warning, here is what is actually in motion.
Held stable by intent, and pinned by tests:
- Your documents stay yours. Every addition is a fenced code block or a
directive line, so a Facet document renders as ordinary Markdown anywhere
else. Facet never writes your
.md. - A render never leaves you with a blank page. A failed pass degrades to the raw markdown; an unknown fence degrades to a code block.
- Loopback only. The daemon binds
127.0.0.1, refuses a foreignHostheader, and the page makes no external request. - Recorded decisions stay readable. Tour journals carry a schema version, and a journal written by an older Facet keeps replaying.
Expected to change before 1.0:
- What Facet recognises will grow. New markdown shapes get picked up over time. This is additive: it makes previously-plain documents render richer.
facet lintstrictness is unsettled. Whether authoring mistakes should fail loudly rather than degrade quietly is an open question, so exit codes and diagnostics may shift.- The MCP tool surface may change.
facet_open,facet_awaitand friends are young, and the underlying transport is expected to move to the MCP Apps AppBridge. Pin a version if you script against them. - The tour journal schema may gain a version. Lines are stamped
"v":1; read byvand ignore fields you do not recognise.
Breaking changes are called out in CHANGELOG.md.
git clone https://github.com/Smitner-Studio/facet.git
cd facet
task build # web bundle, embed, bin/facet
task test # vitest suite plus go test ./...Requires Task, Go 1.25 or newer, Node.js 20.19+ or 22.12+, and npm 10 or newer.
The renderer bundle under web/dist/ is a build output and is not tracked in
Git. It is go:embed'd, so a bare go build fails on a fresh clone until the
bundle exists. Run npm run build --prefix web first, or use any task target,
which sequences it for you.
Renderer syntax and worked examples: docs/authoring.md. Before sending a change: CONTRIBUTING.md.
MIT.





