This is the daily loop that makes the terminal sharper over time. Each run you: read the user's dashboard → pull a market snapshot for the symbols on it → grade yesterday's calls against what actually happened → write today's analysis + a few fresh, checkable calls → append it all to one log file.
The dashboard's daily-analysis frame renders the latest entry. You never
edit dashboard.json — your only write is the analysis log. (Sibling skill:
/zframes builds and edits the dashboard; this one only analyzes it.)
0. Resolve the CLI
You call the zframes CLI once (snapshot). Resolve how to invoke it, same as
the build skill:
- Standalone (normal case) →
npx zframes@latest <cmd>. - Inside the zframes monorepo →
pnpm --silent zframes <cmd>(runpnpm build:clionce ifpackages/cli/dist/is missing).
The CLI isn't published to npm yet, so until it is, use the monorepo
pnpm zframespath. The steps are identical either way.
1. Resolve which dashboard
Dashboards live in a global store ($XDG_CONFIG_HOME/zframes/dashboards,
default ~/.config/zframes/dashboards), and the store can hold several — so
first decide which one you're briefing, the same way serve resolves it:
- The user named one ("brief my crypto desk") → use that name.
- Otherwise → the default. A bare
zframes snapshot(no name) briefs the default, exactly likeserve;zframes listshows the store with the default marked*. - Ambiguous (no default set and several in the store) → run
zframes listand ask which one. Don't guess. - A plain path to a
dashboard.json(or the monorepo'sapps/runtime/) still works — pass it instead of a name.
Each dashboard keeps its own brief: the snapshot (next step) reports that
dashboard's logPath and the frame src for it, so two dashboards never share
one log.
2. Gather the snapshot (the deterministic half)
zframes snapshot [<name>] # omit <name> → the default; or pass a path
This prints one JSON object — capture it. Shape:
{
"date": "2026-06-17",
"run": { // engine stamp — copy into the entry
"timestamp": "2026-06-18T07:30:00Z", // objective, from the CLI
"model": "claude-opus-4-8", // from the runner (flag / ZFRAMES_MODEL env), else null
"effort": "high", // from the runner (ZFRAMES_EFFORT), else null
"config": null // optional runner notes
},
"dashboard": { // which dashboard + where its brief lives
"kind": "store", // "store" | "path"
"name": "crypto", // store name (null for a path target)
"file": "…/dashboards/crypto/dashboard.json",
"logPath": "…/dashboards/crypto/daily-analysis.json" // WRITE today's entry HERE (step 6)
},
"universe": ["xyz:TSLA", "xyz:NVDA", ...], // every symbol on the dashboard
"featured": "xyz:TSLA", // the featured frame's symbol
"market": {
"dayStats": { "xyz:TSLA": { "markPx", "prevDayPx", "changePct" }, ... },
"topMovers": { "gainers": [...], "losers": [...] }, // market-wide context
"candles": [ { "time","open","high","low","close","volume" }, ... ], // featured, ~14d daily
"funding": { "<sym>": [ { "time","fundingRate" }, ... ] },
"fearGreed": [ { "value","classification","time" }, ... ],
"global": { "totalMarketCapUsd","marketCapChangePct24h","dominance" },
"tvl": [ { "name","tvl" }, ... ]
},
"priorEntry": { ...yesterday's entry, or null on the first run }
}
The universe is whatever is on the dashboard — analyze those symbols, not a
remembered watchlist. A provider that was offline shows as null; work with
what's there.
3. One-time setup (idempotent — skip if already done)
The brief needs somewhere to render and somewhere to write — both keyed to the
dashboard you picked, taken from the snapshot's dashboard block, never a
hard-coded path:
- The frame. If the dashboard has no
daily-analysisframe, tell the user and offer to add one (a single instance, e.g.{"id":"daily-brief", "frame":"daily-analysis","title":"Daily Brief","position":{...},"config":{}}). Leaveconfigat its default — the frame fetches/daily-analysis.json, which for a store dashboard is served straight out of its own folder (dashboards/<name>/), so dashboards never collide. Adding the frame is the one time this flow touchesdashboard.json— and only with the user's go-ahead. If they decline, the loop still runs; the brief just isn't displayed. - The log. If
snapshot.dashboard.logPathis missing, create it — and its parent dir — as{ "entries": [] }.
4. Grade yesterday's calls
If priorEntry is non-null, walk its calls. Each has a plain-language
check (e.g. "TSLA 24h change < 0"). Evaluate it against the fresh snapshot
(market.dayStats[symbol].changePct, candles, funding…) and produce a
grades array — one { callId, verdict, note } per prior call:
verdict:hit|miss|partialnote: one line citing the number that decided it ("TSLA +0.6% — stayed in range")
On the first run (priorEntry: null) grades is [].
5. Write today's analysis
Reason over the snapshot and produce:
summary— a few sentences of real analysis (what moved, what's setting up, what to watch) for the symbols on the dashboard. Plain prose; newlines OK.calls— 2–4 explicit, checkable calls. Each:id(short slug),symbol,direction(bullish|bearish|neutral),claim(the call in words),horizon(e.g."1d"), and cruciallycheck— a plain-language criterion the next run can grade from a snapshot ("NVDA 24h change > 0","BTC funding flips negative"). A call you can't check tomorrow is useless to the loop — make it gradeable.
6. Append the entry to the log
Read the log at snapshot.dashboard.logPath, push the new entry onto entries
(newest last), and write it back. Entry shape:
{
"date": "<snapshot.date>",
"run": { ...snapshot.run }, // engine stamp, copied verbatim
"universe": [ ...snapshot.universe ],
"summary": "…",
"calls": [ { "id","symbol","direction","claim","check","horizon" }, ... ],
"grades": [ { "callId","verdict","note" }, ... ] // grades of the PRIOR entry's calls
}
Copy run from the snapshot verbatim. It records which model/effort produced
this brief, so a changing engine is visible in the log instead of silently
skewing the self-grading. If run.model or run.effort came back null (the
runner didn't pass them — see Scheduling), fill them from your own runtime
identity as a best-effort fallback, and keep run.timestamp as the CLI set it.
That's the loop's only write. The frame re-fetches on its own interval, so the brief appears without a reload.
Hard rules
- Write only the dashboard's analysis log (
snapshot.dashboard.logPath). Never editdashboard.json, the layout, or any other frame — the one exception is the optional, user-approved frame-add in step 3. - Universe = the dashboard. Don't analyze symbols that aren't on it; don't carry a separate watchlist.
- Every call must carry a gradeable
check— that's what makes tomorrow's run able to score it, and the terminal better over time. - Free data only — the snapshot already uses the keyless providers; there are no keys to ask for.
- Append, never rewrite — keep the full history; the frame computes a running hit-rate from it.
Running it daily
This skill is just the task. Scheduling it is the user's own Claude Code
/schedule — normal Claude Code functionality, not something this skill
sets up. To make the brief run every morning, the user schedules
/zframes-brief with /schedule (or whatever scheduler they prefer). Don't
write crontab lines or wrap it in claude -p — point them at /schedule.
The one requirement: whatever runs it must be able to write the
dashboard's analysis log (snapshot.dashboard.logPath — for a store dashboard
that's ~/.config/zframes/dashboards/<name>/daily-analysis.json) — i.e. it runs
with access to where the log lives.
- A runner that has those files (local to that machine, or a routine that clones the repo / store and commits the updated log) works.
- A scheduler with no access to where the log lives can update nothing — if the brief isn't appearing, that's the first thing to check.
Engine stamp on a scheduled run: the scheduled agent knows its own model and
effort, so fill run.model / run.effort from your runtime identity (step 6).
If the scheduler can set env, ZFRAMES_MODEL / ZFRAMES_EFFORT let
zframes snapshot stamp them authoritatively instead — either way the engine
ends up on record so a later model change is visible in the history.