Daily Briefing — Configuration Companion
When to use
Activate when the user wants to read or modify the config files
that drive the daily briefing pipeline. The briefing itself is
generated entirely by ~/daily-briefing/morning-briefing.sh under
user cron — Hermes plays no role in producing it. This skill exists
only to handle natural-language management of the policy files that
the pipeline reads.
When NOT to use
- Generating a briefing on demand (it's a cron pipeline; just wait for tomorrow, or run the script manually outside Hermes).
- Answering follow-up questions about today's briefing (the briefing is in conversation context; just respond normally without a skill).
Files this skill manages
| Path | Purpose | Shape |
|---|---|---|
~/daily-briefing/news-source-prefs.json |
Trusted (priority-ordered) and untrusted (blocklist) news sources by domain. | { "trusted": ["domain", ...], "untrusted": ["domain", ...] } |
~/daily-briefing/news-topics.json |
Keywords searched for news each morning. | { "topics": ["keyword", ...] } |
~/daily-briefing/tickers.json |
Stock tickers shown in the __Markets__ block (prior session close + day-over-day Δ). |
[{"ticker": "<symbol>"}, ...] |
~/daily-briefing/calendar-people.json |
Organizer-email → display-name mapping for the [Name] calendar prefix. |
{ "default": "Name", "organizers": { "email": "Name", ... } } |
These files are plain JSON — read with Read, edit with Write (or
edit-in-place tools). Always preserve 2-space indent and a trailing
newline so diffs stay clean.
Operations
For any operation, the procedure is: read the relevant JSON file, mutate the in-memory dict, write it back. Always preserve the existing formatting so diffs stay clean.
Trusted / untrusted news sources (news-source-prefs.json)
The trusted list is priority-ordered: lower index = higher
priority. The untrusted list is unordered.
| User intent | Operation |
|---|---|
"Add <source> to my trusted list" |
Resolve to domain. Append to trusted. Remove from untrusted if present. |
"Add <source> to my trusted list at the top" |
Insert at index 0 of trusted. Remove from untrusted if present. |
"Add <source> to my trusted list above <other>" |
Find <other> index in trusted, insert <source> at that index. Remove from untrusted if present. |
"Add <source> to my trusted list below <other>" |
Find <other> index in trusted, insert at index+1. |
"Move <source> up / down in my trusted list" |
Find current index, swap with neighbor. |
"Add <source> to my untrusted list" |
Append to untrusted. Remove from trusted if present. |
"Move <source> from trusted to untrusted" / "untrust <source>" |
Remove from trusted, append to untrusted. |
"Remove <source> from my trusted/untrusted list" |
Drop from the named list. (If user doesn't say which, infer from where it is.) |
| "Show my source preferences" | Read and pretty-print, with priority numbers on the trusted list. |
| "Untrust the source of that last article" | Identify the URL from the most recent briefing in conversation context, parse the domain, append to untrusted. |
Domain resolution (<source> → domain): Use general web knowledge:
"Reuters" → reuters.com, "NYT" or "New York Times" → nytimes.com,
"Bloomberg" → bloomberg.com, etc. Always normalize to lowercase, no
www. prefix. If the publisher name is ambiguous (e.g. "Reuters" vs
"Reuters UK" → uk.reuters.com), ask the user to clarify before
writing.
Selection-rule recap (so you can explain effects of edits):
- Stories from
untrusteddomains are dropped entirely. - Among surviving candidates, the one with the lowest index in
trustedwins. - If no candidate's domain is in
trusted, the first surviving candidate (search relevance order) wins. - If everything was untrusted for a topic, the topic produces no story that day.
So adding to trusted raises a source's priority; adding to
untrusted blocks it; reordering inside trusted shifts which
trusted source wins on overlapping coverage.
Watched news topics (news-topics.json)
The topics array is the list of keyword strings the news fetcher
searches for each morning. Order doesn't affect priority — every
topic gets one story slot per day if a survivor exists.
| User intent | Operation |
|---|---|
"Add <topic> to my news topics" / "follow <topic>" |
Append to topics if not already present. |
"Stop following <topic>" / "remove <topic>" |
Drop matching entry (case-insensitive match, but preserve the original entry's case if you keep it). |
| "What topics do I follow" | Read and list. |
"Rename <old> to <new>" |
Find and replace one entry in place. |
Topics are passed verbatim to the news search; quoted strings (e.g.
"AI capex") work as phrase searches.
Watched stock tickers (tickers.json)
The array drives the __Markets__ block in the morning briefing. Each
entry is a single object with one required field: ticker, the symbol
in Yahoo Finance convention — bare for US listings, with an exchange
suffix for international: SU.PA, 7203.T, 0700.HK, RIO.L.
The pipeline's ticker fetcher dispatches by suffix:
- Bare ticker (no dot) → quote API. Reliable and official; covers US listings only on the free tier.
- Suffixed ticker (has a dot) → yfinance via the local Python venv. Covers any market Yahoo Finance does, but unofficial — Yahoo can break the lib until maintainers patch.
Class shares like BRK.B should be entered as BRK-B (Yahoo's
hyphen form for share classes) so they route to the quote API instead
of being treated as a foreign exchange suffix.
| User intent | Operation |
|---|---|
"Add <ticker> to my watchlist" / "track <ticker>" |
First web_search to resolve the ticker → company name (see "Verify ticker ↔ company" below). Then append {"ticker": "<ticker>"} to the array if not already present. |
"Stop tracking <ticker>" / "remove <ticker> from my watchlist" |
Drop the matching entry (case-insensitive ticker match; preserve original case if you keep). No lookup needed. |
| "What tickers am I watching" / "show my watchlist" | Read and list each entry's ticker. No lookup needed. |
"Move <ticker> to the top" |
Reorder to index 0. (Order in the file is the order in the briefing table.) |
"Replace <old> with <new>" |
Same lookup rule as Add — web_search for <new> before writing. |
Verify ticker ↔ company
Always web_search to ground the ticker ↔ company mapping before
writing the file or naming the company in your response. This applies
in both directions:
- User gives a ticker (e.g. "Add GEV"): web_search "GEV stock
ticker" → confirm GE Vernova → write tickers.json with
GEV→ respondAdded GEV (GE Vernova) to your watchlist. - User gives a company name (e.g. "Add GE Vernova" / "Add Schneider
Electric"): web_search "GE Vernova stock ticker" → confirm
GEV(orSU.PA) → write tickers.json → respondAdded GEV (GE Vernova) to your watchlist.
The lookup is mandatory even for tickers you think you know. Your training data may not match current ticker reality, and confidently- asserted-but-wrong company names are the canonical failure mode here. Canonical confusion sources:
- GE family:
GE(General Electric, post-spinoff parent),GEV(GE Vernova, energy spin),GEHC(GE Healthcare, separate spin). All three trade independently — none inherits the others' symbol. - Class shares:
GOOGvsGOOGL,BRK-AvsBRK-B— different voting/economic rights, both real tickers. - Multi-exchange listings:
SU.PA(Schneider Electric, Euronext Paris primary) vsSBGSF(US OTC ADR). If the user names the company without specifying exchange, ask which they want before writing.
On user correction, web_search before re-writing. If the user says "you got that wrong, I meant X," do a web_search to confirm X's actual ticker before changing tickers.json. Don't pile a second wrong assertion on the first.
Confirmation format for ticker changes
After adding or replacing, respond with the format Added <TICKER> (<Company Name>) to your watchlist. — both the symbol and the
verified company name. After removing, ticker-only is fine
(Removed GEV from your watchlist.) since the company isn't in
the file.
Calendar organizer mapping (calendar-people.json)
| User intent | Operation |
|---|---|
"Map <email> to <name> in my calendar" |
Add email -> name to organizers. |
"Remove <email> from my calendar mapping" |
Drop the key. |
"Set the default calendar name to <name>" |
Update default. |
| "What names are mapped in my calendar" | Read and list. |
Emails are stored lowercased; the calendar fetcher lowercases before matching, so always lowercase on write.
Verification
Before saving any edit, confirm:
- The file is one of
news-source-prefs.json,news-topics.json,tickers.json, orcalendar-people.jsonunder~/daily-briefing/. Editing anything else through this skill is out of scope. - The JSON parses after the edit (no trailing commas, no top-level key drift — only the documented top-level keys).
- For source-prefs edits: the same domain does not appear in both
trustedanduntrusted. The latest action wins; remove from the other list. - For source-prefs edits where the user named a publisher rather than a domain: the inferred domain matches a well-known mapping, or the user has confirmed.
- After writing, briefly summarize what changed (the operation, the target file, and the resulting priority order if relevant).
When an operation reports an error
- The config file is missing → the briefing pipeline isn't set up on this
machine. Tell the user which file is absent and point them at this
skill's
README.mdfor pipeline setup. - The file doesn't parse as JSON → report that it is malformed and name the file. Leave it as it is; do not rewrite it from memory.
- A
web_searchfor a ticker returns nothing conclusive → say the symbol couldn't be confirmed and ask the user which company they mean. Don't write an unverified ticker. - The user names a publisher that maps to more than one domain → ask which one before writing.
Always ask the user for guidance when there is an error; do not proactively try to resolve errors yourself.
Empty results
An empty trusted, untrusted, topics, tickers or organizers list means
nothing is configured yet — say so plainly ("you aren't tracking any tickers
yet") and offer to add the first entry.