Every tool here ends up as a script plus a page, and until now each one
invented its own way of saying “this passed.” This is the house format: follow it and a
new .py plus its .html drop into the master dashboard as a live
tile — green at a glance, one click to run, one click to open — with no glue code on
either side. It is deliberately small: five marker lines on stdout, one
self-contained page, one JSON entry.
-V) and the
status-every-5-seconds rule for any phase that can outlast a few seconds. Both
live in CLAUDE.md.
A tile is a nicer front end for a tool that is already well behaved on its own — it is
not a substitute for one.
One master launcher owns the browser tab. Everything else is a normal standalone program that happens to describe its own result in a way the master can read.
.py serves one master .html —
the only page you keep open. It runs the sub-tools, tracks their status, and links
their pages..py, still runnable by hand from a
terminal with no dashboard anywhere in sight..html — a full sub-dashboard for
the detail that will not fit on a tile. The master opens it in its own tab.A task reports its result by printing. That is the whole protocol: no imports,
no sockets, no exit-code guessing games. Any line matching
DASH_<KEY>: <value> is consumed by the master; every other line
stays in the live log.
| Marker | Meaning | Example |
|---|---|---|
DASH_STATUS: |
The tile's LED. One of PASS WARN FAIL SKIP. Print it last. | DASH_STATUS: FAIL |
DASH_SUMMARY: |
One line under the tile name. Numbers, not adjectives — this is what gets read at a glance. | DASH_SUMMARY: 10 open · 28 waived · 23 checks |
DASH_METRIC: |
A label=value pair for the detail rail. Emit as many as you like;
the first two also appear on the tile. |
DASH_METRIC: open violations=10 |
DASH_HTML: |
Absolute path to the page this run wrote. It becomes the tile's open button. | DASH_HTML: /…/reports/drc.html |
DASH_LINK: |
label|url — an extra link in the rail (a log, a repo, a ticket). |
DASH_LINK: raw report|file:///…/drc.rep |
Flush as you go (print(…, flush=True)) so the log streams live instead of
arriving all at once at the end.
Four words, so that a wall of tiles means the same thing to everyone reading it.
| Status | Use it when | Exit code |
|---|---|---|
| PASS | Everything checked, nothing to act on. This is the only green. | 0 |
| WARN | Passed, but with something a human should know: waived violations, a margin under 10%, a stale input. | 0 |
| FAIL | A real violation. Someone has work to do before tapeout. | 1 |
| SKIP | Could not run — input file missing, tool not installed. Never report a missing input as PASS. | 0 |
With no DASH_STATUS line the master falls back, in order:
verdict: PASS|FAIL|WARN|SKIP (also
CORRECT / INCORRECT, so Calibre-style output scores itself);0 is PASS, anything else FAIL.So an existing checker usually needs no changes at all to become a tile. Add the markers when you want the summary line and the metrics, not to make it work.
.pypython3 tasks/thing.py --help must work and be
useful with no dashboard running.--rep, --gds, --data. Point the default at a
committed sample so the tile demonstrates itself on a fresh clone.-V / --version. The install convention ends
on it, so the paste proves itself — and the dashboard has something to show when a
tile will not start.stdout stays the pasteable summary and the DASH_ markers, so
the two never collide. Offer --no-progress and -q. The log
pane is live; a silent 90-second run looks hung either way.reports/..htmlfile:// on a machine with no internet.:root and override it
under @media (prefers-color-scheme: dark).One object in the launcher's config. id is what other tiles and the state
file refer to, so keep it short and never reuse one.
{
"id": "drc",
"name": "DRC",
"group": "Physical Verification",
"icon": "▦",
"cmd": "python3 tasks/drc_check.py",
"doc": "Calibre DRC summary — open vs waived violations, grouped by design area.",
"timeout": 900
}
| Field | Required | Notes |
|---|---|---|
id | yes | Short, stable, unique. Used in URLs and in the state file. |
name | yes | Fits a 208-px tile — two words at most. |
group | yes | Groups are the rows of the board. Reuse an existing one before inventing another. |
cmd | — | String or argv list. Omit it for a link-only tile. |
url | — | An external dashboard to open. Mutually exclusive with cmd in practice. |
doc | yes | One sentence, shown in the detail rail. Say what it reads and what it decides. |
timeout | — | Seconds before the run is terminated. Set it for anything that touches a simulator. |
rollup | — | true makes the tile run after all the others — see below. |
The master writes every finished run to state/results.json, keyed by tile
id: status, summary, metrics, and the timestamp. A tile marked
"rollup": true is scheduled last, so it can read that file and turn the whole
board into one verdict — a tapeout gate, a nightly digest, a release checklist.
Two rules make a rollup honest, and both are easy to skip:
A complete, valid task in 30 lines. Start here.
#!/usr/bin/env python3
"""One line saying what this checks and what it decides."""
import argparse, os, sys
def emit(k, v): print("DASH_%s: %s" % (k, v), flush=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-V", "--version", action="version", version="thing 1.0")
ap.add_argument("--no-progress", action="store_true") # per the 5-second rule
ap.add_argument("-q", "--quiet", action="store_true")
ap.add_argument("--rep", default="samples/thing.rep") # a committed sample
a = ap.parse_args()
if not os.path.exists(a.rep): # missing input is SKIP, never PASS
emit("STATUS", "SKIP")
emit("SUMMARY", "no input at %s" % a.rep)
return 0
print("reading %s" % a.rep) # plain prints become the live log
bad, waived = check(a.rep) # ... your actual work ...
path = write_report(bad) # a self-contained page, per section 6
emit("HTML", os.path.abspath(path))
emit("METRIC", "violations=%d" % len(bad))
emit("METRIC", "waived=%d" % waived)
st = "FAIL" if bad else ("WARN" if waived else "PASS")
emit("SUMMARY", "%d open · %d waived" % (len(bad), waived))
emit("STATUS", st) # status last
return 1 if st == "FAIL" else 0 # exit code for cron / CI
if __name__ == "__main__":
sys.exit(main())
python3 tasks/thing.py runs standalone and prints a
DASH_STATUS.python3 tasks/thing.py -V prints a version, so the install paste is
self-testing.-q silences it without touching stdout.1 on FAIL and 0 otherwise.file:// with no network.doc line.