.md file to compare - side-by-side diff against morning-brief
morning-brief
description: "Triggers on prompt mention of 'morning-brief'."
What it does for you
Pulls your day together into one screen of what needs you.
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 2/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/morning-brief/SKILL.md
present
state/lib/morning-brief.ts
not present
state/bin/morning-brief/
not present
state/skills/morning-brief/AGENTS.md
present
how it's graded - what counts as a good run 4 criteria · 2 deterministic · 2 judge
Each row is one thing a good run has to get right. deterministic means a quick check decides, pass or fail. judge means the AI reads the result and rates it. Grading each piece on its own (instead of one overall score) shows exactly where a run fell short, so the fix is obvious.
how it runs - the shared frame every skill uses 5/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 - ALWAYS check sweep-snapshot freshness FIRST — if state/log/sweep-snapshots/latest.json is missing or >4h old, RE-INVOKE the sweep skill, do not show yesterday's inbox
- ALWAYS dedup across channels — keys are channel + ":" + id. Duplicate items collapse the eval to 0.0
- If Krisp cache is empty (refresh.sh didn't run today), skip the commitment cross-check and set primary_issue: "krisp-cache-missing" — do NOT block the brief
- An empty brief MUST set reason_empty — silent empty (zero items, no reason) scores 0.0 (primary_issue: "silent-empty")
- Rank by awaiting_robert = true (Robert is not the last sender AND no Krisp commitment promises future reply); top 15 by age desc
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- who makes it, who checks it
skill is prose — follow steps 1-5 in `state/skills/morning-brief/SKILL.md
SKILL.md- the skill, written out in plain English
morning-brief
Scope-only chain: pull the latest sweep snapshot, merge with Krisp commitment cache, emit a ranked list of "things Robert hasn't closed that are older than X hours."
Steps
- Freshness check - sweep snapshot. Read
state/log/sweep-snapshots/latest.json. Compute sweep_age_hours = (now - file.mtime) / 3600. If the file is missing OR sweep_age_hours > 4, re-invoke the sweep skill and then cp <newest-timestamped-inbox.json> state/log/sweep-snapshots/latest.json before continuing. (Note: sweepAll() writes a timestamped file but does NOT update latest.json automatically - the cp is mandatory.)
- Freshness check - Krisp cache. Check whether
~/.claude/cache/krisp/action-items.json exists and krisp_age_hours = (now - file.mtime) / 3600.
- Missing (
ENOENT): setkrisp_status = "missing", skip commitment
cross-check, continue to step 3.
- Stale (
krisp_age_hours > 8): setkrisp_status = "stale", surface
existing action_items[] but note the age in the brief header.
- Fresh (
krisp_age_hours ≤ 8): setkrisp_status = "ok", use
action_items[] normally.
- For each inbox item, mark
awaiting_robert = trueif Robert is not the
last sender AND (krisp_status = "missing" OR no Krisp commitment exists that already promises a reply by date > now).
- Sort by age descending. Take top 15.
- Emit markdown digest to
state/log/briefs/<date>.mdand return path.
Eval
Actor: the sweep→krisp merge + ranking logic. Auditor: dedup + staleness cross-check.
const unique_ids = new Set(items.map(i => i.channel + ":" + i.id));
const has_dupes = unique_ids.size < items.length;
const all_aged = items.every(i => i.age_hours > 0);
const sweep_stale = sweep_snapshot_age_hours > 4; // tightened from 6h
const krisp_missing = krisp_status === "missing";
const krisp_stale = krisp_status === "stale"; // mtime > 8h
score("morning-brief", run_id, {
score:
items.length === 0 && reason_empty ? 1.0 :
items.length === 0 && !reason_empty ? 0.0 :
has_dupes ? 0.0 :
!all_aged ? 0.0 :
sweep_stale ? 0.5 :
krisp_stale ? 0.5 :
krisp_missing ? 0.75 : // degraded but not blocked
1.0,
item_count: items.length,
unique_count: unique_ids.size,
sweep_stale,
krisp_status,
reason_empty,
primary_issue:
items.length === 0 && !reason_empty ? "silent-empty" :
has_dupes ? "duplicate-items" :
!all_aged ? "zero-age-item" :
sweep_stale ? "stale-sweep-snapshot" :
krisp_stale ? "krisp-cache-stale" :
krisp_missing ? "krisp-cache-missing" : null,
});
Catches: duplicates across channels (dedup regression), stale sweep data (>4h), zero-age items (timestamp bugs), silent empty, stale Krisp cache (>8h), missing Krisp cache (degraded path - brief still emits).
Gotchas
- Sweep snapshot staleness (>4h). Re-invoke sweep skill and cp the
timestamped output to latest.json before continuing. Using a stale snapshot without re-invoking scores 0.5 (primary_issue: "stale-sweep-snapshot"). The cp is mandatory - sweepAll() does NOT update latest.json automatically.
- Krisp cache missing. If
~/.claude/cache/krisp/action-items.jsonis
absent, skip commitment cross-check, set krisp_status = "missing", and continue. The brief still emits; score is 0.75, not a failure.
- Krisp cache stale (mtime >8h). Surface existing
action_items[]but
tag krisp_status = "stale". Score is 0.5 (primary_issue: "krisp-cache-stale"). Do not block the brief.
- LinkedIn fetcher may emit
ts: "Apr 11"(string, no year) -Date.parse
resolves to year 2001 (~200,000h age). Treat non-numeric ts as recent; do not block the score.
Graduation
Stays prose until the chain shape is stable. Sidecar lands when the ranking logic has been tuned twice.
Rubric
criteria:
- name: brief_output_location
kind: deterministic
check: "The skill must write a markdown digest to 'state/log/briefs/<date>.md'."
- name: sweep_freshness_enforced
kind: deterministic
check: "If 'state/log/sweep-snapshots/latest.json' is missing or older than 4 hours, the 'sweep' skill must be re-invoked and 'latest.json' updated from a new timestamped snapshot."
- name: krisp_cache_handling
kind: judge
check: "The skill must correctly handle missing, stale (>8h), or fresh Krisp cache data according to the specification, including appropriate `krisp_status` and content surfacing."
- name: item_ranking_and_count
kind: judge
check: "The skill must sort items by age descending, take the top 15, and mark items as 'awaiting_robert' based on the specified logic."AGENTS.md- what the AI loads when this skill comes up
morning-brief - loader
Per-turn rules for the morning-brief skill. Full reference: state/skills/morning-brief/SKILL.md. Do not skip these.
Critical Rules
- ALWAYS check sweep-snapshot freshness FIRST - if
state/log/sweep-snapshots/latest.jsonis missing or >4h old, RE-INVOKE the sweep skill, do not show yesterday's inbox - ALWAYS dedup across channels - keys are
channel + ":" + id. Duplicate items collapse the eval to 0.0 - If Krisp cache is empty (refresh.sh didn't run today), skip the commitment cross-check and set
primary_issue: "krisp-cache-missing"- do NOT block the brief - An empty brief MUST set
reason_empty- silent empty (zero items, no reason) scores 0.0 (primary_issue: "silent-empty") - Rank by
awaiting_robert = true(Robert is not the last sender AND no Krisp commitment promises future reply); top 15 by age desc
Commands
| ui dashboard | state/skills/morning-brief/resources/ui.openui | |invoke: skill is prose - follow steps 1-5 in state/skills/morning-brief/SKILL.md |sweep snapshot: state/log/sweep-snapshots/latest.json |krisp cache: ~/.claude/cache/krisp/action-items.json (written by state/bin/krisp/refresh.sh; sibling meetings.json also written). Commitments live in action_items[] - there is no commitments.json. |output: state/log/briefs/<date>.md |eval log: state/log/evals.ndjson (skill: "morning-brief")
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/morning-brief/resources/ui.openui. Read it before rendering or editing this skill's generated component surface. - 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.
Known Pitfalls
- Zero-age items signal a timestamp bug -
primary_issue: "zero-age-item"blocks the score - Stale sweep snapshot scores 0.5 even if dedup passes (
primary_issue: "stale-sweep-snapshot") - Krisp cache missing is NOT a failure - it's a documented degraded path with
primary_issue: "krisp-cache-missing"and the brief still emits - Krisp cache stale (file exists and
mtime >8h) is a separate degraded path: surface the existing commitments but tagprimary_issue: "krisp-cache-stale"so the eval scores 0.5, not 1.0 sweepAll()writes a timestamped2026-MM-DDTHH-MM-inbox.jsonbut does NOT updatelatest.json. After re-invoking sweep,cp <newest>-inbox.json latest.jsonor the staleness check keeps reading the old file.- LinkedIn fetcher may emit
ts: "Apr 11"(string, no year) -Date.parseresolves to year 2001, producing a ~200,000h age. Treat non-numerictsas recent and note the quirk; do not block the score. - LinkedIn fetcher timeout is swallowed silently by
sweepAll()-errors: {}even when the page-load timed out and 0 LinkedIn items were returned. Log "linkedin-timeout-swallowed" in the brief's degraded-paths list when LinkedIn item count is 0; do not infer "no DMs" from absence. - Skill router still resolves
morning-briefandsweepto the legacy flat pathstate/skills/<name>.md. Real canonical is folder-shape:state/skills/<name>/SKILL.md(+ AGENTS.md loader). When the launcher says "load the skill page and follow its steps" and that path 404s, fall back to<name>/SKILL.md.
Self-Test
An agent reading this should correctly:
- [ ] Re-invoke
sweepskill when the snapshot is older than 4 hours rather than reusing it? - [ ] Set
reason_emptywhen emitting a zero-item brief, to avoid silent-empty? - [ ] Continue without commitment-check when the Krisp cache is missing, marking primary_issue accordingly?
Self-report
If this loader fell short, append a line:
echo "[$(date -u +%FT%TZ)] morning-brief: <what was missing>" >> state/log/loader-feedback.log
<!-- 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 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
agent-browser screenshot
takes path positionally not as --output <path>. One line in a 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)] morning-brief: <what was missing or fixed> [FIXED|LOGGED]" >> state/log/loader-feedback.log
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.
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.
api.ts- the code it can call
⚠ no api.ts - this skill has no typed action surface
scripts- helper scripts it can run
prose-only skill - 2 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
| timestamp | verb | score | primary_issue | artifact |
|---|---|---|---|---|
| 2026-04-27 20:46Z | - | 1.00 | - | - |
| 2026-04-27 18:48Z | - | 1.00 | - | - |
| 2026-04-27 12:38Z | - | 0.50 | - | - |
| 2026-04-27 00:00Z | - | 1.00 | - | - |
| 2026-04-26 04:39Z | - | 0.50 | - | - |
| 2026-04-25 23:50Z | - | 0.50 | - | - |
| 2026-04-25 22:40Z | - | 0.50 | - | - |
| 2026-04-25 04:36Z | - | 0.50 | - | - |
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-24 22:47Z | - | 0.50 | - | - |