Skip to content

Repository files navigation

Facet

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.

License: MIT Go 1.25+ Status: beta

A plan rendered by Facet: TLDR banner, status table, and triage board

examples/rollout-plan.md, 102 lines of plain Markdown, rendered.

Why this exists

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.

The loop

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
Loading

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.

Decisions come back as data

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,reject

and 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 chart and a decided approval gate

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
Loading

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.jsonl journal next to the file.

Built for long documents

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
Selected text with a comment composer quoting the span
Select to comment: a selection raises the chip, and the composer quotes the span it is about.
The attention drawer showing an open decision and a comment thread
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.

It renders the Markdown your agent already writes

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 ## TLDR heading, 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.

Nothing to be locked into

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.

Install

Download the binary for your platform from the latest release, put it on your PATH, then:

facet path/to/plan.md

The 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 install is not a supported install path. The binary embeds a web bundle that is built by the release pipeline rather than committed to the tree, so go install has nothing to embed. Build from source with task build instead (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

Teaching your agent to use it

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 install

That 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 mcp

or 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.

What Facet adds

Two layers. The first needs nothing from you; the second is a small amount of extra syntax.

Conventions it recognises

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.

Fences

```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 with severity rails
Review findings: severity rails, toned finding rows, offline syntax highlighting.
A themed Mermaid architecture diagram
Diagrams: Mermaid, themed to match and rendered offline.

Token cost

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.

Examples

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.

Security model

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.

Beta, and where it is going

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.

Stability

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 foreign Host header, 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 lint strictness 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_await and 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 by v and ignore fields you do not recognise.

Breaking changes are called out in CHANGELOG.md.

Build from source

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.

License

MIT.

About

Render agent-written Markdown as a live local page, and send the human's click back to the agent as typed data. Human-in-the-loop approval gates for coding agents.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages