Morning Brief Workflow — Implementation Plan
Date: 2026-04-12
Spec: docs/superpowers/specs/2026-04-12-morning-brief-workflow-design.md
Status: Executing
Overview
Wire the morning.pulse rhythm (already fires at 06:00 Berlin) to a real morning brief: gather context from memory + substrate + git + seeds, call Sonnet, post the result to Discord #general as a top-level message.
Task 1: Git log fetcher utility (TDD)
File: packages/cortex/src/git-log-fetcher.ts
Test: packages/cortex/src/git-log-fetcher.test.ts
fetchGitLogs(projectDirs: string[]): Promise<Array<{project: string, commits: string[]}>>- Uses
child_process.execFile('git', ['log', '--oneline', '--since=24 hours ago', '--all'])— no shell injection risk - Empty array if the dir doesn’t exist or isn’t a git repo (catches errors, doesn’t throw)
- TDD: create a tmpdir with
git init, make commits, assert they appear in output
Task 2: MorningBriefComposer (TDD)
File: packages/cortex/src/morning-brief-composer.ts
Test: packages/cortex/src/morning-brief-composer.test.ts
interface MorningBriefDeps {
llmCaller: LlmCaller;
recall: typeof recall; // injected for testability
recallDeps: RecallDeps; // store, substrate, daily, compost, etc.
memoryStore: MemoryStore; // for findSeedsByWorkspace
dailyReader: DailyReader; // for readDay(today)
gitProjectDirs: string[]; // passed from start-rhythms
workspaceId: WorkspaceId;
}
async function composeMorningBrief(deps: MorningBriefDeps): Promise<string>
Gathers:
1. Substrate: recall({ about: 'joshua', layers: ['substrate'], workspace_id })
2. Yesterday’s marks: recall({ about: 'joshua', horizon: '1d', layers: ['marks'], workspace_id })
3. Routing history: recall({ about: 'joshua.discord.routed', horizon: '1d', layers: ['marks'], workspace_id })
4. Git log: fetchGitLogs(gitProjectDirs)
5. Pending seeds: memoryStore.findSeedsByWorkspace(workspace_id, 'pending')
6. Today’s daily: dailyReader.readDay(todayDate)
Assembles the prompt from the spec’s template verbatim. Makes one Sonnet call. Returns the text.
Tests mock the LlmCaller. Verify the prompt contains: substrate section, marks section, routing section, git section, seeds section, daily section, the “Be yourself” line.
Task 3: postToChannel on DiscordClient + morning brief receptor
File (modify): packages/integrations/src/discord/discord-client.ts
File: packages/cortex/src/morning-brief-receptor.ts
Test: packages/cortex/src/morning-brief-receptor.test.ts
Add postToChannel(channelId: string, content: string): Promise<string | undefined> to DiscordClient — simpler than replyInThread, just channel.send(content).
The receptor:
- Binds to morning.pulse
- Checks if LLM is available (graceful no-op if not)
- Calls composeMorningBrief()
- Posts via discordClient.postToChannel(channelId, content)
- Emits morning.brief_delivered signal
- try/catch wraps everything — log error, don’t crash
Task 4: Wire into start-rhythms.ts
File (modify): deployments/obadiah/bridge/start-rhythms.ts
- Replace the
morning-pulse-loggerreceptor with the real morning brief receptor - Pass deps: sonnetCaller, discordClient, recall, memoryStore, substrate, daily, compost, gitProjectDirs, workspaceId
- Channel ID:
1467158393633378447(#general) - Git project dirs:
[spore source_dir (cambium repo), '/Users/joshua/Projects/Obadiah'] - Cambium repo: use
resolve(import.meta.dirname, '../../..')to derive from the spore location - Graceful no-op if
sonnetCalleris undefined (no ANTHROPIC_API_KEY)
Task 5: Integration test
File: packages/cortex/src/morning-brief.integration.test.ts
- Mock LlmCaller returns fixed text
- Mock DiscordClient (just an object with
postToChannelspy) - InMemoryMemoryStore with some marks and seeds
- Emit synthetic
morning.pulse→ verify: - LLM prompt contains substrate + marks + git log sections
- DiscordClient received the brief text for the right channel
morning.brief_deliveredsignal was emitted
Task 6: Full monorepo gate
pnpm turbo test && pnpm turbo typecheck && pnpm turbo build
Notes
- The prompt template from the spec is authoritative — copy it exactly
- All LLM calls mocked in tests
- All Discord calls mocked in tests
postToChannelis new on DiscordClient —replyInThreadwas added in BL-008 v2