Skip to main content

dashboard-template

Kiro Crew repo only: load this when a task will author or change a dashboard page. A page never ships alone: the same PR carries its TypedDict contract, its provider, and a parity test that the two agree. Scaffold all four from one field list; read numbers from an existing fold, never type them.

Informations de source

Dépôt
kirodotdev/KiroCrew
Dernière activité de la source
11 octobre 2026 à 18:07
Langue détectée de SKILL.md
anglais
Étoiles
4 371
Forks
785

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
5 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
dashboard-template
description
Kiro Crew repo only: load this when a task will author or change a dashboard page. A page never ships alone: the same PR carries its TypedDict contract, its provider, and a parity test that the two agree. Scaffold all four from one field list; read numbers from an existing fold, never type them.
triggers
dashboard template, dashboard card, data-dashboard-field, template contract, card contract, dashboard html, panel template, new dashboard, scaffold template
# Dashboard Template A dashboard page is HALF of a template. The other half is a type, and the two halves are checked by different machines, which is why they drift. > This skill is the procedure for the `kirocrew-dashboard-author` crewmate, whose spec > is `agent-spec.md` beside this file: the charter (what it is for and what it may never > do) plus the narrowed toolset that makes the charter true against a tool list rather > than only against prose. A human or another agent following this file gets the same > procedure. The spec is NOT yet registered as an installed agent (no installer or > owned-spec entry ships it; see `docs/system-specs/modules/dashboard-templates.md`), so > today nobody starts with this loaded: load this skill yourself. The page is inert html. The host sanitizes it, renders it, and binds text into it by `data-dashboard-field`. No type checker is anywhere near that binding, because one half is markup. So a page that reads `settled` while its provider fills `settled_count` renders an empty cell, on a status board, forever, with nothing red anywhere. Read the host before you write a page; it is short and it is the authority on what gets refused. `src/kiro_crew/dashboard/dynamic_cards.py` holds `normalize_card`, which decides whether a card is accepted at all, and `website/src/pages/chat/command-center/dashboardDocument.ts` is what queries `[data-dashboard-field]` and writes the text in. **You are done when four files and one registration line exist, not when the page looks right.** Everything here is dev time: it lands through a pull request, mypy is blocking and the parity test is a gate. At run time the gateway evaluates none of it. A publisher supplies three sentences and nothing else. ## What a run produces | # | File | What it is | |---|---|---| | 1 | `<slug>.html` | the page: inert layout, one `data-dashboard-field` per value, zero controls | | 2 | `<slug>_contract.py` | the `TypedDict` naming exactly those fields, plus the judgment type and `CONTRACT_VERSION` | | 3 | `<slug>_provider.py` | `build_<slug>(view, judgment, *, captured_at)` whose RETURN TYPE is the contract | | 4 | `test_dashboard_template_<slug>.py` | the parity gate, both directions, plus the planted failures | | 5 | one line in `registry.py` | `slug -> (contract, provider, source fold, version)` | Files 1-3 live beside the package's other templates; file 4 goes in the repository's test directory. The scaffold writes all four and prints line 5 for you to paste. ## Do it in this order 1. **Name the question the page answers, in one sentence a person would ask.** "How much of this goal is settled, and what is waiting on me." Not "a board of metrics". If you cannot write the sentence, you do not yet know which fold to read. 2. **Pick the fold.** Take one of the ten advertised folds in `FOLDS.md` (not the *(internal)* `class` fold, which `--fold` still accepts). A new fold is the exception and needs its own justification section in the pull request body, because a fold is a durable projection of the append-only log and every reader pays for it. 3. **Write the field list.** One token per value: `name:str|unsaid` for text, or `name:fraction:<total>` for a count, naming the fold key that holds its total. Both halves are checked against the fold, and the numerator is read under the field's own name -- add a fourth token, `shown:fraction:calls:open`, when the card field is deliberately named something else. A text field takes the same escape as one optional third token, `shown:str|unsaid:<fold_key>`. There is no plain `str` kind: every declared field is read out of the fold, and a fold value can always be absent, so a text field is `str | Unsaid` or it is a contract the provider cannot satisfy. (`contract_version` and `captured_at` are plain `str` because the provider writes them; the scaffold adds them itself.) Keep the whole card at 24 fields or fewer, the five the scaffold adds (`contract_version`, `captured_at`, `lede`, `you`, `notes`) included, so at most 19 of yours; the host refuses an over-cap card WHOLE, so a page one field too large stops appearing rather than degrading. 4. **Run the scaffold.** It emits the four files from that one list, so the four cannot disagree about what the fields are. 5. **Read the fold and fill the derivations.** The scaffold guesses that a field named `x` is read from the fold's `x`. Where the fold spells it differently, or where the value is a count over a list, edit the provider. Only the provider. 6. **Make the page worth reading.** The scaffold's html is correct and plain: a label and a value per row. Lay it out properly, group what belongs together, use the theme variables. Do not add a binding without adding the contract key in the same edit. 7. **Register it,** run the gates, open the pull request. ```bash python3 <this skill>/scripts/scaffold.py <slug> \ --fold status \ --fields turns_refused:fraction:turns_completed lifecycle:'str|unsaid' ``` `--print-only` shows the paths and writes nothing. `--force` overwrites, and the output is deterministic, so re-running after a field-list change gives a reviewer a diff of exactly that change. ## The three invariants These are tests, not advice. Each one exists because its absence produced a wrong page that nothing reported. **1. The page and the contract read the same field set, both directions.** Strict equality, no exemption list. An exemption list is a hiding place: a field inconvenient to render gets declared and listed, and every check still passes. A field the page reads and the contract omits is an empty cell nobody owns. A key the contract declares and the page never reads is a provider deriving something nobody sees. **2. Every number carries its denominator.** `7` on a status page reads as complete information and is not: a reader cannot tell 7 of 7 from 7 of 700. So a count reaches the page as `N/M`. That is what the `fraction` kind is, and it is the only way the scaffold will emit a number. A hand-written contract may declare a bare `int` where the template genuinely wants the pair rendered apart, and the gate then requires its `<name>_of` sibling. **3. A value nobody supplied says so, in words.** The host renders a field it was given by setting that element's text, and a field it was NOT given as the empty string. On a page of numbers a blank and a zero are the same reading. So a value that may be unknown is typed `str | Unsaid`, the provider writes the sentinel out explicitly, and the single exit turns it into the words "not said". mypy then refuses both the missing key and a stray `None` from a lookup that found nothing. The corollary that catches people: **never read a fold with `value.get(key, 0)`**. That converts "this fold does not carry that" into a number at the first line of the provider. Use the package's `read_int` and `read_text`, which answer the sentinel for absent, wrong-typed and unparsable alike — including `bool`, which passes `isinstance(x, int)` and would render a flag as `1`. ## What the page may not contain The host's sanitizer strips fourteen of the seventeen outright, so authoring one of those ships a layout with a hole in it and an author who believes the button exists. It keeps `label`, `fieldset` and `option`; this package refuses those three itself, because a dashboard states facts rather than offering a choice. No `form`, `input`, `button`, `textarea`, `select`, `option`, `label`, `fieldset`, `script`, `iframe`, `object`, `embed`, `link`, `meta`, `base`, `template`, `noscript`. That list restates `CONTROL_TAGS` in the package's `parity.py`, which is the authority and is what `control_tags_used` refuses; a test holds this paragraph to it. No `src`, so no images. No `href` that is not a same-document fragment (`xlink:href` included); `outbound_references` refuses those. `style`, `details`/`summary`, inline SVG and CSS are yours -- but put no binding inside the SVG. Whether SVG paints a string depends on the ancestor chain, not on the element holding it: `text` paints, the same `text` under `defs` or `clipPath` paints nothing. The reader refuses the whole foreign subtree rather than guess per tag. Put the value in a sibling `span` positioned over the drawing. A dashboard states facts and offers no actions. A decision goes through the product's own question and approval surfaces, which live outside the rendered frame and carry the identity of the session that owns them. An imitation approval button in your page is a lie about authority, and it is the one thing a status surface must never tell. ## Close every element where it opens Write the end tag for every element, including the ones html lets you leave out -- `</li>`, `</td>`, `</tr>`, `</p>`, `</head>`. The reader refuses a page that omits one, and refuses a misnested or stray end tag the same way. This is stricter than html, and the reason is what the reader has to answer: which element each binding sits inside, because the host assigns `textContent` in document order and an assignment to an outer bound element detaches an inner one before its turn comes. Answering that on markup the browser repairs means reproducing a tree builder, and a reader that gets one subtly wrong reads a child as a sibling and says so in a precise sentence. So it asks for markup it can follow instead. Your remedy is always one end tag, on the line the message names. ## Motion Motion is decoration. With it off, the page must read the same. The reference is the `goal-board` built-in (`src/kiro_crew/dashboard_templates/builtin/goal-board/template.html`). Three kinds of effect are allowed: | Effect | When | Goal-board | |---|---|---| | pop-in | an element's FIRST draw only | `.item.pop` -> `gb-pop`, `.states.grow i` -> `gb-grow` | | subtle live loop | while an item is live or needs the reader | `.item.live::after` -> `gb-shimmer`, its `.dot` -> `gb-blink`; `.band` -> `gb-glow` (3 pulses) | | state-change flash | once, when a value changes | one short non-repeating `animation` | Only a live or needs-you element may loop, and gently: a soft shimmer or glow, never a spinner or a pulsing count. A needs-you glow pulses a few times and stops. Every loop stops under `prefers-reduced-motion`. A host refill redraws the page, so remember what was drawn and do not replay the pop-in (goal-board keeps a `seen` map by item id). The frame, as `CrewDynamicDashboard.tsx` and `widgetSrcdoc.ts` build it: - `sandbox="allow-scripts"` only: no `allow-popups`, no `allow-same-origin`. - CSP: inline `<style>` and `<script>` run; no CDN script, `connect-src 'none'`, `img-src data: blob:`. - No link can leave the frame. A page asks the host instead: `parent.postMessage({ type: 'kirocrew-dashboard:open', url: url }, '*')`. The host (`useFrameOpenLink`) opens it only from its own frame, only right after a click or key press in that frame, and only for a URL matching `PR_URL_RE`, a `https://github.com/<owner>/<repo>/pull/<n>` page. Anything else drops, so post it from a click handler, never on a timer. Script and buttons are for the built-in directory templates. A scaffolded page above stays inert, so its motion is CSS only and it links nowhere. **MUST: every page that animates ships this block, reaching both pseudo-elements.** ```css @media (prefers-reduced-motion: reduce) { .board *, .board *::before, .board *::after { animation: none !important; transition: none !important; } } ``` Use your page's own root class in place of `.board`. ## Who may write what Split on purpose, because the two ends admit of different enforcement. **The provider derives every number.** It is Python in this tree, so mypy checks it: a missing key, a misspelled key or a wrong type is a build failure. A number it derives cannot disagree with the log it summarises, because it read the log. **A publisher writes three sentences: `lede`, `you`, `notes`.** That is the whole run-time surface. Numbers are absent from it deliberately — a publisher that can type a count can make the page contradict the log it claims to summarise. This is not hypothetical: a live board once rendered an owner's name and a session id in the cell that must say what a person should DO, and nothing could refuse it. So `you` is gated on phrase SHAPE: a one-token value is not an action and becomes "not said". No length floor — a floor rejects nothing a one-token check does not already reject, and it does suppress genuine short actions like `fix it`, which is the same information loss pointing the other way. ## The fold menu **Read `FOLDS.md` beside this file.** It is the catalogue: every fold, what it answers, whether it is keyed by session or by slot, which entry types move it, and the name and type of every field in its rendered value. That file is GENERATED from the code by `scripts/fold_catalogue.py`, and a test asserts the committed copy equals the generated one. So it cannot be stale, which is the only reason it is safe to trust without opening the projection kernel. Do not edit it; a fold added or renamed in the product is picked up by `--write`. Two things to read carefully in it: - **Keyed by.** A session fold answers about one conversation. A slot fold answers about a workstream that outlived several conversations and is folded over every log the slot ran under, so serving a slot-wide value under one session's id presents a part as the whole. - **Optional.** A field marked optional is `None` on an empty fold, so a writer could leave it unset. Those are exactly the fields your contract types `... | Unsaid` and your provider reads with `read_text` / `read_int`. A type of `unknown` means neither the empty fold nor any entry declaration says what it holds; treat it as optional text and read the fold's own render function before relying on it. The same catalogue is what `scaffold.py` validates `--fold` against, so a fold the catalogue does not list is refused before any file is written. ## Choosing a new fold Only when no existing fold can answer the sentence, and then say so in the pull request under its own heading: - the question, and which of the ten you tried and why each fails; - the entry type the fold reads, and why the log already carries it; - what it stores, and why that is bounded; - its `affects` set, which may be wider than the truth and must never be narrower — a type wrongly left out drops a real change and serves a stale value with nothing raised. Adding a fold also moves the stored-state version, which retires every saved checkpoint to a cold refold. That cost is the reason this is the exception. ## Before you open the pull request - `mypy` over the source tree. It is the blocking gate and it is what makes the provider's return type mean something. - The package's own gate module and your template's generated test, with the runner scoped to those files. - The formatter and the import sorter over the four new files. - Confirm the page renders: the contract's key set, the page's binding set, and the provider's output keys are all one set. The generated test asserts exactly this, so a green run IS the confirmation. In the body, state which fold you read and why, and paste the page's field list. A reviewer's first question is always "where did this number come from", and the answer is the fold name.
Voir sur GitHub