.md file to compare - side-by-side diff against dashboard-builder
dashboard-builder
What it does for you
Helps build and update the screens in your dashboard.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 3/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/dashboard-builder/SKILL.md
present
state/lib/dashboard-builder.ts
present
state/bin/dashboard-builder/
not present
state/skills/dashboard-builder/AGENTS.md
present
how it runs - the shared frame every skill uses 3/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
state/log/evals.ndjson what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
how the work flows- step by step
what this step does
what this step does
what this step does
what this step does
SKILL.md- the skill, written out in plain English
Backed by: state/lib/dashboard-builder.ts
dashboard-builder
Meta-skill that auto-generates and maintains OpenUI Lang resources for any snappy-os skill. The system extends itself: reusable right-canvas surfaces should live in the skill's resources/*.openui folder, not as one-off chat prose or server special cases.
Why it exists
As we add skills, each one needs reusable visual surfaces: the default dashboard at resources/ui.openui, plus named surfaces such as resources/schedule.openui when a natural intent should open a specific canvas. Hand-writing these surfaces is:
- Error-prone (copy-paste bugs, stale templates)
- Maintenance-heavy (every query signature change breaks N dashboards)
- Repetitive (the same KPI patterns repeat: runs, score, last-run time)
This skill reads a target skill's metadata (SKILL.md, AGENTS.md) and dispatch-log usage, then emits a tailored OpenUI dashboard without human intervention.
Input
slug(string) - the skill folder name (e.g.ai-spend,dogfood-loop,linkedin-post)resourceName(string, optional) -ui.openuifor the default skill dashboard, or a named surface such asschedule.openuiintent metadata(for named surfaces) - top comments:// surface,// intents,// response
Output
state/skills/<slug>/resources/<resourceName>- the generated OpenUI Lang resource (atomic write: temp + rename)- Returns the path on success
Steps
1. Validate the skill exists
const skillMdPath = join(cwd, "state/skills", slug, "SKILL.md");
if (!existsSync(skillMdPath)) {
throw new Error(`Skill not found: ${slug}`);
}
2. Parse the skill's frontmatter
Read SKILL.md, extract the YAML block (--- to ---), and parse:
name- the skill namedescription- what it doescategory- skill category (Channels, System, Memory, etc.)type- explicit type field (optional; can be "channel", "system", "reference", etc.)
3. Detect the skill's operational type
Use heuristics to choose the right dashboard template:
- Pipeline (steps, phases, processes): if category=
"Channels"OR description contains "send", "publish", "dispatch" →TEMPLATE_PIPELINE - System (infrastructure, admin, monitoring): if category=
"System"OR description contains "pipeline", "queue", "monitor" →TEMPLATE_SYSTEM - Channel (mail, Slack, etc.): if explicit
type: "channel"OR description contains activity/communication →TEMPLATE_CHANNEL - Basic (default for ambiguous): all others →
TEMPLATE_BASIC
The type detection code is in state/lib/dashboard-builder.ts as detectSkillType().
4. Compose the dashboard
Pick the template based on detected type. For ui.openui, compose the default skill dashboard. For named resources, compose the exact work artifact implied by the intent metadata.
Named resources must start with:
// surface: Human surface name
// intents: phrase one; phrase two; phrase three
// response: Short middle-chat sentence when this surface opens.
The comments are not decoration. The head-screen saved-surface registry uses them to route natural requests directly to the saved OpenUI resource.
Each default dashboard template:
- Queries
get_evalsfor the last 7 days of runs for this skill - Computes KPIs: run count, avg score, success rate, last run time
- Renders a data table with full run history
- For pipeline/channel types, adds extra visualizations (failure breakdown, recent activity)
All templates emit valid OpenUI Lang that the right-panel Renderer can paint.
5. Write the file atomically
const tmp = `${resourcesDir}/ui.openui.tmp.${process.pid}`;
writeFileSync(tmp, openui);
renameSync(tmp, outputPath);
Never leave a half-written resource file. If rename fails, the original is intact. Use writeOpenUiResource(slug, lang, { resourceName }) so validation, loader-linking, and atomic writes share one path.
Templates
Four templates, auto-selected by skill type:
TEMPLATE_BASIC
Minimum viable dashboard. Header + KPI row (runs, avg score, last run time) + full run table.
Used for: reference skills, utilities, anything that doesn't fit a stereotype.
TEMPLATE_PIPELINE
Pipeline/ETL-focused. Emphasizes success rate and failure breakdown.
Used for: skills with category: "Channels" or description containing "send", "publish", "queue", "process", "stage".
Extra visualizations:
- Success rate (%)
- Failure count
- Separate table of recent failures (score < 1.0) with primary_issue highlighted
TEMPLATE_CHANNEL
Activity-focused. Queries both evals AND recent-activity feed.
Used for: skills with type: "channel" or description containing "post", "message", "dispatch", "notify".
Extra visualizations:
- Recent activity from
get_recentfiltered to this skill's verb - Status (pass / needs-review based on last score)
TEMPLATE_SYSTEM
Infrastructure-focused. Minimal, clean, with implicit "this runs in the background" tone.
Used for: skills with category: "System" or type: "system", or description containing "monitor", "health", "admin".
Extra visualizations:
- Health indicator (good if avg score > 0.8, else review)
- Metadata card stating skill purpose
Invoking the skill
# Generate to stdout (for inspection)
npx tsx state/lib/dashboard-builder.ts ai-spend
# Generate and write atomically to the skill's resources folder
npx tsx state/lib/dashboard-builder.ts dogfood-loop > state/skills/dogfood-loop/resources/ui.openui
# Compare before/after
npx tsx state/lib/dashboard-builder.ts email-send-tick > /tmp/email-send-tick.new
diff state/skills/email-send-tick/resources/ui.openui /tmp/email-send-tick.new | head -20
Critical rules
- Always check if
state/skills/<slug>/resources/<resourceName>already exists. If it does, warn the user and ask before overwriting unless the request is explicitly maintenance/regeneration. - Never regenerate if the file carries a
# DO NOT REGENERATEheader comment at the top. Respect manual customizations. - Atomic writes only. Temp file + rename. If the skill's resources folder doesn't exist, create it first (
mkdir -p). - Frontmatter plus resource metadata is truth. The SKILL.md frontmatter determines ownership and domain. Named-resource comments determine routing.
- All templates must produce valid OpenUI Lang. Test with
findOpenUiResourceContractIssues()before writing. If the syntax is malformed, the right-panel Renderer will fail silently. - Query() and Mutation() calls use the toolProvider contract. Use existing provider names from SnappyChat's
web/src/tool-provider.ts; add a provider before referencing a new Query.
When NOT to use this skill
- Manually customized dashboards: if a skill's ui.openui was hand-crafted and has domain-specific visualizations (e.g. custom math, specific field filtering), don't regenerate. Add a
# DO NOT REGENERATEheader comment so future agents skip it. - Raw React shape work: use
ui-components/shape-builderonly for rare customdefineComponentcomponents that cannot be composed from OpenUI primitives. Normal right-canvas work belongs inresources/*.openui.
Eval
eval: auto-shape - the generated OpenUI is shape-checked by the right-panel Renderer at display time. If it fails to render, the audit agent (eval-watch-right-panel) emits a score row and surfaces the rendering error.
Manually run with: npx tsx state/lib/dashboard-builder.ts <slug> | head -1 to spot-check syntax.
AGENTS.md- what the AI loads when this skill comes up
Critical Rules
- Frontmatter plus resource metadata is truth. SKILL.md frontmatter (name | category | type | description | ui_contract) determines ownership and default dashboard shape. Named resource comments (
// surface,// intents,// response) determine natural-intent routing. - Atomic writes only. Use
writeOpenUiResource(slug, lang, { resourceName })so temp+rename, contract checks, and loader linking stay on one path. Never write directly to target path. - Respect manual resources. If
state/skills/<slug>/resources/<resourceName>exists, check for# DO NOT REGENERATEheader in first 5 lines. Skip if present; warn before overwrite unless explicitly maintaining/regenerating. - Default dashboard is
ui.openui; saved work surfaces are named files. Useresources/ui.openuifor the skill dashboard. Useresources/<name>.openuifor repeatable canvases such as schedules, previews, inspectors, and live work artifacts. - Named resources need registry metadata. The first lines must include
// surface: ...,// intents: phrase; phrase, and// response: ...; the saved-surface registry reads those comments. - Valid OpenUI Lang mandatory. All resources must pass
findOpenUiResourceContractIssues()before write. Renderer drift gets fixed at the resource contract, not by adding server-side regex fallbacks. - Query/Mutation via toolProvider contract. Use
Query("get_evals", {slug, days: 7}, {evals: []}, 60)and existing providers from SnappyChatweb/src/tool-provider.ts. Add the provider before referencing a new Query. Never inline raw SQL or non-Query() sources. - YAML parsing is regex-based.
parseFrontmatter()uses simple regex extraction. Unquoted colons or newlines in description field may fail - check SKILL.md frontmatter syntax. - Type detection is heuristic. Ambiguous category/description defaults to BASIC template (safe but may not be optimal). Add explicit
type: "system"ortype: "channel"to SKILL.md frontmatter if needed.
Commands
# Generate to stdout (inspection)
npx tsx state/lib/dashboard-builder.ts <slug>
# Generate the default dashboard to stdout
npx tsx state/lib/dashboard-builder.ts <slug>
# Validate and atomically write any OpenUI resource
npx tsx state/lib/openui-resource-contract.ts write-file <slug> /tmp/resource.openui ui.openui
npx tsx state/lib/openui-resource-contract.ts write-file <slug> /tmp/schedule.openui schedule.openui
# Test syntax (first line only)
npx tsx state/lib/dashboard-builder.ts <slug> | head -1
# Compare before/after
diff state/skills/<slug>/resources/ui.openui <(npx tsx state/lib/dashboard-builder.ts <slug>)
# List all OpenUI resources
find state/skills -path "*/resources/*.openui" | wc -l
# Find missing default dashboards
for d in state/skills/*/; do [ ! -f "$d/resources/ui.openui" ] && echo "$(basename $d)"; done
Self-Test
- [ ] Four templates exist: BASIC | PIPELINE | SYSTEM | CHANNEL
- [ ] Skill exists check:
existsSync(join(cwd, "state/skills", slug, "SKILL.md")) - [ ] Parse frontmatter: name | description | category | type
- [ ] Detect type via
detectSkillType()heuristic - [ ] Query
get_evalsfor last 7 days, compute KPIs (runs | avg-score | success-rate | last-run-time) - [ ] Render correct template based on detected type or named surface metadata
- [ ] For named surfaces, include
// surface,// intents, and// response - [ ] Validate OpenUI Lang syntax
- [ ] Write atomically (temp + rename)
- [ ] Ensure AGENTS.md links the exact resource path with "Read it before rendering or editing"
- [ ] Return path on success
<!-- kernel-ok: historical attribution for the PID-loader pattern. -->
Self-correcting loader (PID feedback)
This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.
Before you finish, do two things:
1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.
- Default action: edit this file. Surgical. Add the missing line, table row, or code example where it belongs. No restructuring.
- Only
LOGGEDis allowed when: the fix needs >10 lines, spans multiple files, or requires a structural rewrite. In that case thestate/regen/drain.shqueue picks it up asynchronously. - "I didn't have time" / "it's minor" / "the next agent will figure it out" are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.
- Concrete example of a valid P-fix: discover the template syntax uses
@Count()notCount(). One line in the Commands table. Edit it in. Do not just log it. - The goal: the next agent never has to leave the loader.
2. Log the result.
echo "[$(date -u +%FT%TZ)] dashboard-builder: <what was missing or fixed> [FIXED|LOGGED] action_kind=<kind>" >> state/log/loader-feedback.log
<slug>MUST be the literal folder name of this loader (state/skills/dashboard-builder/AGENTS.md).FIXED= you patched this loader inline (P-fix).LOGGED= too large for inline; the PostToolUse enqueue + Stop-hook drain will rewrite the loader from scratch on next session-end.action_kindvalues:shape-ok- only frontmatter-shape verification passed (rare from a human; usually emitted by the lint)skill-ran- the skill ran end-to-end and an eval row landed instate/log/evals.ndjsonloader-rewritten- you EDITED this AGENTS.md inline (the FIXED case), OR the regen drain rewrote itpattern-elevated- you promoted a recurring failure to a Critical Rule
Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/dashboard-builder/resources/ui.openui. Read it before rendering or editing this skill's generated component surface. - Named skill-owned surfaces live beside it as
state/skills/dashboard-builder/resources/*.openui. Read each one before rendering or editing it; named files must carry// surface,// intents, and// responsemetadata for registry discovery. - Treat this resource as a first-class artifact of the skill, not a generic chat response. Improve it when the skill's user-facing output needs to become richer.
- System resources compose OpenUI primitives and inherit SnappyChat tokens. Use
ui_contract: brandedin SKILL.md only for deliberate platform or client visuals.
api.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* snappy-dashboard-builder/api.ts -- Auto-generate resources/ui.openui for any skill.
* Legacy module name; the output is a skill-owned OpenUI resource, not always
* a dashboard-shaped surface.
*
* The system extends itself: given a skill slug, read its SKILL.md + eval logs,
* then emit a tailored OpenUI Lang resource via LLM composition.
* Falls back to heuristic templates if the LLM call fails.
*
* Usage:
* npx tsx state/lib/dashboard-builder.ts <slug>
* npx tsx state/lib/dashboard-builder.ts ai-spend > /tmp/ai-spend.regen.openui
*
* Or import as module:
* import { generateDashboard } from "./dashboard-builder.ts";
* const openui = await generateDashboard("dogfood-loop");
*/
import { readFileSync, existsSync } from "fs";
import { join, resolve } from "path";
import { realpathSync } from "fs";
import { dispatchFor, readDefaultModel, readDispatchConfig } from "./dispatch.ts";
import { findOpenUiResourceIssues, writeOpenUiResource } from "./openui-resource-contract.ts";
interface SkillMeta {
name: string;
description: string;
category?: string;
type?: string;
}
// --- OpenUI Fallback Templates ---
const TEMPLATE_BASIC = (slug: string): string => `evals = Query("get_evals", {slug: "${slug}", days: 7}, {evals: []}, 60)
header = CardHeader("${slug} — last 7 days")
runCount = @Count(evals.evals)
avgScore = @Avg(evals.evals.score)
lastRun = @First(@Sort(evals.evals, "ts", "desc"))
avgScoreText = "" + @Round(avgScore, 2)
lastTsText = "" + lastRun.ts
kpiRow = Stack([
Card([TextContent("Runs", "small"), TextContent("" + runCount, "large-heavy")]),
Card([TextContent("Avg score", "small"), TextContent(avgScoreText, "medium")]),
Card([TextContent("Last", "small"), TextContent(lastTsText, "small")])
], "row", "s", "stretch", "start", true)
table = Table([
Col("When", evals.evals.ts),
Col("Score", evals.evals.score),
Col("Note", evals.evals.notes)
])
root = Stack([header, kpiRow, Card([table])])
`;
const TEMPLATE_PIPELINE = (slug: string): string => `evals = Query("get_evals", {slug: "${slug}", days: 7}, {evals: []}, 60)
header = CardHeader("${slug} — pipeline status")
runCount = @Count(evals.evals)
successCount = @Count(@Filter(evals.evals, "score", "==", 1.0))
failCount = @Count(@Filter(evals.evals, "score", "<", 1.0))
kpiRow = Stack([
Card([TextContent("Runs (7d)", "small"), TextContent("" + runCount, "large-heavy")]),
Card([TextContent("Successes", "small"), TextContent("" + successCount, "medium")]),
Card([TextContent("Failures", "small"), TextContent("" + failCount, "medium")])
], "row", "s", "stretch", "start", true)
failures = @Filter(evals.evals, "score", "<", 1.0)
failureTable = Table([
Col("Timestamp", failures.ts),
Col("Score", failures.score),
Col("Issue", failures.primary_issue)
])
table = Table([
Col("Timestamp", evals.evals.ts),
Col("Score", evals.evals.score),
Col("Note", evals.evals.notes)
])
root = Stack([header, kpiRow, Card([TextContent("Recent failures", "small-heavy"), failureTable]), Card([TextContent("All runs", "small-heavy"), table])])
`;
const TEMPLATE_CHANNEL = (slug: string): string => `evals = Query("get_evals", {slug: "${slug}", days: 7}, {evals: []}, 60)
recent = Query("get_recent", {limit: 30}, {evals: [], total: 0, truncated: false}, 30)
header = CardHeader("${slug} — activity")
runCount = @Count(evals.evals)
sortedEvals = @Sort(evals.evals, "ts", "desc")
lastRun = @First(sortedEvals)
latestScoreText = "" + lastRun.score
kpiRow = Stack([
Card([TextContent("Activity (7d)", "small"), TextContent("" + runCount, "large-heavy")]),
Card([TextContent("Latest score", "small"), TextContent(latestScoreText, "medium")])
], "row", "s", "stretch", "start", true)
recentItems = @Filter(recent.evals, "skill", "==", "${slug}")
activityTable = Table([
Col("Time", recentItems.ts),
Col("Verb", recentItems.verb),
Col("Score", recentItems.score),
Col("Note", recentItems.note)
])
allTable = Table([
Col("When", evals.evals.ts),
Col("Score", evals.evals.score),
Col("Note", evals.evals.notes)
])
root = Stack([header, kpiRow, Card([TextContent("Recent activity", "small-heavy"), activityTable]), Card([TextContent("All runs", "small-heavy"), allTable])])
`;
const TEMPLATE_SYSTEM = (slug: string): string => `evals = Query("get_evals", {slug: "${slug}", days: 7}, {evals: []}, 60)
full = Query("get_full_state", {}, {agents: [], skills: []}, 120)
header = CardHeader("${slug}")
runCount = @Count(evals.evals)
avgScore = @Avg(evals.evals.score)
avgScoreText = "" + @Round(avgScore, 2)
kpiRow = Stack([
Card([TextContent("System runs", "small"), TextContent("" + runCount, "large-heavy")]),
Card([TextContent("Avg score", "small"), TextContent(avgScoreText, "medium")])
], "row", "s", "stretch", "start", true)
table = Table([
Col("When", evals.evals.ts),
Col("Score", evals.evals.score),
Col("Note", evals.evals.notes)
])
metadata = Card([
TextContent("${slug} — system skill", "small-heavy"),
TextContent("Monitors and maintains snappy-os infrastructure", "small")
])
root = Stack([header, kpiRow, metadata, Card([table])])
`;
// --- Skill Type Detection (fallback heuristic) ---
function detectSkillType(skillMeta: SkillMeta): string {
const desc = (skillMeta.description || "").toLowerCase();
const cat = (skillMeta.category || "").toLowerCase();
if (skillMeta.type === "channel") return "channel";
if (skillMeta.type === "system") return "system";
if (skillMeta.type === "reference") return "system";
if (
cat === "channels" ||
cat === "integration" ||
desc.includes("send") ||
desc.includes("publish") ||
desc.includes("dispatch")
) {
return "channel";
}
if (
cat === "system" ||
cat === "admin" ||
cat === "memory" ||
desc.includes("pipeline") ||
desc.includes("queue") ||
desc.includes("monitor")
) {
return "system";
}
if (
desc.includes("step") ||
desc.includes("phase") ||
desc.includes("stage") ||
/(?<![a-z-])process(?![a-z-])/.test(desc)
) {
return "pipeline";
}
return "basic";
}
function generateFromTemplate(slug: string, meta: SkillMeta): string {
const skillType = detectSkillType(meta);
switch (skillType) {
case "pipeline": return TEMPLATE_PIPELINE(slug);
case "channel": return TEMPLATE_CHANNEL(slug);
case "system": return TEMPLATE_SYSTEM(slug);
default: return TEMPLATE_BASIC(slug);
}
}
// --- LLM Composition ---
const LANG_SYSTEM_PROMPT = `You compose live skill-owned OpenUI resources in OpenUI Lang for snappy-os skills.
AVAILABLE DATA (Query tool names + return shape):
- get_evals({slug, days}) → {evals: [{ts, score, notes, primary_issue, ...}]}
- get_agents() → {agents: [{name, status, lastRun, score, ...}]}
- get_skills() → {skills: [{name, runs, score, lastRun, ...}]}
- get_recent({limit}) → {evals: [{ts, skill, verb, score, note, primary_issue, ...}], total, truncated, last_updated}
- get_dispatch_log() → {entries: [{ts, model, durationMs, ...}]}
- get_inbox_email({days}) → {messages: [{from, subject, ts, ...}]}
- get_inbox_slack({}) → {messages: [{channel_name, text, ts, ...}]}
- get_inbox_calendar({days}) → {events: [{summary, start, location, ...}]}
- get_brain() → {nodes: [...], edges: [...]}
- get_recall({days}) → {threads: [...], evals: [...], commits: [...]}
LANG PRIMITIVES (use these and only these):
Card, CardHeader, Stack, Col, Table, BarChart, LineChart, AreaChart, PieChart, Tag, TagBlock, Button, Buttons, Callout, TextCallout, Steps, StepsItem, Tabs, TabItem, Accordion, AccordionItem, ListBlock, ListItem, MarkDownRenderer, Image, ImageBlock, Separator, TextContent, CodeBlock
OFFICIAL OPENUI SIGNATURES:
- Callout(variant, title, description) where variant is "info" | "warning" | "error" | "success" | "neutral".
Do NOT emit legacy Callout("ok", "...") or Callout("warn", "...").
- StepsItem(title, details). Details is required; use "done", "running", "pending", or a short status phrase.
LANG SYNTAX RULES:
- Every variable assignment uses =
- Function calls: BarChart(title, [val1, val2], "blue") positional args
- Stack accepts arrays: Stack([Card([...]), Card([...])], "row", "s", "stretch", "start", true)
- Any horizontal Stack must pass wrap=true as the 6th positional arg so generated components fit the chat viewport.
- Use "column" for vertical Stack direction. Never emit legacy "col".
- Filter/map: @Filter(arr, field, op, value), @Count(arr), @Sum(arr.field), @Avg(arr.field)
- Supported @ built-ins are only @Count, @Sum, @Avg, @Min, @Max, @First, @Last, @Filter, @Sort, @Round, @Abs, @Floor, @Ceil, @Each, @Run, @Set, @Reset, @ToAssistant, @OpenUrl.
- Never emit unsupported helpers such as @If, @Coalesce, @Slice, @Get, @Distinct, or @FormatTime.
- The final assignment MUST be root = <expression>
- Strings must be double-quoted; arrays use []; objects use {key: value}
- Ternary expressions are valid OpenUI Lang; use them sparingly and keep them readable.
- NO Python list comprehensions, NO JS template literals, NO nullish coalescing, NO pipe chains inside Lang
- Every Query() needs a default value as 3rd arg and refresh interval (in seconds) as 4th arg
STYLE GUIDE (snappy-os tone):
- Workshop, not showroom. No bouncy animations or marketing language.
- Use tokens via OpenUI primitives, do not inline color hex.
- Headers: short and direct. "X — last 7 days" not "Welcome to your X dashboard!"
- No emojis, no em dashes, no exclamation points.
- First-line stats over prose. Tables over paragraphs.
OUTPUT RULES:
- Output ONLY the OpenUI Lang code. No explanation, no markdown fences, no comments.
- The very first character of your output must be a letter (start of a variable name).
- Do not wrap in backticks. Do not prefix with "Here is" or similar.`;
function buildUserPrompt(slug: string, meta: SkillMeta, skillBody: string): string {
return `THE SKILL:
name: ${slug}
category: ${meta.category || "unset"}
description: ${meta.description}
Body of SKILL.md (prose that defines behavior):
${skillBody.slice(0, 3000)}
YOUR JOB:
Compose an OpenUI resource that answers "what is happening with this skill right now?" using a layout structure that matches THIS skill's purpose.
LAYOUT BY SKILL TYPE — mandatory. Do NOT default to "Card + CardHeader + description + tags". That is the lazy fallback and it is FORBIDDEN.
PIPELINE skill (npm publish, scheduled job, cron worker, send-tick, queue drainer):
MUST use Steps([StepsItem(...), ...]) to show the pipeline stages.
MUST show a KPI row: throughput / failure-rate / last-run.
MUST show a recent-failures Table.
Shape: Stack([header, phases, kpiRow, Card([failureTable])])
Example:
evals = Query("get_evals", {slug: "${slug}", days: 7}, {evals: []}, 60)
header = CardHeader("${slug} — pipeline status")
phases = Steps([StepsItem("check", "done"), StepsItem("run", "running"), StepsItem("tag", "pending")])
successCount = @Count(@Filter(evals.evals, "score", "==", 1.0))
failCount = @Count(@Filter(evals.evals, "score", "<", 1.0))
kpiRow = Stack([Card([TextContent("Runs", "small"), TextContent("" + @Count(evals.evals), "large-heavy")]), Card([TextContent("Failures", "small"), TextContent("" + failCount, "medium")])], "row", "s", "stretch", "start", true)
failures = @Filter(evals.evals, "score", "<", 1.0)
failTable = Table([Col("When", failures.ts), Col("Issue", failures.primary_issue)])
root = Stack([header, phases, kpiRow, Card([failTable])])
CHANNEL skill (email, slack, calendar, inbox, message delivery, notifications):
MUST use Tabs([TabItem("All", ...), TabItem("Recent", ...), TabItem("Errors", ...)]) to slice data.
Each tab contains a Table with message-level rows.
Shape: Stack([header, kpiRow, Tabs([TabItem("All", allTable), TabItem("Errors", errTable)])])
SYSTEM skill (head-screen, brain-insights, ai-spend, dogfood-loop, monitor, health check):
MUST use Callout("success" or "warning", "Current health", "<status text>") at the top for current health.
MUST show a KPI grid of 4 cards in a row.
MUST show a recent-activity feed at the bottom.
Shape: Stack([header, statusCallout, kpiGrid, activityFeed])
KNOWLEDGE / REFERENCE skill (now, dashboard-builder, catalog, index, reference):
MUST use Accordion([AccordionItem(...), ...]) for collapsible context sections.
MUST use Tag / TagBlock for major facets.
MUST use a Callout for the key prose context.
Shape: Stack([header, facetTags, Accordion([...]), Callout("info", "Context", "...")])
BASIC skill (anything else):
Use run count + avg score KPI row + recent runs table.
Still MUST NOT use a plain generic Card+description. Show numbers.
Pick the Query sources that are most relevant. Not every skill needs get_evals — an inbox skill should show inbox data. Do not force get_evals if it does not fit.
IMPORTANT: Keep the total output under 60 lines. Use at most 3-4 sections. Prefer concise, high-signal layouts over exhaustive ones. The generated surface must fit on screen without scrolling.
Output ONLY the OpenUI Lang. Start immediately with the first variable assignment.`;
}
function validateLang(output: string): boolean {
const trimmed = output.trim();
if (!trimmed) return false;
// Must not start with backticks or explanation prose
if (trimmed.startsWith("```") || trimmed.startsWith("Here ") || trimmed.startsWith("I ")) return false;
// First non-empty line must contain = (variable assignment)
const firstLine = trimmed.split("\n").find(l => l.trim().length > 0) || "";
if (!firstLine.includes("=")) return false;
// Must contain root =
if (!trimmed.includes("root =")) return false;
// Must contain at least one Query call
if (!trimmed.includes('Query("get_')) return false;
const runtimeIssues = findOpenUiResourceIssues(trimmed);
if (runtimeIssues.length > 0) {
process.stderr.write(`[dashboard-builder] invalid OpenUI resource: ${runtimeIssues[0].detail}\n`);
return false;
}
return true;
}
function normalizeLangOutput(output: string): string {
const trimmed = output.trim();
const toolMatch = trimmed.match(/\[\[TOOL:Lang\]\]([\s\S]*?)\[\[\/TOOL\]\]/);
if (toolMatch?.[1]) return toolMatch[1].trim();
const fenceMatch = trimmed.match(/```(?:openui|lang)?\s*([\s\S]*?)```/i);
if (fenceMatch?.[1]) return fenceMatch[1].trim();
return trimmed;
}
async function callLLMViaDispatch(slug: string, systemPrompt: string, userPrompt: string): Promise<string> {
const axis = readDispatchConfig().subagent;
const modelLabel = axis.model === "auto" ? readDefaultModel().slug : axis.model;
process.stderr.write(`[dashboard-builder] composing ${slug} via ${axis.backend}/${modelLabel}\n`);
const result = await dispatchFor("subagent", {
prompt: userPrompt,
systemPrompt,
cwd: resolve(process.cwd()),
tools: ["read", "grep", "ls"],
timeoutMs: 180_000,
interviewMode: false,
});
if (!result.ok || !result.output.trim()) {
const detail = result.error || result.stderr || `exit ${result.exitCode}`;
throw new Error(`${result.provider}/${result.model} failed: ${detail}`);
}
return normalizeLangOutput(result.output);
}
async function callLLM(slug: string, meta: SkillMeta, skillBody: string, systemPrompt = LANG_SYSTEM_PROMPT): Promise<string> {
const userPrompt = buildUserPrompt(slug, meta, skillBody);
return callLLMViaDispatch(slug, systemPrompt, userPrompt);
}
// --- Main Generator ---
// --- Dry-run overload (returns content without writing) ---
export async function generateDashboard(slug: string, opts?: { dryRun?: boolean }): Promise<{ content: string; written: boolean }>;
// Legacy overload: plain string return (backward-compat)
export async function generateDashboard(slug: string): Promise<string>;
export async function generateDashboard(slug: string, opts?: { dryRun?: boolean }): Promise<string | { content: string; written: boolean }> {
const dryRun = opts?.dryRun === true;
const _legacyStringReturn = opts === undefined;
return _generateDashboardImpl(slug, dryRun, _legacyStringReturn);
}
async function _generateDashboardImpl(slug: string, dryRun: boolean, legacyStringReturn: boolean): Promise<string | { content: string; written: boolean }> {
const skillDir = resolve(process.cwd(), "state/skills", slug);
const skillMdPath = join(skillDir, "SKILL.md");
if (!existsSync(skillMdPath)) {
throw new Error(`Skill not found: ${slug} (expected at ${skillMdPath})`);
}
const skillMdContent = readFileSync(skillMdPath, "utf8");
const meta = parseFrontmatter(skillMdContent);
// Strip frontmatter to get just the body prose
const skillBody = skillMdContent.replace(/^---\n[\s\S]+?\n---\n?/, "").trim();
// Try LLM composition first
let lang: string;
try {
const raw = await callLLM(slug, meta, skillBody);
if (validateLang(raw)) {
lang = raw;
} else {
// Retry with a stricter system prompt
const stricterSystem = LANG_SYSTEM_PROMPT + "\n\nCRITICAL: Output ONLY OpenUI Lang. The first character must be a letter. No backticks, no prose.";
try {
const retryRaw = await callLLM(slug, meta, skillBody, stricterSystem);
if (validateLang(retryRaw)) {
lang = retryRaw;
} else {
process.stderr.write(`[dashboard-builder] dispatch output failed validation for ${slug}, using template fallback\n`);
lang = generateFromTemplate(slug, meta);
}
} catch {
process.stderr.write(`[dashboard-builder] dispatch retry failed for ${slug}, using template fallback\n`);
lang = generateFromTemplate(slug, meta);
}
}
} catch (e) {
process.stderr.write(`[dashboard-builder] dispatch call failed for ${slug}: ${(e as Error).message}, using template fallback\n`);
lang = generateFromTemplate(slug, meta);
}
if (legacyStringReturn) return lang;
return { content: lang, written: false };
}
export async function writeDashboard(slug: string): Promise<string> {
const skillDir = resolve(process.cwd(), "state/skills", slug);
const resourcesDir = join(skillDir, "resources");
const targetPath = join(resourcesDir, "ui.openui");
// Respect DO NOT REGENERATE header
if (existsSync(targetPath)) {
const existing = readFileSync(targetPath, "utf8");
const firstFive = existing.split("\n").slice(0, 5).join("\n");
if (firstFive.includes("# DO NOT REGENERATE")) {
process.stderr.write(`[dashboard-builder] Skipping ${slug}: DO NOT REGENERATE header present\n`);
return targetPath;
}
}
const result = await generateDashboard(slug, {});
const lang = typeof result === "string" ? result : result.content;
return writeOpenUiResource(slug, lang).path;
}
// Convenience: generate without writing, return content string only.
export async function generateDashboardContent(slug: string): Promise<string> {
const result = await generateDashboard(slug, { dryRun: true });
return (result as { content: string; written: boolean }).content;
}
function parseFrontmatter(content: string): SkillMeta {
const match = content.match(/^---\n([\s\S]+?)\n---/);
if (!match) return { name: "", description: "" };
const yaml = match[1];
const meta: SkillMeta = { name: "", description: "" };
const nameMatch = yaml.match(/^name:\s*(.+)$/m);
if (nameMatch) meta.name = nameMatch[1].trim();
const descMatch = yaml.match(/^description:\s*['""]?(.+?)['""]?\s*$/m);
if (descMatch) meta.description = descMatch[1].trim().replace(/^["']+|["']+$/g, "");
const catMatch = yaml.match(/^category:\s*(.+)$/m);
if (catMatch) meta.category = catMatch[1].trim();
const typeMatch = yaml.match(/^type:\s*(.+)$/m);
if (typeMatch) meta.type = typeMatch[1].trim();
return meta;
}
// --- CLI ---
if ((() => {
try {
return import.meta.url === `file://${realpathSync(process.argv[1])}`;
} catch {
return false;
}
})()) {
(async () => {
const slug = process.argv[2];
if (!slug) {
console.error("Usage: npx tsx state/lib/dashboard-builder.ts <slug>");
process.exit(1);
}
try {
const openui = await generateDashboard(slug);
console.log(openui);
} catch (err) {
console.error("Error:", (err as Error).message);
process.exit(1);
}
})();
}
scripts- helper scripts it can run
prose-only skill - 4 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 9 runs
| timestamp | verb | score | primary_issue | artifact |
|---|---|---|---|---|
| 2026-05-01 06:13Z | - | 0.90 | - | - |
| 2026-05-01 06:08Z | - | 0.30 | - | - |
| 2026-05-01 06:08Z | - | 0.90 | - | - |
| 2026-05-01 06:13Z | - | 0.90 | - | - |
| 2026-05-01 06:08Z | - | 0.30 | - | - |
| 2026-05-01 06:08Z | - | 0.90 | - | - |
| 2026-05-01 06:13Z | - | 0.90 | - | - |
| 2026-05-01 06:08Z | - | 0.30 | - | - |
| 2026-05-01 06:08Z | - | 0.90 | - | - |