Skip to content
Back to skills

Daily Check

ASecurity

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...

  • 47 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 24, 2026
ai-agentsrustgoshellbashrailstestingapisecurityperformance

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add logly/mureo --skill daily-check --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Daily Check?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Daily Check
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/logly-daily-check/badge)](https://www.skillsdirectory.com/skills/logly-daily-check)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.21.3
---

# 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) — and when that is the reason, say so in the report **naming the file** (`path`): a stale-looking verdict must be checkable against the file the operator expected. 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". The same response names **which file** you are reading — `path` (the STATE.json consulted) and `workspace_id` (the runtime's workspace identifier) — and may carry `notices` from the runtime (for example: launched outside a client workspace while clients are registered). Keep all three for the report header (step 10). **If `notices` is non-empty, stop here**: print the header line and every notice verbatim (step 10's header, nothing else), tell the operator to relaunch from the client's workspace directory (or point the mureo MCP server's cwd at it) and end the run — do not run steps 1–15. A runtime raises a notice only when this session is not on the workspace it should be on, and a diagnosis of the wrong STATE.json costs a full session and misleads tomorrow's run. Like `server_now`, they are response fields only: never write `path`, `workspace_id` or `notices` into STATE.json.

   **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.
   - **A Meta token about to expire is a finding BEFORE it is an outage.** A system-user token issued with the 60-day expiry dies on a date, and mureo records the one Meta reported as `token_expires_at`; the configure UI's Meta status card shows the days remaining and marks it once fewer than **14 days** are left. A token Meta reports as permanent carries `token_never_expires` instead — there is no countdown and nothing to renew, so it is never this finding. When the operator reports that countdown — or the Meta credential is otherwise known to be inside it — say so in this run: **Watch** while days remain, **Action needed** once the token has expired (which is also what `auth_cause: token_invalid` above means). The fix is not something that can wait for the failure: generate a fresh system-user token in Business settings and paste it into `mureo configure`, or store there the app ID and app secret of the app that issued the token — the pair is what lets mureo read the expiry at all, and what lets it extend a 60-day token before it dies. Never report a Meta platform as Healthy on the strength of numbers pulled with a token that expires this week — the figures are real, the coverage is about to stop.
   - 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`.)

   **Grain guard (all modes)**: A verdict about **one placement** — exclude it, restart it, move its bid — may cite only rows that exist **at the grain the verdict is about**. A source may expose placement rows only at a grain coarser than the one the action applies to, or may not expose the finer-grained row at all; a row aggregated above the verdict's grain says what the larger entity did, never which sub-entity did it, so it is evidence neither for nor against any one of them. When the finer row is absent, say the question is **undecidable at this grain** and name the row that would settle it — that is a complete answer, and one the operator can act on, unlike a confident verdict resting on a total. As with the two guards above, this removes the right to assert, not the numbers: report the aggregate as the aggregate and say what it does not decide. **Do not raise it at `action` severity** — an undecidable question is not a risk, and ranking it as the day's top finding spends the operator's attention on a row nobody has looked at; `watch` when the aggregate itself is moving the wrong way, `info` when it is only the missing row that is notable. Same ethic as `analysis_exclusion_impact_preview`, which reports `unknown` coverage rather than "0% impact" precisely so a confident answer from data that cannot support it is not read as approval — a different moment, the same refusal.

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:
   - **`auto_evaluations` are already closed — report them, do not redo them.** `mureo_state_get` (step 0) scores every past-due observation its own document decides — a campaign-level action with a numeric `metrics_at_action`, on a platform whose campaign metrics were collected on or after the due date — and appends the `evaluation_of` record itself. Each item is `{index, evaluation_index, overall, summary}`; `overall` is the same deterministic **improved / regressed / inconclusive** verdict `mureo_outcome_evaluate` returns, so report every one of them in step 10's *Evidence-check verdicts that came due* with the confidence you would give your own scoring. Never re-evaluate one or append a second evaluation record for it: it has left the pending set, and a duplicate double-counts the outcome.
   - **`auto_evaluation_skipped` is your work list** — `{index, reason}` per entry mureo would NOT decide, and each is still yours to evaluate and close below: `external_origin` / `no_baseline` (no baseline exists — review qualitatively, then close), `no_campaign_id` / `platform_not_in_state` / `campaign_not_in_state` / `no_current_metrics` (mureo holds no current figures — collect them as in step 4, score, then close), `current_metrics_undated` / `current_metrics_predate_window` / `period_mismatch` (the stored figures are not from AFTER the window, or they cover a different `period` than the baseline — either way they would score the wrong days; refresh that platform on the SAME window as the baseline, and if you cannot this run, say the outcome is not yet measurable and leave the observation open rather than scoring mismatched numbers), `no_comparable_metric` (normalize the metric names to `cpa` / `conversions` / `ctr` / `cost` and score, or review qualitatively), `unparseable_due_date` (treat it as due now and report the malformed value), `write_denied: <text>` (this host forbids `action_log` appends — evaluate and report, but say the observation stays open). Hosts that pass `auto_evaluate: false`, and older mureo versions, return neither key: then every past-due entry is yours.
   - 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 for anything mureo did not close for you, 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:

    **Header (both modes) — the very first line.** `Workspace: /Users/ops/clients/acme/STATE.json (workspace_id: agency:acme)` — substitute the actual `path` and `workspace_id` from step 0's response; this is the STATE.json this run read and will write. If `notices` is non-empty, print each notice on its own line right under it, verbatim. This is what lets an operator whose card ran in one workspace and whose terminal opened another see the mismatch at a glance instead of a wrong 'N days since the last check' — and when `notices` are present this header is the whole report, because step 0 already ended the run.

    **Partial report (both modes) — this goes right after the header, 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 (name `path` — the file that has no summary) 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. **Give every entry a display line**: `display_title` (at most 40 characters) and `display_summary` (at most 120) — what this action WAS, in the operator's words, as plain text with no markdown (`**bold**` reaches a person as asterisks). The dashboard row shows those two and stops there; the full `summary` is drill-down only, so keep writing it as fully as the next agent needs and let the display line be the short one. Over either bound the append is refused, never truncated. 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"}]`
        - **Read the narrative back before you write this list.** The narrative is written first and is where a reservation naturally lands; a flag is a chip with no room for one. So re-read what you just wrote, and for every reservation in it the matching flag must either **carry the same reservation** or **not be raised at that severity**. Put the reservation in `params` — e.g. `{"code":"zero_cv_adspots","severity":"watch","params":{"grain":"aggregate above the entity judged","undecidable":true}}` — and drop the severity a step: `action` is for a finding that is defensible **without** the caveat. A hedge in `narrative` beside an assertion in `flags` passes every validator and still tells the operator something you did not conclude.
    - `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. **Give every window you write a `period_end`** — the last calendar date those figures cover, `YYYY-MM-DD`, in the **ad account's own timezone** (so a `YESTERDAY` window pulled for an account on JST names the JST yesterday, whatever the clock on this machine says). You are the only one who can: the server stamps `fetched_at` because it knows when the write happened, but it does not know the account's timezone and will not guess a date that could be off by one. Each window states its own — the windows end on different days and one date copied across them mislabels the rest. Without it the card is judged on when it was last written, which is how a run that wrote nothing leaves "Updated 14 hours ago" over figures from two days earlier. **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. **But a window you did not write is never silent in the run output.** This step is conditional by design, and a run that persisted the report (step 12) and the display (step 15) and said so reads as a complete run whether or not the cards moved. So name what you persisted and what you did not, per platform, the way step 3 names a platform it skipped and step 4 names `analytics_not_available_for_<platform>`: report `windows_not_persisted_for_<platform>` with the windows and the reason (`no figures gathered for that window this run`, `analytics run covered a wider window`, `auth_error`, `tools_absent_for_<platform>`, `mureo_state_platform_metrics_set` unavailable). A platform whose windows all landed says so in one line and needs nothing more. **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). **If what failed was resolving the account itself** — no accessible ad account, no customer id configured — pass `account_id` as `""`: that is how mureo spells *unknown*, it is the one write tool that accepts it, and an unknown id never overwrites an id the entry already holds. **Never invent a placeholder** such as `"unknown"` or `"none"` — the same placeholder on two failed platforms reads as one ad account held under two keys, and the dashboard then withholds the client total rather than showing an inflated one. 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.

14. **Persist the day-grain history** (best-effort, **zero extra platform API calls**): step 4's delivery-collapse check already pulled a day-grain delivery report for the platforms it checked that way, and those rows are discarded the moment the check returns — so `platforms[<platform>].periods` can say what yesterday was and nothing can say whether it beat the day before. Fold the rows **you already hold** into `platforms[<platform>].daily`, a map keyed by calendar date (`YYYY-MM-DD`) whose values are totals-shaped rollups in the same canonical metric vocabulary (`spend`, `impressions`, `clicks`, `conversions`, `cpa`, `ctr`): sum that platform's per-campaign rows per date, one bucket per day. On Desktop / Cowork call `mureo_state_platform_daily_set` (pass `platform`, `account_id`, and `days={"2026-08-20": {…}, "2026-08-19": {…}}`); on Code use `Write`. It merges per date key, so a day already stored survives this write and re-writing a day just replaces that day. **Never fire a platform call to fill this in** — this step writes what step 4 fetched and nothing else; a platform whose collapse check ran server-side (`mureo_analytics_run`) returns findings, not rows, so you hold no daily rows for it and write none. **Write only complete PAST days** — today is still being spent into, and a part-spent day stored as a whole one is a false low that nothing ever corrects; the tool refuses today and any later date. **Omit a day you have no figures for — never write zeros for it**: a zero-filled day is indistinguishable from an account that stopped spending, and both the trend line and the collapse detector's baseline read it as one. Nothing needs backfilling: the history accumulates one run at a time, and mureo keeps the most recent 35 days in STATE.json and archives the days past that under `history/daily/<YYYY-MM>.json` rather than dropping them. Best-effort: if `mureo_state_platform_daily_set` is unavailable, skip silently.

15. **Persist the display contract** (best-effort): Call `mureo_state_display_set` with what the DASHBOARD shows. This is a different audience from the summary above — STATE.json is written for the next agent, and this small bounded section is the only thing the operator's screen reads. Write it in the SAME pass as the report, from the SAME figures, and **reach no new verdict here**: every value below is one you already decided, and this step only renders it.
    - `source`: your own skill name — REQUIRED whenever you write any section, at most 24 characters. The contract is replaced wholesale by whoever writes it last, so this is what lets the card say whose answer it is showing. `generated_at` is stamped by the server — do not compute it.
    - `nav_message`: the ONE line the operator should act on today — the single thing today needs, e.g. `"Brand Search stopped delivering 2 days ago — check the account in the UI"`. One line, not a summary of the report.
    - `highlights`: at most 3 chips, and the `tone` must match the verdict you already gave — `good` for a target met or a clear win, `watch` for something to keep an eye on, `bad` for something deteriorating. Choose the three that matter; a fourth is refused and nothing is dropped for you. Map a finding's severity to a chip tone: action → bad / watch → watch / positive → good. info does NOT become a highlight — a neutral note would spend one of the 3 chips an action or a win needed, and it is still in the report for whoever wants it.
    - `breakdown.campaigns` / `.adgroups`: one row per entity you judged, carrying figures you ALREADY hold — `spend`, `mcpa` (the measured CPA) and `target_cpa` — plus `state` from the closed set (`target_met` / `improving` / `watch` / `worsening` / `no_data`) and a `note` of at most 40 characters. Use `campaigns` for the campaigns you verdicted in step 10 and `adgroups` for any ad set / ad group you judged; the `state` is the verdict you already reached, never a fresh one. **Omit a figure you do not have**: `0` states a perfect CPA rather than the absence of one, and `no_data` is the state for an entity with too little delivery to judge.
    - `proposals`: the recommendations you did NOT carry out, one entry each — `title` (what to do) plus `body` (why, in one line) plus `date` = `server_now`'s date as `YYYY-MM-DD`. Something you DID apply this run is the same entry with `status: "done"`. **Record each of these as a decision too** — `mureo_decision_record` with `status: "proposed"`, your reasoning in `rationale` and the figures you judged it on in `metrics`. This section is the screen and is replaced whole by the next skill that writes it; the decision record is the one that survives, and it is what stops the next session re-proposing something the operator already turned down.
    - `stated_values`: label + figure chips ONLY (`{"label": "…", "value": …}`) — the figures your verdicts rested on: `{"label": "CVR", "value": 0.021}`, `{"label": "goals met", "value": "3 of 7"}`. **No prose notes here.** A sentence lands in a numeric column and the tool refuses it; anything that needs a sentence stays in the report's `narrative` above, exactly where it goes today.

    **A verdict hedged in the report stays hedged here.** This step renders the verdicts you already reached — including the reservations attached to them. If the `narrative` says you cannot tell, `nav_message` and `highlights` may not say you can: a finding must not read as certain merely because it moved to a shorter field. A chip holds 60 characters and the nav line 80, so there is no room for the reservation, and dropping it is exactly what turns "we cannot attribute this" into "this is today's top risk". **The finding loses the slot, not the reservation** — give the line to something you can state plainly, and leave the hedged finding in the report, where it is stated in full. Nothing is lost: `narrative` and `flags` are both on the drill-down.

    The bounds, verbatim from the tool: The dashboard reads THIS section and nothing else — keep your reasoning where it already goes. Every bound below refuses the write rather than truncating it, because a sentence cut in half is worse than a long one. nav_message: one line, at most 80 characters. highlights: at most 3 items of {tone, text}, tone one of good/watch/bad, text at most 60 characters. proposals: {title, body, status, date}, title at most 30 and body at most 80 characters, status one of proposed/done. breakdown.campaigns / breakdown.adgroups: rows of {name, spend, mcpa, target_cpa, state, note} — the three figures are raw numbers, state is one of target_met/improving/watch/worsening/no_data, note at most 40 characters. stated_values: {label, value}, label at most 24 characters and value a raw number or a string of at most 12 characters — a sentence there is refused, because it lands in a numeric column. Do NOT write the KPI funnel or the daily chart: mureo computes both from the stored totals.

    **Over a bound the write is REFUSED — shorten and rewrite, do not re-send the same sentence trimmed.** Lead with the point and drop the connectives; a noun phrase is fine (`CPA 12% over target`, not `The CPA is currently running about 12% above the target we agreed`). Trimming one character at a time and calling again spends the run's context on a bound one rewrite would have met.

    **One writer per run — and you may not be the first today.** Within your own run the contract has ONE writer: `mureo_state_display_set` REPLACES the whole section rather than merging into it, so compose every section you want on screen before calling, and never call twice. Across runs on the same day that same replacement is what the rule below is about — the morning's screen is what you are overwriting.

    **You may not be the first writer today.** ``display`` is REPLACED WHOLE and the last writer wins — there is no merge. Before you write it, read the current one (``mureo_state_get``). Of what another skill wrote TODAY, carry exactly one thing into your own write: its ``proposals`` that are still live — not yet done, and not contradicted by what you just found. Everything else you write from your own run alone, because a screen assembled from two runs shows a moment that never happened. And carry over NOTHING ELSE: never copy another skill's ``nav_message``, ``highlights``, ``breakdown`` or ``stated_values``, which would put its judgement under your name when you cannot vouch for it. Name yourself in ``source`` so the screen says whose answer it is.

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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.