Elementy a druhy karet
Typy elementů, karty nástěnky a návody pro skládání obsahu.
Reference slouží pro compose_board, HTTP endpoint pro skládání a dokumenty publikované přes vitrinka board push. Každé téma popisuje formát dat a podporované použití; karty nástěnky a elementy dokumentů nejsou zaměnitelné. Názvy a popisy přebíráme přímo ze serverové dokumentace, proto zůstávají anglicky. Stejný obsah vrací docs {topic} přes MCP nebo vitrinka docs <topic>.
Druhy elementů
chart — data visualization — series charts, proportions, KPI stats, timelines, heatmaps
One charting language for boards, docs and artifacts. Pick a viz, then fill the data slot that viz reads: data.series (line|area|bars|scatter), data.items (pie|donut|bars|stat|timeline|flow|matrix) or data.x/y/values (heatmap). Series points are numbers or [x, y] pairs; scatter is pairs only. Bulk data may live behind the envelope's dataRef instead of inline — compose inline, the server spills past the threshold. Tone is explicit meaning, never inferred from a number's sign. Three optional upgrades: data.refs draws reference lines (a constant value or one point per x) over line|area|bars|scatter; data.calendar turns a heatmap into a calendar of day values (week columns × weekday rows); data.rank turns bars items into ranked share bars with an "other" row. viz:"stat" is a single-stat CHART (board-viz register); the sibling stat KIND is the document register's KPI strip — prefer the kind for headline rows. viz:"compare" is the before/after benchmark: data.items[{label, before, after, note?, tone?}] with data.better ("lower" | "higher", REQUIRED — it declares what an improvement is), data.unit and the optional series names data.before / data.after (default Before / After). Each row paints a muted before bar and an accent after bar on one shared scale plus a delta chip (percent change · ratio, e.g. −83% · 5.9×): an item's tone wins, else moving the better way is good, the worse way bad, unchanged neutral — e.g. {"viz":"compare","title":"API latency, p95","data":{"unit":"ms","better":"lower","items":[{"label":"GET /slots","before":820,"after":140}]}}. A chart that mixes metrics gives an item its own unit — items[].unit ("KB", "%") overrides data.unit for that row's values — and bars share a scale only within one unit: each unit's rows scale to that unit's largest value, never against another unit's.
| Vlastnost | Typ | Poznámky |
|---|---|---|
viz | "line" | "area" | "bars" | "scatter" | "pie" | "donut" | "heatmap" | "stat" | "matrix" | "flow" | "timeline" | "compare" | povinné which visualization to render; decides the required data slot |
title | string | heading rendered above the chart |
caption | string | mono caption under the chart; inline markup allowed |
data | { series?: ({ label?: string; points?: (number | [number, number])[]; values?: (number | [number, number])[] })[]; items?: ({ label?: string; value?: string | number | boolean | null; display?: string; delta?: string | number | boolean | null; unit?: string; tone?: "good" | "warn" | "bad" | "neutral"; when?: string; x?: number; y?: number; spark?: number[]; before?: number; after?: number; note?: string })[]; x?: (string | number)[]; y?: (string | number)[]; values?: number[][]; min?: number; max?: number; stack?: boolean; unit?: string; refs?: ({ label?: string; value?: number; points?: number[] })[]; calendar?: { from: string; values: number[] }; rank?: number; better?: "lower" | "higher"; before?: string; after?: string } | the viz's data — replaceable wholesale by the envelope's dataRef |
Příklad:
{"kind":"chart","payload":{"viz":"line","title":"Weekly actives","data":{"x":["W1","W2","W3"],"series":[{"label":"web","points":[120,180,240]},{"label":"cli","points":[40,90,160]}]}}}chips — meta chip row — small key·value facts in quiet pills
items are {k, v} pairs rendered as outlined pills in order — counts, spans, versions, owners. Facts, not prose: keep both sides short.
| Vlastnost | Typ | Poznámky |
|---|---|---|
items | ({ k: string; v: string })[] | povinné the chips, in order |
Příklad:
{"kind":"chips","payload":{"items":[{"k":"journeys","v":"9"},{"k":"window","v":"Jun 4 – Aug 28"}]}}code — code listing — syntax-highlighted source with an optional caption
source is the code text verbatim; lang picks the highlighter grammar (omit to autodetect). caption renders as a small line under the block.
| Vlastnost | Typ | Poznámky |
|---|---|---|
source | string | povinné the code text, verbatim |
lang | string | highlighter language id (go, ts, sql, …); omit to autodetect |
caption | string | small caption under the listing |
Příklad:
{"kind":"code","payload":{"lang":"go","source":"func main() {\n\tfmt.Println(\"hello\")\n}","caption":"the whole program"}}columns — columns — 2–3 side-by-side columns of nested elements; collapses when narrow
cols is an ordered list of 2–3 columns, each {elements} — full child elements validated recursively (the same nesting cap as doc). ratio sets the widths: 1:1 (default), 1:2, 2:1 for two columns, 1:1:1 for three; a ratio's part count must equal the column count. Below the renderer's narrow threshold the columns stack in order. Use it for a score strip beside its verdict or a meter beside its chart — not for prose, which reads best in one measure.
| Vlastnost | Typ | Poznámky |
|---|---|---|
ratio | "1:1" | "1:2" | "2:1" | "1:1:1" | column widths: 1:1 | 1:2 | 2:1 (two columns), 1:1:1 (three); default 1:1 |
cols | ({ elements: Element[] })[] | povinné the columns, left to right — 2 or 3, each with at least one element |
Příklad:
{"kind":"columns","payload":{"ratio":"2:1","cols":[{"elements":[{"kind":"stat","payload":{"items":[{"label":"score","value":"86 %","tone":"good"}]}}]},{"elements":[{"kind":"verdict","payload":{"state":"Recommend with reservations","tone":"warn"}}]}]}}compare — side-by-side comparison — two labeled columns of nested elements
Two sides, a and b, each {label, elements}. The sides render as columns; each side's elements are full child elements (prose, chart, table, …), validated recursively. Label both sides — the reader can't tell unlabeled columns apart.
| Vlastnost | Typ | Poznámky |
|---|---|---|
a | { label?: string; elements: Element[] } | povinné the left side |
b | { label?: string; elements: Element[] } | povinné the right side |
title | string | heading rendered above the comparison |
Příklad:
{"kind":"compare","payload":{"a":{"label":"Before","elements":[{"kind":"prose","payload":{"md":"Two painters."}}]},"b":{"label":"After","elements":[{"kind":"prose","payload":{"md":"One painter."}}]}}}deploy — deploy tile — what reached an environment, and whether it runs there now
The newest deploy to env (sha, at); count is how many landed in the period; live marks the one the environment runs now.
| Vlastnost | Typ | Poznámky |
|---|---|---|
env | string | povinné environment, e.g. prod |
sha | string | povinné deployed commit |
at | string | when it landed (RFC 3339) |
count | number | deploys in the period |
live | boolean | the environment runs this one now |
Příklad:
{"kind":"deploy","payload":{"env":"prod","sha":"3f9a1c2e","at":"2026-10-12T08:12:00Z","count":4,"live":true}}diagram — architecture diagram — semantic nodes/edges/lanes; the server computes all layout
Send SEMANTICS ONLY: nodes (typed: box|entity|endpoint|service|note), groups (nestable), swimlanes, edges (from/to by id or node.port), a stat strip and a legend. The server lays out every rectangle and route deterministically and stamps geo/overrides/metrics into the payload — those fields are server-owned output: accepted on round-trip, never authored. Tones/shapes/edge styles are an open vocabulary — unknown values degrade to defaults, so misspellings warn with a fix instead of blocking.
| Vlastnost | Typ | Poznámky |
|---|---|---|
dir | "TD" | "LR" | flow direction: top-down (default) or left-right |
kicker | string | small eyebrow line above the title |
title | string | the diagram's title — required unless a mermaid import supplies one |
subtitle | string | supporting line under the title |
caption | string | mono caption under the plate; inline markup allowed |
stats | ({ value: string; label: string })[] | headline stat strip above the content |
nodes | ({ id?: string; type?: string; title: string; body?: string; chips?: ({ text: string; tone?: string })[]; icon?: string; shape?: string; tone?: string; ports?: ({ id: string; label?: string; side?: string; order?: number })[]; columns?: ({ name: string; type?: string; pk?: boolean; fk?: string; unique?: boolean; nullable?: boolean })[]; method?: string; path?: string; codes?: number[]; tag?: string; image?: string; replicas?: number; mappings?: ({ host: string; cont: string; proto?: string })[]; doc?: number; docAnchor?: string })[] | loose nodes (laneless diagrams) |
groups | unknown[] | top-level groups {id?, title, meta?, tone?, nodes?, groups?} — nest freely |
lanes | ({ label: string; note?: string; nodes?: ({ id?: string; type?: string; title: string; body?: string; chips?: ({ text: string; tone?: string })[]; icon?: string; shape?: string; tone?: string; ports?: ({ id: string; label?: string; side?: string; order?: number })[]; columns?: ({ name: string; type?: string; pk?: boolean; fk?: string; unique?: boolean; nullable?: boolean })[]; method?: string; path?: string; codes?: number[]; tag?: string; image?: string; replicas?: number; mappings?: ({ host: string; cont: string; proto?: string })[]; doc?: number; docAnchor?: string })[]; groups?: unknown[] })[] | swimlane bands; contain their members |
edges | ({ from: string; to: string; style?: string; label?: string; card?: string })[] | connections between nodes |
legend | ({ style: string; label: string })[] | explains the edge styles in use |
footer | string | footnote under the diagram |
quality | string | per-WRITE strict tier ("showcase" fails on composition findings) — consumed at write time, never stored |
mermaid | string | mermaid source to IMPORT when the payload has no nodes, groups or lanes: the server converts it at write time, then lays out as usual. Types: flowchart/graph (subgraphs become groups), sequenceDiagram (participants in one lane; alt/else, opt, loop, par/and, critical/option and break blocks prefix their message labels, e.g. "[alt intake disabled] save") and stateDiagram/stateDiagram-v2 ([*] becomes Start/End, a composite state block a group, notes skipped). Any other type (class, ER, gantt, pie) is refused with the supported list; rewrite it as a flowchart or author nodes/edges |
source | unknown | living-source pointer ({kind, ref, rev}) — refreshed server-side via POST /cards/{id}/refresh |
geo | unknown | SERVER-OWNED layout output (boxes/ports/order/routes) — never author it |
render | unknown | SERVER-OWNED stamped theme renders ({svgLight, svgDark}) — never author it |
overrides | unknown | SERVER-OWNED human ⊕ edit layer, survives re-layout — never author it |
metrics | unknown | SERVER-OWNED quality score output — never author it |
repairs | unknown | SERVER-OWNED mechanical-repair log — never author it |
diagnostics | unknown | SERVER-OWNED scored findings — never author it |
Příklad:
{"kind":"diagram","payload":{"title":"Ingest path","dir":"LR","nodes":[{"id":"cli","title":"CLI","type":"box"},{"id":"api","title":"API","type":"service"}],"edges":[{"from":"cli","to":"api","label":"push"}]}}doc — composite document — an element composing child elements, the artifact register
elements is an ordered list of full child elements (prose, chart, table, diagram, compare, …), validated recursively — findings carry the nested path (payload.elements[2].payload…). direction seeds the design register — release is the release-notes register (chapter-style area sections, KPI tiles, a gradient display title); accentColor — a named accent or a {light, dark} pair of #rrggbb hexes — is the document's own colour, stored as the pair; without it the workspace accent applies. A pushed doc.json IS this element, the same shape a board composes; nesting caps at 6 levels: flatten instead of matryoshka-ing docs in docs. One document can serve several readers: any element (a doc.json block likewise) may carry an envelope audience — 1–3 of public|technical|internal, beside kind and payload — and the viewer's audience switch and the exports (?audience=) show an element only to the audiences it names; untagged means everyone, a table row may carry its own ({cells, audience}), and a container (doc, section, columns, compare, gallery, a table's rows) whose children are all filtered out disappears with them.
| Vlastnost | Typ | Poznámky |
|---|---|---|
title | string | the document's title |
kicker | string | small eyebrow line above the title |
direction | "editorial" | "dossier" | "terminal" | "gallery" | "release" | the design register the renderer leans into |
accent | string | a phrase of the title the display type highlights |
intro | string | the lede under the title — inline markdown |
chips | ({ k: string; v: string })[] | quiet key · value facts under the lede — window, branch, status |
accentColor | "red" | "coral" | "amber" | "olive" | "teal" | "azure" | "violet" | "magenta" | { light: string; dark: string } | the document's accent: a named accent or a {light, dark} pair — the push stores the pair; absent = the workspace accent |
project | string | the project the document reports on — with no accentColor the set's raw page hashes it to one of the named accents (the card keeps the workspace accent) |
sources | { [key: string]: { repo: string; ref: string } } | repository aliases a link target may cite as alias:path#L3-L9 — a push expands each to its GitHub URL and verifies it when the deployment has a GitHub credential |
elements | Element[] | povinné the document's content, in order — full child elements validated recursively |
Příklad:
{"kind":"doc","payload":{"title":"Weekly readout","direction":"editorial","elements":[{"kind":"prose","payload":{"md":"## Shipped\n\nThe element contract."}},{"kind":"stat","payload":{"items":[{"label":"PRs","value":4}]}}]}}epic — feature tile — a feature with its phase, progress and result screenshot
A feature as a person reads it: its result (title), the phase it reached (planned · built · verified · shipped — History's palette), done/total of its work and a cover screenshot of the result. live pulses it amber while someone works on it; who names them. mark says why a timeline shows it on a week.
| Vlastnost | Typ | Poznámky |
|---|---|---|
title | string | povinné the feature's result, else its title |
phase | "planned" | "built" | "verified" | "shipped" | phase reached; default planned |
mark | "built" | "verified" | "shipped" | "building" | "due" | "pinned" | why a timeline shows it on a week |
done | number | work children done |
total | number | work children in all |
cover | string | result screenshot URL |
live | boolean | someone works on it right now |
who | string[] | who works on it right now |
id | number | task id |
url | string | task URL as the server printed it |
page | string | feature page URL |
type | string | task type word |
at | string | when it reached the mark (RFC 3339) |
Příklad:
{"kind":"epic","payload":{"title":"Checkout works offline","phase":"built","done":5,"total":8,"cover":"https://app.vitrinka.ai/w/acme/uploads/qa/checkout.png"}}figure — image figure — a picture with caption and alt text
Reference the image by src (URL), ref (a blob/upload reference) or shot (an index into the owning set's data.json shots — only meaningful inside a doc minted from a semantic push). caption renders under the figure; alt is the accessible description — write it for a reader who can't see the image.
| Vlastnost | Typ | Poznámky |
|---|---|---|
src | string | image URL |
ref | string | blob/upload reference (server-resolved) |
shot | number | index into the owning set's data.json shots (doc-card form) |
caption | string | caption rendered under the figure |
alt | string | accessible description of the image |
Příklad:
{"kind":"figure","payload":{"src":"/uploads/flow.png","caption":"The ingest flow","alt":"Flowchart: CLI pushes to the API, which fans out to layout and search"}}finding — one review finding — severity rail, locator, cause/cost, trace, fix and evidence receipt
sev (high|medium|low) colors the left rail; tag is a free category ("perf", "a11y"); status is the verification chip (verified|confirmed|fixed read green, suspected|likely|open amber, refuted red, wont-fix gray). title, where (a mono locator: file:line, route, query), body, cause and cost cards, summary (the one bold conclusion), fix (one line) and trace (before/after resolution) are all optional. evidence is the mono receipt footer — one line or a list; a finding without evidence reads as an opinion (warned).
| Vlastnost | Typ | Poznámky |
|---|---|---|
sev | "high" | "medium" | "low" | severity: high|medium|low — colors the rail |
tag | string | free-text category chip ("perf", "a11y", "race") |
status | string | verification state chip: verified|confirmed|fixed · suspected|likely|open · refuted · wont-fix |
title | string | one-line finding title |
where | string | mono locator: file:line, route, query |
body | string | body prose; blank lines split paragraphs |
cause | string | what produced the defect — a labeled "cause" card |
summary | string | the one bold conclusion sentence |
cost | string | what the defect costs (time, ms, money) — a labeled "cost" card |
fix | string | the fix, one line |
trace | { before: { key: string; dest: string; note?: string }; after: { key: string; dest: string; note?: string } } | before/after resolution trace |
evidence | string | string[] | the receipt footer — one line or a list of lines citing the exact log, file, query or check |
Příklad:
{"kind":"finding","payload":{"sev":"high","tag":"perf","status":"fixed","title":"Cart cache never invalidates after a coupon edit","where":"internal/cart/cache.go:141","cause":"The coupon write path bypasses cart.Invalidate.","fix":"Route the write through the repository.","evidence":["cart_cache_miss_total{path=\"coupon\"} 1.0 → 0.02"]}}gallery — image gallery — 1–24 images as a grid, bento mosaic, masonry wall or carousel
items are 1–24 images, each referenced like a figure: src (URL), ref (a blob/upload reference) or shot (an index into the owning set's data.json shots — only inside a doc minted from a semantic push); alt is the accessible description (a missing one warns) and caption a line of inline markdown under the image (links render). layout picks the arrangement: grid (default) — uniform 2–3 columns of 16:10 tiles; bento — a 6-column mosaic, the first item large, other spans by count, span (wide|tall|large, bento only) overriding an item's tile; masonry — 2–3 columns at each image's natural aspect; carousel — one slide at a time with prev/next and position dots. Every layout prints and exports as a 2-column grid of ALL items, so a carousel never hides a slide on paper. Clicking an image opens the lightbox stepping through the gallery. An item may carry its own audience (like an element's) — a gallery left with no item for the reader disappears. Use a figure for one image.
| Vlastnost | Typ | Poznámky |
|---|---|---|
layout | "grid" | "bento" | "masonry" | "carousel" | grid (default) | bento | masonry | carousel — the arrangement; the painter owns the geometry |
caption | string | caption under the whole gallery; inline markup allowed |
items | ({ src?: string; ref?: string; shot?: number; alt?: string; caption?: string; span?: "wide" | "tall" | "large"; audience?: ("public" | "technical" | "internal")[] })[] | povinné the images, in order — 1 to 24, each with src, ref or shot like a figure |
Příklad:
{"kind":"gallery","payload":{"layout":"bento","caption":"The new booking flow","items":[{"src":"/uploads/slots.png","alt":"Slot picker with three free times","caption":"Pick a slot"},{"src":"/uploads/cart.png","alt":"Cart with one booking"},{"src":"/uploads/paid.png","alt":"Payment confirmation","span":"wide"}]}}goal — goal tile — a sprint's outcome with its dates and progress
A goal (sprint) as a person reads it: title, the outcome it promises, its status word and its work done of total.
| Vlastnost | Typ | Poznámky |
|---|---|---|
title | string | povinné the goal's name |
outcome | string | the outcome it promises |
status | "running" | "overdue" | "planned" | "done" | status word; default planned |
starts | string | start (RFC 3339) |
ends | string | end (RFC 3339) |
percent | number | work done, 0-100 |
done | number | work done |
total | number | work in all |
url | string | the goal's card |
Příklad:
{"kind":"goal","payload":{"title":"Offline checkout","outcome":"A cashier can sell without a network","status":"running","percent":62,"done":13,"total":21}}html — custom HTML island — sandboxed escape hatch when no hard component fits
Provide src (a complete inline HTML document) or files (a flat path → content map that must include index.html); a pushed file set's index.html arrives as href, written by the server, never authored. The island renders in the sandboxed iframe with the unified injector stack — theme sync, size beacon, capture, wheel chaining — so it behaves like a native face. The card takes the document's natural size — any width, 1:1 up to 1600px, wider scales down — so a desktop or multi-phone mock is as welcome as a widget; width/height pin the box instead of measuring. Reach for a hard component first: an island is opaque to search, distillation and in-place editing.
| Vlastnost | Typ | Poznámky |
|---|---|---|
src | string | a complete HTML document, inline |
files | { [key: string]: string } | flat path → content map; must include index.html |
href | string | served document URL (a pushed set's raw index.html) — written by the push, not authored |
title | string | the island's label (the face's kicker and the frame's accessible name) |
width | number | pinned document width in px (omit to auto-measure) |
height | number | pinned render height in px (omit to auto-measure) |
natW | number | measured document width in px — written by the board's size beacon, not authored |
natH | number | measured document height in px — written by the board's size beacon, not authored |
Příklad:
{"kind":"html","payload":{"src":"<!doctype html><html><body><h1>Custom island</h1></body></html>"}}list — marked list — bullet, check or numbered lines
items are the lines (inline markup allowed); style picks the marker: bullet (em-dash, default) | check (green ✓) | number (mono decimal). Prose markdown lists are for running text; this kind is a standalone checklist or ordered plan.
| Vlastnost | Typ | Poznámky |
|---|---|---|
style | "bullet" | "check" | "number" | marker style: bullet (default) | check | number |
items | string[] | povinné the lines, in order; inline markup allowed |
Příklad:
{"kind":"list","payload":{"style":"check","items":["Cache fix merged and verified in prod","Alert threshold lowered to 380 ms"]}}media — media file — video, audio, PDF or download, by URL or blob ref
Reference the file by src (URL) or ref (a blob/upload reference); mime hints the renderer when the URL doesn't say. PDFs embed as live documents, video/audio get players, anything else renders as a download chip. alt describes the content accessibly.
| Vlastnost | Typ | Poznámky |
|---|---|---|
src | string | file URL |
ref | string | blob/upload reference (server-resolved) |
mime | string | MIME type hint when the URL doesn't reveal it |
caption | string | caption rendered under the media |
alt | string | accessible description of the content |
Příklad:
{"kind":"media","payload":{"ref":"blob:demo-walkthrough","mime":"video/mp4","caption":"The 90-second walkthrough"}}meter — meters — labeled value/max bars with optional thresholds
Each item draws value against max (the scale is the point — max is mandatory and positive). label and a numeric value are required; threshold draws a tick; tone states explicit meaning (good|warn|bad|neutral), never inferred from the numbers.
| Vlastnost | Typ | Poznámky |
|---|---|---|
items | ({ label?: string; value?: number; max?: number; threshold?: number; unit?: string; note?: string; tone?: "good" | "warn" | "bad" | "neutral" })[] | povinné the meters, one bar each |
title | string | heading rendered above the meters |
Příklad:
{"kind":"meter","payload":{"items":[{"label":"Blob storage","value":41,"max":100,"threshold":80,"unit":"GB","tone":"good"}]}}milestone — milestone tile — a dated mark, reached or still ahead
due is the date it is set for; reached, once set, is when it was met.
| Vlastnost | Typ | Poznámky |
|---|---|---|
title | string | povinné the milestone's name |
due | string | due date (RFC 3339) |
reached | string | when it was reached (RFC 3339) |
Příklad:
{"kind":"milestone","payload":{"title":"Public beta","due":"2026-11-02T00:00:00Z"}}mockup — UI mockup — frames of primitive trees; sketch screens cheaply, edit by direct manipulation
Frames hold primitive trees rendered top-to-bottom (stacks nest row/col layout). The vocabulary is deliberately lo-fi — sketching a proposed screen costs ~100-250 tokens, and the armed editor moves/resizes/restyles primitives directly. Style tokens (palette/radius/font) tint the whole mockup. A board wireframe payload maps in as one frame. Vectors and component variants are a later initiative.
| Vlastnost | Typ | Poznámky |
|---|---|---|
frames | ({ name: string; w?: number; h?: number; tree: ({ t: string; label?: string; primary?: boolean; h?: string | number; n?: number; dir?: "row" | "col"; name?: string; children?: unknown[] })[] })[] | povinné the screens, side by side |
tokens | { palette?: string; radius?: number; font?: string } | style tokens tinting the whole mockup |
Příklad:
{"kind":"mockup","payload":{"frames":[{"name":"phone","tree":[{"t":"nav","label":"Inbox"},{"t":"list","n":4},{"t":"button","label":"Compose","primary":true}]}]}}page — document page — markdown truth with a collaborative editor mirror
md is the durable truth and the only field an agent writes. The editor keeps its own ProseMirror mirror (pm) with pmStale mediating — both are server-owned: accepted on round-trip, never authored. source {kind:"mdfile", ref, rev?} ties the page to a repo file the push path refreshes.
| Vlastnost | Typ | Poznámky |
|---|---|---|
md | string | povinné the page's markdown — the document's durable truth |
source | { kind: string; ref: string; rev?: string } | repo-file tie; the push path refreshes md from it |
pm | unknown | SERVER-OWNED ProseMirror mirror — never author it |
pmStale | boolean | SERVER-OWNED mirror-freshness flag — never author it |
Příklad:
{"kind":"page","payload":{"md":"# Element contract\n\nOne vocabulary, every surface."}}prose — markdown text — headings, lists, links, icons, inline math
Markdown is the truth format. The read face renders the shared inline vocabulary (links, [!icon name], wikilinks, $math$, ==highlight== for a marked phrase — no == inside it); write characters directly (→, never →). Headings get stable anchor slugs.
| Vlastnost | Typ | Poznámky |
|---|---|---|
md | string | povinné the markdown source |
Příklad:
{"kind":"prose","payload":{"md":"## Findings\n\n- The cache **never** invalidates\n- See [[element-contract]]"}}ref — ref — a reference to another document card: link chip, preview or full embed; the server stamps the target and draws the edge
Exactly one of card (a card id on this workspace), page (a slug of a sibling page in the same push batch; the server resolves it to a card id and rewrites the ref) or section (the id of a section in THIS document — an in-page anchor link, rejected at push when no section carries that id) is required. mode is link (default) | preview | embed; a section ref is always a link. label overrides the chip text. target is SERVER-OWNED: the resolved card's summary (id, kind, title, kicker, board) stamped on write and refreshed on the target's change — never author it; an authored value is overwritten.
| Vlastnost | Typ | Poznámky |
|---|---|---|
card | number | the target card id (this workspace) |
page | string | a sibling page slug inside the same push batch — resolved server-side to card |
section | string | a section id in this document — renders as an in-page anchor link |
mode | "link" | "preview" | "embed" | link (inline chip, default) | preview (the target's face at LOD height) | embed (the full face) |
label | string | chip text override; default is the target's title |
target | unknown | SERVER-OWNED resolved target summary ({id, kind, title, kicker, board, …}) — never author it |
Příklad:
{"kind":"ref","payload":{"card":22351,"mode":"preview"}}release — release tile — a cut release, or the open one gathering what ships next
status cut is a release that went out (at = its cut); next is the open release and its merged PR count.
| Vlastnost | Typ | Poznámky |
|---|---|---|
title | string | povinné the release's name |
status | "next" | "cut" | cut, or the open next release; default cut |
prs | number | merged pull requests in it |
at | string | when it was cut (RFC 3339) |
url | string | the release board |
Příklad:
{"kind":"release","payload":{"title":"Release 14","status":"cut","prs":23,"at":"2026-10-09T15:00:00Z"}}section — titled section — eyebrow label, title, note and nested child elements
label is the mono uppercase eyebrow ("01 · Findings"), title the display heading (inline markup allowed), note the muted line under it. elements are full child elements validated recursively — the same 6-level nesting cap as doc. An empty section renders as a bare heading (warned). Every section is addressable: id is its anchor (the <section> element's id, so #<id> deep-links and {kind:"ref", section:"<id>"} cross-references land on it). Author it explicitly (^[a-z0-9][a-z0-9-]{0,63}$) or leave it out — canon derives it from the label (falling back to the title) with SectionSlug — diacritics folded to ASCII ("02 · Pozvánka" → 02-pozvanka) — suffixing -2, -3 on collisions in document order, so an unchanged label keeps its anchor across pushes.
| Vlastnost | Typ | Poznámky |
|---|---|---|
id | string | the section's anchor id — authored (lowercase, digits, hyphens; ≤64) or derived from the label |
label | string | mono uppercase eyebrow label ("01 · Findings") |
title | string | display-type section title; inline markup allowed |
note | string | muted note line under the title |
elements | Element[] | povinné the section body, in order — full child elements validated recursively |
Příklad:
{"kind":"section","payload":{"label":"01 · Findings","title":"What broke, and what it cost","elements":[{"kind":"prose","payload":{"md":"Two regressions, one fixed in-flight."}}]}}stat — KPI stat strip — headline numbers with tones, deltas and sparklines
3-6 stats read as headlines; more reads as a table (warned). label and value are required; value is any scalar (put formatted text in display when the raw value drives nothing); tone states MEANING (good|warn|bad|neutral) and is never inferred from the sign; spark draws an inline sparkline from plain numbers. Distinct from chart viz:"stat", which is a single-stat chart in the board-viz register — this kind is the document register's KPI strip.
| Vlastnost | Typ | Poznámky |
|---|---|---|
items | ({ label?: string; value?: string | number | boolean | null; display?: string; delta?: string | number | boolean | null; note?: string; unit?: string; tone?: string; spark?: number[] })[] | povinné the stats, left to right |
title | string | heading rendered above the strip |
Příklad:
{"kind":"stat","payload":{"items":[{"label":"Weekly actives","value":1240,"delta":180,"tone":"good","spark":[720,890,1060,1240]}]}}table — typed-column data table — filter/sort/group views, aggregates, in-place editing
Typed columns carry the cell vocabulary (text|number|date|select|check|link); rows are objects keyed by column key. A cell is a scalar, or {v, href?, tone?}: tone when its meaning needs stating (good|warn|bad|neutral — explicit, never sign-inferred), href when the cell is a link (https, mailto, #section-id, /path or a doc.json meta.sources alias — the cell renders as an anchor whose text is v). String cells render inline markdown exactly like prose and list items (**bold**, code, text). Saved views hold filter/sort/group state; aggregates render in the footer. Bulk rows may live behind the envelope's dataRef. The formula column type is reserved for a future formula engine — the validator names it, so don't emit it yet. A row written for some readers only wraps its cells: {cells: {…keyed…}, audience: [public|technical|internal, …]} (a doc.json row: {cells: [cell, …], audience}) — the viewer's audience switch and the exports drop it for everyone else, like an element's audience; a table left with no row for the reader disappears, and a bare row shows to every audience.
| Vlastnost | Typ | Poznámky |
|---|---|---|
title | string | heading rendered above the table |
caption | string | mono caption under the table; inline markup allowed |
cols | ({ key: string; label?: string; type?: "text" | "number" | "date" | "select" | "check" | "link"; options?: string[]; width?: number })[] | povinné the column definitions, in display order |
rows | ({ [key: string]: string | number | boolean | null | { v: string | number | boolean | null; href?: string; tone?: "good" | "warn" | "bad" | "neutral" } } | { cells: { [key: string]: string | number | boolean | null | { v: string | number | boolean | null; href?: string; tone?: "good" | "warn" | "bad" | "neutral" } }; audience: ("public" | "technical" | "internal")[] })[] | row objects keyed by column key; a cell is a scalar or {v, href?, tone?}; sparse rows are fine; a row for some readers only is {cells: {…}, audience: [...]} |
views | ({ name?: string; filter?: ({ col: string; op: "eq" | "ne" | "lt" | "gt" | "contains" | "empty" | "nonempty"; value?: string | number | boolean | null })[]; sort?: ({ col: string; dir?: "asc" | "desc" })[]; group?: string })[] | saved filter/sort/group views |
aggregates | { [key: string]: "sum" | "avg" | "count" } | footer aggregates, column key → function |
Příklad:
{"kind":"table","payload":{"cols":[{"key":"task","label":"Task"},{"key":"pts","label":"Points","type":"number"},{"key":"state","type":"select","options":["todo","doing","done"]}],"rows":[{"task":"Design the contract","pts":3,"state":"done"}],"aggregates":{"pts":"sum"}}}timeline — timeline — dated cards along a spine, each with status and evidence
items are the dated cards in story order: when (the date/time label) and title are the spine; status is a free chip (done|good|warn|bad, or the finding vocabulary); text is the body, summary the one bold line; evidence is the receipt — one line or a list. axis picks the form: spine (default, vertical, any when label) or time (a proportional horizontal axis; needs parseable dates, falls back to spine otherwise).
| Vlastnost | Typ | Poznámky |
|---|---|---|
items | ({ when?: string; title?: string; status?: string; text?: string; summary?: string; evidence?: string | string[] })[] | povinné the dated cards, in story order |
title | string | heading rendered above the timeline |
axis | "spine" | "time" | spine (default): a vertical dated spine that takes any when label; time: a proportional horizontal time axis when every when parses as a date — falls back to spine when one does not |
Příklad:
{"kind":"timeline","payload":{"items":[{"when":"Jul 18","title":"v3.5.2 regresses checkout p95","status":"bad","text":"Alert fires at 14:02 UTC.","evidence":"alertmanager: CheckoutP95High firing 2026-07-18T14:02Z"},{"when":"Aug 28","title":"v3.6 ships the fix","status":"good"}]}}verdict — the call — one line of state with its reasoning on a toned rail
state is the one-line call ("Ship it", "Hold the release"); text is the reasoning under it (blank lines split paragraphs). At least one of the two is required. tone colors the rail: good|warn|bad|info (default info) — explicit meaning, never inferred. Put it where the reader decides: top or bottom of the document.
| Vlastnost | Typ | Poznámky |
|---|---|---|
state | string | the one-line call (required unless text is given) |
text | string | the reasoning under the call; blank lines split paragraphs |
tone | "good" | "warn" | "bad" | "info" | rail color: good|warn|bad|info (default info) |
Příklad:
{"kind":"verdict","payload":{"state":"Ship v3.6 with the cache fix","text":"The p95 regression is explained and fixed on main.","tone":"warn"}}Druhy karet nástěnky
text — A markdown card: the basic thought unit, with a role that styles it
Full GFM markdown: headings, emphasis, ordered/unordered/task lists, tables, blockquotes, --- rules, images, links, fenced code with language highlighting, $…$/$$…$$ katex math, and [!icon <name>] inline Lucide glyphs. Raw HTML renders as literal text; unsafe link/image schemes are neutralized. The same renderer serves callout md, doc-card leaves, diary and personal boards.
note — A bare text note without markdown chrome
shape — A colored geometric shape
callout — A toned emphasis block
checklist — An interactive checklist — users tick items live and state persists
cluster — A grouping frame; untitled = invisible row/col group, pure flow intent
List member refs in children and they lay out inside it. {title, role:"kanban"} renders a status column. An UNTITLED cluster renders no chrome — 'these three on one row' is {kind:"cluster", payload:{layout:{mode:"row"}}, children:[…]}. Use it liberally instead of letting cards stack into a column. Frames nest depth ≤5.
Viz také: composing
section — A named journey frame: always visible, pill-navigable, the unit of scoping and iteration
Renders as a HEADED GROUP, not a box — kicker title plus optional one-or-two-line description (border only on hover/selection); give a description whenever the title alone doesn't orient a newcomer. Membership is spatial. Every tool scopes to sections; iterations are new sibling sections. A composed section lays its children out itself — the default is layout {mode:"grid", cols:2}: children fill rows left→right in compose order and wrap every cols cards, so a figure+finding pair stays side by side only when cols is even and a document flow (prose, findings, verdicts) reads best at cols:1 or 2; layout:null hands placement back to you.
Viz také: sections
step — A numbered journey step that embeds its screen — the canonical walkthrough shape
image names a file inside a set you already pushed (vitrinka board capture/push) — one steps-only section, no separate flow of shot cards; the server sizes the card to the screen's true aspect. cardId instead references an EXISTING board card for a live thumbnail. Use one or the other; never re-upload a screen you can reference.
compare — Two existing cards' faces behind a wipe/ghost slider — iteration pixel-diff
doc — A fractal document in one card — child elements composed top-to-bottom, ~300 tokens for a dense readout
Důležité poznámky:
- inner elements are pixels (not annotatable/wirable) — use real cards when structure matters
- the legacy {nodes:[{t,…}]} grammar is retired (migrate-and-drop): stored node docs were canonicalized by the v95 sweep; new writes reject it
Viz také: element:doc
link — A website card: server-unfurled metadata, screenshots, optional live embed
The server unfurls title/og:metadata/favicon async, probes iframe embeddability, and (with the capture sidecar) takes real 16:10 desktop + phone-viewport screenshots. Previews fetch public internet addresses only: a loopback, private or link-local target (the redirect chain included), or any link on an air-gapped install, keeps the bare clickable URL with the reason in payload.error (and in the reply's lint when the host itself is refused). preview = 16:10 face · phone = mobile frame · widget = compact favicon+title row · live = embedded iframe when the site allows framing.
board — A portal card: a live minimap window into another board
Důležité poznámky:
- the target board must exist; self-portals 400
diagram — An architecture page in one card: lanes, groups, typed nodes, ports, edges — server-laid-out
The agent sends semantics; the server computes ALL geometry (Sugiyama layout + orthogonal routing + ports) into payload.geo and the client scales it uniformly. Nodes take icon?: <lucide-name>. Living sources (payload.source) refresh via POST /cards/{id}/refresh; human overrides survive the refresh. Full vocabulary: the artifact skill's diagram reference.
Viz také: api
api — An OpenAPI reference card: the whole raw spec, rendered as a browsable grouped-endpoints reference
Send the WHOLE spec object — no conversion, no summarizing. Must carry openapi/swagger + paths and re-marshal under 512 KB; title defaults to info.title. Complements the openapi→diagram importer (vitrinka board diagram import <spec> --board <slug>): the diagram face shows shape, the api face shows the endpoint contract — one spec, two faces.
Viz také: diagram
page — A Confluence-grade living document card: markdown truth, block editor, repo-backed refresh
payload.md (markdown) is the durable agent-native truth; payload.pm is the human editor's ProseMirror mirror; the server never converts — it maintains pmStale. Agents write markdown; human block edits round-trip. #E<elemNo> anchors deep-link. Full contract: the artifact skill's pages reference.
artifact — A self-contained HTML bundle in a sandboxed iframe — the custom-HTML escape hatch
Send ONE shape, never both. html = full document (must start with <!doctype or <html>, ≤8 MiB; sandbox CSP blocks external network — no CDNs, no fetch/XHR; inline your assets). The pinned /vendor/… runtime shelf is the one exception: the vitrinka host serves React, Recharts and friends so a full document imports them instead of inlining. body+runtime "tw4" = markup only, server-wrapped with the vendored Tailwind v4 build — dramatically cheaper; prefer it whenever the design fits Tailwind utilities.
Důležité poznámky:
- prefer chart/table/mockup/doc first — an HTML artifact is the LAST resort, not the default
Viz také: chart, mockup, uploads
chat — A multi-turn AI conversation as a thread of filled bubbles with forensic evidence
Always send messages INLINE — the server persists them to a board data blob and stamps dataRef + a summary (never construct dataRef yourself). Under 1 MiB serialized; split longer runs. text is markdown-lite and survives verbatim — a transcript is evidence. references:[cardId] draw bubble-anchored wires to existing cards. Transcripts are immutable — replace the whole payload via update_cards. Importing beats composing: vitrinka board import-chat --eve <sessionId>|--ndjson <file> --board <slug> runs the pre-publish redaction gate (emails, keys, JWTs, IBANs) and refuses sensitive findings unless you choose --redact or --allow-unredacted. The chat-evidence template composes the readout layout.
Důležité poznámky:
- compose by hand only when you built the transcript yourself — imports must pass the redaction gate
Průvodci skládáním
composing — How to place work on a board: batching, layout intent, frames, templates, questions, undo
One batch per coherent unit — a section, a readout, a comparison — because placement and wiring are computed per batch; a single card is a batch of one. Anchor by relation: anchor {cardId|ref, relation: after|before|under|beside|into} at batch level or per card (a card's own anchor overrides the batch's and may target a ref in the same call); after/before insert in reading order inside the anchor's container, under is the support row below (after, inside a managed layout), beside is a peer to the right, into appends to a frame; the receipt reports placedAs and container; an unsatisfiable relation is a 400 relation_unsupported and nothing is placed. Never send coordinates; express intent (anchor, section, layout mode column|row|grid, order) and the server guarantees collision-free placement (occupancy scan) with geometry-true image aspect. Use arrange to tidy — choose the mode by meaning: graph follows document ref wires, journey follows flow wires, compare aligns two sections pairwise, row/grid/flow are for peers; scope it to what you placed ({ids} or {scope:"section:Name"}, gap/padding/cols knobs) and never reflow a shared board to fix your own addition; save:true persists a section layout (graph/journey refuse it). A receipt proves placement, not readable fit: read back what you placed and report unchecked fit honestly. Frames nest (depth ≤5) and any frame payload may carry layout {mode: column|row|grid|flow|lanes|masonry, gap: S|M|L|px, cols, padding}; any card may carry payload.lane for the lanes mode. Anchored multiple-choice questions ride the same call — options carry {label,hint,recommended,cardId}; cardId wires the option to the card it names and answers arrive as staged choice items. Templates: get_templates returns compose-ready skeletons (QA session, iteration review, decision map, metrics dashboard, presentation deck, finding triage, UI proposal, architecture map, data story, testing-journey suite, page, importers, docs board, chat evidence) — start from one instead of inventing structure. Presentation decks: sections are slides (one heading + 2-3 supporting cards each, arrange each with save:true; the user presents with shift-P). Every mutation is undoable per actor: POST /api/v1/boards/{slug}/undo reverts YOUR last batch; the user's ⌘Z never touches your work. Everything you place lives under the board's ✦ eve toggle unless it is shared structure (section frames). OVER HTTP the same call is POST /api/v1/boards/{slug}/compose with the envelope {cards:[{kind, payload, ref?, children?:[refs], anchor?}], edges?, questions?, section?, layout?, flow?, anchor?:{cardId|ref, relation}} — a card may carry its own anchor; section (frame NAME) and layout ("column"|"row"|"grid") are batch-level strings, never card fields; a frame's own layout object lives in ITS payload.layout; a frame card lists its members as children:[refs] of cards in the same batch. Every element kind except media and section is ALSO a standalone card (prose, stat, meter, list, timeline, chips, verdict, finding, …) — compose it top-level when it stands alone, or ride the same payload inside a doc element's payload.elements when several belong together as one readout (section is a doc block only; the board section is the journey frame). To read back what you placed: scrape is a token-economical digest (ids, kinds, sections, text summaries — never payloads); GET /api/v1/boards/{slug} returns every card with its full payload. Edit or remove what you placed with PATCH / DELETE /api/v1/cards/{id} (your own layer plus section frames).
Arguments in full (moved out of the tool schemas):
ask_board— Ask the operator a multiple-choice (or kind:"text") question on a board, rendered as one-click pills, optionally anchored to a card. Answers are STAGED until ↑ Send releases the batch on the work wire (wait_for_work choices[]); get {kind: "questions"} is the durable record. Pass only real options — "Other" is added by the engine. Doctrine: docs topic "composing".compose_board— Place a BATCH of AI-authored content on a board — cards, frames, wires, anchored questions — one batch per coherent unit. The server owns placement: express intent (anchor {cardId|ref, relation}, section, layout), never coordinates. All-or-nothing; a 400 carries {code, index}. Contracts: docs {topic: "composing"} and docs {topic: "<kind>"}.update_cards— Batch-edit board cards: PATCH payload/geometry on your OWN AI-layer cards and section frames, REMOVE cards (soft, restorable 30 days), RESTORE your trashed ones. Removing anyone else's card or a batch above the cap answers 409 needs_confirm with a token and preview — repeat the same ids with confirm and a reason. A payload patch re-sizes a content-sized card (text, callout, checklist, finding, verdict, list, chips, timeline, doc, …) to its new content unless the same patch sets h or the card is user-sized. Never compose duplicates to fix a card.ask_board · stakes— What this decision constrains downstream — rendered under the question while answering and returned on the choice work item. Always pass it for decision-shaped questions.ask_board · options— 1-12 answer options — strings, or {label, hint?, recommended?, cardId?} to carry tradeoffs (hint shows on hover, recommended gets a hairline mark). The "Other" escape is appended automatically.ask_board · pivot— This QUESTION is a pivot: its answer reshapes the rest of the map. Answers release only on the operator's ↑ Send / ↻ Regenerate — the flag marks the fork; nothing streams on its own.compose_board · requestKey— Stable identity of this logical composition. Retrying unchanged input returns the original receipt; changed input with the same key conflicts. Omit for a fresh operation.compose_board · layout— How the batch flows: omit for the default (aspect-packed shelf rows — the batch fills a screen-shaped 2D block, reading left→right in compose order), or force column (strict top-to-bottom), row, or grid (3 per row). USE THE FLOW VOCABULARY: for shelves of screenshots and readouts don't settle for one tall column — group cards that belong side-by-side in an UNTITLED cluster with layout {mode:"row"|"grid", cols} (renders NO chrome — the invisible row/col group), give sections layout + an optional payload.description line, and pick a batch aspect that uses the canvas. A document flow (prose, findings, verdicts, figures with their finding) is the exception: one or two columns read, three clip — and a section grid wraps everycolscards in compose order, so keep pairs aligned to the column countcompose_board · aspect— Target block aspect (w/h) for the default shelf pack: omit or 0 = 16:10 (1.6); clamped to [1, 3.2]. Pick wider (2.2-3) for shallow wide narratives, squarer (1-1.3) for deep column-ish batches — the knob that spreads a batch across the canvas instead of one tall columncompose_board · flow— Reading flow "document" makes the board read top-to-bottom like a chaptered document: this batch's top-level blocks stack in compose order (layout defaults to column; sections are the chapters, each still laying its members out inside), and the mode STICKS on the board — every later free batch starts a new row below instead of continuing shelf rows. The decision-map template sets it automatically. Use for brainstorms, decision logs and any board a human should READ in order; omit for spatial boards (screenshot shelves, journey maps).compose_board · anchorCardId— Alias for anchor {cardId, relation: "beside"}: place the batch beside this existing card (omit both for free space beside the board)compose_board · section— Journey section name (see scrape_board sections[]): the batch lands INSIDE that frame, below its existing content, and the frame grows to fit. Mutually exclusive with journey and with a beside anchor; with a relation anchor it must name the anchor's own container.compose_board · createSection— Insist on creatingsectionas a NEW frame. A missing section is created on demand on every board type; when the name is a near match of an existing section the call is refused with 409 section_near_match and a didYouMean list — re-run with the exact name, or set this to true.compose_board · journey— Pass-chain key ("checkout"): target a journey's ITERATION chain instead of a named section. Alone = the batch lands in the chain's LATEST pass (creates pass 1 if none — a plain section whose title matches is adopted as pass 1). This is THE way to present round 2/3 of a journey: one call opens the next pass and fills it.compose_board · pass— With journey: "next" CREATES the next pass section (auto-named "Checkout — pass N", placed beside pass N−1, layout inherited) and fills it in this same call; a number targets that existing pass. Omit = latest.compose_board · template— Registry template name (save_template / get_templates): its cards/edges/questions are instantiated FIRST, then any inline ones — one call renders the whole skeletoncompose_board · params— Template {{slot}} substitutions, e.g. {version: "2.4"}; unfilled slots render literally. The BUILT-IN template "decision-map" takes structured params instead: {title, context, decisions: [{key, title, stakes, options:[{label,hint,recommended,pivot?}], multiSelect?, kind?:"text", pivot?}], extend?} — the server lays the whole map out as ONE OUTLINE DOCUMENT column (context strip + a one-line dotted-leader entry per decision; answering lives in the operator's docked question brief, the entry shows state + call). ALWAYS use it for brainstorm decision maps. Decisions whose answer reshapes the rest of the map take pivot:true (question-level, or per-option). {extend:true} composes the entries INTO the existing map section named by title instead of creating a new heading+section — the way to grow ONE living map round by round, on the operator's ↑ Send / ↻ Regenerate, never off unreleased picks (the batch 404s cleanly if no such section exists).compose_board · cards— Any number of cards in one atomic batch (the 8 MiB body is the only bound). kind is the CLOSED vocabulary in the kind enum below — generated from the server's registry for THIS deploy; the server validates loudly (unknown kind → 400 with the allowed list; retired viz/wireframe → 400 naming the canonical kind; payload mismatch → 400 with {code,index} and details). PAYLOAD CONTRACTS ARE NOT REPEATED HERE: docs {topic:"<kind>"} answers one kind's exact shape (docs with no topic = the index). Two anchors: text {md: full GFM markdown, role?: idea|note|pro|con|risk|heading|caption, level?: 1|2|3 for headings}; every element kind (chart, table, diagram, page, stat, …) takes the canonical element payload the docs tool describes. Give a card a ref so edges/questions in the same batch can point at it.compose_board · cards[].children— Ordered child refs for a cluster/section. May include nested frames (max depth 5). Overrides cards array order.compose_board · cards[].kind— Card kind — one of the enum, which is generated from the server's registry (tools/gen-mcp-tools). Call docs {topic:"<kind>"} for the payload contract; a retired name (viz, wireframe) 400s naming its canonical replacement.compose_board · cards[].anchor— This card's own anchor, overriding the batch anchor: {cardId | ref, relation}. ref targets a card of the SAME call (resolved in dependency order; a cycle 400s relation_unsupported). Only on top-level cards — a child's place is its frame's children order.compose_board · edges— Wires between cards. from/to: a ref string from this batch, or an existing card id (number). Region anchors (journey signposts): fromRegion/toRegion pin the wire's end to a SPOT on the screen — the clicked element — in the card image's natural px (same space as annotation regions; measure with getBoundingClientRect at capture time, never eyeball). A screen with several outgoing region-anchored wires renders as a signpost: each wire leaves from its own outlined click-target. Tidy the whole flow afterwards with arrange {mode:"journey"}.compose_board · edges[].fromRegion— {x,y,w,h} in the from-card image's natural px: the clicked element the wire leaves from (outlined on the screen, glows with the wire)compose_board · edges[].toRegion— {x,y,w,h} in the to-card image's natural px: what appeared / where the flow arrives (needs a card "to", not a point)compose_board · questions— Questions, optionally anchored to a batch card (cardRef) or existing card (cardId). Same staged-answer mechanics as ask_board.compose_board · questions[].stakes— What this decision constrains downstream — rendered under the question while answering. Always pass it for decision-shaped questions.compose_board · questions[].pivot— Pivot question: its answer reshapes the rest of the map (live boards flush it to the wire immediately).compose_board · anchor— Place the batch IN RELATION to an existing card: {cardId, relation}. after = joins the anchor's row right after it (same-row neighbours move right, never wraps); before = takes the anchor's spot, the anchor and its row move right; under = a support row below the anchor at its x (content below moves down); beside = right of the anchor in free space (the default); into = appended as the last child of the anchor FRAME (section/cluster; a non-frame anchor 400s relation_unsupported). The anchor's container is the container: inside a managed section the relation becomes an insertion in its order (under reads as after there) and the frame reflows and grows. A multi-card batch follows the relation in order (card 2 lands after card 1). Each receipt entry reports placedAs and container.update_cards · requestKey— Stable identity of this atomic batch. Unchanged retries return the original receipt; changed input with this key conflicts. Omit for a fresh operation.update_cards · remove— Card ids to TRASH (soft delete — restorable). Free for your own cards (AI layer, section frames, cards you created); anyone else's, or more than 25 / 30% of the board, needs the confirm handshakeupdate_cards · reason— Why these cards go — optional on your own cards, REQUIRED (≥ 20 characters, ≤ 1000) with confirm; shown to the operator in HISTORY and Trashupdate_cards · confirm— The token from a 409 needs_confirm answer (rm_…); repeat the exact same remove ids with it, plus reasoncompose_board · questions[].options[].cardId— Card this option names — the board draws a trace wire, hover glows it, picking flies there.compose_board · questions[].options[].pivot— This option is the pivot: picking it reshapes the rest of the map. Answers still release only on the operator's ↑ Send / ↻ Regenerate — the flag marks the fork; nothing streams on its own.compose_board · questions[].options— 1-12 options — strings or {label, hint?, recommended?, cardId?} (the "Other" escape is appended automatically)compose_board · questions[].kind— "text" = free-text prompt (no options; the typed answer is the choice item's option).compose_board · edges[].rel— Typed canvas relationship; omitted means a plain flow arrow. Displayed alongside an optional label.compose_board · edges[].from— Card ref (string) or existing card id (number)compose_board · edges[].to— Card ref (string) or existing card id (number)compose_board · cards[].payload— Kind-specific payload — contracts in the vitrinka plugin skills (see cards description)compose_board · cards[].anchor— This card's own anchor {cardId | ref, relation}, overriding the batch anchor; top-level cards only.compose_board · cards[].children— Ordered child refs for a cluster/section (nested frames, depth ≤5); overrides array order.compose_board · cards[].ref— Batch-local handle for edges/questions/children (e.g. "idea1")compose_board · requestKey— Stable identity of this composition: unchanged retries return the original receipt; changed input conflicts.compose_board · layout— Batch flow: omit for shelf rows, or force column, row or grid (3 per row).compose_board · aspect— Target block aspect (w/h) for the shelf pack: 0 = 1.6, clamped to [1, 3.2].compose_board · section— Journey section name: the batch lands inside that frame; exclusive with journey and beside anchors.compose_board · createSection— true createssectionas a new frame even beside a near match (409 section_near_match otherwise).compose_board · pass— With journey: "next" creates and fills the next pass section; a number targets that pass.compose_board · template— Registry template name: its cards/edges/questions are instantiated first, then the inline ones.compose_board · flow— "document" makes the board read top-to-bottom in compose order and sticks on the board.compose_board · journey— Pass-chain key ("checkout"): target a journey's iteration chain; alone = its latest pass.compose_board · anchor— shape {cardId: number, relation?: after|before|under|beside|into} — Place the batch relative to a card: {cardId, relation: after|before|under|beside|into}.compose_board · cards[].anchor— shape {cardId?: number, ref?: string, relation?: after|before|under|beside|into} — This card's own anchor {cardId | ref, relation}; top-level cards only.compose_board · edges[].fromRegion— shape {x: number, y: number, w: number, h: number} — {x,y,w,h} natural px of the from-card: the clicked element.compose_board · edges[].toRegion— shape {x: number, y: number, w: number, h: number} — {x,y,w,h} natural px of the to-card: where the flow arrives.compose_board · questions— shape [{prompt: string, stakes?: string, options: [string | {label: string, hint?: string, recommended?: boolean, cardId?: number, pivot?: boolean}], multiSelect?: boolean, cardRef?: string, cardId?: number, kind?: choice|text, pivot?: boolean}] — Staged questions (cardRef | cardId anchors); as ask_board.update_cards · patch— shape [{id: number, payload?: object, x?: number, y?: number, w?: number, h?: number}]
Viz také: organization, sections, cluster, section
organization — Sidebar routing: every board gets a project, a subgroup, and optional cross-cutting tags
Every board belongs to a project and, within it, a subgroup — canonical vocabulary: reviews (PR-review boards) | testing (QA/set boards) | sessions (recorded journey-recorder sessions, nested under the project's <project>-sessions hub) | brainstorming | design (mockups/design directions) | journeys (testing-journey suites). Pass subgroup on create {kind: "board"} whenever the board fits a family; the server infers reviews/testing/sessions/brainstorming/journeys from slug or board_type when omitted, but design must always be explicit. TAGS are a third, additive axis: freeform, many per board, the way to group boards across projects (an area, a client, a release). Call list {kind: "tag"} FIRST to reuse existing tags instead of minting typo-duplicates; create {kind: "board"} {tags} and set_board_meta {tags} attach them (unknown slugs autocreate and return in a created array — always check it); list {kind: "board"} {tags, match} queries (AND default, match:"any" for OR, '-slug' excludes). Slugs are folded — case and diacritics never fork a tag.
Arguments in full (moved out of the tool schemas):
set_board_meta— Update a board's subgroup, theme, tags, archived flag and/or meta bag. meta is a MERGE-PATCH (passed top-level keys shallow-merge, others survive) — stamp {commitSha, tracedAt} incrementally; the server's own stamps (task, composed, planComposed, sessionId, sessionHub, bugBoard, lane) go back unchanged or not at all — name a board's task with add_task_ref, keep a free note of it as meta.taskRef; {index: true} makes the board an index (its board cards fold their boards under it in the sidebar). File a board into an index with top-level parentBoard + section on create {kind: "board"} or set_board_meta: the server places its board card in that section (a raw meta.parentBoard only nests). Tags are additive and autocreate.share_board— Mint a capability share link for outside people — the URL is the credential. Every link shares the WHOLE board; cardId only chooses what opens first. Never mint on a board carrying drafts the recipient must not see. Returns {id, mode, url}: hand the url out bare. Personal boards never mint.set_board_meta · theme— Per-board visual theme token — open vocabulary matching [a-z0-9][a-z0-9_-]{0,31}; "" restores the house default; omit to leave unchanged. Known bundles: sketch (hand-drawn rendering — rough geometry, Caveat hand type, hachure fills; SVG/PNG exports follow the look), light-paper, dark-paper (the house look pinned light or dark whatever the reader's app theme), playful, technical, diary, girlies, release. Open tabs restyle live.set_board_meta · tags— Tags to attach. Unknown tags are created automatically and reported in the response'screatedarray. Adds by default — pass tags_mode:"set" to replace the board's whole tag set instead.set_board_meta · tags_mode— "add" (default) attaches the given tags; "set" makes them the board's complete tag set, detaching any othersset_board_meta · archived— true retires the board (it leaves the board lists; nothing is deleted), false restores it; omit to leave unchangedshare_board · mode— view (read-only, default) or annotate (link holders can leave feedback that routes back like any annotation)share_board · cardId— Open the link on this element as a document. The link still grants the whole board — this is a landing choice, not an access boundary. Omit to open on the canvas.
lanes — Device tabs: one run board whose lanes are child boards, minted by ONE create call, each lane a whole board under a tab
A run that must read per device (desktop · tablet · phone · the iOS Simulator) is ONE run board with LANES: create {kind: "board", slug: "<branch>-test-run-<n>", lanes: [{key: "desktop", label: "Desktop 1440×810"}, {key: "phone"}, {key: "ios-safari", label: "iOS Simulator · Safari"}]} mints the run board (the Overview tab) AND one child board per lane in the same call — slug <slug>-<key>, meta.parentBoard = the run board, meta.lane = the key, the same board_type/project/subgroup/task stamp — and stamps meta.lanes on the parent (the tab order). The receipt answers lanes: [{key, label, slug, url}]; a run slug that already carries lanes answers the same receipt again (200), so a retried create never half-builds a run. Keys are [a-z0-9][a-z0-9-]{0,31}, unique, at most 12, never "overview". Each lane IS a board: compose its passes there as sections ("Pass 1", "Pass 2") with compose_board {board: <lane slug>, section}, annotate it, scrape it — its own history, its own annotation queue. The parent holds the run summary and one verdict per lane (a board portal card per lane is the link). The board page renders the same tab strip on the parent and on every lane (Overview first, then the lanes); the sidebar nests the lanes under the run board. scrape_board on the parent lists lanes[] with each lane's openAnnotations; on a lane it lists its siblings with active on itself. set_board_meta {meta: {lanes: [...]}} edits the order by hand; a lane is never deleted, only archived like any board.
Viz také: organization, sections, board
sections — Journey sections: named frames as navigation, iteration surface, and slides
Name board regions after customer journeys with kind "section" frames ("Onboarding", "Checkout") — always visible, auto-indexed (list {kind: "section"}, pill navigation, scrape_board sections[]), and every tool scopes to them: compose_board {section}, arrange {scope:"section:Name"}, scrape_board {section} for an isolated digest, request_review {section} for a scoped AI pass. Sections are the ITERATION surface: present the next take on a journey as its OWN section beside the current one ("Checkout — iteration 2") — never mix new cards into the original; iterations then sit side by side, each pill-navigable, separately scrapeable and reviewable. update_cards patches only your own layer plus section frames (shared structure); it removes freely what you own — AI-layer cards, section frames, cards you created — and gates anyone else’s behind the 409 needs_confirm handshake (repeat the ids with confirm + a reason ≥ 20 chars; soft trash, reason visible in HISTORY/Trash).
Arguments in full (moved out of the tool schemas):
arrange— Auto-align board cards — the layout engine, zero tokens of geometry. Express intent with the knobs: mode (column · row · grid · flow · lanes · masonry · timeline · timeaxis · compare · align-* · distribute-*), gap/padding, scope (ai · all · section:<Name> · ids). save:true on a section persists the layout so it re-flows on every change.scrape_board— Read a board as one structured digest: cards with section, image/document URLs, notes, annotations, journey chains (flow), a questions digest and the sections/journeys map. Image cards addvs(0–100 visual score; absent until the background lane scored the blob),dup(on near-duplicates only: the id of the best-scored card of the group — keep the card whose id equals it) anddevice(desktop | tablet | phone). Pass section to isolate one journey — the scoped read for section work. Page cards come as previews; read_element reads a whole document.arrange · mode— column|row|grid|flow|lanes|masonry|timeline|timeaxis|compare|journey|graph|align-left|align-top|distribute-h|distribute-v — open vocabulary, server-validated (unknown mode 400s with the list). journey: left-to-right tree driven by the board's wires — linear runs stay in a lane, a signpost screen fans its branches vertically (wire order), unlinked cards grid below the tree; not persistable (save 400s). graph: top-down tree of doc cards driven by their ref wires (nested documents) — roots are docs nothing points at, siblings in ref order, a parent centered over its children, unlinked cards grid below; not persistable (save 400s)arrange · save— With a section scope: persist this layout onto the section (payload.layout) so the server auto-reflows it on every future changearrange · scope— Which cards move: "ai" (default — the expression layer), "all", or "section:<Name>" (that journey's members, arranged inside the frame)arrange · sections— mode compare: exactly the two section names to align side by side; any other mode: ONE section name, the same call as scope "section:<Name>" (the receipt echoes that scope); several names, or one beside scope or ids, is refused with the one-call-per-section fixarrange · cols— grid/masonry: cards per row (default 3) or "auto" = fit as many columns as the section's inner width allowsarrange · sort— grid/column/row/flow order: reading (default — where cards sit) or oldest (chronological: the uploaded file's original timestamp, falling back to card creation)scrape_board · pages— page cards: preview (default) = title + excerpt; full = the whole markdown of every page card (read_element reads one)scrape_board · diff— Exactly two section names — the ITERATION DIFF: members matched pairwise (same file, then reading order) as {pairs:[{a,b}]} plus unpaired leftovers. Mutually exclusive with section.
Viz také: section, composing
uploads — Putting files (PDFs, documents, images) on a board from the shell
Run vitrinka board upload <files…> --board <slug> [--section "Name"] [--anchor <cardId> --side right|below] — bytes stream from disk over multipart HTTP and never enter your context; the command prints the created card JSON. PDFs render as live embedded documents (kind "file", A4 card, page count), other files as download chips, images as media cards. When a capture sidecar is configured the server also rasterizes a PDF's pages asynchronously into payload pageImages (/uploads/<slug>/<rand>-pN.png, ≤24 pages), which the face paints instead of the native viewer under press mode (?press=1, captures); POST /api/v1/cards/{id}/pages re-renders them on demand (501 without a sidecar). Without placement flags the server places the batch in free space — same no-coordinates doctrine as compose. A --section that does not exist is created on demand on every board type, unless the name is a near match of an existing section: then the call is refused with 409 section_near_match and a didYouMean list — re-run with the exact name, or --create-section (form field createSection, compose_board createSection: true) to insist. Raw API fallback: POST {base}/api/v1/boards/{slug}/upload multipart field "files" (+ optional section / anchor+side / createSection form fields). Task evidence has its own door: vitrinka task upload <id> <files…> (POST {base}/api/v1/tasks/{id}/upload, same multipart) stores the files under /uploads/tasks/<id>/ and attaches each as a file ref carrying the URL — the task's Evidence row links straight to it from any machine; small text can go inline through MCP upload_task_file {id, filename, content}. scrape_board returns file cards as kind "file" with filename · mime text and the document URL under artifact.
Viz také: artifact, composing
recall — The pre-build ritual: three cheap read-only calls before ANY UI work on an indexed project
Before UI work, spend three read-only calls: (1) get {kind: "project_memory"} — the project's ranked memory, the operator's source of truth; a conflict with the user's ask gets surfaced, never silently overridden. (2) search {kind: "shot"} for the target screen — a hit means the screen exists; look at how it looks today first and reference existing shots on boards instead of re-uploading what the library holds. (3) list {kind: "component"} — mock with the REAL component vocabulary; an empty index means offer vitrinka project index --components, don't proceed blind. Learned something durable while working (a convention, a gotcha, a decision)? Close the loop where the project keeps it: rows from a repo island → memo add <kind>/<topic> "<sentence>." in a worktree of that repo (it lands by PR); only a live legacy document (legacy.state: live) is still edited — legacy.body, edit surgically, save_project_memory.
search — How workspace search reads a query: ids in any shape pin, typos also search their correction, and every task hit says where it matched
One call, search {q}, answers every group (tasks, boards, pages, cards,
sessions, shots, tags, annotations). Pass the text as the person typed or
pasted it; the server does the parsing.
Ids pin, in any shape
A task named by number is read directly and PINNED first in its group
(pinned: true), ahead of everything that merely ranked:
#3263,vt-3263—vt-<n>anywhere in a token, so a branch (work/search-retake-vt-3263) or a PR title ((vt-3263)) pins too.acme/3263— a workspace slug, then the number (aKEY-3263key is checked against the task's project).- a task URL —
/w/<slug>/p/<project>/t/<id>or/p/<project>/t/<id>.
A bare 3263 pins only as the WHOLE query: "release 4" never pins task 4.
A task named by id also pins the board it references in the boards group.
An id in ANOTHER workspace you belong to (globex/123, a /w/globex/…
URL) pins with a workspace object and that workspace's url; one you
cannot open answers no row — the same silence as a missing task.
Everything a task carries is searchable
Title, body, every text field (gates and their evidence, next action,
outcome, decisions, branch), the addresses of its refs (PR number and
title, board, branch, worktree, Jira key) and its comments. A hand-back
summary is NOT indexed. A task hit names where its snippet came from:
matchedIn is title | body | field:<key> | ref | comment, and
matchedLabel the name a person reads ("Gates", "PR", "Comment").
Typos also search their correction
Every letters-only word of four or more letters is looked up in the
workspace's vocabulary; its nearest words (one edit, two from eight
letters, same first letter) are searched WITH it, and the reply lists
them in corrections ([{from, to}]). A row only a corrected word
matched carries corrected: true and ranks below exact matches. Tokens
with digits, dashes or slashes — slugs, ids, URLs — are never corrected.
Ranking is relevance only; a task's state and age never move it.
Meaning, when no task holds every word
On a deployment with an embedding sidecar, a task search none of whose
tasks holds every word also ranks tasks by meaning and fuses the two
orders. Such a reply carries mode: "hybrid"; its rows stay leads, and
one only meaning found has matchedIn: "meaning". When meaning was
wanted but unavailable the reply says mode: "lexical" with a
modeReason. Meaning reads only the most recently updated tasks inside
the search's facets; a hybrid reply whose facets held more carries a
modeReason too — narrow by project, type or state to reach older ones.
So does one while much of that window still waits for its vectors (the
index is building) — search again shortly.
No mode means the words alone answered.
The task-list text filter (list {kind: "task", f: {text}}) matches the same
fields exactly — it is a filter every count agrees on, so it never
corrects.
Resolve: one task, one call
search {q, mode: "resolve"} (the API's ?mode=resolve, the CLI's
vitrinka search <text> --resolve) reads q as ONE task reference and
answers a verdict: exact (an id, key or URL named it, or a branch a
task carries), confident (the ranker's leader stands clear), ambiguous
(pick from the candidates) or none. Beside it: interpreted (what the
query was read as), at most 5 candidates (id, key, title, type,
state, parentId, parent, childCount, url, why), missing ids
and next, the call to make. It is the same one ranker every task search
uses.
It reads the palette's fields (project:, type:, state: / in:) and,
at the edges of the text, type and state words in English or Czech
(bugs / chyby, epic / epik, story / příběh, open /
otevřené, in progress / rozpracované, done / hotové),
under <task> / pod <task> (every task under it, at any depth) and
mine / moje. When no task inside a typed word's facet holds every
word, the word reads as title text again ("Todo list loses order") or is
dropped, and is listed in interpreted.relaxed; a title holding under
or pod resolves as itself. project, type and state narrow as
everywhere; groups and tags are refused. Candidates holding only some
of the words are leads under verdict none; an id outside project,
type or state is listed in outside, where it lives; a draft's id
answers exact with draft: true.
A tool that takes a task id, given a reference instead —
get {kind: "task", id: "Release restart"}, a vt-42, a URL as a
parentId — answers the resolver's verdict and candidates with the exact
call to make, never a guess.
Viz také: tasks, recall
project-transfer — Move a whole project between two workspaces you own — copy, verify, then archive the source
A transfer copies EVERYTHING a project owns into another workspace: its sets, boards with cards/annotations/questions/tags, sessions, reports, releases, and the whole PM engine (tasks, events, states, labels, sprints, milestones, rules, views, fields), plus every blob those rows reference. It does NOT move tenant-wide state — workspace settings, templates, connectors, listeners, undo history, AI usage — and it never moves memberships: the destination workspace's members are its own. Gate: the caller must be an OWNER of both workspaces (agent/service tokens are refused); a single-tenant deployment answers 501. Always dry-run first (dryRun/--dry-run counts rows and blobs, writes nothing). The copy is idempotent — a re-run reports skipped per table instead of duplicating — and it refuses (409, naming the slug/key) when a board slug or set key already exists in the destination; rename there, then retry — or MERGE. merge is for a destination that already holds a project of the same slug (the same team's work split across two workspaces): per-project vocabulary — states, labels, fields, sprints, milestones, saved views, rules — resolves to the destination's row of the same key (reported as merged per table), content (tasks, sessions, boards, cards, comments) is appended with fresh ids, and a board slug or set the destination already owns is renamed on the way in (<slug>-from-<source workspace>; a set takes the next free version on its branch, so latest on that branch becomes the merged set) with its blob keys rekeyed everywhere they are written (a renamed board's live collaborative page state is not carried — the editor re-seeds it from the saved document); the report's renamed lists every one, so dry-run and read it first. Only what no rename can reconcile stays a 409 under merge: two next releases. Nothing is deleted unless you ask: archiveSource retires the project in the source only after the destination committed — boards archived (meta says where they went), live tasks trashed (recoverable, one deleted ledger row each), the registry row marked moved with the destination workspace so the name leaves both sidebar trees; sets keep the name (URLs and blob keys never change). Three doors, one contract: POST /api/v1/projects/{project}/transfer {to, dryRun, archiveSource, merge}, vitrinka project transfer <project> --to <workspace> [--dry-run] [--archive-source] [--merge], and the MCP tool transfer_project. The response is the report: boards moved, sets, rows per table (with skipped and merged), renames, blobs and bytes, archived and trashed counts.
brief — The feature brief — the ONE thing a fresh session reads before building an epic: server skeleton from the brainstorm's picks, prose filled by the brainstorming skill under hard caps, surfaced first by pickup
A brainstorm leaves a decision log and a board full of rejected takes; the BRIEF is the analyst's document a session that did not brainstorm reads instead (brainstorming levels 2026-09-11, D3–D6). Sections, in order, each with a cap the skill trims to and never exceeds: Intent (3 sentences) · Vocabulary (≤ 8 terms) · As-is (≤ 12 lines, real files and routes) · To-be (≤ 15 numbered user stories) · Architecture (one diagram + ≤ 10 units, each: what it does / how it is used / what it depends on) · Decisions that matter (≤ 8, each chosen over X because Y) · Prerequisites (≤ 8) · Acceptance criteria (≤ 15, each naming a story number and the test seam) · Work packages (≤ 8, ordered, disjoint files — what a subagent-driven build dispatches) · Out of scope · Open questions · Refs. TWO DOORS, one contract: POST /api/v1/tasks/{epic}/brief (MCP compose_brief {id}, CLI vitrinka task brief <id> [--out <file>]) composes or rebuilds ONE Plan chapter at the end of the epic's brainstorm board — intent, each decision's CHOSEN take only (a ref element pointing at the winning card, never a copy), the architecture diagram, the acceptance checklist — and returns the brief SKELETON: the deterministic half filled from the board (intent, a | # | Decision | Chosen | Rejected | Why | row per decision, unanswered decisions under Open questions, the refs) and every prose section holding only <!-- skill -->. The first run uploads that skeleton as brief version 1; the skill fills the placeholders and uploads the completed file as the NEXT version of the same filename (upload_task_file {kind: "brief"} / vitrinka task upload <id> <file> --kind brief --hint "read first; sufficient to build"). A brief that still carries a placeholder is visibly incomplete on purpose. LOAD FLAGS: task refs carry meta.load ∈ first | on-demand | never — the door stamps the brief first and the brainstorm board on-demand with meta.section: "Plan"; pickup (vitrinka task pickup <id|url>, /continue <task url>) puts first refs in its read rail ahead of the cap, lists on-demand refs as read on demand: <kind> <hint> (section Plan) and never inlines them, and hides never. Read the Plan chapter alone with scrape_board {board, section: "Plan"}; open the whole brainstorm board only to reopen a decision. 404 no_brainstorm_board when the epic has no brainstorm board ref yet — brainstorm first.
Arguments in full (moved out of the tool schemas):
compose_brief— Compose or rebuild the epic's feature brief from its brainstorm board's picks: rebuilds the Plan chapter and returns the brief SKELETON (deterministic sections filled, prose sections as placeholders). First run uploads version 1 of the<slug>-brief.mdlineage; upload later versions yourself with upload_task_file {kind: "brief", versionOf}. 404 no_brainstorm_board without a board ref.
Viz také: project-transfer
review — The review rubric as data: one markdown rule per id with YAML frontmatter, layered built-in ▸ workspace ▸ project, rendered into the brief every review pass receives
A review rule is one markdown file: YAML frontmatter, then the rule sentence as the first paragraph, then a ## Verify bullet list a reviewer (human or model) can tick. FRONTMATTER: id (slug ^[a-z][a-z0-9-]{1,40}$ — the identity every layer, finding category and stat keys on), family (free text, the topic the rule belongs to: spacing, copy, data honesty …), check ∈ mechanical | partial | judgment (how much of it a program can measure — a mechanical rule's finding carries a number, count or diff as its proof; a judgment rule quotes the rendered evidence), severity ∈ high | medium | low (the default severity of a finding filed under it), platform ⊆ [web, mobile] (omit for both), enabled (default true). LAYERS: the built-in set ships embedded (broken · ia-flow · redundant · spacing · scope-directive · native-shell · consistency · visual-polish · copy · clarity · reuse · data-honesty · hierarchy · layout-width · redesign-open · states · dark-theme · repeat-class · not-a-finding); a workspace rule with the same id replaces the built-in whole, a project rule replaces both; a new id at any layer is simply appended; deleting a layer's override (reset) lets the layer below show through. Disabling a rule is a rule with enabled: false at that layer. THE BRIEF: review_brief {project, persona?} / vitrinka review brief [--project] [--persona] / GET /api/v1/projects/{project}/review/brief render the resolved enabled rules as one prompt block — a fixed preamble (what a finding is, how to file it: annotate with severity and category = rule id, cluster ≥3 instances of one rule across ≥2 screens into one finding, an unreachable screen is an error never zero findings), then [id · platform · severity] sentence + verify lines per rule, then the persona seat line. request_review without instructions sends exactly this brief; explicit instructions win for that pass (≤ 12 000 chars). EDITING: Settings → Projects → review and Settings → Workspace → review (enable · reset · add, the body in the page editor, lint on save); vitrinka review rules list | get <id> | lint <file…> [--fix] | put <file…> [--workspace]; GET/PUT/DELETE /api/v1/projects/{project}/review/rules[/{id}] and …/workspace/review/rules[/{id}] (PUT body {source} = the whole markdown, or a field patch {enabled, body, …} over the resolved rule; writes are admin+ and audited as review.rules.update, every write is a new version). LINT rides the docspec diagnostics envelope with stable codes (missing-frontmatter · invalid-frontmatter · missing-id · invalid-id · missing-check · invalid-check · missing-severity · invalid-severity · invalid-platform · invalid-enabled · missing-body · missing-sentence · missing-family · missing-verify · unknown-key); autofix (lint --fix) repairs a BOM, HTML entities, enum near-misses, an id derived from the file name or first heading, and a missing check (→ judgment) — an ambiguous near-miss is never guessed. REVIEW RUNS (review_plan, a rare-tier door callable by name; CLI vitrinka review plan; POST /api/v1/review/plan): review_plan {boards: [slugs of one project], scope?: latest | all, sections?: [names], full?} decides what a review must judge and opens a run. latest = each journey's last capture pass (a board without journeys: every screen); no scope over boards holding several passes, or over 600 screens, answers 409 needs_scope with each option's cost (images, judge, sheets, gaps, reused) and the corrected call — nothing is planned until the scope is chosen. The plan drops capture gaps (blank, skeleton, undecodable, a runner's failed shot: never a finding, never a pass), reuses every image judged in a completed run under the same rubric (the rendered brief's hash: any rule edit re-judges; full: true reuses nothing), judges byte-identical screens once and a near-duplicate (within one device × theme) by a crop of the pixels that differ from its group's representative — a change over 35% of the screen, or another size, is its own tile. Reply: run, sheets, tiles {whole, crop}, planned {judge, exactDuplicates, reused, gaps}, gaps, prior (open findings on the in-scope card versions), tokens {baseline: every screen on 12-tile survey sheets, plan, saved}. SHEETS: get_card_image {run, sheet} — whole screens ≤ 12 a sheet, crops ≤ 24; the text block maps each tile to {cardId, board, cardVersion, width, height (annotate's basis), at, crop + vs (its representative) for a crop, prior[], lint[] (mechanical candidates from text boxes)}. Sheet 1 opens with the SIGHT CODE, printed in pixels only: echo it as sight on the next call (get_card_image {run, sheet: 2, sight}, or review_plan {run, sight} for a one-sheet run) — sheet 2+ and filing answer 403 sight_required until then, and three wrong codes lock the run (409 sight_locked): a host that cannot read the code sees no images and files nothing. RECEIPT: review_plan {run, cards?} — one state per in-scope card: inspected (a whole tile) · represented (a crop or an exact duplicate of a covered representative) · reused · gap · blocked, pending until the run completes (verified, every sheet served). FILING: annotate {board, run, items} files only against a verified run, on its boards and cards; an item restating an open finding on the same card version (same category, regions overlapping by half the smaller) answers duplicate with that finding's id and writes nothing. Measure regions on a single-card read (get_card_image {board, cardId}), never on a sheet.
Viz také: annotation
annotation — Agent-authored annotations: coordinate space, revision pinning, attribution, lifecycle, batch idempotency
An annotation is a region on a shot/media card plus a thread; it is the ONE shape for a finding an agent files — "review these screens and annotate the inconsistencies" is answered with annotate, never with document/finding cards from compose_board and never with highlight (a stroke, not a record). READ FIRST: get_card_image {board, cardId, version?, region?, maxDim?} returns the pixels (image content block) plus {width, height, cardVersion} — width × height is the SOURCE image's size and cardVersion its revision; scrape_board reports the same width/height/cardVersion per card. SURVEY A SET: get_card_image {board, cardIds:[…]} (at most 12) returns ONE contact sheet, each tile labelled #<cardId>, plus cards — per tile its cardId, width, height, cardVersion and tile rectangle on the sheet; it is for spotting what to look at, never for measuring, and replaces fetching screens to tile them yourself (image bytes never belong in a shell command; in a code-mode script hand the image block to the host's image call, e.g. image(block), and print only the text block — never stringify or log the whole result). COORDINATE SPACE: every region is {x, y, w, h} in source-image pixels of the declared cardVersion, origin top-left, bounds = that width × height; a small overshoot is clamped, one mostly or wholly outside is refused per item as out_of_bounds. Measure on the pixels you saw — never guess from captions, and never measure on a downscaled (maxDim) image's own size. Declare image: {width, height} from the get_card_image read on each item so a region measured on another size is refused as basis_mismatch (the error names both sizes and the rescale ratio) instead of landing wrong. Every result carries image (the basis used) and covers — the text under the filed region — so read covers back, and dryRun: true first when unsure what a region points at. REVISION: each item declares the cardVersion it was measured on; when the card has moved on the item answers stale with currentVersion and writes nothing (re-inspect, retry) — pinStale: true on the batch files it against the older revision instead, shown with the ·vN badge. ATTRIBUTION: the batch's agent (claude-code | codex | eve | gemini | other:<slug>) is the displayed author agent:<runtime>; the authenticated principal (PAT user or agent token) is stored beside it as the verified identity and shown as "via <name>" — a misdeclared runtime is visible next to the proof. reply and attach_after take the same agent argument. LIFECYCLE: agent findings are born staged and never ride the board's Send; a human Accepts (staged → open, then the normal queue: working → in_review → resolved) or Dismisses (→ cancelled), per item or Accept all / Dismiss all on the board's agent-findings group; the intent you set (fix default · investigate · refactor · redesign · comment · other — never reshoot/remind/task) can be retriaged before accepting. Agents never verdict their own findings. BATCH: ≤ 100 items per call, prompt (summary + detail) ≤ 8 KiB each; key per item is the idempotency key, scoped to the BOARD and agent (not the card — use one key per finding, e.g. c<cardId>-<n>): a retry with the same keys returns existing ids, never a duplicate, never a 409; a partially failed batch lands the rest. related: [{cardId, region?}] pins the other screens a finding spans, each to that card's current version, listed as jump links in the thread. Result per item: {key, status: created | existing | stale | out_of_bounds | rejected, id?, url? (/a/<id>, the solo view), currentVersion?, error?} plus created / existing / refused counts. OVER HTTP: POST /api/v1/boards/{slug}/annotations/batch with the same body; the human triage door is POST /api/v1/boards/{slug}/annotations/agent-verdict {action: accept | dismiss, ids?} (board UI only, not an MCP tool). No AI-credit debit — you brought your own model.
Arguments in full (moved out of the tool schemas):
set_status— Move an annotation through its lifecycle: "working" when picking it up, "in_review" after attaching the after-shot and replying. Never "resolved" — the user accepts in the board UI. A 409 "cancelled" means the user withdrew it: revert and move on.attach_after— Attach the "after" proof shot to an annotation by naming a file inside a set version you already pushed. Appends a fix attempt to the before/after deck and advances the card's face until the user accepts or rejects — the canonical way to show a fix; never paste the after as a new card.annotate— File your OWN findings as native annotations on shot cards, born staged for the human to Accept or Dismiss — THE answer to "review these screens", never finding cards or highlight. Regions are SOURCE-IMAGE pixels of the declared cardVersion (read get_card_image first); a stale version answersstale;keyper item makes retries idempotent. ≤100 items. Doctrine: docs topic "annotation".get_card_image— SEE a shot/media card's pixels: an image block plus {width, height, cardVersion, cropped, imageWidth, imageHeight, region?} — width/height/cardVersion are the SOURCE space annotate regions use; imageWidth/imageHeight are the downscaled bytes, never measure on them. region zooms; version reads an older face.annotate · agent— The agent runtime speaking: claude-code | codex | eve | gemini | other:<slug> | judge/<model>. Judge provenance preserves the model for review statistics (up to 96 characters). Becomes the displayed authoragent:<value>; the authenticated principal is stored beside it as the verified identity. Required unless the caller authenticates with an agent token, whose registered name is used (an empty value on a PAT is a 422).annotate · pinStale— true = pin items whose cardVersion is no longer current to that older revision instead of refusing them as stale (an audit of an earlier push, filed deliberately). Default false.annotate · dryRun— Resolve and validate without writing findings. Ready items return their resolved region and a version-pinned cropUrl for inspection.annotate · items[].key— Your stable id for this finding within the batch (e.g. "login-cta-contrast") — the idempotency key; reuse it on retry.annotate · items[].region— Exactly one of source-image {x,y,w,h}, normalized {nx,ny,nw,nh}, or exact case-sensitive {text,nth?}. Text resolves against boxes.json of cardVersion; duplicates require zero-based nth. Missing/ambiguous map returns unresolved. Rectangles are intersected with source-image bounds.annotate · items[].related— Other screens the same finding spans — listed as jump links in the thread, each pinned to that card's current versionget_card_image · maxDim— Longest side of the returned image in pixels (default 1600) — smaller is cheaper; coordinates stay in source pixels regardlessreply / attach_after · agent— The agent runtime speaking: claude-code | codex | eve | gemini | other:<slug>. The author becomesagent:<runtime>; the authenticated principal is stored beside it as the verified identity. Omit only on a legacy registration (then the bare "claude" label is used).get_card_image · region / annotate · related[].region— Rectangle in SOURCE-IMAGE pixels of the declared cardVersion (origin top-left, bounds = the image's width × height as get_card_image / scrape_board report them). Partially outside is clamped; no overlap = that item is refused as out_of_bounds.annotate · agent— Agent runtime: claude-code | codex | eve | gemini | other:<slug> | judge/<model>; required unless an agent token.annotate · pinStale— true pins items whose cardVersion is stale to that revision instead of refusing them.annotate · dryRun— Validate without writing; ready items return their resolved region and a version-pinned cropUrl.annotate · items[].cardVersion— The card's face version the region was measured on (get_card_image / scrape_board cardVersion)annotate · items[].region— One of source-image {x,y,w,h}, normalized {nx,ny,nw,nh}, or {text,nth?} resolved against boxes.json.annotate · items[].summary— One line — the finding as the annotation prompt's first lineannotate · items[].detail— Evidence and the suggested change (markdown-ish plain text; ≤ 8 KiB with summary)annotate · items[].category— Required review rule id or legacy category.annotate · items[].related— Other screens the same finding spans, pinned to each card's current version.annotate · items[].cardId— The shot/media card the finding is onannotate · items[].region— shape {x?: number, y?: number, w?: number, h?: number, text?: string, nth?: integer, nx?: number, ny?: number, nw?: number, nh?: number} — {x,y,w,h} source px, {nx,ny,nw,nh} normalized, or {text,nth?}.annotate · items[].related— shape [{cardId: number, region?: {x: number, y: number, w: number, h: number}}] — Other screens the finding spans.
Viz také: composing, uploads
tasks — The task engine's MCP doors in full: hand_back, spot, propose_tasks, bulk updates, attachments, task-id spelling, the work wire
The task-engine tools advertise one line each; this page carries their full argument contracts. Ids are per-workspace: spell them "<workspace>/<id>" on every id-only tool. A bare id resolves only when your connection reaches a single workspace; otherwise the reply names the ambiguity and the corrected spelling.
Arguments in full (moved out of the tool schemas):
wait_for_work— BLOCK until scoped work exists — long-polls, costs nothing idle, returns up to 3 ready-to-act capsules: annotations, released question answers (choices[], regenerate:true after ↻ Regenerate), ask-AI requests (asks[] — answer via answer_ask) and new recorder sessions (sessions[]). {"idle":true} means call again. ALWAYS SCOPE with project + branch or board.add_task_ref— Attach a session, shot, board, file, PR ("owner/repo#n"; meta.merged completes the task) or run to a task, or link a typed document by URL. Re-attaching updates meta. Upload kinds take a URL only — inline text goes through upload_task_file, files throughvitrinka task upload. Refs are versioned, never replaced.upload_task_file— Store inline TEXT as a typed attachment on a task (kind, hint, filename; .md .txt .json .jsonl .csv .html .yaml .log .diff .patch; 1 MiB). The same filename is the NEXT VERSION; versionOf continues a lineage explicitly. Binaries and files on disk go throughvitrinka task upload. Reply: {task, refs}.hand_back— The ONE door that ends a session's work on a task:nextlands as steps and gates on the task's checklist (a check on the work you just did, a merge, a look, a human decision),task: truefiles a child for work that needs its own session, PR or QA record;omittedwithdecide: trueis a human gate; movesstatus, attachesrefs, lands the summary as the next handback.md version, updates the parent epic, ends the live run. PRINTrenderedVERBATIM as your hand-back; never file next steps again through create {kind: "task"}.spot— File something you spotted but will NOT do in this change — a defect or follow-up — as a child of the bound task (parentId) or the project's rolling 'Found during implementation' epic. Body = what · where · why out of scope.fromSteppromotes a step of the bound task into the child (the step leaves the checklist). Print the returned url. Read the task's children first; never batch spots.read_task_ref— Load ONE task attachment progressively: underbudgettokens the raw text; over it, or withquestion, the AI backend distills (without one, the head cut at the budget with truncated:true). Reply: {ref, version, size, tokens, truncated, content}. Read the refs index (meta.tokens, meta.hint) first.ask_task— Ask ONE plain-language question over a task's whole corpus (id: row, fields, comments, events, every attachment version, boards, PRs) and get an answer with citations [{ref, version, at, quote}] — or over a whole project (projectinstead ofid): Eve reads the project's tasks and answers {answer, tasks: [{id, key, title, type, state, url, why}], confidence}, only live tasks of that project, each with its url. The project form answers open questions ("what blocks 4.0.0?", "who is on the checkout work?"); finding ONE task stays search {q, mode: "resolve"}.intent(developer · product · qa · reviewer · onboarding) shapes it. Writes nothing; costs AI credits — 501 without a backend, do not retry.bulk_update_tasks— Apply ONE patch to up to 200 tasks in one transaction (all-or-nothing). Patch fields: state, status, type, priority, assignee, assigneeId, sprintId, milestoneId, parentId, dueAt, estimate, project — zero values clear like update {kind: "task"};projectmoves the tasks (subtrees whole) into another project of the same workspace, for a whole project use merge_project.propose_tasks— File one or more task DRAFTS through the intake door for a human to accept — the "file these" path. Each draft is deduped against the project's open tasks and carries its verdict. Drafts never appear in normal lists until accepted.wait_for_work · project— Only wait for one project's work (matches the card set_ref); pair with branch to scope to your repo+branchupload_task_file · hint— One line telling a future reader WHEN to load this ("load when continuing the auth work"); lands in meta.hinthand_back · summary— The higher-level summary everyone reads (required, ≤ 4000 chars): what the work achieved, for a PM as much as for the next agenthand_back · readFirst— Ref ids (on this task or its epic) the next session must read first — they lead the pickup's READ sectionhand_back · status— Move the task; omit to leave the status to the rules (a run starts it, an open PR reviews it, a merged PR completes it)spot · parentId— The bound task (the pickup you started from, the checkout's vt-<id>); omit when no task is bound — the rolling epic catches itspot · fromStep— A step of parentId (its item id or exact name) that outgrew the checklist: it is removed there and the child carries the work; 400 when no such step, parentId requiredask_task · scope— withidonly: self reads the task alone; tree adds its descendants (three levels, 200 tasks): rows, last 10 comments each and their attachment index, newest first. Default: tree for an epic, self otherwisecreate · update {kind: "task"} · human— The human-view flag (vitrinka for humans): true lists the task on the human surfaces (Mission control, Needs you, the project and feature pages) as a row of its own, false keeps it off; unset, the type rule decides — epics and human-asked stories are human-level, everything else rolls up as progress. Over REST,nullhands it back to the rule.bulk_update_tasks · patch.project— Move the tasks into another EXISTING project of the same workspace (target slug; an unknown project is a 422, never minted): each subtree moves whole; state, labels, fields, sprint and milestone remap by key, a key the target lacks is created there, the source vocabulary is never touched. For a whole project use merge_project.propose_tasks · drafts[].key— A caller-chosen name for this draft inside this call, so a later draft can file under it with parentKeypropose_tasks · drafts[].parentKey— File under the draft of this batch carrying that key (must come earlier in drafts) — a qa draft and its journey drafts in ONE call. Exclusive with parentIdpropose_tasks · drafts[].parentId— File under this task — a live one or a still-pending intake row (a qa draft under its epic, journey drafts under a pending qa draft). Declining a parent draft declines its pending children; accepting a child accepts a pending parent firstpropose_tasks · drafts[].fields— Custom-field values keyed by field key (list {kind: "field"}), validated per kind the way update {kind: "task"}.fields is — e.g. a journey's {role, route, steps: [{name, done}], expected, verdict}task id (every id-only tool)— Task id — or "<workspace>/<id>" for a task in another workspace of your organisation (ids are per-workspace; the same spelling project takes)hand_back · summary— The summary everyone reads (required, ≤4000 chars): what the work achieved.hand_back · surfaces— New APIs, components, doors — where the next session builds onhand_back · buildOn— Where the next session should start building (a file, a door, a seam)hand_back · next— Next steps, in this order: each lands as a step on the task'sgateschecklist unlesstask: true, which files a child (ranked after the existing children). A check on the work you just did, a merge, a look, a verify, a human decision is a step or gate — never a taskhand_back · next[].kind— step (default; the agent's own item, never blocks) · human (a gatewhoresolves; blocks Done; awhowith no kind makes the item human) · date (a gate reached onat, required) · action (work still to be done; never blocks). A step re-filed by name keeps its tickhand_back · next[].ask— A human gate's sentence for the PERSON (Needs you prints it, never the title): ≤ 90 characters, verb first ("Approve the release copy"), never an id, key, path, link or session id — the door refuses one and returns the item with it stripped;whyis one line of what waits on the answer,verb(decide · approve · review · test · answer) the chip, derived from the ask when absent. Accepted without an ask for now, with awarningsline; the same three rideomitteddecide items and anyfields.gateshuman itemhand_back · next[].when— The todo grammar ("pr 12 merged", "deployed prod", "after 718"), read by kind. On a human or date gate it is the proof: the fire door ticks the gate by itself when it fires, evidence attached — a merge that should close itself iskind: human("Merge PR #12",whothe person who merges,when: pr 12 merged). On a step (the default) or an action it is the moment the work falls due, never proof it happened: the fire door stampsripeAt(evidence attached), the pickup's NEXT shows itwaiting — <when>before anddue — <when>after, and it stays open until someone does the work and ticks it ("Ship after the merge" withwhen: pr 12 merged)hand_back · omitted— Work seen and deliberately left out:decide: truelands as ahumangateDecide: <title>on the task (waiting onwho, else the assignee;whyis its evidence) — only in a run nobody attends; with a human in the session, ask the call before the hand-back and record the answer insummary— other omissions are filed as children so they are never losthand_back · prerequisites— What must happen before the next step can start (moves the parent epic to waiting)hand_back · readFirst— Ref ids the next session must read first — they lead the pickup's READ section.hand_back · status— Move the task; omit to let the rules decide (run starts, PR reviews, merge completes).hand_back · refs— Refs to attach beside the hand-back — the PR, the board made during testinghand_back · refs[].ref— The ref key: owner/repo#n for a pr, the slug for a boardhand_back · refs[].meta— Ref meta, merged on conflict (a pr's {url, branch, state}; a board's {board: true})hand_back · sessionId— This session's id: ends its live run on the task with the summaryhand_back · run— a memo run island {island, repo, branch, commit, checkpoint?}, printed bymemo run locate <task> --jsonas data.ref: the continuation lives in the run, so no handback.md is uploaded — the task's run ref gains a version carrying the hand-back; refs of the moved kinds (handoff, plan, notes, distill, brainstorm, conversation, transcript) are refused; an omission that is not a decision stays in the run and files no child. The pickup's RUN row names the run andmemo run locate; read_task_ref on the run ref is its checkpoint view, rendered from the verified archive or the receipthand_back · next— shape [{title: string, task?: boolean, kind?: step|human|date|action, who?: string, at?: YYYY-MM-DD, when?: string, ask?: string, why?: string, verb?: decide|approve|review|test|answer, type?: task|bug, body?: string, priority?: urgent|high|medium|low|}] — Steps and gates on the task;task: truefiles a child (type, body, priority apply to it).hand_back · omitted— shape [{title: string, why?: string, decide?: boolean, who?: string, ask?: string, verb?: decide|approve|review|test|answer}] — Left out deliberately; decide → a human gate on the task (unattended runs only), else a child.hand_back · refs— shape [{kind: session|shot|board|file|pr|run|transcript|conversation|brainstorm|plan|handoff|decision|distill|commit|final|brief|notes, ref: string, meta?: object}] — Refs to attach — the PR, the board.spot · refs— shape [{kind: session|shot|board|file|pr|run|transcript|conversation|brainstorm|plan|handoff|decision|distill|commit|final|brief|notes, ref: string, meta?: object}] — Evidence you already have — a board, a session, a PRpropose_tasks · source— shape {kind?: string, ref?: string, url?: string, meta?: object} — Where the drafts come frompropose_tasks · drafts[].refs— shape [{kind: session|shot|board|file|pr|run|transcript|conversation|brainstorm|plan|handoff|decision|distill|commit|final|brief|notes, ref: string, meta?: object}]bulk_update_tasks · patch— shape {state?: string, status?: backlog|todo|in_progress|in_review|done|cancelled, type?: task|bug|story|epic|todo|qa|journey|meeting, priority?: |urgent|high|medium|low, assignee?: string, assigneeId?: number, sprintId?: number, milestoneId?: number, parentId?: number, dueAt?: string, estimate?: number, project?: string} — The patch
Viz také: brief, review