Skip to content

Live-data contract

Status: Shipped

Generated from app @ 1b3d5a69d73473db077dfb44f4390b42cded6bd4 Regenerate: pnpm gen:reference

Before an agent writes a live page, it calls the get_live_data_contract MCP tool. The contract below is what that tool returns, section by section: how a page acts through the bridge verbs, how it uses the dashboard kit, and how it reads routine results. The verb list itself is on Page SDK.

A page never fetches external services itself and never holds a credential. External data is collected by an agent routine, using the agent’s own tools, and the page reads the routine’s completed result.

What it’s for

TopicContract
role Author local HTML dashboards using scoped local queries, the agents read model (sdk.agents_and_inbox), routines, agent runs, worktrees and machines (sdk.routines_runs_and_machines), actions and completed routine snapshots. Agents own external credentials and requests.

Acting

TopicContract
role Beyond reading data, any HTML doc in your spaces can act — mutate CodeSense state through the bridge verbs (flip a story's status, route a file into context, draft a doc, open a workspace, run a skill, chat to a running agent). Every allowlisted verb runs when called: there is nothing to declare and no prompt.
call await window.codesense.act(verb, args) → Promise. `act` is a thin convenience wrapper over the same invoke RPC. Resolves with the verb's result; rejects with a typed-prefix Error when the call fails.
tiers.metadata Silent frontmatter/file and inbox-state writes (setStatus/setField/routeToContext/draft, markRead/setHumanStatus). A runaway guard caps a burst of write calls (a batch `setStatus` is one call); past it the call rejects `denied: write budget` until the burst window passes.
tiers.spawn Opens a workspace / drives an agent (openWorkspace/runPrompt/runSkill/openTab), or opens a file in the viewer (openFile) — bounded by a concurrency cap (`busy` when saturated). Opens that take no cap slot (openFile, openTab, a headed dir/epic openWorkspace) are bounded by a runaway guard instead: past a burst the call rejects `denied: spawn budget` until the window passes.
tiers.send Injects a chat turn into an already-running agent (sendToSession/sendToWorkspaceTab, and runPrompt), or sends an inbox message (postMessage, sendAsHuman, replyAsHuman) — rate-limited (anti-flood; `denied: send budget`).
tiers.read Reads a file / lists a dir confined to the doc's own directory subtree (readWorkspace), or observes a workspace — audited (read:true), never budgeted.
tiers.manage Closes a session the board opened (closeWorkspace) — audited, but not budgeted by the metadata write guard and not rate-limited like a chat send, so an autonomous cleanup pass can close many finished sessions in a row.
errors A failed call rejects the act() Promise with a stable typed prefix — branch on `err.message.startsWith(...)`: `denied: write budget` (metadata runaway guard) · `denied: route not applied (…)` · `denied: send budget` (chat rate) · `denied: prefs budget` (setPrefs burst) · `denied: spawn budget` (cap-free open burst) · `busy` (concurrency cap saturated) · `not-allowed:` (runSkill: a prompt-splitting skill name / control-char arg) · `unavailable:` (backend / workspace context / agent CLI down) · `traversal:` / `unsupported-type:` / `too-large:` / `binary:` / `not-found:` (readWorkspace path/content rejects) · `refused:` / `not-found:` (openFile outside a known team content root, or a missing file) · `invalid:` (setField: a bad key name or value) · a verbatim CodeSense reject for an out-of-bounds (non-managed) path. Same typed-prefix convention as `analyze`.
enforcement The runaway guards, the concurrency cap and managed-path confinement run in the CodeSense process, never in your doc's JS. Your act() call is convenience; those guards live behind it.
safe_output Treat any value an action returns — and all upstream/model data — as unsafe input: render with textContent / createElement, never innerHTML / outerHTML / document.write.
authoring.role Everything a dashboard author needs to build an acting dashboard without reading the CodeSense source — the doc scaffold, the epic-addressing primitive, the working-directory rule, and the pre-ship check.
authoring.scaffold Add `<meta name="cs:type" content="dashboard">` to <head> so the doc is enumerable as a dashboard. Nothing else needs declaring: call any verb directly.
authoring.get_doc_dir.call await window.codesense.invoke("getDocDir")
authoring.get_doc_dir.returns the absolute directory of this dashboard doc; null when no dashboard is mounted.
authoring.get_doc_dir.use The anchor for building an absolute path to a sibling doc (e.g. a story) for runSkill — available without an open epic workspace. `abs = getDocDir() + '/' + name`.
authoring.get_epic_handle.call await window.codesense.invoke("getEpicHandle")
authoring.get_epic_handle.returns the epic's absolute doc path (epicDocPath) when this dashboard is hosted inside an open epic workspace; otherwise null.
authoring.get_epic_handle.use How a dashboard addresses the epic's already-running agent (the `target` for sendToSession). When null, the doc is not inside an open epic workspace — hide the epic-chat controls, never render a dead button. (For sibling absolute paths, prefer getDocDir, which needs no open epic.)
authoring.cwd_rule runSkill and sendToSession launch the user's agent CLI in a different working directory than the doc — pass absolute paths, never doc-relative ones. Build them from getDocDir() (the board's own dir, available without an open epic); getEpicHandle() also yields an absolute anchor but only inside an open epic. readWorkspace is the exception: it is confined to the doc's own directory subtree, so it takes './'-relative paths.
authoring.non_blocking Never await a full readWorkspace sweep before first paint. Paint the shell + skeleton rows synchronously, then hydrate rows off the read promises (Promise.all) so the UI is interactive immediately.
authoring.validate Run validate_live_data_doc on your HTML before shipping: it flags a setStatus call addressed by storyId instead of path (blocking), a sendToSession target not resolved via getEpicHandle (advisory), and unsafe innerHTML (advisory).

Dashboard kit

TopicContract
role Every live page (a dashboard file, a right-pane pane, an html inbox message) gets the dashboard kit with no tag to add: a stylesheet of tokens and opt-in `cs-` classes, and window.codesense.kit. A page that uses neither looks as it did. Use them to look like the built-in panes (Agents tree, Pipeline, Inbox, Routines).
tokens CSS custom properties beside the theme colours: --cs-space-1..6 (4, 8, 12, 16, 24, 32px), --cs-radius-sm / --cs-radius / --cs-radius-lg / --cs-radius-full, --cs-text-xs / -sm / -base / -lg / -xl (11–18px), --cs-leading, --cs-font-mono, --cs-hover and --cs-inset (derived surfaces), --cs-shadow; the agent-state palette --cs-state-running, --cs-state-idle, --cs-state-needs-you, --cs-state-exited, --cs-state-offline, --cs-state-unknown; signal_progress phases --cs-phase-planning | -dev | -review | -pr | -blocked | -done; default story statuses --cs-status-planning | -todo | -in-progress | -blocked | -review | -qa | -done (a team's taxonomy() colours win).
classes cs-app (on <body>: theme background, text, font, spacing), cs-stack / cs-row / cs-grid / cs-spacer / cs-toolbar (layout), cs-card + cs-card-header + cs-card-body (is-interactive, is-selected), cs-list, cs-table (cs-num cells), cs-badge (is-accent | is-success | is-warning | is-danger, or style --cs-badge-color), cs-count, cs-dot (data-state = a sessions() state), cs-btn (is-primary | is-ghost | is-danger | is-sm | is-icon), cs-input, cs-tabs + cs-tab (aria-selected="true"), cs-empty, cs-notice (is-warning | is-danger), cs-avatar, cs-progress (a <span> child with a width), cs-kv (a <dl>), cs-pre, cs-drawer + cs-drawer-header + cs-drawer-body (a side panel; toggle `hidden`), cs-segmented (a group of buttons, aria-pressed="true" on the chosen one), cs-btn[aria-pressed] (a toggle), cs-live (a live/stale indicator: is-stale, is-loading), cs-skeleton*, cs-h1 / cs-h2 / cs-h3, cs-muted, cs-small, cs-xs, cs-mono, cs-truncate, and cs-dag* (kit.dagView / kit.dagSvg; nodes get is-selected, is-focus, is-upstream, is-downstream, is-dim). Inside cs-app, [hidden] always hides.
helpers.h kit.h(tag, {class, text, style, dataset, onClick, ...attrs}, ...children) → an element; strings become text, never HTML. kit.s(...) is the same for svg.
helpers.icon kit.icon(name, {size?, strokeWidth?, class?, title?}) → an inline <svg> Lucide icon (the app's glyphs, isc licence). kit.iconNames() lists the bundled set (agents, stories, routines, machines, chrome: bot, network, workflow, git-branch, git-pull-request, inbox, calendar-clock, server, terminal, ...); an unknown name draws circle-help.
helpers.status kit.statusDot(state) and kit.statusBadge(state) for a sessions() state (running | idle | needs-you | exited | offline | unknown), with the app's colours and labels; kit.phaseBadge(phase) for progress.phase; kit.storyBadge(status, taxonomyState?) for a story. kit.agentStates / kit.phases / kit.storyStatuses are the palettes ({label, color, icon}).
helpers.avatar kit.avatar({face?, name, size?}) → a round avatar: a built-in face ref (a sessions() row's avatar, e.g. 'builtin-funny:coffee-bot') draws that face, anything else the name's initials.
helpers.layout_dag kit.layoutDag(nodes, edges, {direction: 'tb'|'lr', nodeWidth: 160, nodeHeight: 44, nodeSep: 24, rankSep: 56, group?: (node) => key, maxWidth?}) → {width, height, nodes: [{id, x, y (top-left), cx, cy, width, height, layer, order, data, group?}], edges: [{from, to, points: [{x, y}], reversed, cross?, data}], groups?: [{key, x, y, width, height, count}], reversed, skipped, crossings}. nodes are ids or {id, width?, height?, ...}; edges {from, to} with `from` drawn first (launcher → launched, dependency → dependent). Layered by longest path, ordered to cut crossings; a cycle is drawn by reversing a back edge (listed in `reversed`); self-loops and unknown ids are skipped. With `group` (an epic, a machine) each group is laid out in its own framed cluster and edges between clusters are marked `cross`.
helpers.dag_view const view = kit.dagView(container, {renderNode: (node) => element, renderGroup?, group?, onSelect?(id), onActivate?(id) (double-click), nodeVersion?(data), ...layoutDag options}); view.update(nodes, edges) on every read. A pannable, zoomable graph (drag or wheel to pan, pinch or ⌘-wheel to zoom, buttons to zoom and fit), packed to the view's shape. A re-read with the same shape re-renders only the nodes whose data changed and keeps pan and zoom; hovering or selecting a node lights everything upstream and downstream. view.select(id), view.center(id), view.fit(), view.layout(). Use this for an agent launch tree or a story dependency graph.
helpers.dag_svg kit.dagSvg(layout, (node) => element, {edgeClass?}) → a static <div class="cs-dag">: an svg of curved arrows with each node's element (e.g. a div.cs-dag-card) placed over it. kit.edgePath(points, direction) is the path data alone.
helpers.keyed kit.keyed(container, items, {key, render, version?}) renders a list by key, reusing an item's element while its version (default JSON) is unchanged, so a live re-read patches rows instead of redrawing them.
helpers.skeleton kit.skeleton({rows, avatar?}) → shimmering placeholder rows to show until the first read lands.
helpers.call kit.call(verb, args) → the named helper (codesense[verb]) when this app has it, else invoke(verb, args); a call this app does not know rejects with err.unavailable === true, so show a notice instead of failing. kit.has(verb) checks first.
helpers.live const stop = kit.live(render, {events: ['sessions-changed', 'agents-changed'], pollMs: 30000}) runs render now, again (coalesced) on each named host event (codesense:<event>), and every pollMs while on screen. kit.on(event, cb) listens for one event.
helpers.misc kit.timeAgo(msOrIso), kit.empty(text, icon?) (say what to do next, e.g. how to start one), kit.notice(text, 'info'|'warning'|'danger', {label, onClick}?) (e.g. an error with Retry).
states Show every state: kit.skeleton() while loading, kit.empty() that says how to get started when there is nothing, kit.notice(…, 'danger', {label: 'Retry', onClick}) on an error, and a short notice when kit.call rejects with err.unavailable (an app too old for the call). Below ~560px (a narrow right pane) prefer a list or tree to a graph.
references Working rebuilds of built-in panes in the CodeSense docs context repo (getting-started team), content/examples/dashboards/: agent-dag.html (the Agents tree as a launch DAG), pipeline-graph.html (story dependency graph), routines-console.html (routines, run history, run detail). Copy one to start.

Routine results

TopicContract
refresh Read routineResult/routineStatus on page load. Only an explicit user action may request routineRefresh; page load must not launch an agent.

Snapshots

TopicContract
snapshot codesense.snapshot captures a frozen record; never execute saved snapshot state.

Analyze

TopicContract
analyze invoke('analyze') uses the configured agent CLI; handle unavailable, timeout and busy errors, and render model output as text.

Dashboards as inbox messages

TopicContract
inbox_messages An agent can send the human a dashboard as a message: send_message with {to:"human", contentType:"html", text:<the HTML>}. It renders in the Inbox exactly like a dashboard file — scripts, window.codesense (reads, act() through the same action gate) and pushed events — sized to its content, with Open in tab. Always pass {summary}: one plain-text line (≤280 chars) the Inbox and lists show for it, since a line derived from the markup reads badly. Team-scoped calls act in the message's team. Example text: `<p id=o>…</p><script>codesense.sessions().then(s=>{o.textContent=s.length+' sessions running'})</script>`.

Retired services

TopicContract
retired_services Built-in provider connections, connector polling and /api/v1/proxy are unavailable. Publish external data through a routine and read its result.

See also

TopicContract
see_also validate_live_data_doc, routine_result, routine_publish_result

Linter rules

validate_live_data_doc checks a page before it’s saved. Blocking findings fail the check; advisory ones are warnings.

CodeSeverityFindingFix
HARDCODED_TOKEN blocking The doc appears to contain a hardcoded credential (a known provider token prefix). Remove it. Credentials belong to the configured agent, never the dashboard.
EMBEDDED_AUTH_HEADER blocking The doc sets an Authorization header to a literal credential. Use routine snapshots; keep external credentials in the configured agent.
RETIRED_SERVICE_ACCESS blocking This dashboard uses retired built-in service access. Use an agent-owned routine to publish data and read it with routineResult; refresh only on an explicit user action.
UNSAFE_INNERHTML advisory Upstream provider data or analyze model output may be rendered via innerHTML/outerHTML/document.write. Treat all upstream and model output as unsafe input — render with textContent / createElement.
ANALYZE_NO_DEGRADE advisory An analyze() call has no error handling — no catch or startsWith branch for the rejection. Wrap the call in try/catch (or a two-arg .then) and branch on err.message.startsWith('unavailable'|'timeout'|'busy') — the CLI may be absent, slow, or busy.
ANALYZE_EXPECTS_STREAMING advisory The doc appears to expect a token stream around analyze(). analyze() returns a single completed string — it does not stream. Render an 'Analyzing…' placeholder, then swap in the completed result via textContent. Drop any streaming/token-by-token handling.
SNAPSHOT_STATE_EXECUTED advisory Embedded snapshot state appears to be executed (eval / new Function) instead of parsed as inert data. Re-hydrate snapshot state with JSON.parse(el.textContent) and render via safe DOM — never eval / new Function / innerHTML.
SETSTATUS_STORYID_ARG blocking setStatus is called with a `storyId` and no `path` — setStatus addresses the story doc by its content path, so this call always rejects. Pass the doc's path: act('setStatus', {path: doc.path, status}). queryContext() rows carry `path`.
SENDTOSESSION_HARDCODED_TARGET advisory sendToSession is called but getEpicHandle() is never used — the target is likely a hard-coded path that won't resolve on another machine or epic. Resolve the target at runtime: sendToSession(getEpicHandle(), …). Hide the control when getEpicHandle() is null.

Starter page

The contract ships this page as a starting point. It follows every pattern above.

live-data-starter.html
<!doctype html>
<html><head><meta charset="utf-8"><meta name="cs:type" content="dashboard">
<title>Routine snapshot</title></head><body>
<h1>Routine snapshot</h1><p id="status">Loading cached result…</p>
<button id="refresh" type="button">Refresh routine</button><pre id="result"></pre>
<script>
// Replace with your routine key.
const key = "team:example";
const status = document.getElementById("status");
const result = document.getElementById("result");
async function readSnapshot() {
  if (!window.codesense) { status.textContent = "Open this dashboard in CodeSense to read routine results."; return; }
  try {
    const value = await window.codesense.act("routineResult", {key});
    result.textContent = JSON.stringify(value, null, 2);
    status.textContent = "Cached routine result";
  } catch (error) { status.textContent = String(error); }
}
document.getElementById("refresh").addEventListener("click", async () => {
  if (!window.codesense) return;
  try { await window.codesense.act("routineRefresh", {key}); status.textContent = "Refresh requested; reopen or read the result after completion."; }
  catch (error) { status.textContent = String(error); }
});
readSnapshot(); // Read only: page load never launches a routine.
</script></body></html>

Generated from app @ 1b3d5a69d73473db077dfb44f4390b42cded6bd4 Regenerate: pnpm gen:reference

How we know this is accurate 2 sources

Checked against the app at 1b3d5a69d73473db077dfb44f4390b42cded6bd4.

Code

  • src-tauri/src/mcp/live_data_authoring.rs
  • src-tauri/src/mcp/live-data-starter.html