cambium ▸ docs/superpowers/specs/2026-04-12-morning-brief-workflow-design.md
updated 2026-04-12

Morning Brief Workflow — Design

Date: 2026-04-12 Status: Draft — awaiting review Touches: @cambium/cortex, deployments/obadiah/bridge/start-rhythms.ts

Purpose

When morning.pulse fires at 06:00 Berlin, a receptor catches it and composes a real morning brief for Joshua — then posts it to Discord #general. This is the original promise from when we designed Rhythms: “the morning brief actually arrives at 6am.”

The brief currently just logs. After this work, it speaks.

How it differs from BL-008 v2 (persona chat replies)

Concern BL-008 v2 (persona reply) Morning brief
Trigger Reactive — Discord message arrives Scheduled — morning.pulse rhythm fires
Format Conversational, single-turn Structured briefing with sections
Persona Domain-specific (Arnold, MacKenzie, …) Chief of staff (Obadiah itself)
Sources Message content + memory recall Substrate + memory + git log + daily file + yesterday’s routing history
Reply surface Threaded reply on the triggering message New top-level message in #general

They share the same LLM calling infrastructure (Anthropic via @cambium/providers) but the brief composer is a distinct module, not a reuse of PersonaInvoker.

Architecture

03:00 Berlin  memory.consolidate rhythm fires
              → consolidation receptor processes yesterday's daily file
              → epigenetic marks written to Postgres
              (this already works — shipped in Memory v1)

06:00 Berlin  morning.pulse rhythm fires
              → morning brief receptor catches it
              → composer gathers sources:
                 1. Substrate excerpts (identity, priorities, current projects)
                 2. Memory recall for joshua.* with horizon=1d (yesterday's marks)
                 3. Routing history: recall joshua.discord.routed.* horizon=1d
                 4. Git log: last 24h commits across known project dirs
                 5. Today's daily file (if any early entries exist)
              → one Sonnet LLM call composes the brief
              → DiscordClient posts to #general as a top-level message
              → emit morning.brief_delivered signal

The consolidation rhythm at 03:00 is important context: by the time the brief fires at 06:00, yesterday’s daily file has already been digested into marks. The brief reads from those marks, not from the raw daily file — which means the brief has a clean, structured view of yesterday.

Brief format

The LLM receives a structured prompt with all the gathered context and produces a brief in this shape:

**Good morning, Joshua.** Here's your brief for Saturday, April 12.

**Yesterday's highlights:**
- [from memory marks: what happened, what was observed, routing decisions made]

**Active projects:**
- [from substrate: current-projects.md excerpts]

**Git activity (last 24h):**
- [from git log: commit summaries across known repos]

**Today's priorities:**
- [from substrate: goals-and-priorities.md + any relevant marks]

**Pending seeds:**
- [from memory_seeds WHERE state='pending': proposals waiting for your review]

☕ Have a good day.

The exact format is part of the prompt template, not hardcoded in the output. The LLM has creative latitude within the structure — it’s not a mechanical template fill.

Source gathering

1. Substrate excerpts

const substrate = await recall({
  about: 'joshua',
  layers: ['substrate'],
  workspace_id,
});
// substrate.substrate contains: identity, priorities, projects, workflow, boundaries
// These rarely change — they're the "who you are" context for the brief

2. Yesterday’s memory marks

const yesterday = await recall({
  about: 'joshua',
  horizon: '1d',
  layers: ['marks'],
  workspace_id,
});
// yesterday.marks contains: observations from consolidation + routing decisions

3. Routing history

const routing = await recall({
  about: 'joshua.discord.routed',
  horizon: '1d',
  layers: ['marks'],
  workspace_id,
});
// routing.marks contains: every Discord message routed to a persona yesterday

4. Git log

// Shell out to git for last 24h across known project directories
const gitLog = await execGitLog([
  '/Users/joshua/Projects/Unicorn.Land/cambium',
  '/Users/joshua/Projects/Obadiah',
  // other project dirs from spore or substrate
]);

The git log fetcher is a small utility that runs git log --oneline --since='24 hours ago' --all in each directory and concatenates the results. Project dirs can be hardcoded for v1 (from spore or substrate) — a future version reads them from a spore.paths.projects[] field.

5. Pending seeds

const seeds = await store.findSeedsByWorkspace(workspace_id, 'pending');
// Show any seeds the memory system planted that Joshua should review

6. Today’s daily file

const today = daily.readDay(new Date());
// If Joshua wrote anything before 6am, include it
// Usually empty — the daily file fills during the day

The prompt template

You are Obadiah, Joshua's chief of staff AI. You are composing his morning briefing.

Your voice: direct, warm, efficient. No fluff. Lead with what matters. Use humor
sparingly. Joshua appreciates brevity — if a section has nothing notable, skip it
entirely rather than saying "nothing to report."

## Context about Joshua
${substrate excerpts — identity, priorities, projects, workflow}

## Yesterday's observations
${yesterday's marks — observations, strengths, evidence summaries}

## Messages routed to personas yesterday
${routing marks — which personas matched which messages}

## Git activity (last 24h)
${git log summaries}

## Pending memory seeds
${seeds awaiting review, with their proposed changes}

## Today's daily file (early entries)
${today's daily content, if any}

Compose a morning briefing. Use the format:
- "Good morning, Joshua" opening with the date
- Yesterday's highlights (2-5 bullets, skip if nothing notable)
- Active projects (only mention projects with recent activity or pending work)
- Git activity (summarize themes, don't list every commit)
- Today's priorities (inferred from substrate + yesterday's activity)
- Pending seeds (if any exist — list each with a one-line summary)
- Brief, warm closing

Be yourself. Joshua likes you direct and honest.

What Ships in v1

  1. MorningBriefComposer module at packages/cortex/src/morning-brief-composer.ts — gathers all sources, composes prompt, calls Sonnet, returns the brief text
  2. Git log utility at packages/cortex/src/git-log-fetcher.ts — runs git log in configured directories, returns structured summaries
  3. Morning brief receptor — binds to morning.pulse, calls the composer, posts to Discord, emits morning.brief_delivered
  4. Wiring in start-rhythms.ts — register the receptor, pass spore-derived config (project dirs, Discord channel)
  5. Integration test — mock LLM + mock DiscordClient; verify the composer is called with the right sources and the mock reply is posted
  6. Graceful no-op without ANTHROPIC_API_KEY — brief receptor logs a warning and skips composition if no LLM is available

Dependencies

Deferred

Item Why Trigger
Calendar integration Gmail/Calendar is federated through Claude Code; the daemon can’t read them directly When the daemon gets its own OAuth organ — or when Joshua writes calendar context into his daily file before bed
Email summary Same federated-via-Claude-Code model Same as calendar
Project dir auto-discovery v1 hardcodes project dirs from spore; future reads them from substrate or a spore field When Joshua adds or removes projects frequently
Brief delivery confirmation v1 posts and logs; future could react-emoji for “read” confirmation When Joshua wants to track whether he actually read the brief

Non-Goals

Success Criteria

  1. At 06:00 Berlin (or whenever morning.pulse fires), a real brief appears in Discord #general as a top-level message
  2. The brief includes: yesterday’s marks summary, git activity summary, active projects (from substrate), and pending seeds (if any)
  3. A morning.brief_delivered signal is emitted after posting
  4. Without ANTHROPIC_API_KEY, the receptor logs a warning and skips — no crash, no empty post
  5. Integration test passes with mock LLM + mock DiscordClient
  6. The brief reads naturally — it’s Obadiah’s voice, not a mechanical template fill