---
name: daily-check
description: "Run a daily health check on all configured ad accounts (Google Ads, Meta Ads, Amazon Ads, TikTok Ads, Search Console, GA4, and any configured plugin platform). Use when the user asks for a daily review, health check, status update, anomaly detection, or 'how are my campaigns doing today'. Reads STRATEGY.md and STATE.json, runs platform-specific health diagnostics, checks goal progress, evaluates pending action_log observations, and reports findings as Healthy / Watch / Action-needed. Also use when the user asks in Japanese (デイリーチェック / 今日のアカウント状況は / 異常がないか確認して / 日次ヘルスチェック)."
metadata:
  version: 0.12.1
---

# Daily Check

> PREREQUISITE: Read `../_mureo-shared/SKILL.md` for auth, security rules, output format, and **Tool Selection** (Read/Write on Code, `mureo_strategy_*` / `mureo_state_*` MCP on Desktop / Cowork).

Run a daily health check on all marketing accounts using the strategy context.

## Modes

Pick the mode **before step 0** — it decides how much you gather and how you report:
- **Incremental (default)** — the cheap daily run. Gather compact findings **analytics-first** (step 4), load the action_log as a **subset** (step 0), and report **only what changed** since the previous `reports.daily` summary (step 10's diff-first contract). This is what keeps context small and the report short.
- **Deep** — the full run: every step below at full depth (raw per-platform pulls, the full action_log, the full Healthy / Watch / Action-needed report). Use it when the user says **"deep"** or **"full"**, on the **first-ever run**, or when the previous `reports.daily` summary is **missing or older than 7 days** (compare its `generated_at` / `period` against `server_now`'s date). Recommend running a deep check **once a week** so the incremental diffs never drift from ground truth.

## Mixed operation

Manual operation in the platform UI and mureo-driven operation coexist — most accounts run that way for a while after onboarding, and it applies to every platform. Treat any change you find that mureo did not make as a first-class fact, not an anomaly: the `action_log` records what mureo did, never the full history of the account, so never infer current state from it. When a diff surfaces a change made outside mureo, say so in the report rather than silently absorbing it into the new state.

Step 2b **imports** what mureo can fetch, so much of that work is now in `action_log` as `origin: "external"` before you start diagnosing. Two rules follow. First, an entry with `origin: "external"` is something mureo OBSERVED — never describe it as something mureo did, and never offer to roll it back (mureo refuses; it never saw the prior value). Second, and more important, import covers only the platforms with a change feed: for every platform step 2b reports `unavailable` or `error`, you are still working blind, and a quiet report from such a platform is **not** evidence that it was quiet. Say which platforms those were.

## Prerequisites
- STRATEGY.md and STATE.json must exist in the current directory (run the `onboard` skill first if not)

## Steps

**Before you start**: Run the **Diagnostic preamble** from ../_mureo-shared/SKILL.md — load learning insights (mureo_learning_insights_get) and consult advisors (mureo_consult_advisor) before drawing conclusions.


0. **Establish today**: call `mureo_state_get` **first, on every host** (including Claude Code, where you would otherwise `Read` the file) and take `server_now` from its response — an ISO 8601 timestamp with UTC offset, e.g. `2026-07-28T10:12:33+09:00`. Its date is the **only source of the current date** for this run; do not ask the user, do not guess, and do not shell out (this skill must run in Bash-less headless hosts). Every other date you will see — `reports.daily.period`, `last_synced_at`, `action_log` timestamps — is **history**, never evidence of what day it is now. Also **never write `server_now` into STATE.json**: it is a response field, and a persisted copy becomes tomorrow's stale "today".

   **Scope the action_log to save context.** In **incremental** mode call `mureo_state_get` with `action_log: "pending"` — it returns only the entries with an OPEN `observation_due` (the past-due ones you owe an evaluation this run in step 9, and the future-due ones still under observation), and adds `action_log_scope: "pending"` + `action_log_total: <int>` so you know the log you were shown is a **subset**, not the whole history (never read it as complete — see *Mixed operation*). In **deep** mode omit the parameter (or pass `action_log: "all"`) to load the full log.

   **Never short-circuit on history.** Do not conclude "today's report is already fetched — re-displaying it" (or skip a step) because a stored `period` / `last_synced_at` looks like today. A daily check **always fetches fresh platform data** for the period ending on `server_now`'s date; a stored report is at best a prior run to compare against.

1. **Load context**: Read STRATEGY.md (especially Operation Mode, Data Sources, and all Goal sections) and STATE.json (the same `mureo_state_get` response from step 0 on MCP hosts). In incremental mode that response's `action_log` is the `pending` subset (marked by `action_log_scope`) — all the evidence loop in step 9 needs; deep mode carries the full log.

2. **Discover available platforms**: Identify all configured platforms from STATE.json `platforms` and check which data sources (Search Console, GA4) are accessible. Also enumerate installed **plugin / bridged** platforms (`mcp__mureo__<plugin>_*` tools, plus **Amazon Ads** under Amazon's own tool names — STATE.json key `plugin:mureo-amazon-ads-bridge:amazon_ads`) and any **hosted official-MCP connector** present in the session (e.g. TikTok's `tt-ads-*` tools, STATE.json key `tiktok_ads`); include them best-effort — see `_mureo-shared` → *Plugin platforms* and *Hosted-connector platforms*. For every platform you enumerate, record whether its tools are **callable in this session**, not just whether its key is configured: a hosted connector is enumerated from STATE.json `platforms` alone, so one that is disconnected host-side stays in scope with its whole tool namespace missing. That is the input step 4's absent-tool-surface branch reads.

2b. **Import changes made outside mureo**: call `mureo_external_changes_import` (no arguments — it covers every platform in STATE.json). It appends anything mureo did not do to the `action_log` with `origin: "external"`, skipping changes already imported and changes mureo itself made. Do this **before** step 3's sync, so a status you are about to diff already has its explanation attached. Safe to re-run; importing the same change twice is a no-op.

   Read the response for the blind spots, not just the finds. `blind_spots` lists every platform that came back `unavailable` (not checked — either mureo has no change feed for it, or a feed could not answer for this account, e.g. BYOD mode, which has no change history; `notes` says which — see `docs/change-import.md`) or `error` (the feed exists but could not be read). **Neither means "no changes".** Carry those platform names into step 10's report and state that manual work there is invisible to mureo; when `truncated_platforms` is non-empty, add that older changes inside that window are unreachable and cannot be recovered later. If the tool is unavailable (older mureo), say so and fall back to the step-11 status diff alone — and note that the diff sees only status changes, not budget, targeting or creative edits.

   Newly imported entries carry an `observation_due` anchored on when the change actually happened, so an older one is already past due and comes back to you in step 9. See `../_mureo-shared/SKILL.md` → *Changes made outside mureo*.

3. **Sync state**: For each platform in STATE.json `platforms`, fetch current campaign data and update STATE.json.
   - **Google Ads**: prefer mureo native `google_ads_campaigns_list`. If mureo's Google Ads tools are unavailable (i.e. `MUREO_DISABLE_GOOGLE_ADS=1` was set when the user installed the official MCP via `mureo providers add google-ads-official`), fall back to the official `google-ads-official` MCP's equivalent campaign-list tool (typically `list_campaigns` or `report_campaigns`).
   - **Meta Ads**: prefer mureo native `meta_ads_campaigns_list`. If unavailable, fall back to the official `meta-ads-official` hosted MCP's campaign-list tool (the official MCP exposes the Marketing API surface, so basic listing is available). In BYOD mode this response is wrapped as `{"source": "byod_import", "as_of": <import time>, "data": [...]}` — take the rows from `data` and treat them as of `as_of`, not as live.
   - **TikTok Ads (hosted connector, `tiktok_ads`)**: mureo has no native TikTok tools — call the TikTok connector's own campaign-list / reporting tools (e.g. `tt-ads-*`) for the current period, and write the per-campaign numbers to STATE.json via `mureo_state_upsert_campaign` (`platform="tiktok_ads"`). Skip mureo-only fields (`result_indicator`, anomaly, RSA audit). See `_mureo-shared` → *Hosted-connector platforms*.
   - **Ad-level delivery status — active campaigns only**: for each campaign whose status reads as active **in its own platform's vocabulary** — `ACTIVE` / `ENABLED` / `enabled`, matched **case-insensitively** (see `../_mureo-shared/SKILL.md` → *Status vocabulary contract*) — list its ads and keep each ad's `ad_id`, name, `status` **and** `effective_status`. `status` is only what the ad is configured as; `effective_status` is whether it is actually delivering, and the two disagreeing is exactly how an ad stopped outside mureo (by hand in the platform UI, by its ad set / campaign, or by policy) shows up. **Meta**: `meta_ads_ads_list` (scope by `ad_set_id`, or list once per account and group by `campaign_id`). **Google Ads**: `google_ads_ads_list` for the campaign's ad groups, keeping the ad status plus the policy review/approval status. **Amazon Ads (official-MCP bridge, `plugin:mureo-amazon-ads-bridge:amazon_ads`)**: `campaign_management-query_ad` — the body REQUIRES `accessRequestedAccount` (`{advertiserAccountId}` or `{profileId}`; a global account accepts only `profileId`) and `adProductFilter.include` with **exactly one** ad product (`SPONSORED_PRODUCTS` / `SPONSORED_BRANDS` / `SPONSORED_DISPLAY` / `SPONSORED_TELEVISION` / `AMAZON_DSP`), so a full sweep is one call per product; narrow with `campaignIdFilter.include` and `stateFilter.include` (`ENABLED` / `PAUSED` / `ARCHIVED`), and take the rows from the `ads` envelope. Amazon exposes **one state per ad** (`state`, the configured state) and no serving/review status: record it as the ad's `status` and **leave `effective_status` unset** — its effective status is **unknown**, never `state` copied across (see `../_mureo-amazon-ads/SKILL.md` → *Ad-level delivery state*). **Other plugin / hosted-connector platforms**: use the equivalent ad-listing tool where one exists; where none does, skip that platform **and report the skip** — name the platform and say that no ad-listing tool was found — so a missed or mis-inferred tool is distinguishable from a platform that genuinely has nothing to report. Cost guard: do **not** make ad-level calls for PAUSED / REMOVED campaigns. In BYOD mode these reads are served from the imported CSVs and come back wrapped as `{"source": "byod_import", "as_of": <import time>, "data": [...]}` — take the rows from `data`, and treat the statuses as of `as_of`, not as live (a live response has no such wrapper). This ad-level fetch runs in **both** modes — it is cheap and the external-change diff in step 11 needs it.
   - mureo BYOD mode applies only to mureo native tools — do **not** look for raw CSVs in the project directory; mureo BYOD data is centralized in the workspace `byod/` directory (or `~/.mureo/byod/` for legacy CLI users) and is only accessible through mureo MCP tools.

4. **Platform health checks**: Run health diagnostics on each configured ad platform.
   - **Auth failure is not data — it is a hole in the report.** Any tool result carrying `"status": "auth_error"` means mureo could not read that platform in steps 3–4 at all: `auth_cause` is `no_credentials` (nothing is configured) or `token_invalid` (a credential exists and the platform rejected it — typically an expired token), and `detail` is the operator-facing sentence. **Never render `detail` where a metric belongs, and never read that platform's missing or empty numbers as "quiet"** — unreadable and quiet are different facts. Record the platform name with its `auth_cause` and carry both into step 10 exactly as step 2b's `blind_spots` are carried, then keep going for every other platform: one platform's credentials failing degrades the report, it never aborts the run.
   - **An absent tool surface is the same hole, and nothing will report it to you.** A platform is also unreadable when there is no tool to call: its key is in STATE.json `platforms` but its tools are not in this session at all — a hosted connector disconnected host-side, so the entire `tt-ads-*` namespace is missing. There is no call to make and no error envelope to catch, so unlike `auth_error` this one has to be checked **before you report**, not after a result comes back: for each platform from step 2, confirm you actually called a tool of that platform's own and got rows back. Exhaust the fallbacks first — mureo native, then that platform's official / hosted MCP, per step 3's sync bullets and the per-platform bullets below — and raise `tools_absent_for_<platform>` only when **no surface** remained to try, so this never fires on a platform a fallback could still read. Then treat it exactly as `auth_error`: record the platform, carry it to step 10, and keep going for every other platform.
   - **Analytics-first (both modes' first move).** Consult `mureo_analytics_modules_list` for **every** platform (not just external ones) — it maps each platform to its advertised capabilities (`detect_anomalies`, `diagnose_performance`, `audit_creative`, `analyze_budget_efficiency`, `detect_delivery_collapse`). Match its `platform` field against the platform keys you already hold: it is the **canonical platform key** (`plugin:<dist>:<provider>` for a plugin, and the older `plugin:<dist>` still resolves), the same key STATE.json and `action_log` use, while `registry_name` / `source_distribution` are identifiers, not keys — never look a platform up by them (see `../_mureo-shared/SKILL.md` → *Canonical platform key*). Then per platform:
     - **Module registered AND advertises the capability you need** → run it via `mureo_analytics_run` (`DETECT_ANOMALIES` / `DIAGNOSE_PERFORMANCE`; pass `platform`, `capability`, `account_id`; add `window_days` for `detect_anomalies` or `scope` for `diagnose_performance`) and fold its **compact findings** (`result`) into the health summary. In **incremental** mode this REPLACES the raw pulls — do **not** ALSO fetch raw campaign / insights rows to re-derive what the module already summarized. A `status` other than `ok` (`no_analytics_module` / `capability_not_available` / `error`) means treat that capability as unavailable and fall through to the raw checks below — never fail the whole daily-check because one platform's module errored.
     - **No analytics module, or the module lacks the capability** → report `analytics_not_available_for_<platform>` for the **module-specific** deep diagnostics (RSA audit, `result_indicator`, budget-efficiency) — `<platform>` being the canonical platform key, so the notice names the same platform as the rest of the report — and do NOT fabricate heuristics from the integration's tool schemas (Issue #120). **BUT still run the generic, platform-agnostic `analysis_anomalies_check` per campaign** (metrics normalized to `campaign_id`/`cost`/`clicks`/`impressions`/`conversions`/`cpa`/`ctr`): it builds a median baseline from `action_log` and flags zero-spend / CPA-spike / CTR-drop for TikTok, plugins and official-MCP platforms too — a principled detector, not an auto-derived heuristic (see `_mureo-shared` → *analytics-module parity* → generic anomaly check).
     - **Deep** mode runs the raw per-platform diagnostics below in full regardless of what analytics advertise; **incremental** mode runs them only as the fallback above (a platform with no analytics module, or a module that lacks the capability).
   - **Delivery collapse — run this in BOTH modes, for every platform.** A campaign that is still ENABLED while its impressions have gone to zero is the single most detectable failure mode in ad ops, and it is the **inverse** of the cost-spike checks above: `google_ads_cost_increase_investigate` finds spend that ran away, this finds delivery that stopped. Detection is cheap and unattended — never skip it to save context.
     - **Platform has an analytics module advertising `detect_delivery_collapse`** (Google Ads and Meta Ads do) → `mureo_analytics_run` with `capability="detect_delivery_collapse"`. Do **not** pass `window_days`: the baseline is a same-weekday median and needs weeks of daily data, so the 7-day default would make it unusable. Read the result's `status` before its `signals`: only `status="ok"` makes an empty `signals` list an all-clear. `status="no_credentials"` and `status="data_unavailable"` mean **nothing was checked** — report them as such, never as "no problems found".
     - **Then read `unreported_days` before concluding anything.** It is the number of complete days the platform has reported no delivery for, on any campaign. `0` is the healthy case. A non-zero value is **not** a collapse — a platform that lags by a day is routine, and mureo deliberately does not guess — but an empty `signals` list next to a non-zero `unreported_days` is **not an all-clear either**: those days were never evaluated. If it is **2 or more, or has grown since yesterday**, report it as **Action needed**: "the platform has reported no delivery since `reported_through`" — a total account outage and a platform-side reporting failure look identical from here, and only the operator can tell them apart in the UI.
     - **Blind spot to state, not to hide:** a campaign with **no rows at all** in the window is invisible to this detector (no first row, no series, and mureo will not invent one). If a campaign you expect to see is absent from `evaluated_campaigns`, say so rather than treating it as healthy.
     - **Every other platform** (TikTok and other hosted connectors, Amazon via the official-MCP bridge, plugin platforms) → pull a **day-grain** report from that platform's own tools covering the last ~35 days, normalise each row to `{campaign_id, campaign_name, status, end_date, date, impressions, clicks, cost}`, and pass them to **`analysis_delivery_collapse_check`** with the canonical `platform` key. It returns the same `reported_through` / `unreported_days` fields — read them the same way. Pass rows for **all** campaigns in one call, not one campaign at a time: a campaign that is still delivering is what proves the platform covered those dates, and without it a silent campaign cannot be distinguished from an unreported one. **And pass rows from a single fetch — never stitch together rows you retrieved at different times.** The check treats the newest date anywhere in `rows` as how far the platform has reported, so mixing a freshly-pulled campaign with a staler one makes the fresh one's frontier the yardstick and reports the staler (healthy) campaign as collapsed. If you cannot avoid mixing — or the connector finalises campaigns at different times — set `reported_through` to the **oldest** per-campaign last date you trust; it costs a day or two of recency and never hides a real collapse. Never pass the end of the range you asked for. The detector is the same code the native platforms run. If a platform cannot produce day-grain delivery at all, say so — `delivery_collapse_not_checkable_for_<platform>` — instead of leaving the impression it was checked.
     - **Do not hand-roll the thresholds.** The detector already suppresses the false positives that would get it muted: weekend/weekday seasonality (same-weekday baseline), the partial current day, intentional pauses, finished flights, low-volume campaigns, and campaigns with under two weeks of days that actually delivered. It is also independent of how long the outage has been running and of whether the platform emits zero-delivery rows at all. The operator tunes it in STRATEGY.md `## Guardrails` (`delivery_collapse_drop_pct`, `delivery_collapse_consecutive_days`, `delivery_collapse_min_baseline_impressions`, `delivery_collapse_baseline_days`); read them, do not re-derive them.
     - **A signal is always `Action needed`**, and it goes in the report in both modes (see step 10's safety rule — an unresolved outage never goes quiet). Report `campaign_name`, `collapse_start_date`, `days_at_collapse` and the baseline it fell from. To find out WHY, run `/rescue` → *Delivery collapse* — do not guess a cause here.
   - **Google Ads** (built-in module — capabilities `detect_anomalies`, `diagnose_performance`; raw pulls run in **deep mode, or as the incremental fallback per *Analytics-first* above**): prefer mureo native — `google_ads_performance_report` (campaign-level metrics — works in BYOD), `google_ads_health_check_all` (returns `[]` in BYOD; only meaningful with Live API), `google_ads_cost_increase_investigate` (per-campaign anomaly check), `google_ads_monitoring_zero_conversions` (per-campaign). If mureo's Google Ads tools are unavailable, fall back to the official `google-ads-official` MCP for the basic performance report; **skip the mureo-only anomaly-detection tools** (`google_ads_health_check_all`, `google_ads_cost_increase_investigate`, `google_ads_monitoring_zero_conversions`) and note to the user that those checks require mureo's native MCP (install or re-enable via `mureo setup claude-code`).
   - **Meta Ads** (built-in module — capabilities `detect_anomalies`, `diagnose_performance`; raw pulls run in **deep mode, or as the incremental fallback per *Analytics-first* above**): prefer mureo native `meta_ads_insights_report` — surfaces a `result_indicator` field per campaign (`actions:link_click` vs `actions:offsite_conversion.fb_pixel_lead`); use this to detect CV-definition mismatches across campaigns where one campaign's "results" are clicks while another's are real leads. If mureo's Meta Ads tools are unavailable, fall back to the official `meta-ads-official` hosted MCP for the basic insights / campaign listing surface; note that the `result_indicator` analysis is a mureo-specific value-add and will not be present in the official MCP's responses.
   - In mureo BYOD some tools return `[]` for unsupported features (auction insights, ad performance) — that's by design, not missing data; carry on with the rest of the diagnostics.
   - Present a unified health summary across all platforms, including any `analytics_not_available_for_<platform>` notices.

5. **Mode-specific checks** based on Operation Mode:
   - **ONBOARDING_LEARNING**: This mode means the *campaign* is in its learning period — NOT merely that mureo was recently set up. First confirm it still applies: check each platform's learning status and the accumulated conversion history. If the campaigns are actually mature (well past the learning window, with accumulated conversions — common when mureo is added to an already long-running account), do **not** blindly warn "prioritize data accumulation". Instead proceed with the full analysis and proactively offer to switch the Operation Mode to a steady-state mode (e.g. `EFFICIENCY_STABILIZE`). Only hold back on changes when the data confirms a genuine learning period is still underway.
   - **EFFICIENCY_STABILIZE**: Analyze CPA trends across all platforms. Flag if CPA increased >10% on any platform.
   - **TURNAROUND_RESCUE**: Identify zero-conversion campaigns and cost spikes across all platforms.
   - **SCALE_EXPANSION**: Check budget utilization across all platforms. Flag underspending campaigns.
   - **COMPETITOR_DEFENSE**: Run each platform's competitive insights where it publishes them — on Google Ads, auction insights and impression share — and flag impression share drops >5%. A platform that publishes no competitive data contributes nothing to this mode: report that gap rather than inferring competitor activity from its own delivery numbers.
   - **CREATIVE_TESTING**: Audit ad asset performance across all platforms. Flag underperforming creatives.
   - **LTV_QUALITY_FOCUS**: Review search term quality and audience alignment across all platforms.

   **Learning-state guard (all modes)**: An entity inside a platform learning state — a Meta ad set in its learning phase, a Google Ads bid strategy showing "Learning" after a strategy or target change — is **Watch at most, never Action needed on efficiency grounds**. Its cost per result is not steady-state, so do not read it as performance and do not propose edits that would reset the clock. See `../_mureo-pro-diagnosis/SKILL.md` → *Allocation & Learning-State Discipline*.

   **Delivery-state guard (all modes)**: An entity that was **not delivering for part of the period** — paused, disapproved, or budget-exhausted, read off the delivery status you fetched in step 3 (`effective_status` when the platform reports one, otherwise `status`) — is **Watch at most, never Action needed on efficiency grounds**. Its cost per result is not steady-state either: the spend and results cover only the hours it actually ran, so the report must state that its figures are **partial** rather than reading them as performance. And never recommend pausing an entity that is already not delivering — the pause has happened, what it needs is an explanation, not the same action again. **When an entity carries no `effective_status` at all, its delivery state is `unknown` — not `delivering`.** A stored `status` with no `effective_status` beside it is only the **configured** state, so it tells you the entity is *not paused*; it is never evidence that it served. Report such an entity's delivery state as unknown, take delivery evidence from spend / impressions in the period instead, and say so when you have none. Unknown does not by itself trip this guard — it removes the right to assert steady-state delivery, not the numbers. (Today's example: Amazon Ads, whose ads expose a single `state` — `ENABLED` / `PAUSED` / `ARCHIVED` — and therefore store no `effective_status`.)

6. **Organic search pulse** (if Search Console is available): Check top organic queries for the site. Identify any organic ranking drops that may need paid coverage, or organic gains where paid spend can be reduced.

7. **On-site behavior check** (if GA4 is available): Correlate ad platform metrics with on-site behavior — LP conversion rates, bounce rates, session quality. Flag discrepancies between ad platform and GA4 conversion data.

8. **Goal progress check**: For each Goal in STRATEGY.md, gather current metric values from all relevant platforms and data sources (ad platforms, GA4, Search Console) based on each Goal's declared platform/source. Present a Goal progress summary:
   ```
   Goal: CPA < 5,000 -- Platform A: 4,800 OK, Platform B: 6,200 OVER TARGET
   Goal: CV >= 100/month -- Platform A: 62, Platform B: 28, Total: 90 AT RISK
   Goal: Organic clicks +20% -- Search Console: +12% IN PROGRESS
   ```

9. **Evidence check**: Review `action_log` entries that have `observation_due` dates (in incremental mode these are exactly the `pending` subset step 0 returned) — compare each one against `server_now`'s date, never against another `action_log` timestamp:
   - For entries whose observation window has passed: collect current metrics for the same campaign and call **`mureo_outcome_evaluate`** with `before` = the entry's `metrics_at_action` and `after` = the current metrics. It returns a deterministic per-metric + overall **improved / regressed / inconclusive** verdict (applying the ±noise band and metric directions for you) — use it instead of eyeballing the numbers, then report with the confidence it implies (see `_mureo-learning` skill). This tool is pure/platform-agnostic, so it works for **any** platform including hosted connectors and plugins — for those, first **normalize the connector's metric names to the standard keys** (`cpa`, `conversions`, `ctr`, `cost`, …) so they can be scored (unknown names are treated as neutral/no-verdict).
     - **Then close the observation.** `mureo_outcome_evaluate` is **pure** — it records nothing — so you MUST append an evaluation record via `mureo_state_action_log_append`: `action` = a one-line summary of the verdict, `evaluation_of` = that source entry's index. Use the `index` field carried on the pending entry (deep mode: its position in the full log). This takes the entry **out of the pending set** so it is not re-evaluated tomorrow (without it, the observation stays open forever and the pending set grows without bound).
   - For entries still within their observation window: note them as "pending observation" and do NOT recommend further changes to the **same target on the same `platform`**. The action log records one canonical target: use `entity_type` + `entity_id` when present (an explicitly declared generic target), otherwise `ad_id`, otherwise `campaign_id`. Parent ids required only as API context (for example Google's `ad_group_id` beside an `ad_id`) are not targets. Only suppress the whole campaign when campaign is the finest identity recorded; a pending ad-group / ad-set / placement action must not block unrelated entities in that campaign. If no identity is present, report the pending observation but do not pretend it matches a current recommendation.
   - Entries **already closed** by a later `evaluation_of` (or `rollback_of`) are done — do not re-evaluate them. In **incremental** mode you will not even see them: the `pending` scope from step 0 already excludes closed entries.
   - `platform="plugin:<dist>:<provider>"` entries participate in this loop on equal footing with built-ins; they have no `metrics_at_action` baseline, so evaluate them **qualitatively/advisory** (see `_mureo-shared` → *Mutating plugin tools — structural strategy parity*).
   - Entries with `origin: "external"` (imported at step 2b) participate on equal footing too, with two differences. They have **no** `metrics_at_action` — mureo was not present when the change was made — so evaluate them **qualitatively**, never by scoring a delta against a baseline that does not exist. And their outcome must be phrased as what happened after a change **someone else made**: if it needs undoing, that is a manual action in the platform, because `rollback_plan_get` refuses every external entry. Their `occurred_at` is when the change happened; `timestamp` is only when mureo noticed it, so measure the window from the former.
   - Do NOT attribute metric movements to specific actions without checking sample sizes and observation windows.

10. **Report** — the mode decides the shape:

    **Partial report (both modes) — this goes FIRST, above the verdicts.** If any platform came back `"status": "auth_error"` in steps 3–4, **or was recorded `tools_absent_for_<platform>` there**, this report is **partial**: open it by saying so and name each affected platform with why it was not checked and the recovery — `auth_error` carries its `auth_cause` (`no_credentials` → configure the credential via `mureo configure` / `mureo auth setup`; `token_invalid` → re-authorize, the credential is present and dead), and `tools_absent` means the platform's tools were not in this session, so reconnect the connector host-side (or drop the platform from STATE.json if it is gone for good). Then **withhold every verdict, goal-progress line and recommendation that depends on that platform's numbers** — an unreadable platform has no performance to judge, and cross-platform advice (budget shifts, "scale here, cut there") derived only from the platforms that did answer is not the advice you would give with all of them. Report the platforms that did answer as usual, and make clear which is which. **Never estimate or extrapolate what you could not read.** A platform that was not checked gets **no KPI table, no target/actual columns, no attainment percentage and no pass/fail icon** — those say "measured against a target", which is exactly what did not happen, and a table shaped like the measured ones is what the reader acts on however honest the prose above it was. A one-line disclaimer does not license the table underneath it: name the platform, say it was not checked, and stop there.

    **Deep mode — full report.** Summarize findings as: **Healthy** (no action needed), **Watch** (minor issues to monitor), **Action needed** (requires immediate attention). For each issue, suggest specific actions aligned with the current Operation Mode. Do NOT recommend actions based on single-day fluctuations — at least 7 consecutive days of critical metrics (>30% off target) before suggesting rescue.

    **Incremental mode — diff-first.** Compare this run against the previous `reports.daily` summary and report ONLY the deltas:
    - **Verdict transitions** — any campaign/platform that moved between Healthy / Watch / Action needed since yesterday.
    - **New and resolved findings** — flags raised this run, and flags present yesterday but gone today (name what resolved, e.g. "CPA back under target on Brand").
    - **External changes** — what step 2b imported (`origin: "external"` entries, with what changed and when), plus the campaign- / ad-level status diffs from step 11's machinery. Also report step 2b's `blind_spots`: the platforms mureo could not check at all. A platform that was not checked is not a platform that was quiet.
    - **Evidence-check verdicts that came due** — the step-9 outcomes whose observation window closed this run.
    - **Goal progress** — only when a threshold was crossed or the trend flipped; otherwise omit it.
    Everything unchanged collapses to ONE line, e.g. `N campaigns unchanged since yesterday (Healthy)`. **Safety rule:** every current **Action needed** item is reported in full **even when unchanged** from yesterday — an unresolved problem must never go quiet. If the previous `reports.daily` summary is **missing** (first run, or it was never persisted), say so and **fall back** to the deep full report — there is no anchor to diff against.

    **Output discipline (both modes):** do **not** reproduce raw tool output tables in the report — give verdict lines and deltas only, and cite raw numbers only where they justify a flag (a threshold crossed, an anomaly). The reader wants the verdict, not the spreadsheet.

11. **Update STATE.json**: Update campaign snapshots, add notes for flagged issues, and log this daily check to the `action_log` with a summary of findings. Persist the ad-level statuses from step 3 as the campaign's `ads` array (`ad_id`, `name`, `status`, `effective_status`) — omitting `effective_status` for a platform that reports no serving status (Amazon Ads; see step 3) — on Desktop / Cowork pass `ads` to `mureo_state_upsert_campaign` (the server stamps each ad's `as_of`), on Code write the same array with an `as_of` of `server_now`; omit `ads` for campaigns whose ads you did not fetch. Then report **Ad-level status changes** against the `ads` the previous run stored — ads added or removed, plus **any change to a stored status field**: compare each ad's `status`, and its `effective_status` when the platform reports one, against the previous run's stored value for that *same* field, and report every difference. An ad **stopped delivering** when such a value moved away from the platform's active value — matched **case-insensitively**, so `ACTIVE`, `ENABLED` and `enabled` all count as active — which makes the limb fire on a platform that reports only one status field, in its own vocabulary, instead of falling through (see `../_mureo-shared/SKILL.md` → *Status vocabulary contract*). Report **Campaign-level status changes** the same way: each campaign's `status` against the `status` the previous run stored on its `CampaignSnapshot`, same case-insensitive comparison. A campaign- or ad-level change with no matching `action_log` entry was made outside mureo: surface it in step 10's findings (see *Mixed operation*) rather than absorbing it silently, and treat its spend as part of the performance you are explaining. Check step 2b's import first — an entry with `origin: "external"` on that same target IS the matching record, and reporting the diff a second time as unexplained would double-count one change. On the Code `Write` path, use the canonical STATE.json field names — `campaign_name` (NOT the tool-output `name`), `campaign_id`, and each platform's `account_id`; a snapshot/platform missing a required field is dropped by the reporting dashboard. On the Code `Write` path also **stamp the top-level `last_synced_at` to `server_now`** so the dashboard's "Synced N ago" freshness reflects this run (the `mureo_state_upsert_campaign` / `_platform_metrics_set` tools set it for you). See `../_mureo-shared/SKILL.md` → *STATE.json Schema*.

12. **Persist the report summary** (best-effort): Call `mureo_state_report_set` with `report="daily"` and a concise `summary` object so the read-only dashboard can render this report without re-running you. **Persist in incremental mode too** — step 10's diff-first report is computed against this stored summary, so skipping it blinds tomorrow's run. Follow this convention:
    - `generated_at`: ISO 8601 timestamp of this run — use `server_now`
    - `period`: the day reviewed, derived from `server_now` (e.g. `"2026-06-17"`)
    - `totals`: the account's headline figures, using the canonical metric vocabulary — `spend`, `conversions`, `cpa`, `ctr`, `clicks`, `impressions`. This is the block the dashboard renders **as figures**. **Raw numbers only**: `773957`, not `"¥773,957"`; `0.0466`, not `"4.66%"` — one of those keys carrying a string is refused, because it sits where the view reads a figure and renders as nothing. A key outside the vocabulary may ride along: it is stored, just not shown as a headline number. Omit `totals` entirely if this run gathered no account-level figures.
    - `kpis`: the per-platform split — the breakdown, not the headline row — **measured only**. Omit a platform that was not checked (`auth_error` / `tools_absent`) rather than persisting an estimate for it, and record the gap as a flag (`{code:"custom", severity:"action", label:"<platform> not checked: <cause>"}`) rather than as a sentence in `narrative`: tomorrow's incremental run diffs against this object, so a figure invented here becomes the baseline it trusts.
    - `flags`: a list of **structured** flags — each a small object `{code, severity, params}` so the dashboard renders a coarse, localizable chip with the numbers on drill-down:
        - `code`: a canonical vocabulary key — one of `goals_met`, `cpa_over_target`, `cpa_under_target`, `cv_below_target`, `cv_above_target`, `spend_spike`, `cpa_spike`, `invalid_traffic_suspected`, `zero_cv_adspots`, `budget_overspend`, `budget_drift`, `tracking_suspect`, `zero_conversions`, `supply_tools_unconfigured`, `anomaly_baseline_insufficient`, `pending_observations`, `search_console_no_property`, `ga4_not_configured`.
        - `severity`: one of `action`/`watch`/`info`/`positive` (omit to take the code's default — `info`/`positive` keep informational and good-news flags visually distinct from alarms).
        - `params`: an object holding the DETAIL (adspot ids, yen, ctr, dates). Keep detail in `params`, **NOT baked into the code** — write `{"code":"invalid_traffic_suspected","params":{"adspot":"4311492","spend":115740,"cv":0,"ctr":0.0466}}`, never a slug like `adspot_4311492_invalid_traffic_spike_115740yen`.
        - For a finding outside the vocabulary use `{code:"custom", severity, label}` where `label` is a string or a `{"ja":…,"en":…}` map. Unknown non-`custom` codes are rejected. (A legacy bare-string flag still works but renders without the drill-down — prefer the object form.)
        - Example: `[{"code":"invalid_traffic_suspected","params":{"adspot":"4311492","spend":115740,"cv":0,"ctr":0.0466}}, {"code":"budget_drift","params":{"live":170000,"configured":150000,"unlogged":true}}, {"code":"goals_met"}]`
    - `narrative`: the Healthy / Watch / Action-needed verdict and what you propose next, **at most 400 characters** — the tool refuses a longer one rather than truncating it (a sentence cut in half is worse than a long one). Do not restate the figures and do not list the findings here: numbers belong in `totals`, findings in `flags` — see `../_mureo-strategy/SKILL.md` → *Reports section* for the before/after of the paragraph this replaces.

    **Reflect the FINAL state, and persist this LAST.** Write this summary
    AFTER every STATE change and `action_log` entry you made this run (it is
    the last persistence step — keep it after any Operation Mode switch,
    campaign pause, budget edit, etc.). If you changed something, the
    `narrative` + `flags` must describe the POST-change state (e.g.
    "switched to `EFFICIENCY_STABILIZE`", not "recommend switching"; flag
    `{code:"custom", severity:"info", label:"Switched to EFFICIENCY_STABILIZE"}`, not a now-resolved recommendation). Stamp
    `generated_at` at the moment you write this — so the dashboard's
    "Latest report" is never an older snapshot that contradicts the action
    log or STRATEGY.md. (If you only *recommended* a change and did NOT make
    it, the recommendation wording is correct — and there is then no matching
    `action_log` entry, so nothing contradicts it.)

    This is best-effort: if `mureo_state_report_set` is unavailable (e.g. a pure file-mode host without the context MCP), skip it silently — the rest of this skill still works.

13. **Persist per-window per-platform rollups** (best-effort): so the reporting dashboard's KPI tiles AND its *Yesterday / Last 30 days* period toggle have data, write each platform's totals to `platforms[<platform>].periods[<window>]` using the canonical metric vocabulary (`spend`, `impressions`, `clicks`, `conversions`, `cpa`, `ctr`, the Meta `result_indicator` when present). Write **every window whose numbers you already gathered this run** — typically `YESTERDAY` (the prior-day totals) and `LAST_30_DAYS` (the trailing-30-day totals your step-4 health checks / step-8 goal progress already pulled). The period toggle only appears once **two** windows have data, so populating `LAST_30_DAYS` here — from numbers you already hold — is what makes the toggle show even when a separate `sync-state` run has not happened. On Code use `Write`; on Desktop / Cowork call `mureo_state_platform_metrics_set` (pass `platform`, `account_id`, and `periods={"YESTERDAY": {…}, "LAST_30_DAYS": {…}}` — include only the windows you actually have) — it merges per window key, so this never disturbs a window a prior `sync-state` wrote. **Honest scope / cost:** only write a window whose numbers you actually pulled; do **not** fire an extra API call solely to populate a window, and write **nothing at all** for a platform recorded `auth_error` or `tools_absent_for_<platform>` this run — it has no numbers, and an empty tile is honest where an extrapolated one is not. In incremental mode you may only hold the window your analytics run covered — write just that one, and never mislabel a wider window as `YESTERDAY`. Best-effort: if `mureo_state_platform_metrics_set` is unavailable, skip silently. **For a platform you could NOT collect, record why** — call `mureo_state_platform_not_collected_set` (`platform`, `account_id`, `reason`: the `auth_error` / `tools_absent_for_<platform>` finding in one actionable sentence). Writing no numbers is right, but silence alone leaves the card indistinguishable from an account that genuinely spent nothing, which is how a stale card goes unnoticed for days. Symmetrically, **for every platform you DID collect, clear any note** by calling the same tool with `reason` omitted — nothing else retires it, and a note that outlives its failure is worse than none.
