---
name: morning-digest
description: Synthesize the operator's overnight work (cse-forecast, cse-intake, cse-engagement-finder runs; war-room, forecast, and 1:1 prep outputs; any [ATTN]/[ERR] lines from the skill logs) into one Slack self-DM written in the operator's voice. Use when the user asks for the morning digest or overnight recap, or when an operator-installed external scheduler invokes it at 06:30 Pacific local time on weekdays. This repository does not install a scheduler.
argument-hint: "[date or 'yesterday']"
---

# Morning Digest

## Compact MCP routing

Follow the shared [compact MCP routing contract](../../shared/compact-mcp-routing.md). The facade tools are `cse_capabilities`, `cse_read`, `cse_apply`, `context_assemble`, and `cse_session_info`; named operations are capability ids. On interactive `/mcp/full`, direct named reads and each named write's per-tool guarded preview/apply flow are allowed; `cse_read`/`cse_apply` are also available facades. On facade-only `/mcp`, route reads through `cse_read` and mutations through `cse_apply` twice: preview first, then the identical capability and arguments with underlying `execute:true`, justification, and the returned `preview_digest`. Call `cse_session_info` directly for auth recovery. Do not call `context_assemble` or run `cse-sweep` for this digest.
Worked ticket read:

```json
jira_get_issue({"key": "CSE-123", "fields": ["summary", "status"]})
```

Read the operator's skill logs from the past ~24h, sparsely verify surfaced Jira keys and unanswered Slack messages, and post a single self-DM summarizing what needs attention vs. what was handled autonomously overnight. Written in the operator's voice per `.agents/shared/prose-voice.md`.

## Domain Configuration

No domain discovery is needed. Ticket verification requests only standard `jira_get_issue` fields; customer names come from the log line being verified.

## Voice

This skill writes in the authenticated operator's voice. Follow the register rubric `.agents/shared/prose-voice.md` (sentence-case opens, contractions, dash palette, declarative cadence, two-facts-max, no em-dash) plus the `writing` skill for the patterns to favor and the AI-slop tells to avoid. `$CSE_OPERATOR` is only a bootstrap-resolved voice selector; if it is unset or ambiguous, fail closed.

What this means concretely:

- Declaratives are the default closer. Use "Parked." "Tracking." "Should be all you need." Avoid "gut check?" / "thoughts?" on every section.
- Two facts max per bullet, connected with `--` / `;` / `,`. Never `and ... and ...` stacking.
- No Unicode em-dash (`—`). Use `--` for dashed asides.
- Contractions on. "It's", "don't", "we've", "I'll". Never "do not" / "cannot".
- First-person for writes I performed; passive for state I'm just reporting.

## Setup

Before running, `source` `.agents/shared/skill-bootstrap.sh` so `CSE_TOOLD_BIN` and `CSE_OPERATOR` resolve. The daemon keeps the self-DM write path's keychain auth fresh on its own; no mint step is needed:

```sh
# shellcheck source=.agents/shared/skill-bootstrap.sh
source "${PLUGIN_ROOT:-${DROID_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$PWD/plugins/cse-tools}}}/.agents/shared/skill-bootstrap.sh"
```

After that: `$CSE_OPERATOR` and `$CSE_TOOLD_BIN` are available; Jira and Slack auth are read from daemon keychain entries. On a 401, request `cse_session_info({"force_refresh": true})` once and retry once. A 403 means access is blocked or revoked: surface the named operator action and stop without retrying.

Direct-tool guardrails for this skill:
- For Slack direct search prefer `q`; `query` remains a compatibility alias.
- Jira reads use `jira_get_issue` only for keys already surfaced by logs; request explicit `fields`.
- Slack reads stay bounded to one overnight search plus the candidate threads needed to determine whether the operator replied; on `/mcp/full`, use the named tools directly, and on `/mcp`, route their capability ids through `cse_read`.

Required surfaces:

- **Read strategy:** Follow [`.agents/shared/read-strategy.md`](../../shared/read-strategy.md). This is a log-centric digest, not an entity-retrieval skill. Do not route it through `context_assemble` or `cse-sweep`. Re-resolve only specific logged ticket keys and unanswered Slack candidates with capability reads.
- **Tool frugality:** Follow [`.agents/shared/tool-frugality.md`](../../shared/tool-frugality.md). Use compact defaults, explicit Jira fields, bounded Slack results, and no speculative source fan-out.
- **Skill logs** -- `~/.config/cse-tools/logs/*-YYYYMMDD-*.log` (each skill run writes one). Inside the log, grep for `[ATTN]`, `[ERR]`, and the "ACTIONS EXECUTED" / "HELD" blocks that `cse-forecast` and `cse-engagement-finder` emit. Repo-relative `logs/` is also accepted on a dry-run from a checkout.
- **Meeting / forecast artifacts** -- confirm expected overnight files under `war-rooms/`, `forecasts/`, and `1on1s/` when overnight logs name them.
- **Slack reads** -- `slack_search`, `slack_read_thread`, and `slack_users`: call named tools directly on `/mcp/full`, or route their capability ids through `cse_read` on `/mcp`. Use only for unanswered mentions and DMs received overnight.
- **Slack writes (self-DM)** -- `slack_open_dm` and `slack_post_message` follow the two-phase write flow: use each named tool's guarded preview/apply on `/mcp/full`, or `cse_apply` on `/mcp`; prose goes through a `/tmp` `text_path`.
- **Jira** -- `jira_get_issue`: call the named tool directly on `/mcp/full`, or route its capability id through `cse_read` on `/mcp`, only for ticket keys surfaced by `[ATTN]`, `[ERR]`, or `HELD` lines. Do not run a broad `jira_search`.

If a surface is unavailable, skip that section and note it in the digest body as `DATA UNAVAILABLE -- <surface>`. Never fabricate activity.

## When to use this skill

- External schedule (not installed by this repository): the operator may add `CRON_TZ=America/Los_Angeles` followed by `30 6 * * 1-5 <morning-digest invocation>` to their crontab. This runs at 06:30 Pacific local time with daylight-saving changes handled by the timezone. On-demand invocation remains fully supported.
- On-demand: operator types "morning digest" / "overnight recap" / "what happened while I was asleep" in chat.
- Argument: optional date (default = today; operator can pass `yesterday` to re-compute for the prior day's logs if the cron missed).

## Inputs

- `$1` (optional) -- date slug: `today`, `yesterday`, or `YYYY-MM-DD`. Default `today`.
- Implicit: authenticated operator identity from `cse_session_info` (routes the self-DM to the right user); `$CSE_OPERATOR` is a display/voice selector, never an email derivation source.

## Process

### Phase 1: Resolve operator identity

1. Call `cse_session_info` and read `operator_identity` from the daemon-owned authenticated operator identity. Require `operator_identity.available === true` and a non-empty `operator_identity.email`. Never construct an email from the `$CSE_OPERATOR` slug. Stop if the session does not expose that email. `cse_session_info` does not supply a Slack user ID.
2. Resolve that email with capability `slack_users` and arguments `{action:"search", query:<operator_identity.email>}`. Keep only results whose profile email equals the daemon email (case-insensitive exact match). Fail loudly if zero or multiple exact matches are returned. Profile the single match (`action:"profile"`) and store its ID as `$OPERATOR_SLACK_UID`. Abort if the profile email does not equal `operator_identity.email`.
3. Dry-run `slack_open_dm({users:$OPERATOR_SLACK_UID, dry_run:true})`, then execute with identical arguments plus `execute:true`, a justification of at least 16 trimmed characters, and the returned `preview_digest`. Before accepting the returned DM channel, profile `$OPERATOR_SLACK_UID` again and hard-verify both: (a) the profile ID equals `$OPERATOR_SLACK_UID`, and (b) the profile email equals `operator_identity.email` (case-insensitive). **ABORT without posting** if either check fails; never fall back to a username, hard-coded email, `$CSE_OPERATOR`, or default person.

### Phase 2: Gather the bounded Pacific-local interval

Resolve `$1` (`today`, `yesterday`, or `YYYY-MM-DD`) as a calendar date in `America/Los_Angeles`; reject invalid dates. The digest window is `[06:30 on the preceding Pacific date, 06:30 on the target Pacific date)`. Convert both endpoints to instants for timestamp filtering. Enumerate candidate filenames for both Pacific calendar dates touched by the interval (and both filename-date prefixes when the UTC conversion crosses midnight), then retain only records whose opening timestamp is inside the half-open interval. Do not use the host timezone or a UTC-date wildcard as the filter.

**Skill logs:**

1. List candidate `$LOG_DIR/*-YYYYMMDD-*.log` for every filename date identified by the bounded interval, where `$LOG_DIR = ~/.config/cse-tools/logs`, or the resolved repository root's `logs/` on a repo-relative dry-run. Filter by each log's opening timestamp against the interval before consuming it.
2. Per-log, extract:
    - Opening timestamp and real completion receipt. For `cse-tools-run-skill` logs, parse its final stdout JSON object: schema `cse_tools.skill_run_result.v1`, `status:"pass"`, plus process exit code 0 is success; `status:"blocked"` or exit code 2 is blocked, and a thrown runner error exits 1. Its JSONL transcript's `context_pack` record is retrieval evidence, not a generic completion trailer. For other producers, consume an explicit supervisor/process exit receipt when present; if none exists, mark completion `UNKNOWN` rather than inventing failure or success.
   - `[ATTN]` lines (soft-skip items the skill surfaced).
   - `[ERR]` lines (hard failures with a `ecs-claude --resume <session-id>` hint).
    - `ACTIONS EXECUTED` block (`cse-forecast` format): transitions, field updates, comments when execute mode ran.
    - `HELD` block: intended actions blocked by H1/H2/H3 hold conditions.
3. De-duplicate: if both `[ATTN]` and `HELD` reference the same ticket + field, collapse to one digest line with the more specific reason.

**Slack overnight activity:**

1. After parsing the logs, run one bounded `slack_search` with `after:<yesterday-local-date> from:<not-operator> (channel_has:<operator-handle> OR dm)` scoped to `#cse-requests`, `#help-cse`, `#gold-accounts`, plus the operator's DMs. Use the capability's `q` argument.
2. Filter to messages that contain a direct @-mention of the operator OR a DM to the operator.
3. Use `slack_read_thread` only for those candidates to see if the operator already replied. If not: candidate for "Needs your attention".

**Jira verification:**

1. Extract unique ticket keys from `[ATTN]`, `[ERR]`, and `HELD` lines.
2. Call `jira_get_issue` once per unique key, in parallel, requesting only `summary` and `status`.
3. Do not enumerate Jira or infer new overnight activity outside the logs.

**Meeting and forecast coverage from local artifacts:**

Use only war-room / forecast / 1:1 output paths named by overnight logs. Confirm whether each expected file exists under `war-rooms/`, `forecasts/`, or `1on1s/`; do not call another evidence source to discover meetings or forecasts.

### Phase 3: Classify and prioritize

Sort findings into three buckets:

**Needs your attention** (put at the TOP of the Slack DM):

- Any `[ERR]` line with a session-resume handle. Include `ecs-claude --resume <session-id>` verbatim.
- Any `[ATTN]` tagged to a specific ticket + missing field that the skill attempted to resolve and couldn't.
- Held H1 (missing prerequisite field that auto-research failed to fill).
- Held H3 (customer unresponsive 10+ days where the ticket still sits in `Customer Validation` or later -- those need a strategic call the skill shouldn't make alone).
- Any Slack DM or direct @-mention the operator hasn't replied to.

**Handled autonomously**:

- Successful transitions (group by target stage, one line per stage: "3 moved from Pilot Scoping to Pilot Validation").
- Successful field resolutions (count + one example).
- Tagged escalation comments posted.
- Intake tickets created by cse-engagement-finder Bucket A.
- Ruled-out leads appended by cse-engagement-finder.

**Meeting and forecast coverage**:

- Yesterday's meetings and forecasts with files on disk (`war-rooms/*`, `forecasts/*`, `1on1s/*`) -> one-line "Prep landed" or "Forecast landed" summary.
- Logged prep/forecast runs whose expected output file is missing -> "No prep artifact for <meeting>" or "No forecast artifact for <date>" (gap signal).

Drop anything that doesn't fit a bucket. The digest is a synthesis.

### Phase 4: Compose the DM (operator voice)

The DM is a single file-backed `slack_post_message`. The Slack adapter accepts `blocks` only as inline arguments, not by file path, so do not send blocks; put the complete fallback-formatted Slack text in `text_path`. Structure:

```
*Morning digest -- <date formatted like "Mon Apr 21">*

*Needs your attention*
- <bullet per item, 1-2 sentences max, operator voice>
- <each bullet carries a ticket key, permalink, or session-resume handle>

*Handled*
- <one line per collapsed category, count + representative example>

*Meetings*
- <yesterday's meetings and forecasts with/without artifacts>

_Full logs: <link to most recent cse-forecast log>_
```

### Voice rules for the body

Apply `.agents/shared/prose-voice.md` rubrics:

- Open with sentence-case. Never "Auto-digest:" / "Here is your..." / "AI summary".
- Declaratives beat elicitations. Use "3 moved to Pilot Validation." Avoid "Should I move these?"
- One connective of any type per bullet. No `and ... and ...` chains.
- 60-140 chars per bullet is the sweet spot. Two sentences only when the caveat requires it.
- Hedge palette: `kind of`, `basically`, `my read is`, `might be worth`. One per bullet max.
- Contractions required.
- No emojis. Period.
- For `Needs your attention` bullets, include the ticket key first (e.g. `CSE-94 Hatch Bank`) so the operator can scan; the operator reads left-to-right and wants the ticket identity before the issue category.
- For `[ERR]` lines, lead with the skill + time + one-line cause: `cse-forecast 06:12 -- 502 from Jira during transition sweep. ecs-claude --resume <session>`
- Hedges on judgment calls only: `My read is the Hatch exec sponsor is still Brad but he was quiet last week` is in-voice; `My read is the transition succeeded` is not (that's a fact, state it).

### Phase 5: Post

1. Compose the message body in a `/tmp` file so the operator can inspect it before the write (set `DIGEST_DRY_RUN=1` to skip the write and print to stdout; respect this in cron invocations during the first week).
2. Dry-run `slack_post_message({channel:$OPERATOR_DM_CHANNEL, text_path:<temp path>, dry_run:true})`. Re-call with the same arguments plus the returned `preview_digest`, `execute:true`, and a specific justification of at least 16 trimmed characters. Do not pass inline prose or `blocks`.
3. Capture the returned `ts` so the digest can be edited later. For an edit, write the full replacement text to a `/tmp` file, dry-run `slack_update_message` with arguments `{channel, ts, text_path, dry_run:true}`, then execute with identical arguments plus the returned `preview_digest`, outer `execute:true`, and a justification of at least 16 trimmed characters.
4. Record the channel and `ts` in the secondary digest file only. Do not append to a producer log; `~/.config/cse-tools/logs/morning-digest-YYYY-MM-DD.md` is the canonical writable receipt target for digest updates.

## Output

- Primary output: one Slack DM in the operator's self-conversation.
- Secondary output and canonical digest-update receipt: digest body written to `~/.config/cse-tools/logs/morning-digest-YYYY-MM-DD.md` for grep-friendly replay, including the channel, `ts`, and raw inputs hash (sha256 of the concatenated log filenames + Slack activity IDs) so a deterministic re-run can detect when nothing changed.
- Exit codes:
  - `0` -- digest posted cleanly (or successful dry-run).
  - `2` -- auth preflight failed.
  - `3` -- Slack post failed; digest body IS on disk at the secondary-output path, operator can copy-paste from there.

## Validation checklist (run before posting)

- [ ] `$OPERATOR_SLACK_UID` was resolved from an exact Slack profile email match to `cse_session_info.operator_identity.email`; re-profile before post confirms the same ID and email; `$CSE_OPERATOR` is not used as an identity source
- [ ] "Needs your attention" bullets each carry a concrete handle (ticket key, Slack permalink, or `ecs-claude --resume <session>`)
- [ ] No bullet starts with "Auto-", "AI-", "I have ", "I was unable to"
- [ ] No Unicode em-dash in the body (grep the composed text for U+2014)
- [ ] No emojis anywhere
- [ ] At least one declarative closer (not every bullet ends with "?" or "thoughts?")
- [ ] "Handled" section is counts + one example
- [ ] If `[ATTN]`/`[ERR]` is zero and "Handled" is empty, the DM opens with "Quiet overnight -- nothing to flag." and exits

## Behavioral rules

- Never fabricate. If cse-forecast didn't run, say so: `cse-forecast 06:07 -- no log on disk. Check supervisor on the instance.`
- Never page the operator twice for the same issue. If the last digest's `ts` referenced the same ticket + error, skip re-surfacing it; the previous message is still in the DM.
- Never dump the raw skill log into the DM. The digest is a synthesis; the log is a link at the bottom.
- Never include cleartext customer-sensitive info (churn signals, exec feedback) in the digest body. Surface the ticket key + "needs a read" instead; the operator can open the ticket to read the raw context.
- Never skip the voice loader. A digest in wrong-operator voice is worse than no digest.
- If the daemon connection is refused, preserve/write the secondary digest file with all locally gathered content, point the operator to `cse-mcp-setup`, and stop. Do not attempt `slack_open_dm`, `slack_post_message`, or `slack_update_message` while the daemon is unreachable.
- If the voice loader cannot resolve the operator profile or `prose-voice.md` is unreadable, abort and post a single line: `Morning digest offline -- voice profile unavailable. Check ~/.config/cse-tools/ or re-source skill-bootstrap.sh.`. Missing raw corpus or examples alone is not a reason to use generic prose; continue from `prose-voice.md`.
