Imported from ianchesal/dotfiles (
home/dot_claude/skills/morning-startup/SKILL.md). Install upstream withnpx skills add ianchesal/dotfiles --skill morning-startup. Copyright stays with the author.
Morning Startup
This skill helps you start your workday by:
- Checking all data sources are available (preflight check)
- Reading context from your Obsidian daily journal and the previous Daily Startup entry
- Gathering today's context from Calendar, Slack, and Jira in parallel
- Writing a full briefing page to the Daily Startup database in Notion
- Writing a slim
## Work Daystub (Notion link + coaching highlights) into today's personal journal
Obsidian is read-only for this skill, with exactly one exception: the ## Work Day
stub written in Step 5. Every other output goes to Notion. This skill no longer writes
a work daily note to disk.
Before You Start: Load Your Config
Look for a config.md file in the same directory as this SKILL.md. Read it
now — it supplies all personal values referenced throughout this skill.
If config.md does not exist, tell the user:
"I don't see a
config.mdin your morning-startup skill directory. Please copyconfig.example.mdtoconfig.mdand fill in your values, then try again." Then stop.
Throughout this skill, references like {{SLACK_USER_ID}} mean: use the value
from your config.md.
Step 0: Preflight Check
Before reading any notes or gathering any data, verify all four MCP data sources are reachable. Run the following probe calls in parallel:
| Source | Probe call |
|---|---|
| Google Calendar | mcp__claude_ai_Google_Calendar__list_calendars |
| Slack | mcp__claude_ai_Slack__slack_search_users with query {{SLACK_USER_ID}} |
| Jira | mcp__claude_ai_Atlassian__atlassianUserInfo |
| Notion | mcp__claude_ai_Notion__notion-fetch with id {{NOTION_STARTUP_DATA_SOURCE}} |
The Notion probe doubles as a schema read — keep its response. Step 4 needs the exact property names, and they are authoritative over the ones written in this file.
If all four probes succeed, print:
✅ All data sources available (Calendar, Slack, Jira, Notion) — starting briefing…
Then proceed to Step 2.
If any probe fails, print an error block naming every failed source:
❌ Preflight check failed — cannot start morning briefing.
The following data sources are not available:
- [Source name] — [diagnosis]
Fix the above and re-run your morning startup.
Then stop immediately. Do not read any notes. Do not write any files.
Error diagnosis by error type:
- Tool not found / schema not loaded: "MCP server may not be running. Restart Claude Code or check your MCP config."
- Authentication / authorization error: "Auth expired or invalid. Re-authenticate via the MCP server settings."
- Unexpected / unknown error: "Unexpected error — [include the raw error message]."
Step 2: Read Context from Notes
Before gathering external data, read the following files. Do this silently — no need to narrate it to the user.
Yesterday's personal daily note:
- Compute yesterday's date and build the path using
{{DAILY_NOTES_PATH}}and{{DAILY_NOTES_STRUCTURE}}. - Read the file. If it doesn't exist, skip silently.
- Extract: what got done (Day in Review), what carried forward, any themes, mood, energy signals, or personal context mentioned.
Today's personal daily note:
- Read today's file if it exists.
- Extract: any intentions already written, todos, personal context, or notes already captured before you started.
The previous Daily Startup entry (Notion):
-
Query
{{NOTION_STARTUP_DATA_SOURCE}}for the most recent entry before today — do not assume it was yesterday, since weekends and PTO leave gaps:SELECT url, "Day", "The One Thing", "Today Needs", "Phase 2 Prep" FROM "{{NOTION_STARTUP_DATA_SOURCE}}" WHERE "date:Entry Date:start" < '[today YYYY-MM-DD]' ORDER BY "date:Entry Date:start" DESC LIMIT 1 -
Fetch that page by its
urlto read the body. If there are no prior entries, skip silently — this is a first run. -
Extract: work carries-forward, Jira state, unresolved incidents, key decisions made, and whether the previous
The One Thingactually moved. -
Note how many days back the entry is. A three-day-old entry is stale context, not yesterday's — say so rather than treating it as current.
Hold all context. Use it in Step 3 to ground the Executive Coaching in both personal state and work continuity.
Step 3: Gather Context (Run in Parallel)
Gather all three data sources simultaneously. Do NOT wait for one to finish before starting the others.
3a. Google Calendar
CRITICAL — Always pass explicit date bounds. The calendar MCP can return
stale or historical data if called without a time range. You MUST always pass
timeMin, timeMax, and timeZone when calling gcal_list_events.
If today is Monday:
- Fetch the full week (Monday through Friday) with:
timeMin:YYYY-MM-DDT00:00:00using Monday's datetimeMax:YYYY-MM-DDT23:59:59using Friday's datetimeZone:{{YOUR_TIMEZONE}}
- You will produce both a today-view AND a
### Week Aheadsection (see template).
All days:
- Fetch today's events with:
timeMin:YYYY-MM-DDT00:00:00using today's datetimeMax:YYYY-MM-DDT23:59:59using today's datetimeZone:{{YOUR_TIMEZONE}}
After fetching, validate the results: Check that the returned events have dates within the requested range. If any events predate the last 30 days, the MCP returned stale/historical data. In that case, retry once with the same explicit parameters. If the second attempt also returns stale data, write a warning in the Calendar section, skip calendar analysis entirely, and continue with Slack and Jira — do not block the rest of the briefing on a broken calendar.
For each event, analyze and note:
- External/vendor meetings — attendees from outside your company domain
- Customer meetings — known customer names or domains from your config
- VIP meetings — events that include people from your
{{VIP_NAMES}}list - Exceptional/uncommon meetings — things that don't recur regularly; flag these so the user isn't surprised
- Scheduling tool links — meetings booked via
{{SCHEDULING_TOOL}}(visible in event description/organizer metadata) - Focus time blocks — time blocked for deep work
- Lunch opportunity — suggest the best window given the user's timezone
(
{{YOUR_TIMEZONE}}) vs company HQ timezone ({{COMPANY_HQ_TIMEZONE}}); east-coasters at west-coast companies often miss lunch - When writing the table out, make sure there's a space between the
Key meetingsheader and the table. Otherwise Obsidian doesn't render it properly as a table.
Conflict detection — check RSVP status, never raw time overlap. A double-booking
only counts as a conflict if the user has accepted (or tentatively accepted)
both overlapping events. Each event's attendee list carries the user's own
responseStatus (accepted, tentative, needsAction, or declined) on the entry
where self: true. Before flagging any conflict (here, in ⚠️ Heads up, or in the
Week Ahead "Prepare now" list):
- Ignore
declinedevents entirely — the user isn't attending, so they cannot conflict. A team meeting the user declined sitting on top of a 1:1 is NOT a conflict. This is the #1 source of false positives — calendars are full of declined/optional invites that still occupy a time slot. - Only an overlap between two
accepted/tentativeevents is a real conflict. - An unanswered invite (
needsAction) overlapping an accepted event is a pending decision, not a confirmed conflict — surface it separately as "unanswered invite that overlaps X" if relevant, but never count it in the conflict tally. - Confirm with
responseStatusdirectly (e.g. viajqon the saved events filtering theself: trueattendee) rather than eyeballing times — raw time overlap inflates the count dramatically.
Heads up filter — manager-level only. Only include an item under ⚠️ Heads up
if it requires you to act or decide as the head of infrastructure (RSVP, resolve
a conflict, prepare for something exceptional, make a call only you can make). Do not
include IC-level operational status (tool hiccups, routine standups starting on time,
a meeting ending early). Test: would you behave differently as the head of infrastructure
today because of this item? If no, omit it.
1:1 detection. Identify every 1:1 on today's calendar — a title containing "1:1",
a two-person pattern like "X / Y", "X <> Y", "X and Y", or any meeting with exactly two
attendees where one is you. Collect the list of 1:1 people (match each against the
{{PERSON_SIGNAL_REGISTRY}} to resolve their shortname). Do not write prep into the
Key Meetings table — the dedicated ## 1:1 Prep section is produced by the Phase 2
meeting & 1:1 prep agent (see below). Just mark each 1:1 in the Notes column with
1:1 — see 1:1 Prep and pass the resolved 1:1 people list forward to that agent.
Produce a ### Calendar section:
### Calendar
**Today at a glance:** [X meetings · Y hours of focus time]
**⚠️ Heads up / Unusual today:**
- [Only items requiring your action or decision as head of infrastructure — no IC operational noise]
**Key meetings:**
| Time | Event | Notes |
|------|-------|-------|
| 9:00 AM | Google Cloud sync | External; RSVP needed |
| 12:35 PM | 1:1 Jordan | 1:1 — see 1:1 Prep |
**Focus time:** [X hours — when]
**Lunch window:** [Best suggested time and why]
If today is Monday, also produce a ### Week Ahead section:
Scan the full week and flag anything the user needs to prepare for now to avoid being caught off-guard later. Think about prep time — if there's a big review on Thursday, flag it Monday so prep can start Tuesday.
When flagging scheduling conflicts here, apply the Conflict detection rule from
section 3a: only count an overlap as a conflict when the user has accepted (or
tentatively accepted) both events. Filter out anything declined before counting.
### Week Ahead
**Week at a glance:** [X total meetings · heaviest day · lightest day · total focus time]
**⚠️ Prepare now — don't get caught flat-footed:**
- [Non-recurring, VIP, external, high-stakes, or unusual meetings this week. For each: why it matters and what to do before it.]
| Day | Key Events | Load |
|-----|-----------|------|
| Mon | [events] | Heavy / Medium / Light |
| Tue | [events] | Heavy / Medium / Light |
| Wed | [events] | Heavy / Medium / Light |
| Thu | [events] | Heavy / Medium / Light |
| Fri | [events] | Heavy / Medium / Light |
3b. Slack — Unread Messages Triage
Use mcp__claude_ai_Slack__slack_search_public_and_private and
mcp__claude_ai_Slack__slack_read_channel to check recent messages.
Priority order:
-
Mentions — search
mentions:{{SLACK_USER_ID}} after:{{YESTERDAY_DATE}}Before surfacing any result: verify that
{{SLACK_USER_ID}}appears literally in the message text, or the message is a direct reply to you in a thread you started. Drop false positives silently — do not list them.An empty result is NOT proof of no mentions. This search has returned zero while three genuine literal @-mentions existed in the window (observed 2026-09-02). Never report "no mentions" on the strength of an empty search alone — confirm it against the monitored-channel reads in step 4, scanning message text for
<@{{SLACK_USER_ID}}|. If the channel reads are also clean, then report nothing outstanding. -
VIP DMs — search for recent messages
from:{{VIP_SLACK_USERNAMES}}usingslack_search_public_and_private -
Incidents — read the incidents/on-call channel from your config. Also search
incident SEV after:{{YESTERDAY_DATE}}to catch recentinc-*channels created overnight — the main incidents channel can be noisy with old bot posts.⚠️ Lifecycle re-posts are NOT new incidents. The incident.io bot re-posts an incident's full summary on every status change — "Incident updated", "Incident resolved", or a transition like
Reviewing → Closed. These re-posts embed impact times like "began at 21:14 UTC" with no date, so a re-post that fires today looks exactly like a fresh overnight outage even when the incident is days or weeks old and already closed. Theincident SEV after:search surfaces them because the message timestamp is recent, not the incident. Before you badge anything as new / active / overnight:- Read the message body's status line. If it shows
Closed,Resolved,Documenting,Reviewing, or any→transition, treat it as a lifecycle re-post, not a new event. - Verify the incident's real
reported_at/created_atvia the incident.io MCP (incident_listquery by name, orincident_show) rather than trusting the Slack post's recency. Only badge incidents whosereported_atfalls inside the briefing window (since yesterday). When in doubt, look it up — never infer the date from the undated impact times in the summary text.
Incident badge and filter rules. Assign each active incident one badge, in priority order:
[LEAD]— you are the incident lead or comms lead[FOLLOW-UP]— you have an open follow-up Jira ticket linked to this incident[ADJ]— the incident involves infrastructure you own: GCP, Kubernetes, Mongo/ datastores, networking, compute, CI/CD, or any service owned by your team — even if another team leads the incident[WATCH]— SEV-1 or SEV-2 company-wide with none of the above
Show any incident that earns a badge. SEV-1 and SEV-2 always earn at least
[WATCH]. Show SEV-3 only if it earns[LEAD],[FOLLOW-UP], or[ADJ]. Collapse all other active incidents to a single line:N other active incidents — no infra involvement. Omit the incidents block entirely (not even a header) if zero incidents earn a badge.Format for shown incidents:
[BADGE] INC-XXXX · SEV-N · Short name · Day N · status → [your role / open follow-up key / affected infra services] - Read the message body's status line. If it shows
-
Monitored channels — read each channel from your
{{SLACK_CHANNELS}}config table; summarize what's worth reading.Channel highlight filter — manager-level only. Surface only items you need to act on or be aware of as the head of infrastructure. Skip IC-level operational status (engineers handling their own work, transient tool hiccups, PR activity not requiring your review or decision). Test: would you act differently as head of infrastructure because of this item?
Thread-verify channel highlights too — the rule is not just for mentions.
slack_read_channelreturns only top-level messages, so an infra help request, a bot-filed ticket, or a PR-review ask will look untouched even when it was answered and closed in its own thread. Before writing that anything "needs your call" or is "blocked on you", get the parentMessage_ts(afrom:<person> <keywords>search returns it plusReply countand thread context) and read the resolution. For a claim that you owe code, also check directly:gh pr list --repo persona-id/<repo> --author <your-gh-handle> --search "<keyword>" --state all. Observed failure (2026-09-02): a help request was reported as blocking when the fix had shipped two days earlier, ~2h after it was raised.
For each item requiring action, produce a clickable Obsidian deep link using
the Slack slack:// URL scheme:
- Channel:
[#channel-name](slack://channel?team={{SLACK_TEAM_ID}}&id=CHANNEL_ID) - Message:
[description](slack://channel?team={{SLACK_TEAM_ID}}&id=CHANNEL_ID&message=MESSAGE_TS)
If you have an https://slack.com/archives/... permalink but can't determine
the team ID, use the https:// URL directly — still clickable in Obsidian.
Produce a ### Slack section:
### Slack
**🔴 Action Required (Mentions):**
- [verified mention description] — [link]
**💬 VIP Activity:**
- [Person]: [summary] — [link]
**🚨 Incidents:**
[LEAD] INC-001 · SEV-3 · Storage capacity · Day 14 · documenting
→ lead: you · open follow-ups: INFR-101, INFR-102
[ADJ] INC-002 · SEV-2 · Dashboard 500s · Day 4 · mitigating
→ infra: K8s deploy pipeline affected
2 other active incidents — no infra involvement
**📣 Channel Highlights:**
- [#channel]: [management-level signal only] — [link]
Omit any category where there's nothing to report, or note "Nothing new." Omit the Incidents block entirely if no incident earns a badge.
3c. Jira — Assigned Issues
Use mcp__claude_ai_Atlassian__searchJiraIssuesUsingJql with cloud ID
{{JIRA_CLOUD_ID}}.
Run these queries:
- All open:
assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC
Flag each issue:
- 🆕 New — created in the last 3 days
- ⏰ Languishing — no update in
{{LANGUISHING_THRESHOLD}}days - 🔥 Urgent — priority is High or Highest
- 📋 Normal — everything else
Produce a ### Jira section as a markdown table:
### Jira
| Status | Key | Summary | Priority | Last Updated |
|--------|-----|---------|----------|-------------|
| 🔥⏰ | [KEY-123]({{JIRA_BASE_URL}}KEY-123) | Summary | High | 2026-01-01 (30d) |
If no open issues, note "No open Jira issues assigned."
Step 3d: Verification Gate (do this BEFORE writing anything)
Everything gathered so far is raw. Run these three checks before Step 4. They are cheap relative to handing the user a false action item, and both failures they guard against have actually happened.
1. Every action item must be resolution-checked.
List the items you are about to write under ⚠️ Heads up, 🔴 Action Required,
📣 Channel Highlights, Today needs from you, and The One Thing. For each one that
asserts something is unresolved — "needs your call", "blocked on you", "no reply yet",
"nobody owns it" — confirm it in the thread, in Jira, or in gh before it survives into
the note. Anything you could not confirm gets written as a question ("worth checking
whether X is still open"), never as an assertion. Delete items that turn out closed;
do not soften them.
2. Every number must be derived, not repeated. When someone quotes a dollar figure in Slack, find the underlying quantity and unit price in the same thread and recompute it yourself. Show the arithmetic (volume × rate), not the quoted total. Rules:
- Never convert someone's aggregate into a different rate. Dividing a quoted "$100k/mo" by 30 to get a daily burn manufactures false precision and hides where the number came from.
- A contested figure may not be transformed into any other figure. If a number is disputed anywhere in your own gathered context, quote it with the dispute attached or drop it — never derive from it.
- Watch units. TiB→GiB is ×1024; per-day vs per-month is the easiest slip to make sound authoritative.
- Check which workload a number describes before attributing it to a narrower one.
- Observed failure (2026-09-02): "$3.3k/day" was reported for a cost that the source's own inputs put at $45–131/day, because a contested monthly aggregate for a different workload was divided by 30.
3. Pressure-test The One Thing. Name the two or three facts it rests on and confirm each one survived checks 1 and 2. If a load-bearing fact collapses, pick a different One Thing rather than propping up the original. Observed failure (2026-09-02): The One Thing rested on a colleague being blocked, who had in fact been unblocked two days earlier.
If the briefing has already been delivered when a correction surfaces, patch both the Notion page and the journal stub in place and mark the change with a dated revision line (e.g. "Revised at 12:10 PM") naming what changed and why — the user re-reads these later and needs the trail.
Step 4: Write the Daily Startup Page in Notion
The briefing is a page in the Daily Startup database, {{NOTION_STARTUP_DATA_SOURCE}}.
One page per workday. Re-running the skill on the same day updates that day's page —
it never creates a second one.
4a. Find today's entry, if it already exists. Query with
mcp__claude_ai_Notion__notion-query-data-sources in sql mode:
SELECT url FROM "{{NOTION_STARTUP_DATA_SOURCE}}"
WHERE "date:Entry Date:start" = '[today YYYY-MM-DD]'
Zero rows means create; one row means update that page. If the query somehow returns more than one row, update the oldest and tell the user there are duplicates for today — do not delete anything yourself.
4b. Compose the page body in Notion-flavored Markdown. Do not include an H1
title — the title lives in the Day property. The body is:
## Calendar
[calendar section from Step 2a — full content including Today at a glance,
Heads up, Key meetings table, Focus time, Lunch window]
[## Week Ahead — Monday only]
[week ahead section — full content including Week at a glance,
Prepare now flags, and weekly table]
## Slack
[slack section from Step 2b — full content including all categories]
## Jira
[jira table from Step 2c]
## Executive Coaching
**Today needs from you (as Head of Infrastructure):**
[One synthesis sentence answering "what does today need from me as the head of
infrastructure?" — specific and actionable, naming 2–3 things maximum. Draw from
everything gathered: unresolved RSVP/calendar decisions that must happen before the
day progresses, new high-priority or Blocked Jira interrupts needing routing, any
incident requiring your action (especially `[LEAD]`/`[ADJ]`), any 1:1 person whose
signal warrants a conversation today, and the carry-forward from the previous Daily
Startup entry.
Example: "Today needs: RSVP the 2 PM Google sync, route INFR-456 to the on-call owner,
and ask Jordan about the migration cutover in your 1:1." If nothing is urgent, say so:
"Quiet day — protect the afternoon focus block and prep for Thursday's review."]
**Today's Shape:**
[In 2–3 sentences, describe the texture of today. Is it meeting-heavy with
little focus time? A day with a big deliverable? An unusually light day?
Name what the user is walking into so they can set the right intention.]
**The One Thing:**
[Given everything — calendar, Slack, Jira, yesterday's personal note,
the previous Daily Startup entry, and today's intentions — what is the single most
important thing to move forward today? Name it explicitly. If the day risks
filling up with busyness that doesn't move the needle, say so.]
**Watch Out For:**
[1–3 specific, concrete observations about risks or patterns. Reference real
data — actual meeting names, Jira keys, Slack threads, note content.
Examples: "Your 1:1s are back-to-back from 3–5 PM — you'll be drained by
the last one. Prep quick agendas for each now." Or: "INFR-1673 has been open
44 days with no movement. Decide today: do it or defer it explicitly."]
**Energy & Wellbeing:**
[Based on context from yesterday's personal note or today's. Flag anything
worth attending to: disrupted routines, personal stressors, a heavy week
that needs pacing. If yesterday's note shows the user was productive and
energized, note that too — momentum is real.]
**One Question:**
[A single sharp coaching question to carry into the day. Make it specific to
today's context, not generic. E.g.: "The incident debrief is at 1 PM and
you're the organizer — do you have a clear outcome in mind for what you want
to leave the room having decided?"]
4c. Compose the properties. Use the exact property names from the schema you read in Step 0. Values are a JSON map of property name to SQLite value:
| Property | Value |
|---|---|
Day |
YYYY-MM-DD Ddd — e.g. 2026-09-10 Thu. This is the page title |
date:Entry Date:start |
YYYY-MM-DD (today) |
date:Entry Date:is_datetime |
0 |
Day Shape |
One of Focus-heavy, Balanced, Meeting-heavy, Back-to-back, Light. Must agree with what Today's Shape says in the body — if the prose says the day is packed, the property cannot say Light |
Energy |
One of High, Steady, Guarded, Low, read from the personal journal notes in Step 2 — never inferred from work signals. Omit the property entirely if the journal gave you nothing to go on; a guess here corrupts the trend line |
The One Thing |
The One Thing, verbatim from the body, as plain text (strip markdown) |
Today Needs |
The "Today needs from you" line, verbatim, plain text |
One Question |
The One Question, verbatim, plain text |
Flags |
Array, any of Incident, Blocked Jira, RSVP Needed, Vendor, Org Change, Travel/PTO, Interview, Week Ahead. Set each only when something in today's gathered data actually warrants it. Add Week Ahead on Mondays when you wrote that section |
1:1s With |
Array of display names of the people you have 1:1s with today. Only names that already exist as options can be written — see the note below |
Meetings |
Number of real meetings today (exclude focus blocks, Clockwise holds, and solo events — same exclusions as Phase 2) |
Focus Hours |
Hours of uninterrupted focus time available, as a number (2.5) |
Phase 2 Prep |
"__NO__" — Phase 2 sets it to "__YES__" when it finishes |
Journal |
obsidian://open?vault={{OBSIDIAN_VAULT}}&file=<vault-relative path>. Build the full note path from {{DAILY_NOTES_PATH}} + {{DAILY_NOTES_STRUCTURE}}, strip the {{OBSIDIAN_VAULT_ROOT}} prefix and the .md suffix, then percent-encode. For ~/Documents/Personal/Daily/2026/09-September/2026-09-10.md under vault root ~/Documents/Personal/ that is Daily/2026/09-September/2026-09-10. The path is relative to the vault root, not to {{DAILY_NOTES_PATH}} — dropping the leading folder produces a link that silently opens nothing. Omit the property if {{OBSIDIAN_VAULT}} is blank in config |
Leave Notes alone. It is the user's own scratch field on the row — never write to it.
On 1:1s With: Notion does not create multi-select options on the fly. Writing a
name that is not already an option fails the entire page write with
validation_error, not just that property. The options are seeded from
{{PERSON_SIGNAL_REGISTRY}}, so spell names exactly as that table does. If today's 1:1
is with someone who is not yet an option, add them first with notion-update-data-source:
ALTER COLUMN "1:1s With" SET MULTI_SELECT('Existing Name':blue, ..., 'New Person':gray)
SET replaces the whole option list, so include every existing option or you will drop
the ones you leave out. If that call fails, omit the person from the property and write
the page anyway — the body still names them, and a missing tag beats a lost briefing.
4d. Write the page.
If no entry exists for today, call mcp__claude_ai_Notion__notion-create-pages with:
parent:{"type": "data_source_id", "data_source_id": "<uuid from {{NOTION_STARTUP_DATA_SOURCE}}>"}pages: a single entry withproperties(4c),content(4b), andicon"☀️"allow_async:false— you need the page URL back immediately for Step 5
If an entry already exists, make two calls against that page ID:
notion-update-pagewithcommand: "replace_content"andnew_strset to the 4b bodynotion-update-pagewithcommand: "update_properties"and the 4c properties
Replacing content wipes the Phase 2 prep sections from an earlier run today. That is
intended — Phase 2 runs again after Step 5 and rewrites them. Leave Phase 2 Prep set
to "__NO__" on a re-run so the checkbox never claims prep that is no longer on the page.
Keep the returned page URL and page ID. Step 5 needs the URL; Phase 2 needs both.
Also hold the Today needs from you (as Head of Infrastructure), The One Thing, and One Question values verbatim — you will copy them into Step 5.
Step 5: Write Personal Journal Stub
This is the one write this skill makes to Obsidian.
Determine today's personal note path from your config:
- Base path:
{{DAILY_NOTES_PATH}} - Structure:
{{DAILY_NOTES_STRUCTURE}}
Read the current personal daily note. Find {{ANCHOR_SECTION}}. Insert the
## Work Day section above that line.
If ## Work Day already exists in the file, replace it rather than
inserting a duplicate. An old stub may contain a wikilink to a retired work note
from before this skill moved to Notion — replacing the section clears it, and you
should leave any such note on disk alone.
Use the Notion page URL returned in Step 4 as a plain Markdown link — not a wikilink. Nothing in the vault backs it any more.
Content to insert:
## Work Day
[Daily Startup — YYYY-MM-DD →](https://www.notion.so/...)
**Today needs from you (as Head of Infrastructure):** [verbatim from Step 4 coaching]
**The One Thing:** [verbatim from Step 4 coaching]
**One Question:** [verbatim from Step 4 coaching]
---
If the personal daily note doesn't exist, tell the user — they may need to create it from their note template first. The Notion page is already written at this point, so say that too: the briefing is safe, only the journal pointer is missing.
After writing the stub, confirm:
"Phase 1 complete — Notion briefing written and journal updated. Starting deep prep…"
Phase 2: Deep Prep
Once Step 5 is complete, run the meeting & 1:1 prep agent.
When building the agent's prompt, substitute all {{CONFIG_TOKEN}} values with the
actual resolved values from config.md. The agent runs in a fresh context without access
to config.md.
Note: this routine does not draft Slack replies. Surfacing what needs a reply is
handled in Step 3b (the 🔴 Action Required (Mentions) section of the Notion page) —
deciding and writing the replies is left to you.
Phase 2: Meeting & 1:1 Prep Agent
Dispatch an Agent with description "Prepare meeting and 1:1 notes for today".
Build the following prompt, substituting real values for all {{}} tokens and inserting
the actual meeting list gathered in Step 3a (including the resolved 1:1 people list):
You are preparing meeting notes and 1:1 prep for a morning startup routine. The user
is the Head of Infrastructure — frame all prep at the manager/leadership level, not
the IC level.
**Your context (all values are real — use them directly):**
- Today's date: [today YYYY-MM-DD]
- Seven days ago: [YYYY-MM-DD]
- Jira cloud ID: {{JIRA_CLOUD_ID}}
- Jira base URL: {{JIRA_BASE_URL}}
- Today's Daily Startup page ID: [page ID returned in Step 4]
- Today's Daily Startup page URL: [page URL returned in Step 4]
- Signal cache directory: ~/.claude/signal-cache/ (one JSON file per person, named {shortname}.json)
**Today's meetings (from Phase 1 calendar — do not re-fetch):**
[insert each meeting entry from the Phase 1 Calendar section:
time, title, attendees list with names/emails, meeting link if available]
**Today's 1:1s (resolved in Step 3a — prep these specially, see "1:1 PREP" below):**
[insert each 1:1: time, title, person display name, person shortname (or "unregistered"),
person Slack username, person GitHub handle if known]
**VIPs — flag any unresolved items explicitly:**
[insert {{VIP_NAMES}} from config]
---
**Skip the following meetings entirely — do not write prep notes for them:**
- Focus time blocks or "No meetings" blocks
- Events whose title starts with "❇️" or whose description contains "Clockwise"
- Events with only one attendee (solo blocks, personal reminders)
---
## 1:1 PREP (do this first, for each 1:1 person above)
For each 1:1, build manager-level prep grounded in that person's signal cache.
**Step 1:1-A — Read the signal cache.**
Read `~/.claude/signal-cache/{shortname}.json` if the person is registered.
- If it exists: load `jira_active`, `prs_open`, `pr_volume_7d`, `recent_context`,
`known_context`, and note `last_updated`.
- If it does not exist (or the person is unregistered): you'll build a fresh one from
live signals in the next step, and note "no prior cache — first capture" in the prep.
**Step 1:1-B — Gather fresh signals (live).**
- Jira: resolve the person's account ID first with `lookupJiraAccountId` (by display name),
then `searchJiraIssuesUsingJql` with cloudId {{JIRA_CLOUD_ID}}, JQL:
`assignee = "[accountId]" AND statusCategory != Done ORDER BY updated DESC`.
Identify stuck tickets (no update in 7+ business days, or status Blocked).
- Slack: `slack_search_public_and_private` query `from:[slack-username] after:[seven-days-ago]`
for recent decisions, blockers, or unresolved threads.
- (Optional) PR volume: if a GitHub handle is known and the `gh` CLI is available, count
merged + open PRs in the last 7 days. Skip silently if unavailable.
**Step 1:1-C — Refresh the cache.**
Write `~/.claude/signal-cache/{shortname}.json` (create the directory if missing),
following the schema in the signal-person skill. Set `last_updated` to now and
`last_updated_by` to `"morning-startup"`. Preserve any `known_context` and richer fields
written by the signal-person skill — only overwrite the lightweight signals you gathered.
Skip the write only for unregistered people (no shortname to key the file on).
**Step 1:1-D — Produce the prep.** For each 1:1, answer the central question:
**"What does this person need from me today?"** Ground every line in the signals — never
generic. Cover:
- **What they need from me:** the single most useful thing you can give them today —
a decision, an unblock, air cover, recognition, or a course-correction. Be specific.
- **Stuck / open:** the most relevant stuck ticket (key + short summary + days stuck), or
"no stuck tickets" if clean.
- **One question:** one specific question to ask, derived from the signal data.
- **Watch:** optional — any signal worth noting (PR volume spike/drop, Jira going stale
while PRs merge, a thread that stalled).
---
**For each real (non-skipped) meeting that is NOT a 1:1, do the following:**
**Step A — Attendee context (Slack):**
For each attendee, search Slack using mcp__claude_ai_Slack__slack_search_public_and_private:
- Query: `from:[attendee-first-name] after:[seven-days-ago]`
- Look for: pending questions, recent decisions, anything unresolved
- **Check resolution state before calling anything open.** Read the thread to its end,
not just the opening message. If someone asked for a decision, find out whether
another person already made it — a teammate answering "we're already working on it,
don't do that yet" IS the decision, and the ask is closed. Framing a settled thread
as needing the user's ruling wastes a 1:1 and makes them re-open something their own
team already handled. When a decision was made by someone else, say who made it and
what it was; the useful move is usually for the user to ratify or explain it, not
adjudicate it.
- **Do not repeat dollar figures without recomputing them** from the quantity and unit
price in the same thread. See the numbers rule in Step 3d — it applies here too.
- For VIPs: mark any unresolved thread explicitly with "⚠️ unresolved"
- If an attendee has no Slack activity: note "no recent Slack context"
**Step B — Topic context (Jira):**
Use mcp__claude_ai_Atlassian__searchJiraIssuesUsingJql with cloudId {{JIRA_CLOUD_ID}}:
- JQL: `text ~ "[2-3 keywords from meeting title]" AND statusCategory != Done ORDER BY updated DESC`
- Take the top 3 most relevant results
- If no results: note "no related Jira tickets found"
**Step C — Talking points:**
Generate exactly 3 talking points. Requirements:
- Each must be specific, not generic (no "discuss project status")
- Each must be grounded in what you found: cite the Slack thread, Jira ticket, or context
- Each must be actionable: framed as a question to resolve or decision to make
- If no Slack/Jira context was found: infer from meeting title and attendees, but
prepend "**Note: no recent context found**" before the talking points for that meeting
---
**Output:**
Write TWO sections into the Daily Startup Notion page. Write `## 1:1 Prep` before
`## Meeting Prep`, and put both at the end of the page, after `## Executive Coaching`.
Fetch the page first with `mcp__claude_ai_Notion__notion-fetch` on the page ID, then:
- If neither section is present, add them with `mcp__claude_ai_Notion__notion-update-page`,
`command: "insert_content"`, `position: {"type": "end"}`.
- If a section is already present, replace it with `command: "update_content"`, passing
the whole existing section as `old_str` and your new version as `new_str`. Do not
append a second copy, and do not use `replace_content` — that would wipe the rest of
the briefing.
First, the 1:1 section (omit entirely if there are no 1:1s today):
## 1:1 Prep
### [HH:MM AM/PM] — 1:1 [Person Name]
**What they need from me:** [the single most useful thing you can give them today —
specific, grounded in signals]
**Stuck / open:** [[KEY-123]]({{JIRA_BASE_URL}}KEY-123) — [summary], stuck [N] bd
(or "no stuck tickets"). [If first run: "no prior cache — first capture."]
**One question:** [specific question derived from the signals]
**Watch:** [optional signal worth noting, or omit the line]
[repeat the ### block for each 1:1]
Then the meeting section (omit entirely if every meeting was a 1:1 or skipped):
## Meeting Prep
### [HH:MM AM/PM] — [Meeting Title]
**Attendees:** [Name, Name (+N more if >3)]
**Context:**
- [Person or ⚠️ VIP name]: [1-line Slack summary, or "no recent Slack context"]
- [[KEY-123]]({{JIRA_BASE_URL}}KEY-123): [ticket summary] — [status, days since update]
**Talking Points:**
1. [specific, grounded, actionable point]
2. [specific, grounded, actionable point]
3. [specific, grounded, actionable point]
[repeat the ### block for each non-1:1, non-skipped meeting]
---
**When complete, return ONLY this one-line summary (no other output):**
"Prepped N 1:1s (cache refreshed), N meetings, skipped N (focus/Clockwise/solo blocks)"
Phase 2 Summary
After the agent completes, print the following (substituting the agent return summary):
## Morning Prep Complete
✅ Meeting & 1:1 Prep: [paste Meeting & 1:1 Prep agent return summary here]
Anything that needs your reply is flagged under 🔴 Action Required in the Notion page.
📄 **Today's briefing:** [Daily Startup — YYYY-MM-DD](page URL)
<page URL>
The last two lines are the whole point of the summary — the briefing lives in Notion, so the run must end with a way to get there in one click. Print both forms: the Markdown link (renders as a clickable title in terminals that support it) and the bare URL on its own line (clickable everywhere else, and copy-pasteable). Substitute the real page URL returned in Step 4 — never a placeholder, never a shortened or guessed URL. This is always the final thing printed; nothing goes after it.
If the agent succeeded, set the checkbox: notion-update-page with
command: "update_properties" and {"Phase 2 Prep": "__YES__"} on today's page.
If the agent failed or returned an error: replace ✅ with ⚠️ and describe the
error in place of the return summary, and leave Phase 2 Prep unchecked — the
checkbox is how you know later whether the prep actually landed. If the agent timed
out, say so. Still print the briefing link block — a partial page is exactly when the
user most needs to click through and see how far it got.
Troubleshooting
- Calendar returns historical/stale data: Always pass explicit
timeMin,timeMax, andtimeZoneparameters togcal_list_events. If results still look wrong (dates from years ago), retry once. If the second attempt fails, write a warning in the Calendar section and continue — do not skip Slack and Jira. - Slack mentions returning nothing: Verify
{{SLACK_USER_ID}}is correct. Usementions:USER_IDsyntax, notto:me. But do not assume an empty result means the config is wrong OR that there are no mentions — the search returns false negatives even when correctly formed. Cross-check against the monitored-channel reads before reporting either a config problem or a clean inbox. - Jira returns too many results: Focus on 🔥 Urgent and 🆕 New first; summarize languishing items in a collapsed group.
- Incidents channel is noisy: Supplement channel read with a keyword search
for
incident SEV after:{{YESTERDAY_DATE}}to surface recent incidents. - An "incident" looks new but might be old: incident.io re-posts the full summary
on every status change (incl. auto-close), with undated impact times that read like a
fresh overnight outage. Check the status line and verify
reported_atvia the incident.io MCP before badging it — see the lifecycle re-post guard in Step 3b.3. - Notes don't exist: Skip silently and proceed without that context. Don't block on missing notes.
validation_errornaming a multi_select value: someone in today's 1:1s is not an option on1:1s Withyet. Add the option first (see the note under Step 4c), then re-run the write. Nothing was written when this error fires — the whole page write is rejected, so there is no half-written page to clean up.- A Daily Startup page already exists for today: Update it in place (Step 4d) —
never create a second page for the same date. The
Entry Datequery in Step 4a is what prevents duplicates; run it even when you are sure this is the first run. - Notion returns an
async_taskinstead of a page: passallow_async: falseon the create call. If you still get one, pollnotion-get-async-taskuntil it reportssucceededbefore Step 5 — you need the real page URL for the journal stub. - The Notion write fails after everything is gathered: do not silently drop the briefing. Print the full composed body in chat so the user has it, say plainly that the Notion write failed and why, then retry once. Do not fall back to writing a markdown file in the vault.
- Signal cache empty or missing: On first run, no
~/.claude/signal-cache/exists. The Phase 2 meeting & 1:1 prep agent creates it and captures fresh signals — 1:1 prep that day notes "first capture." Caches fill in and get richer over subsequent runs (and via thesignal-personskill). Unregistered 1:1 people are prepped from live signals but not cached (no shortname to key on) — add them to{{PERSON_SIGNAL_REGISTRY}}inconfig.mdto start caching them.
Morning Startup Skill
- Always verify Slack MCP tools are available BEFORE starting context gathering; if unavailable, note this upfront rather than mid-flow
- When user mentions calendar items, treat each meeting as independent unless explicitly linked (e.g., don't assume Monday prep relates to Wednesday meetings)
- After verbally updating priorities or plans, ALWAYS write the changes to today's Daily Startup page in Notion immediately—don't wait to be asked