vitrinka.docs
Reference

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.

VlastnostTypPoznámky
viz"line" | "area" | "bars" | "scatter" | "pie" | "donut" | "heatmap" | "stat" | "matrix" | "flow" | "timeline" | "compare"povinné which visualization to render; decides the required data slot
titlestringheading rendered above the chart
captionstringmono 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.

VlastnostTypPozná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.

VlastnostTypPoznámky
sourcestringpovinné the code text, verbatim
langstringhighlighter language id (go, ts, sql, …); omit to autodetect
captionstringsmall 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.

VlastnostTypPozná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.

VlastnostTypPoznámky
a{ label?: string; elements: Element[] }povinné the left side
b{ label?: string; elements: Element[] }povinné the right side
titlestringheading 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.

VlastnostTypPoznámky
envstringpovinné environment, e.g. prod
shastringpovinné deployed commit
atstringwhen it landed (RFC 3339)
countnumberdeploys in the period
livebooleanthe 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.

VlastnostTypPoznámky
dir"TD" | "LR"flow direction: top-down (default) or left-right
kickerstringsmall eyebrow line above the title
titlestringthe diagram's title — required unless a mermaid import supplies one
subtitlestringsupporting line under the title
captionstringmono 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)
groupsunknown[]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
footerstringfootnote under the diagram
qualitystringper-WRITE strict tier ("showcase" fails on composition findings) — consumed at write time, never stored
mermaidstringmermaid 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
sourceunknownliving-source pointer ({kind, ref, rev}) — refreshed server-side via POST /cards/{id}/refresh
geounknownSERVER-OWNED layout output (boxes/ports/order/routes) — never author it
renderunknownSERVER-OWNED stamped theme renders ({svgLight, svgDark}) — never author it
overridesunknownSERVER-OWNED human ⊕ edit layer, survives re-layout — never author it
metricsunknownSERVER-OWNED quality score output — never author it
repairsunknownSERVER-OWNED mechanical-repair log — never author it
diagnosticsunknownSERVER-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.

VlastnostTypPoznámky
titlestringthe document's title
kickerstringsmall eyebrow line above the title
direction"editorial" | "dossier" | "terminal" | "gallery" | "release"the design register the renderer leans into
accentstringa phrase of the title the display type highlights
introstringthe 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
projectstringthe 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
elementsElement[]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.

VlastnostTypPoznámky
titlestringpovinné 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
donenumberwork children done
totalnumberwork children in all
coverstringresult screenshot URL
livebooleansomeone works on it right now
whostring[]who works on it right now
idnumbertask id
urlstringtask URL as the server printed it
pagestringfeature page URL
typestringtask type word
atstringwhen 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.

VlastnostTypPoznámky
srcstringimage URL
refstringblob/upload reference (server-resolved)
shotnumberindex into the owning set's data.json shots (doc-card form)
captionstringcaption rendered under the figure
altstringaccessible 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).

VlastnostTypPoznámky
sev"high" | "medium" | "low"severity: high|medium|low — colors the rail
tagstringfree-text category chip ("perf", "a11y", "race")
statusstringverification state chip: verified|confirmed|fixed · suspected|likely|open · refuted · wont-fix
titlestringone-line finding title
wherestringmono locator: file:line, route, query
bodystringbody prose; blank lines split paragraphs
causestringwhat produced the defect — a labeled "cause" card
summarystringthe one bold conclusion sentence
coststringwhat the defect costs (time, ms, money) — a labeled "cost" card
fixstringthe fix, one line
trace{ before: { key: string; dest: string; note?: string }; after: { key: string; dest: string; note?: string } }before/after resolution trace
evidencestring | 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"]}}

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.

VlastnostTypPoznámky
layout"grid" | "bento" | "masonry" | "carousel"grid (default) | bento | masonry | carousel — the arrangement; the painter owns the geometry
captionstringcaption 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.

VlastnostTypPoznámky
titlestringpovinné the goal's name
outcomestringthe outcome it promises
status"running" | "overdue" | "planned" | "done"status word; default planned
startsstringstart (RFC 3339)
endsstringend (RFC 3339)
percentnumberwork done, 0-100
donenumberwork done
totalnumberwork in all
urlstringthe 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.

VlastnostTypPoznámky
srcstringa complete HTML document, inline
files{ [key: string]: string }flat path → content map; must include index.html
hrefstringserved document URL (a pushed set's raw index.html) — written by the push, not authored
titlestringthe island's label (the face's kicker and the frame's accessible name)
widthnumberpinned document width in px (omit to auto-measure)
heightnumberpinned render height in px (omit to auto-measure)
natWnumbermeasured document width in px — written by the board's size beacon, not authored
natHnumbermeasured 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.

VlastnostTypPoznámky
style"bullet" | "check" | "number"marker style: bullet (default) | check | number
itemsstring[]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.

VlastnostTypPoznámky
srcstringfile URL
refstringblob/upload reference (server-resolved)
mimestringMIME type hint when the URL doesn't reveal it
captionstringcaption rendered under the media
altstringaccessible 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.

VlastnostTypPozná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
titlestringheading 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.

VlastnostTypPoznámky
titlestringpovinné the milestone's name
duestringdue date (RFC 3339)
reachedstringwhen 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.

VlastnostTypPozná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.

VlastnostTypPoznámky
mdstringpovinné 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
pmunknownSERVER-OWNED ProseMirror mirror — never author it
pmStalebooleanSERVER-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.

VlastnostTypPoznámky
mdstringpovinné 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.

VlastnostTypPoznámky
cardnumberthe target card id (this workspace)
pagestringa sibling page slug inside the same push batch — resolved server-side to card
sectionstringa 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)
labelstringchip text override; default is the target's title
targetunknownSERVER-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.

VlastnostTypPoznámky
titlestringpovinné the release's name
status"next" | "cut"cut, or the open next release; default cut
prsnumbermerged pull requests in it
atstringwhen it was cut (RFC 3339)
urlstringthe 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.

VlastnostTypPoznámky
idstringthe section's anchor id — authored (lowercase, digits, hyphens; ≤64) or derived from the label
labelstringmono uppercase eyebrow label ("01 · Findings")
titlestringdisplay-type section title; inline markup allowed
notestringmuted note line under the title
elementsElement[]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.

VlastnostTypPozná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
titlestringheading 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.

VlastnostTypPoznámky
titlestringheading rendered above the table
captionstringmono 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).

VlastnostTypPoznámky
items({ when?: string; title?: string; status?: string; text?: string; summary?: string; evidence?: string | string[] })[]povinné the dated cards, in story order
titlestringheading 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.

VlastnostTypPoznámky
statestringthe one-line call (required unless text is given)
textstringthe 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

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 every cols cards in compose order, so keep pairs aligned to the column count
  • compose_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 column
  • compose_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 creating section as 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 skeleton
  • compose_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 handshake
  • update_cards · reason — Why these cards go — optional on your own cards, REQUIRED (≥ 20 characters, ≤ 1000) with confirm; shown to the operator in HISTORY and Trash
  • update_cards · confirm — The token from a 409 needs_confirm answer (rm_…); repeat the exact same remove ids with it, plus reason
  • compose_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 creates section as 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's created array. 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 others
  • set_board_meta · archived — true retires the board (it leaves the board lists; nothing is deleted), false restores it; omit to leave unchanged
  • share_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 add vs (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) and device (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 change
  • arrange · 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 fix
  • arrange · cols — grid/masonry: cards per row (default 3) or "auto" = fit as many columns as the section's inner width allows
  • arrange · 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.

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 (a KEY-3263 key 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.md lineage; 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 answers stale; key per 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 author agent:<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 version
  • get_card_image · maxDim — Longest side of the returned image in pixels (default 1600) — smaller is cheaper; coordinates stay in source pixels regardless
  • reply / attach_after · agent — The agent runtime speaking: claude-code | codex | eve | gemini | other:<slug>. The author becomes agent:<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 line
  • annotate · 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 on
  • annotate · 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 through vitrinka 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 through vitrinka task upload. Reply: {task, refs}.
  • hand_back — The ONE door that ends a session's work on a task: next lands 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: true files a child for work that needs its own session, PR or QA record; omitted with decide: true is a human gate; moves status, attaches refs, lands the summary as the next handback.md version, updates the parent epic, ends the live run. PRINT rendered VERBATIM 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. fromStep promotes 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: under budget tokens the raw text; over it, or with question, 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 (project instead of id): 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"}; project moves 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+branch
  • upload_task_file · hint — One line telling a future reader WHEN to load this ("load when continuing the auth work"); lands in meta.hint
  • hand_back · summary — The higher-level summary everyone reads (required, ≤ 4000 chars): what the work achieved, for a PM as much as for the next agent
  • hand_back · readFirst — Ref ids (on this task or its epic) the next session must read first — they lead the pickup's READ section
  • hand_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 it
  • spot · 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 required
  • ask_task · scope — with id only: 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 otherwise
  • create · 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, null hands 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 parentKey
  • propose_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 parentId
  • propose_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 first
  • propose_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 on
  • hand_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's gates checklist unless task: 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 task
  • hand_back · next[].kind — step (default; the agent's own item, never blocks) · human (a gate who resolves; blocks Done; a who with no kind makes the item human) · date (a gate reached on at, required) · action (work still to be done; never blocks). A step re-filed by name keeps its tick
  • hand_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; why is 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 a warnings line; the same three ride omitted decide items and any fields.gates human item
  • hand_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 is kind: human ("Merge PR #12", who the 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 stamps ripeAt (evidence attached), the pickup's NEXT shows it waiting — <when> before and due — <when> after, and it stays open until someone does the work and ticks it ("Ship after the merge" with when: pr 12 merged)
  • hand_back · omitted — Work seen and deliberately left out: decide: true lands as a human gate Decide: <title> on the task (waiting on who, else the assignee; why is 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 in summary — other omissions are filed as children so they are never lost
  • hand_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 testing
  • hand_back · refs[].ref — The ref key: owner/repo#n for a pr, the slug for a board
  • hand_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 summary
  • hand_back · run — a memo run island {island, repo, branch, commit, checkpoint?}, printed by memo run locate <task> --json as 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 and memo run locate; read_task_ref on the run ref is its checkpoint view, rendered from the verified archive or the receipt
  • hand_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: true files 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 PR
  • propose_tasks · source — shape {kind?: string, ref?: string, url?: string, meta?: object} — Where the drafts come from
  • propose_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

Na této stránce

Druhy elementůchart — data visualization — series charts, proportions, KPI stats, timelines, heatmapschips — meta chip row — small key·value facts in quiet pillscode — code listing — syntax-highlighted source with an optional captioncolumns — columns — 2–3 side-by-side columns of nested elements; collapses when narrowcompare — side-by-side comparison — two labeled columns of nested elementsdeploy — deploy tile — what reached an environment, and whether it runs there nowdiagram — architecture diagram — semantic nodes/edges/lanes; the server computes all layoutdoc — composite document — an element composing child elements, the artifact registerepic — feature tile — a feature with its phase, progress and result screenshotfigure — image figure — a picture with caption and alt textfinding — one review finding — severity rail, locator, cause/cost, trace, fix and evidence receiptgallery — image gallery — 1–24 images as a grid, bento mosaic, masonry wall or carouselgoal — goal tile — a sprint's outcome with its dates and progresshtml — custom HTML island — sandboxed escape hatch when no hard component fitslist — marked list — bullet, check or numbered linesmedia — media file — video, audio, PDF or download, by URL or blob refmeter — meters — labeled value/max bars with optional thresholdsmilestone — milestone tile — a dated mark, reached or still aheadmockup — UI mockup — frames of primitive trees; sketch screens cheaply, edit by direct manipulationpage — document page — markdown truth with a collaborative editor mirrorprose — markdown text — headings, lists, links, icons, inline mathref — ref — a reference to another document card: link chip, preview or full embed; the server stamps the target and draws the edgerelease — release tile — a cut release, or the open one gathering what ships nextsection — titled section — eyebrow label, title, note and nested child elementsstat — KPI stat strip — headline numbers with tones, deltas and sparklinestable — typed-column data table — filter/sort/group views, aggregates, in-place editingtimeline — timeline — dated cards along a spine, each with status and evidenceverdict — the call — one line of state with its reasoning on a toned railDruhy karet nástěnkytext — A markdown card: the basic thought unit, with a role that styles itnote — A bare text note without markdown chromeshape — A colored geometric shapecallout — A toned emphasis blockchecklist — An interactive checklist — users tick items live and state persistscluster — A grouping frame; untitled = invisible row/col group, pure flow intentsection — A named journey frame: always visible, pill-navigable, the unit of scoping and iterationstep — A numbered journey step that embeds its screen — the canonical walkthrough shapecompare — Two existing cards' faces behind a wipe/ghost slider — iteration pixel-diffdoc — A fractal document in one card — child elements composed top-to-bottom, ~300 tokens for a dense readoutlink — A website card: server-unfurled metadata, screenshots, optional live embedboard — A portal card: a live minimap window into another boarddiagram — An architecture page in one card: lanes, groups, typed nodes, ports, edges — server-laid-outapi — An OpenAPI reference card: the whole raw spec, rendered as a browsable grouped-endpoints referencepage — A Confluence-grade living document card: markdown truth, block editor, repo-backed refreshartifact — A self-contained HTML bundle in a sandboxed iframe — the custom-HTML escape hatchchat — A multi-turn AI conversation as a thread of filled bubbles with forensic evidencePrůvodci skládánímcomposing — How to place work on a board: batching, layout intent, frames, templates, questions, undoorganization — Sidebar routing: every board gets a project, a subgroup, and optional cross-cutting tagslanes — Device tabs: one run board whose lanes are child boards, minted by ONE create call, each lane a whole board under a tabsections — Journey sections: named frames as navigation, iteration surface, and slidesuploads — Putting files (PDFs, documents, images) on a board from the shellrecall — The pre-build ritual: three cheap read-only calls before ANY UI work on an indexed projectsearch — How workspace search reads a query: ids in any shape pin, typos also search their correction, and every task hit says where it matchedIds pin, in any shapeEverything a task carries is searchableTypos also search their correctionMeaning, when no task holds every wordResolve: one task, one callproject-transfer — Move a whole project between two workspaces you own — copy, verify, then archive the sourcebrief — 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 pickupreview — 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 receivesannotation — Agent-authored annotations: coordinate space, revision pinning, attribution, lifecycle, batch idempotencytasks — The task engine's MCP doors in full: hand_back, spot, propose_tasks, bulk updates, attachments, task-id spelling, the work wire