OR Key
drop another .md file to compare - side-by-side diff against dashboard-builder

dashboard-builder

Helps build and update the screens in your dashboard.
personal 2 files 9 recent evals

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.

Work with me
For developers how this skill is built, graded, and how it runs

at a glance- the short version

eval modeauto-shape
categorySystem
stages5

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.

The skill
state/skills/dashboard-builder/SKILL.md present
the skill itself, in plain text
The main file. It says what the skill is and lays out the steps in plain English.
Code
state/lib/dashboard-builder.ts present
code the skill can run
Reusable code this skill can call when it needs to.
Scripts
state/bin/dashboard-builder/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/dashboard-builder/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

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.

makes the work The worker
inferred
# Generate to from a command
No worker named, so the first command in the skill is treated as the worker.
checks the work The reviewer
inferred
shape gate an automatic check
The check is an automatic pass or fail on the shape of the result, run separately from the work itself.
frame
learns Self-correction
present
fixes itself learns from gaps
When a run hits a gap, the skill gets edited on the spot [FIXED] or queued for a bigger rewrite [LOGGED], so it keeps getting better.
tidies up Background fixes
present
queued for rewrite runs in the background
Bigger fixes that can't be made on the spot get queued and rewritten in the background later.
remembers Run history
present
state/log/evals.ndjson auto-shape runs
Every run is written down here, so the next time this skill is used it already knows how the last runs went.
Critical rules the things this skill must not get wrong
No must-not-break rules called out for this skill. Anything important lives in the writeup below.

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.

  1. Loading feedback rows…

how the work flows- step by step

1 stage
Validate the skill exists
```typescript
2 stage
Parse the skill's frontmatter
Read SKILL.md, extract the YAML block (--- to ---), and parse:
what this step does
Read SKILL.md, extract the YAML block (--- to ---), and parse: - name — the skill name - description — what it does - category — skill category (Channels, System, Memory, etc.) - type — explicit type field (optional; can be "channel", "system", "reference", etc.)
3 stage
Detect the skill's operational type
Use heuristics to choose the right dashboard template:
what this step does
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**
4 generator
Compose the dashboard
Pick the template based on detected type. For ui.openui, compose the default skill dashboard. For named resources, compo
what this step does
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: 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_evals for the last 7 days of runs for this skill - Computes KPIs: run coun
5 generator
Write the file atomically
```typescript
what this step does
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.

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:

  1. Error-prone (copy-paste bugs, stale templates)
  2. Maintenance-heavy (every query signature change breaks N dashboards)
  3. 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.openui for the default skill dashboard, or a named surface such as schedule.openui
  • intent 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 name
  • description - what it does
  • category - 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_evals for 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_recent filtered 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

  1. 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.
  2. Never regenerate if the file carries a # DO NOT REGENERATE header comment at the top. Respect manual customizations.
  3. Atomic writes only. Temp file + rename. If the skill's resources folder doesn't exist, create it first (mkdir -p).
  4. Frontmatter plus resource metadata is truth. The SKILL.md frontmatter determines ownership and domain. Named-resource comments determine routing.
  5. All templates must produce valid OpenUI Lang. Test with findOpenUiResourceContractIssues() before writing. If the syntax is malformed, the right-panel Renderer will fail silently.
  6. 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 REGENERATE header comment so future agents skip it.
  • Raw React shape work: use ui-components / shape-builder only for rare custom defineComponent components that cannot be composed from OpenUI primitives. Normal right-canvas work belongs in resources/*.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

  1. 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.
  2. 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.
  3. Respect manual resources. If state/skills/<slug>/resources/<resourceName> exists, check for # DO NOT REGENERATE header in first 5 lines. Skip if present; warn before overwrite unless explicitly maintaining/regenerating.
  4. Default dashboard is ui.openui; saved work surfaces are named files. Use resources/ui.openui for the skill dashboard. Use resources/<name>.openui for repeatable canvases such as schedules, previews, inspectors, and live work artifacts.
  5. Named resources need registry metadata. The first lines must include // surface: ..., // intents: phrase; phrase, and // response: ...; the saved-surface registry reads those comments.
  6. 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.
  7. Query/Mutation via toolProvider contract. Use Query("get_evals", {slug, days: 7}, {evals: []}, 60) and existing providers from SnappyChat web/src/tool-provider.ts. Add the provider before referencing a new Query. Never inline raw SQL or non-Query() sources.
  8. YAML parsing is regex-based. parseFrontmatter() uses simple regex extraction. Unquoted colons or newlines in description field may fail - check SKILL.md frontmatter syntax.
  9. Type detection is heuristic. Ambiguous category/description defaults to BASIC template (safe but may not be optimal). Add explicit type: "system" or type: "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_evals for 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 LOGGED is allowed when: the fix needs >10 lines, spans multiple files, or requires a structural rewrite. In that case the state/regen/drain.sh queue 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() not Count(). 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_kind values:
  • 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 in state/log/evals.ndjson
  • loader-rewritten - you EDITED this AGENTS.md inline (the FIXED case), OR the regen drain rewrote it
  • pattern-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 // response metadata 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: branded in 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

rubric auto-shape no rubric declared
recent mean 0.70 · 9 runs actor/auditor: unverifiable
deps none declared
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 - -