# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.9.5] - 2026-05-21

### Added — Web extensions: third-party tabs and API routes for `mureo configure`

A new entry-point group `mureo.web_extensions` lets a plugin register additional tabs and API routes inside the `mureo configure` wizard without each surface having to know about the plugin. The mechanism mirrors the existing `mureo.providers` / `mureo.runtime_context_factory` entry-point patterns: discovery iterates the group exactly once at startup, isolates per-plugin faults (`WebExtensionWarning`), and exposes survivors as frozen `WebExtensionEntry` records consumed by `mureo.web.handlers`.

- **`mureo.web.extensions`** — public surface: `WebExtension` Protocol (`name`, `display_name`, `routes()`, `view()`), frozen dataclasses `RouteContribution(method, subpath, handler)`, `ViewContribution(html_fragment, scripts, styles)`, `StaticAsset(filename, content_type, body)`, plus `discover_web_extensions()` / `reset_web_extensions()` and the regex constants (`NAME_PATTERN`, `SUBPATH_PATTERN`, `FILENAME_PATTERN`) shared with the dispatch layer.
- **HTTP surface** in `mureo.web.handlers`:
  - `GET /api/extensions` — index for the front-end renderer (one entry per extension; `view` is `null` for headless / route-only plugins).
  - `GET /api/ext/<name>/<subpath>` — extension GET route; payload is the flattened query string (first-value-wins).
  - `POST /api/ext/<name>/<subpath>` — extension POST route, gated by the existing Host + body-cap + CSRF pipeline (the plugin author inherits CSRF protection for free).
  - `GET /static/ext/<name>/<filename>` — extension-shipped static asset served from in-memory bytes with the same Content-Security-Policy + X-Frame-Options + Cache-Control header stack as the bundled static files.
- **Front-end** (`mureo/_data/web/extensions.js`): the configure UI fetches `/api/extensions` once when the dashboard opens, renders one nav tab per extension, and lazy-loads each extension's `html_fragment` / scripts / styles on first tab activation. Operators who never visit a given tab pay zero added page weight.
- **Plugin author guide**: `docs/plugin-authoring.md` §13 documents the contract end-to-end (entry-point setup, sample `WebExtension`, URL surface, CSP / CSRF / fault-isolation model, lazy-load behaviour, debugging recipe).
- **Security**: subpaths and filenames are regex-validated at both registration and dispatch so `..`, double-slash, trailing slash, `?`, `#`, and directory separators cannot smuggle the dispatcher outside `/api/ext/<name>/` or `/static/ext/<name>/`. Static asset bodies stay in memory; the dispatcher never reads from disk so filesystem traversal is impossible by construction. `html_fragment` is rejected at registration if it contains `<script>`, `<style>`, `on*=` event handlers, or `javascript:` URLs — the CSP (`script-src 'self'; style-src 'self'`) is the runtime enforcement, the regex is the explicit author-feedback signal. Handler exceptions are caught by the dispatcher and surfaced as a generic `{"error": "extension_handler_error"}` 500 envelope; exception details are logged server-side only (they may carry secrets the handler touched).

Backward compatibility: when no third-party `mureo.web_extensions` entry points are installed, `discover_web_extensions()` returns an empty tuple, `/api/extensions` returns `[]`, the renderer creates zero DOM nodes, and the configure UI is byte-identical to v0.9.4.

## [0.9.4] - 2026-05-21

### Added — Extension Protocols and `mureo learn` CLI (#125)

A new public surface under `mureo.core` lets alternate backends and tests inject pluggable persistence without forking call sites. The shape mirrors the existing `mureo.core.providers` and `mureo.core.skills` extension patterns. Every default reproduces today's file-backed behaviour, so existing users see no change.

- **`mureo.core.SecretStore`** — `Protocol` for credential round-trip (load / save / delete). Default `FilesystemSecretStore` reads and writes `~/.mureo/credentials.json` byte-for-byte equivalent to the previous flow (atomic write, `0o600` via `mureo.fsutil.secure_fchmod`, `ensure_ascii=False`).
- **`mureo.core.StateStore`** — `Protocol` for `STATE.json` / `STRATEGY.md` / action_log persistence. Default `FilesystemStateStore` composes the existing helpers in `mureo.context.state` / `mureo.context.strategy`.
- **`mureo.core.KnowledgeStore`** — two-tier `Protocol` for `/learn` insights (operator + workspace). Default `FilesystemKnowledgeStore` writes to today's `~/.claude/skills/_mureo-pro-diagnosis/SKILL.md` location with the same frontmatter scaffold.
- **`mureo.core.ThrottleStore`** — `Protocol` for per-key API rate limiting. Default `ProcessLocalThrottleStore` wraps `mureo.throttle.Throttler`; `register(key, config)` pre-installs custom buckets matching the MCP server's `_PLUGIN_TOOL_THROTTLERS` pattern.
- **`mureo.core.RuntimeContext`** — frozen dataclass aggregating the four stores plus a `workspace_id`. `DEFAULT_WORKSPACE_ID = "default"` is the canonical single-workspace sentinel.
- **`mureo.core.default_runtime_context()`** — factory wiring the four file-backed defaults.
- **`mureo.core.get_runtime_context()`** — process-cached resolver that discovers a single zero-arg factory under the `mureo.runtime_context_factory` entry-point group; raises `RuntimeContextFactoryError` on multiple registrations or a returning-non-`RuntimeContext` factory.

### Added — `mureo learn add` CLI

`mureo learn add <text> [--scope {operator,workspace}]` persists `/learn` insights through `RuntimeContext.knowledge_store` rather than writing files directly. Default scope `operator` writes the cross-workspace tier (today's pro-diagnosis location); `--scope workspace` writes a workspace-scoped tier if one is configured. The `/learn` skill (`skills/learn/SKILL.md`) now invokes the CLI instead of carrying its own copy of the file scaffold.

### Changed — Consumers routed through the new Protocols

These refactors are call-site changes only; all on-disk artefacts and CLI behaviour are byte-equivalent in the default file-backed runtime.

- `mureo.auth.load_google_ads_credentials` / `load_meta_ads_credentials` read through `SecretStore` (`get_runtime_context().secret_store` when `path` is not passed; one-shot `FilesystemSecretStore(path=…)` when it is).
- MCP handlers `mureo_strategy_*`, `mureo_state_*`, `rollback_*`, `analysis_anomalies_check` resolve their `path` / `state_file` argument against `state_store.workspace` rather than raw CWD. Error messages and traversal-refusal semantics are preserved; symlink refusal in the analysis handler is unchanged.
- MCP plugin dispatch acquires its throttle slot via `RuntimeContext.throttle_store`. The default `ProcessLocalThrottleStore` is seeded with the existing per-tool `Throttler` instances (`_PLUGIN_TOOL_THROTTLERS`) on first call; alternate backends receive `acquire(name)` and own per-key fallback semantics.
- `mureo.cli.rollback_cmd` `--state-file` default is now resolved through `RuntimeContext` (rather than the literal `Path("STATE.json")`).
- `mureo.byod.runtime.byod_data_dir()` adds a middle-priority resolution path: when a non-default `RuntimeContext` exposes a filesystem `workspace`, BYOD data lives at `<workspace>/byod/`. `MUREO_BYOD_DIR` env var and the legacy `~/.mureo/byod/` fallback are unchanged.

## [0.9.3] - 2026-05-19

### Fixed — Windows compatibility (#122)
- mureo crashed on Windows: `os.fchmod` is Unix-only (`AttributeError`) in **every** credential/config write path (credentials.json save, provider-config write, settings rewrite, OAuth token store, plugin audit). New `mureo/fsutil.py` (`secure_fchmod` / `secure_chmod`) applies owner-only `0o600` on POSIX (byte-identical — no Linux/macOS change) and a best-effort, never-raising no-op on Windows. All 6 call sites migrated. On NTFS, file confidentiality relies on the `%USERPROFILE%` profile ACL (documented best-effort, not a silent regression).
- The interactive setup menu (`simple_term_menu`) imports Unix-only `termios` and raises `NotImplementedError` (not `ImportError`) on Windows, so the plain number-input fallback was unreachable. Both fallbacks now `except (ImportError, NotImplementedError)` — also degrades gracefully in non-terminal environments (CI, pipes, PyCharm console) instead of crashing.
- `mureo configure` resolved the wrong Claude Desktop config location on Windows. `host_paths` now returns `%APPDATA%\Claude\claude_desktop_config.json` on Windows (macOS unchanged; Linux keeps the Code-style fallback — Claude Desktop has no Linux build).

### CI
- Added a `windows-latest` CI job. mureo has no Windows dev machines, so this is the real-Windows verification (the fixes were otherwise only simulated on Linux) and an automatic regression guard. POSIX-only test assertions (file-mode, absolute paths, the macOS-only `install-desktop` `.sh` wrapper, a Windows socket-close timing difference) were made platform-aware.

### Known limitations (out of scope; not crashes)
- `mureo install-desktop` (CLI) is still macOS-only by explicit design — a Windows launcher is a separate feature; it errors gracefully off macOS.
- Real-desktop UX not exercisable by headless CI (browser auto-open, the native file/folder picker dialog) is not yet end-to-end verified on Windows.

## [0.9.2] - 2026-05-18

### Fixed — `mureo configure` no longer misroutes Claude Desktop users on the Meta connector finalize (#118)
- A Claude **Desktop** user who had connected the Meta hosted MCP saw the misleading *"not connected yet — finish the Meta login"* when clicking *finalize*. The in-memory `session.host` could reset to the `claude-code` default (configure process restart; `/api/host` was sent only on a radio change, fire-and-forget, errors swallowed), so `confirm_hosted_provider` ran the Claude **Code** `claude mcp list` verification path for a Desktop user; with no Claude Code CLI present, a bare `False` became an accusatory dead-end. The official Meta MCP itself was always usable — only the *switch-native-off* step was wrongly blocked (tool ambiguity, never a strand).
  - **Client-authoritative host:** `/api/providers/confirm` and `/api/providers/native-toggle` resolve `host` from the request payload (validated against the supported hosts, written back to self-heal a stale session); the finalize button now sends the UI's known host.
  - **Host-sync hardening:** the wizard persists the explicit host choice to `localStorage` and prefers it over the server-echoed value; the `/api/host` sync retries once and surfaces a toast instead of silently swallowing a failure; the host is re-asserted on host-step entry and on load.
  - **Tri-state connectivity:** `hosted_provider_connectivity` now distinguishes `connected` / `not_connected` / `unknown`. `unknown` (no Claude Code CLI, `claude mcp list` timeout, or non-zero exit) is **not** reported as "not connected". `confirm_hosted_provider` gained an explicit-affirmation path: on Desktop, or when connectivity is `unknown`, the user can confirm "I've verified it" to apply the native↔official switch — preserving the no-strand guarantee (the switch still requires a positive signal: an auto-verified `connected` or a deliberate user affirmation). Existing no-strand guards are unchanged (`is_hosted_provider_connected` kept as a `== "connected"` wrapper). New EN/JA strings + an affirm button; reworded the Desktop "manual" copy so it no longer dead-ends.

## [0.9.1] - 2026-05-18

### Added — mureo safety layer for third-party plugin tools (#114, #116)
- Entry-point plugin providers (`MCPToolProvider`) dispatch straight to the plugin and previously bypassed mureo's per-handler audit, throttle, and strategy plumbing. mureo now wraps the plugin dispatch path with its own safety layer — **opt-in & purely additive via standard MCP `Tool` metadata, no plugin-side changes required**:
  - **Phase 1 — audit / throttle / fault-isolation.** Every plugin tool call is appended (secret-masked, over-long values truncated) to a dedicated `~/.mureo/plugin_audit.jsonl` (created `0600`), gated by a conservative shared token bucket, and a plugin exception is recorded then re-raised unchanged (mureo never crashes on, nor silently swallows, a plugin error). Auditing never raises.
  - **Phase 2 — classify + promote mutating calls.** Safety semantics are derived from standard `Tool.annotations.readOnlyHint` (undeclared ⇒ *mutating*, conservative default) and optional `_meta["mureo"]` (`reversal`, `throttle`). A successful **mutating** call is promoted into `STATE.json`'s `action_log` (`platform="plugin:<dist>"`) — only when a `STATE.json` already exists in the cwd (mureo never creates one for a plugin) — so it is visible to the agent / strategy review / `rollback_plan_get` like a built-in op.
  - **Phase 3 — provider-aware skill guidance.** Workflow skills now enumerate installed plugin platforms best-effort and treat their findings as advisory, skipping mureo-only value-adds that do not exist for an unknown platform.
  - **Phase 4 — structural strategy parity.** A mutating plugin call now also receives an `observation_due` window (conservative 14-day default, overridable via `_meta["mureo"]["observation_days"]`) so daily-check's evidence loop reviews its outcome like a built-in write. **Honest scope:** confirm + `STRATEGY.md` gating are skill-mediated and audit / `action_log` / observation / rollback-intent are mechanical — the *same channel built-ins use* — but mureo's platform-specific analytics (anomaly detection, `result_indicator` CV-mismatch, RSA-asset audit, rule-based scoring) and *executable* auto-rollback for arbitrary operations are **not** generically possible and are not claimed. Documented in `docs/plugin-authoring.md` and `docs/ABI-stability.md`.

### Fixed — `mureo configure` frees the terminal on finish / Ctrl+C (#111)
- The `configure` local web server now releases the terminal on completion and on Ctrl+C via an explicit stop event plus signal handling, so the shell prompt returns instead of hanging.

### Docs
- Getting-started now leads with `mureo configure` as the easy path (EN/JA) and adds a "Before you start" section (terminal + Python/pip) for non-engineers (#109, #110).
- Clarified that BYOD/Demo are mureo-native only and not driven by the official MCPs (#112).

### Chore
- Removed `.mailmap` (added then reverted — ineffective for folding the Claude co-author identity; #107, #108).

## [0.9.0] - 2026-05-16

### Fixed — `/learn` slash command restored (regression from #77)
- Phase 3 plugin packaging (#77) migrated every `.claude/commands/*.md` slash command into an operational skill under `skills/` + the bundled `mureo/_data/skills/`, but **dropped `learn` entirely** (deleted `.claude/commands/learn.md`, never created a `learn` skill). `/learn` became uninvocable while every other workflow command kept working, even though README/docs still document it. Restored as an operational skill (`skills/learn/` + byte-identical bundled copy): `name: learn` (no `_` prefix → appears in the picker), saves insights to `../_mureo-pro-diagnosis/SKILL.md` (scaffolding that canonical-only knowledge base on first use), approval-required and append-only, never Claude memory or secrets/PII.

### Changed — `mureo configure` visual refresh + official mureo logo
- The configure Web UI got a cohesive design-system pass (refined spacing/type/color tokens, light **and** dark via `prefers-color-scheme`, crafted cards/buttons/focus states, system fonts only — strict CSP, no web fonts/CDN/build). The header now shows the official mureo wordmark (bundled `logo.png` / `logo-dark.png`, scheme-swapped). CSS-only; every `data-*` / `data-i18n` hook and EN/JA parity preserved.

### Added — Google Ads OAuth-scope guidance in the auth step
- The Web UI Google Ads auth step and `docs/authentication.md` now explain that a reused refresh token **must** carry the Google Ads scope `https://www.googleapis.com/auth/adwords` or API calls fail with `ACCESS_TOKEN_SCOPE_INSUFFICIENT`, with a link to the official Google scope reference. mureo's own OAuth already requests it; the note prevents the failure when users supply a hand-minted token.

### Fixed — Meta hosted MCP on Claude Code goes through the Claude.ai connector (supersedes the earlier "/mcp register" Unreleased note)
- A prior Unreleased change had `mureo configure` / `mureo providers add` / the wizard **register** `meta-ads-official` into `~/.claude.json` on Claude Code and tell the user to finish OAuth via `/mcp` → Authenticate. Real-environment verification proved this **cannot work**: Meta's hosted MCP (`https://mcp.facebook.com/ads`) does **not** support OAuth Dynamic Client Registration, so Claude Code's `/mcp` OAuth fails with `SDK auth failed: The provided redirect_uris are not registered for this client`. Registering it locally only creates an unauthenticatable user-scope server. Corrected behavior: on Claude Code, mureo now **does not register Meta locally at all** — `install_provider` / `mureo providers add` return `manual_required` (no `~/.claude.json` write, no subprocess) and the UI/CLI point the user to add Meta as a **Claude.ai account connector** (claude.ai → Settings → Connectors → Add custom connector → `https://mcp.facebook.com/ads`; Anthropic brokers the Meta Business sign-in there, requires a paid plan, then works account-wide in Claude Code *and* Claude Desktop and surfaces as `mcp__claude_ai_MetaAds__*`). mureo-native Meta is still **not** auto-disabled (nothing registered/verified — native steps aside only via `mureo providers confirm` once the connector is verified Connected; no-strand preserved). Claude **Desktop** is unchanged (`manual_required`, Settings → Connectors). This re-aligns with the "no dead config entry / Connectors" behavior described in the bullets below; the intervening "/mcp register on Code" wording is withdrawn.

### Docs — `mureo configure` is now the documented front door; `auth setup --web` removed
- `mureo auth setup --web` was removed (its browser credential flow is now part of the unified `mureo configure` UI). README and docs (`cli.md`, `authentication.md`, `getting-started.md`/`.ja.md`, `byod.md`/`.ja.md`, `architecture.md`) updated: every `auth setup --web` reference now points to `mureo configure` (or terminal `mureo auth setup`). README gained a top-of-"Choose your setup" quickstart — `pip install mureo` + `mureo configure` — enumerating what the browser UI does (host pick, basic setup, OAuth/credentials, official MCP providers, native↔official toggle, Demo/BYOD).

### Fixed — official/native precedence when mureo MCP is configured after an official provider
- `MUREO_DISABLE_<PLATFORM>` (which makes the mureo-native MCP step aside so an official provider is the single source for a platform) was only auto-set by `mureo providers add` when a `mcpServers.mureo` block already existed. A user who registered the official provider **first** and configured the mureo MCP **later** ended up with native + official both active and no deterministic precedence (tool ambiguity). `install_mureo_mcp` (the path both basic-setup and the dashboard use) now backfills the disable env, after the mureo block is written, for already-registered **pipx/npm** official providers (google-ads-official, ga4-official) detected by a pure host-config registry read. Meta (hosted) is intentionally **out of backfill scope** — detecting it needs a network `claude mcp list` probe that must not run on the basic-setup path; Meta native↔official is the explicit `mureo providers confirm` / dashboard native-toggle (both gate on the verified connector — no-strand preserved). Best-effort and idempotent (never raises, never invents a mureo block); Search Console is never disabled. Works for both Claude Code (`~/.claude.json`) and Claude Desktop (`claude_desktop_config.json`).
- **Web-UI per-platform native↔official toggle** — the dashboard now shows, per official provider (when the mureo MCP is configured), the current tool source for that platform and a button to switch. `POST /api/providers/native-toggle` sets/unsets `MUREO_DISABLE_<PLATFORM>`; `status` exposes the per-platform state. Switching **to official** is allowed only when the official path is actually usable (pipx/npm provider registered, or Meta connector verified Connected) — refused with an actionable message otherwise; switching **back to native** is always allowed (the un-strand path). Restart Claude to apply (the flag is read once at MCP start). Host-aware (Code + Desktop), EN + JA.

### Fixed — GA4 wizard inputs were collected but never saved
- The configure-UI auth wizard's GA4 step rendered the service-account-path / project-id inputs and a "Done" button whose handler only advanced the wizard — the entered values were **discarded**, so `GOOGLE_APPLICATION_CREDENTIALS` / `GOOGLE_PROJECT_ID` were never written to `credentials.json` and the official `ga4-official` MCP launched unauthenticated (same class as the earlier Google Ads bug). The Done handler now POSTs each value through the allow-listed `/api/credentials/env-var` writer (into the `ga4` section) **before** advancing, only proceeding if every write succeeds (otherwise it surfaces a save-failed message and stays on the step). Host-accurate labels + saving/failure status added (EN + JA).

### Changed — host selector clarity + Desktop-unavailable credential-guard hook note
- The configure-UI host selector labels were ambiguous (`Claude Code (terminal)` implied terminal-only). Relabelled to **`Claude Code (CLI, Desktop app)`** vs **`Claude Desktop app (Chat, Cowork)`** so users running Claude Code *inside* the Desktop app correctly pick the Claude Code option (which targets `~/.claude.json`). Japanese punctuation made consistent (fullwidth `、`).
- The credential-guard hook has no surface on Claude Desktop (`install_auth_hook` is a `noop:unsupported_on_desktop` there). The basic-setup list (wizard **and** dashboard) now appends "(not available on the Desktop app)" / "（デスクトップアプリでは利用できません）" to that row when the chosen host is Claude Desktop, instead of implying it can be installed.

### Fixed — dashboard "mureo integrations" listed GA4 (not native) and omitted Search Console
- The configure-UI dashboard's **mureo integrations** section listed `Google Ads / Meta Ads / GA4`. mureo ships **no native GA4 tools** (GA4 is official-provider-only), so GA4 did not belong there; meanwhile the genuinely mureo-native **Search Console** was missing (only a sub-note under Google Ads). GA4's presence came from the `ga4` credentials.json section, which actually stores the *official* GA4 MCP's service-account env — not a mureo-native integration.
- Removed the GA4 row; added a **Search Console** row. Search Console has no own credentials section (it reuses the Google Ads Google OAuth — adwords + webmasters scopes), so the row is status-only: it shows configured the moment the wizard's Search Console / Google sign-in is done (driven by the existing `credentials_oauth.google` signal) and has no standalone Remove (a note directs removal to the Google Ads row, since the sign-in is shared).

### Fixed — official Meta Ads provider registered a dead entry on Claude Code
- Adding the **official** Meta Ads MCP (`meta-ads-official`, hosted at `https://mcp.facebook.com/ads`) on Claude Code wrote a raw `{"type":"http","url":…}` entry into `~/.claude.json` **and** set `MUREO_DISABLE_META_ADS=1`. But Meta's hosted MCP has **no OAuth Dynamic Client Registration**, so Claude Code can never connect that raw entry (`✗ Failed to connect`) — while mureo-native Meta was disabled, leaving the user with **zero** Meta capability (the model fell back to a mureo-native "credentials not found" error). The Desktop path already short-circuited this; the Code path and the `mureo providers add` CLI did not.
- Claude Code, the `mureo providers add` CLI, **and** Claude Desktop now treat `hosted_http` providers consistently: **no dead config entry is written and mureo-native tools are NOT auto-disabled** (auto-disabling before the official path is verified strands the user). The result is `manual_required`, and the UI/CLI now point the user to **Claude's account-level Connectors** (the working path mureo cannot create programmatically). Connectors setup guidance is host-accurate — terminal (Claude Code) vs Claude Desktop have genuinely different steps — EN + JA. `mureo providers remove` still self-heals a stale `MUREO_DISABLE_<PLATFORM>` left by the old logic so native Meta comes back.
- **`mureo providers confirm <id>`** (new) + a Web UI "I've connected it — finalize" button close the post-setup coexistence gap: once the official hosted connector is **verified Connected** (parsed from `claude mcp list`), the overlapping `MUREO_DISABLE_<PLATFORM>` is set so the model stops calling the credential-less mureo-native tools. Native is **only** disabled after the official path is confirmed working — never before (no stranding). Claude Desktop returns `manual` (no programmatic signal there; the user verifies in Settings → Connectors). The `claude mcp list` probe is timeout-bounded so an unreachable endpoint can't hang the call.
- The dashboard's **Official MCP providers** list previously showed `meta-ads-official` as permanently `✗` (mureo never registers a hosted connector in the config file). It now reflects the real account-level Connector state: a new `POST /api/providers/hosted-status` endpoint (lazily fetched, cached) flips the row to `✓` once the Connector is Connected. The probe is a SEPARATE endpoint, intentionally not folded into `/api/status`, so it never slows every status poll. Hosted rows never get a Remove button (mureo cannot unregister an account Connector).

### Fixed — official Google Ads provider was registered but unusable
- Selecting the **official** Google Ads MCP from the `mureo configure` Web UI registered the provider but left it unable to authenticate: (1) the wizard's Developer-Token + Google OAuth step only ran for the mureo-**native** provider choice, so picking *official* skipped credential collection entirely; and (2) the registered `mcpServers["google-ads-official"]` block carried no `env`, while the upstream `google-ads-mcp` reads its config **only** from environment variables (never mureo's `~/.mureo/credentials.json`). Net effect: "✓ registered" then "✗ Failed to connect".
- The Web UI auth step now runs for **both** native and official Google Ads (same Developer Token + OAuth refresh token), the wizard now orders `auth` **before** `providers_install` so credentials exist when the block is written, and `install_provider` resolves the credential env from `credentials.json` (closed allow-list, reverse of the per-var writer) and injects it into the registered provider block — Claude Code (`~/.claude.json`) and Claude Desktop alike. Meta's hosted MCP is correctly excluded (it authenticates via browser OAuth on first connect; no env to inject). Credential values are redacted from any `claude mcp add-json` failure message before it can reach a log line.

### Added — `mureo configure` Web UI (Issue #88, Phase 1: localhost setup wizard)
- **`mureo configure`** — local Web UI for non-engineer onboarding. Spawns a stdlib-only HTTP server bound to `127.0.0.1` (ephemeral port by default; `--port` to pin; `--no-browser` to suppress auto-open; `--timeout-seconds` for idle exit). Renders a single Japanese settings page with cards for current status, official MCP providers (add/remove via the audited `mureo.providers.config_writer` helpers), OAuth handoff stubs, and an allow-listed env-var writer that routes through `mureo.auth_setup.save_credentials` (mode `0o600`). Defense-in-depth: hard-coded localhost bind, Host-header validation on every route (DNS-rebinding defense), per-process CSRF (`secrets.token_urlsafe(32)` + `secrets.compare_digest`), full CSP / `X-Content-Type-Options` / `X-Frame-Options: DENY` / `Referrer-Policy: no-referrer` headers, 16 KiB POST cap, basename allow-list for static assets, 405 responses for unsupported HTTP methods. Stdlib only — no Flask/FastAPI/jinja2 dependency added (regression-pinned by `test_pyproject_does_not_depend_on_web_framework`).

### Added — plugin → MCP tool exposure (Issue #89 follow-up)
- **`mureo.mcp.tool_provider`**: new `MCPToolProvider` opt-in secondary Protocol (`mcp_tools()` + `async handle_mcp_tool()`) and `collect_plugin_tools()`. A third-party provider discovered via the `mureo.providers` entry-point group that *also* satisfies `MCPToolProvider` now has its operations published as `mcp__mureo__*` tools. A provider that does not implement it is still discovered and skill-matched, just not exposed (graceful). Added to the stable plugin ABI (see `docs/ABI-stability.md` §1).
- **`mureo/mcp/server.py`**: purely additive wiring — built-in platforms keep their static tool list and are not routed through the plugin path (no double-exposure). With no third-party plugins installed, the tool list and behaviour are byte-identical to before. Built-in tool names are reserved (a colliding plugin tool is dropped, built-ins win); plugin↔plugin collisions are first-wins; a broken/malicious plugin (construct / `mcp_tools()` / non-async handler / wholesale discovery failure) is skipped with a `PluginToolWarning` and can never crash the server or starve other plugins.
- Docs: `plugin-authoring.md` §3 "Exposing operations as MCP tools", `mcp-server.md` "Plugin-Provided Tools", `ABI-stability.md` updated.

### Fixed — BYOD/demo relative windows silently went empty over time
- The CSV-backed BYOD/demo clients resolved relative MCP query windows (`LAST_7_DAYS`, …) against `date.today()`. The demo dataset has fixed historical dates, so as wall-clock time moved past it the demo silently returned `[]` — `LAST_7_DAYS` first, then every window — making Meta insights / anomaly checks / Search Console appear broken even though the data was present.
- `_period_to_range` now accepts an optional `anchor`; the BYOD/demo metrics readers anchor relative windows on the **dataset's own latest date** (same span, ending at the most recent available day) so the demo stays non-empty regardless of the current date. `anchor=None` preserves the exact legacy wall-clock behaviour, so live-API and non-demo callers are completely unaffected.

### Added — `mureo providers` CLI (Issue #86, Phase 1: Claude Code)
- **`mureo providers list / add / remove`** — one-command install of the official platform MCP servers into Claude Code's `~/.claude/settings.json`. mureo owns both package acquisition and config registration. `add` is idempotent; `add --all` installs every catalog entry and continues past per-provider failures (non-zero exit overall). `--dry-run` prints the planned `pipx`/`npm` argv (or, for hosted endpoints, the "no local install step" notice) plus the JSON delta without touching disk or subprocess. `remove` only edits the settings file (it does not uninstall the underlying pipx/npm package; no-op for hosted-HTTP entries which have none).
- Phase 1 catalog (3 entries):
  - **Google Ads** (`google-ads-official`, `pipx` from `github.com/googleads/google-ads-mcp` — env-var Client Library config, no OAuth Proxy / ADC modes in Phase 1).
  - **Meta Ads** (`meta-ads-official`, **hosted HTTP** at `https://mcp.facebook.com/ads` — Meta's official "Meta Ads AI Connectors" announced 2026-04-29; interactive Meta Business OAuth in the browser on first connect, no Meta Developer App, no API tokens, no env vars to pre-populate; currently in public beta and free).
  - **GA4** (`ga4-official`, `pipx install analytics-mcp` from `github.com/googleanalytics/google-analytics-mcp` — service-account JSON via `GOOGLE_APPLICATION_CREDENTIALS`, read-only Reporting + Admin APIs).
- New `mureo/providers/` package: `catalog.py` (frozen `ProviderSpec` + immutable Phase 1 catalog with three `install_kind` families: `pipx`, `npm`, `hosted_http`), `installer.py` (list-form subprocess with `pipx`/`npm` executable allow-list — no `shell=True`, no `env=` kwarg; `hosted_http` short-circuits subprocess entirely), `config_writer.py` (atomic `os.replace` from a same-dir `.tmp.<pid>` file with `0o600` mode and fd-fsync + best-effort parent-dir fsync; refuses to overwrite malformed-JSON existing settings via `ConfigWriteError`), `coexistence.py` (warning when an official provider overlaps with a mureo-native platform), `mureo_env.py` (per-platform `MUREO_DISABLE_*` env-var writers).
- **Auto-disable mureo's native tool family** when an official provider for an overlapping platform is added (extension to Phase 1, per Founder Q1/Q2 review). `mureo providers add google-ads-official` now also writes `mcpServers.mureo.env.MUREO_DISABLE_GOOGLE_ADS="1"` (and analogous keys for Meta Ads / GA4) in the **same atomic `os.replace`** that adds the official provider — no torn-write window. The mureo MCP server reads these env vars **once at import time** and excludes the matching tool family from `_ALL_TOOLS` and the dispatch table; the comparison is exact-string `== "1"` (any other value keeps tools enabled). `mureo providers remove <id>` pops the matching env var. Search Console is intentionally exempt — mureo remains canonical for SC, no `MUREO_DISABLE_SEARCH_CONSOLE` is honored. When no `mcpServers.mureo` block exists, the CLI registers the official provider as before and emits a degraded coexistence note pointing at `mureo setup claude-code`.
- **Workflow skills are now provider-aware** when an official MCP is installed. Skills under `mureo/_data/skills/` (`daily-check`, `rescue`, `search-term-cleanup`, `budget-rebalance`, `creative-refresh`, `competitive-scan`, `weekly-report`, `sync-state`, `onboard`) keep mureo native tool names as the **primary path** — so existing users running `mureo setup claude-code` see zero behavior change. The skills now also instruct the LLM to fall back to the official MCP's equivalent tools when mureo's tools for that platform are unavailable (i.e. `MUREO_DISABLE_<PLATFORM>=1` is set because the user ran `mureo providers add <id>`), and to gracefully skip mureo-only value-add features (anomaly detection, RSA asset audit, `result_indicator` analysis, rule-based scoring, etc.) with a user-facing note pointing back to `mureo setup claude-code` for full coverage.
- Phase 2 (Cursor / Codex / Gemini config writing for official providers) is deferred to a separate Issue. Search Console intentionally remains out of the catalog — mureo's native MCP stays canonical.

## [0.8.0] - 2026-05-02

Non-engineer onboarding release. mureo is now usable from Claude Desktop chat / Cowork directly, with one-command setup, workspace-local BYOD, and a unified skill model that replaces the old slash-commands directory.

### Added — Desktop / Cowork host support
- **`mureo install-desktop`** (PR #75) — one-command setup that creates a workspace, generates a wrapper script (`~/.local/bin/mureo-mcp-wrapper.sh`), and merges a `mureo` MCP entry into `~/Library/Application Support/Claude/claude_desktop_config.json`. Flags: `--workspace`, `--with-demo`, `--force`, `--dry-run`. Idempotent; takes timestamped backups of the existing config; refuses to follow symlinked configs (Dropbox / iCloud sync setups). Workspace path goes through `shlex.quote` so HOMEs containing spaces or shell metacharacters work.
- **5 new MCP tools** for the strategic-context layer (PR #74): `mureo_strategy_get`, `mureo_strategy_set`, `mureo_state_get`, `mureo_state_action_log_append`, `mureo_state_upsert_campaign`. Lets Desktop chat / Cowork / web hosts (which have no `Read` / `Write` tools) read and update STRATEGY.md and STATE.json without filesystem access. All writes go through pre-existing atomic helpers (`mureo.context.state._atomic_write`); cwd-traversal refuse symmetric with the rollback surface.
- **`MUREO_BYOD_DIR` environment variable** (PR #75) — points BYOD at a workspace-local `byod/` directory instead of the legacy global `~/.mureo/byod/`. The install-desktop wrapper exports it automatically so each Claude Desktop workspace has its own BYOD store. Demo and Live-API setups can now coexist without `rm -rf ~/.mureo/byod/` between them.
- **Cowork plugin packaging** (PR #77) — `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, and `.mcp.json` at the repo root let Cowork register `logly/mureo` as a plugin marketplace and surface the skill bundle. The `.mcp.json` shell-gates the wrapper invocation so a fresh contributor without `install-desktop` does not see repeated launch errors in Claude Code.
- **`docs/getting-started.md`** + **`docs/getting-started.ja.md`** (PR #78) — full 3 modes × 3 hosts walkthrough guide (Demo / BYOD / Live API on Claude Code / Desktop chat / Cowork) including how to obtain BYOD XLSX bundles, where to put them, and how to import per host.

### Changed (BREAKING) — skill / command consolidation (PR #77)
- **Slash commands are now skills.** The 10 files under `mureo/_data/commands/*.md` (daily-check, budget-rebalance, search-term-cleanup, creative-refresh, rescue, goal-review, weekly-report, competitive-scan, onboard, sync-state) have been promoted to first-class skills under `skills/<name>/SKILL.md` with proper YAML frontmatter and a `PREREQUISITE: Read ../_mureo-shared/SKILL.md` link. Skills work as `/<name>` in Claude Code (same as before) AND via natural language in Desktop / Cowork — single source of truth across hosts. **Migration impact**: most users see no difference (`/daily-check` etc. still work in Code). Operators who imported `install_commands` from `mureo.cli.setup_cmd` or `install_codex_command_skills` from `mureo.cli.setup_codex` must remove those calls — both functions were deleted; `install_skills` / `install_codex_skills` now cover the unified bundle.
- **Foundation skills renamed with `_` prefix.** The 6 reference skills consumed via PREREQUISITE were renamed: `mureo-shared` → `_mureo-shared`, `mureo-strategy` → `_mureo-strategy`, `mureo-google-ads` → `_mureo-google-ads`, `mureo-meta-ads` → `_mureo-meta-ads`, `mureo-learning` → `_mureo-learning`, `mureo-pro-diagnosis` → `_mureo-pro-diagnosis`. The `_` prefix follows the standard "private / hidden" shell convention so they stay out of Claude Code's user-facing slash menu while remaining readable as PREREQUISITE links. **Migration impact**: anyone with custom skills that hard-code `../mureo-shared/SKILL.md` etc. must rewrite to `../_mureo-shared/SKILL.md`. Bundled mureo skills are already updated.
- **`skills/mureo-workflows/`** — deleted. It was the index of the 10 commands; now superseded by per-skill files. Anyone deep-linking to `skills/mureo-workflows/SKILL.md` will hit a 404.
- **`mureo/_data/commands/`** directory deleted. The Code's `~/.claude/commands/` install path is gone; `mureo setup claude-code` no longer copies command files (skills cover the same surface via `~/.claude/skills/`).

### Changed (BREAKING from 0.7.x) — MCP tool name spec (PR #73)
- **MCP tool names switched from dot to underscore separators** to comply with the MCP spec regex `^[a-zA-Z0-9_-]{1,64}$`. Without this, Claude Desktop's chat (and any other spec-strict MCP host) rejected the entire mureo MCP server at registration time with errors like `tools.42.FrontendRemoteMcpToolDefinition.name: String should match pattern '^[a-zA-Z0-9_-]{1,64}$'`. Claude Code accepted dotted names through lenient validation, which is why the bug went undetected. **Migration impact**:
  - **Claude Code users**: no action required. Slash commands (`/daily-check`, etc.) and natural-language tool calls are unaffected.
  - **Claude Desktop / claude.ai web users**: this fix unblocks registration; the server now appears in the tool surface as expected.
  - **Operators with custom `excludeTools` lists** (e.g. Gemini CLI extension config at `~/.gemini/extensions/mureo/gemini-extension.json`): rename entries in your `excludeTools` array to the new underscore form. Example: `"google_ads.budget.update"` -> `"google_ads_budget_update"`. Otherwise your previous exclusion list silently stops blocking those tools after upgrade.
  - **Anyone with code or scripts referencing tool names directly**: rename `prefix.X.Y` -> `prefix_X_Y` (173 tools across `google_ads`, `meta_ads`, `search_console`, `rollback`, `analysis` prefixes).
- New regression test `tests/test_mcp_tool_name_spec.py::test_all_registered_tools_match_mcp_spec` enforces the spec regex in CI for every future tool addition.

### Documentation
- README.md and README.ja.md gain a **"Choose your setup"** 3 modes × 3 hosts matrix above the quick-start with a link into `docs/getting-started.md(.ja.md)`.
- AGENTS.md gains a **"Commit Workflow"** section codifying the rule that every code commit (including fixup / review-response commits) requires a `code-reviewer` pass before commit. Rule reinforced after PR #20 (OAuth helper) and PR #75 (install-desktop fixup).
- Terminology unified (PR #79): all references to "Real-API" / "real-api" / "real API" rewritten as **"Live API"** for consistency.

### Tests
- 22 new tests for `desktop_installer` covering fresh install, force overwrite, dry-run, demo seeding, idempotence, corrupt-config refusal, symlinked-config refusal, shell quoting of workspace paths with spaces, version drift between `pyproject.toml` and `.claude-plugin/plugin.json`.
- 5 new tests for `MUREO_BYOD_DIR` env var override, including `~`-expansion and whitespace-only fallback.
- 7 new sanity tests for plugin manifests (`tests/test_plugin_manifests.py`): JSON validity of all 3 plugin metadata files, version-drift guard, `.mcp.json` shell-gate semantics, byte-for-byte sync between `skills/` and `mureo/_data/skills/`, foundation/operational skill naming invariants.

## [0.7.1] - 2026-04-29

PyPI re-publish of v0.7.0 with the post-#54 fixes folded in. The original `0.7.0` is on PyPI but predates these patches; PyPI does not allow re-uploading the same version, so the same change set ships as `0.7.1`.

### Added
- **Currency-agnostic Meta Ads spend column** — Meta exports the spend header as `Amount spent (XXX)` where `XXX` is the account's ISO currency code (`JPY` / `USD` / `EUR` / `GBP` / `KRW` / `INR` / etc.). The previous JPY-only path rejected non-JPY accounts with `UnsupportedFormatError`, blocking every non-JP user. New `_resolve_spend_idx` strips the trailing `(XXX)` suffix before alias matching; `_to_float` strips a leading currency symbol from a known set (¥ / $ / € / £ / ₩ / ₹ / ¢ / etc.) from cell values. Cost values are stored raw in the account's own currency — cross-account currency conversion is out of scope. Regression tests cover USD / EUR / GBP / KRW / INR header + cell-prefix combinations.

### Fixed
- **Meta Ads adapter alias corrections for de_DE / es_ES / fr_FR** — 7 mismatched header strings (best-effort guesses in 0.7.0) replaced with strings observed in real Ads Manager exports across 9 locales. Notable: French uses U+2019 right single quotation mark (`'`) not ASCII (`'`); German "Name der Anzeige(ngruppe)" not the compound forms; Spanish plural "clics" not singular. All 14 mureo-recognized columns now resolve in all 9 locales (126/126 column matches verified against user-provided exports).
- **BYOD Google Ads zero-impressions/zero-clicks regression** — `_to_int` in `mureo/byod/clients.py` rejected float-formatted strings like `"98.0"`, which is exactly what the bundled Apps Script writes for impressions / clicks per day. The strict `int("98.0")` raised `ValueError` and the helper silently returned the default `0`, so every Google Ads BYOD `get_performance_report` row reported `impressions=0 / clicks=0 / ctr=0 / average_cpc=0` even when the underlying CSV was complete. `_to_int` now falls back to `int(float(v))` before returning the default. Regression tests in `tests/test_byod.py`.
- **BYOD Meta `get_performance_report` now surfaces `result_indicator`** — the per-campaign output dict gained a `result_indicator` field (e.g. `actions:link_click`, `actions:offsite_conversion.fb_pixel_lead`, or empty when the campaign carries no conversion-event optimization). The value was already written to `metrics_daily.csv` by the Phase 3 importer but was being dropped before reaching the agent, so `/daily-check` could see "42 results vs 3 results" across two campaigns with no signal that the units were incomparable. The first non-empty indicator seen for each campaign across the period's daily rows is exposed.

### Changed
- **All bundled command skills now name the specific MCP tools to call** — `mureo/_data/commands/{daily-check,search-term-cleanup,budget-rebalance,competitive-scan,creative-refresh,rescue,sync-state,weekly-report,onboard}.md` previously instructed the agent to "use the platform's analysis tools" without naming them. Real BYOD sessions hit a reproducible failure mode where the agent looked for raw CSVs in the project directory and aborted when it couldn't find any (BYOD data lives under `~/.mureo/byod/`, not in the project). Skill bodies now list the concrete tool names per platform plus an explicit reminder that BYOD data is centralized under `~/.mureo/byod/`. Each command also documents which BYOD tools return `[]` by design (auction insights, RSA asset ratings, etc.). Markdown skill bodies only — no code changes.
- **Japanese BYOD walkthrough** (`docs/byod.ja.md`) added — native Japanese counterpart of `docs/byod.md`. Section flow restructured for Japanese readability rather than direct translation; all 9 verified Meta export locales named explicitly. Cross-link added at the top of the English `byod.md`. `README.ja.md` deep links repointed from `docs/byod.md` to `docs/byod.ja.md`.

### Security
- **Resolved 8 CodeQL Code Scanning alerts** in OAuth + GAQL paths:
  - `py/http-response-splitting` (error) — `mureo/cli/web_auth.py` `Location` header now constructed from a CR/LF-stripped URL via `_validate_oauth_url`, blocking response-splitting injection.
  - `py/clear-text-logging-sensitive-data` (4 errors) — OAuth URL-rejection logs and GAQL `_search` start/done logs no longer echo any value derived from the URL or query string. The terminal `print` of the Meta OAuth URL in `run_meta_oauth` was removed entirely.
  - `py/incomplete-url-substring-sanitization` (3 warnings) — `tests/test_web_auth.py` deep-link substring assertions tightened to match the full URL path, not the host alone.
  - The OAuth-URL validation now runs **before** any wizard session mutation, so a rejected redirect cannot leave the wizard with half-populated auth material.

## [0.7.0] - 2026-04-29

### Added
- **BYOD Meta Ads adapter** — `mureo/byod/adapters/meta_ads.py` consumes the user's Ads Manager Excel export (Reports → Customize → Export → Excel) and normalizes it to CSVs under `~/.mureo/byod/meta_ads/`. Identity (campaign_id / ad_set_id / ad_id) is synthesized from name via deterministic SHA-256 hash so re-imports keep stable IDs. **Multilingual header support** — recognizes column names in English / 日本語 / Español / Português / 한국어 / 繁體中文 / 简体中文 / Français / Deutsch (e.g. キャンペーン名, インプレッション, 消化金額 (JPY), 結果), verified against actual exports in each locale. Multiple rows per (day, campaign) — typical when Ad-set or Ad breakdown is enabled — are summed before write. Pivot subtotal rows (date cell = `All` or locale equivalent) are skipped automatically. Currency is JPY-only; non-JPY symbol prefix (`$`, `€`, `£`, …) raises `UnsupportedFormatError` to prevent silent over/under-reporting. (Restriction lifted in 0.7.1 — see above.) Disjoint from the Google Ads adapter via the long-form vs short-form campaign header distinction, so a single workbook can carry both adapters' data.
  - **Phase 3 schema (richer analytics):** `metrics_daily.csv` extended with `reach`, `frequency`, `result_indicator` columns (frequency falls back to impressions/reach when not directly exported). New per-grain CSVs are written when the export carries the relevant columns: `ad_set_metrics_daily.csv` ((date, campaign_id, ad_set_id) × metrics+reach), `ad_metrics_daily.csv` ((date, campaign_id, ad_set_id, ad_id) × metrics+reach), `demographics_daily.csv` (one row per (date, campaign_id, dimension, value) for age / gender / region / placement breakdowns — these rows are excluded from `metrics_daily` to avoid double-counting), and `creatives.csv` (best-effort: ad_id, name, image_url, video_url, headline, body, cta — only written when those columns are present in the export). Each new CSV is suppressed when the source export lacks the corresponding columns, so existing campaign-only exports import unchanged.
- **BYOD Sheet bundle pipeline (Google Ads only)** — XLSX-in, Google-Ads-out import. Users run the mureo Google Ads Script (`scripts/sheet-template/google-ads-script.js`) inside Google Ads → Tools → Bulk actions → Scripts, which populates a Google Sheet with `campaigns / ad_groups / search_terms / keywords / auction_insights` tabs. They download the sheet as XLSX and run `mureo byod import bundle.xlsx`. The bundle importer (`mureo/byod/bundle.py`) opens the XLSX with openpyxl read-only, dispatches the workbook to the Google Ads adapter, writes per-platform CSVs to `~/.mureo/byod/google_ads/`, and updates `manifest.json` atomically with rollback on partial failure. New `openpyxl>=3.1,<4` runtime dependency.
- **Richer Google Ads adapter** — surfaces `search_terms.csv`, `keywords.csv`, and `auction_insights.csv` alongside the previous `campaigns.csv` / `ad_groups.csv` / `metrics_daily.csv`, giving `/daily-check` access to query-level and competitor-level data the v0.6 CSV path could not.
- **Google Ads Script** under `scripts/sheet-template/google-ads-script.js`. **No mureo-managed OAuth client, no GCP project, no developer token** — Ads Scripts run under the user's Google Ads identity on Google's infrastructure, including on Google Workspace organization accounts where Apps Script auto-GCP-creation is blocked.
- **Richer Google Ads analysis from BYOD bundles**: `ByodGoogleAdsClient.get_search_terms_report` is now backed by the bundle's `search_terms.csv` (was `return []`), and new `get_auction_insights` / `analyze_auction_insights` methods read `auction_insights.csv`. Together these surface query-level performance and competitor share metrics through the existing `google_ads_search_terms_report` / `google_ads.auction_insights.{get,analyze}` MCP tools — turning the v0.6 BYOD path's "Campaign × Day rollup only" into something `/daily-check` can actually reason about.

### Changed
- `mureo byod` CLI — `mureo byod import <file>` now requires an XLSX (Sheet bundle). The previous per-platform CSV path is gone; flags `--google-ads / --meta-ads / --search-console / --as` were removed from `import` because the workbook tab names determine which adapter runs. `status` and `remove` cover the BYOD-supported platforms (`--google-ads` / `--meta-ads`); GA4 and Search Console remain on the existing Live API OAuth path and are not part of the bundle pipeline.
- MCP tool descriptions rewritten for 49 tools across Google Ads and Meta Ads — `google_ads.campaigns.*`, `google_ads.ad_groups.*`, `google_ads.ads.*`, `google_ads.budget.*`, `google_ads.accounts.*`, `google_ads.keywords.*`, `google_ads.negative_keywords.*`, `meta_ads.campaigns.*`, `meta_ads.ad_sets.*`, `meta_ads.ads.*`. Each description now covers verb + resource + returned fields + side effects (read-only / mutating / reversible via `rollback_apply`) + sibling tool differentiation, following the new `docs/tdqs-style-guide.md`. Targets improving Glama's Tool Definition Quality Score from C (3.1 avg) toward B+. No behavioral changes — descriptions and parameter hints only.
- MCP tool descriptions rewritten for the remaining ~51 Meta Ads tools — `meta_ads.creatives.*` (including the TDQS lowest-scoring `meta_ads_creatives_list`), `meta_ads.images.*`, `meta_ads.videos.*`, `meta_ads.audiences.*`, `meta_ads.pixels.*`, `meta_ads.insights.*`, `meta_ads.analysis.*`, `meta_ads.catalogs.*`, `meta_ads.products.*`, `meta_ads.feeds.*`, `meta_ads.conversions.*`, `meta_ads.lead_forms.*`, `meta_ads.leads.*`, `meta_ads.split_tests.*`, `meta_ads.ad_rules.*`, `meta_ads.page_posts.*`, `meta_ads.instagram.*`. Same TDQS template as PR #43. No behavioral changes.
  - The OAuth-URL validation now runs **before** any wizard session mutation, so a rejected redirect cannot leave the wizard with half-populated auth material.

### Removed (BREAKING)
- **Single-CSV BYOD import path** (`mureo byod import <file>.csv`, the auto-detection logic, the 15-locale Google Ads Report Editor alias dictionary, preamble handling, PII column rejection, and the `--google-ads / --meta-ads / --search-console / --as` flags on `import`). Users who imported a CSV under v0.6.x must re-run the Sheet flow described in `docs/byod.md`. Public symbols removed: `mureo.byod.installer.import_csv`, `mureo.byod.adapters.google_ads.GoogleAdsAdapter.detect()`, `mureo.byod.adapters.google_ads.GoogleAdsAdapter.normalize()`, `mureo.byod.adapters.google_ads.PIIDetectedError`. The `source_format` manifest key changes from `"google_ads_report_editor_v1"` to `"mureo_sheet_bundle_google_ads_v1"`.

## [0.6.0] - 2026-04-20

First production PyPI release (`pip install mureo`). Supersedes the internal `0.6.0.dev1` preview that was published to TestPyPI during colleague beta testing.

### Security
- `mureo setup codex` command-skill generation now escapes all control characters and unicode line separators (U+2028 / U+2029) in the skill `description:` frontmatter field, so a future bundled command whose first line contains a tab, CR, LF, NEL, or other byte that YAML treats as a line break cannot silently truncate the description or corrupt the frontmatter block. Today's bundled commands don't trigger the old behavior — this is a defense-in-depth guard against a future maintainer adding a command with an unusual first line.
- The legacy `~/.codex/prompts/<bundled>.md` cleanup in `install_codex_command_skills` now skips symlinks. Previously a user who had symlinked a bundled filename at their own file (e.g. via a dotfiles repo) would see the symlink silently removed on every `mureo setup codex` re-run, even though the target stayed intact. The symlink now survives so the operator's intentional link-over-bundled-name is preserved.

### Fixed
- `mureo auth setup --web` — Google Ads now saves a real `customer_id` and `login_customer_id` instead of `null`. Previously the web wizard stopped at `refresh_token` and never reached `list_accessible_accounts()`, so every Google Ads credentials block shipped with null IDs and every subsequent API call failed. The wizard now has a `/google-ads/select-account` page rendered after OAuth callback: list is fetched via Google Ads API, rendered as radio picker, submit resolves MCC hierarchy (`login_customer_id = parent_id if parent_id else chosen_id`). Same treatment for Meta: `/meta-ads/select-account` calls `list_meta_ad_accounts` and saves `account_id`. If either list API fails or returns empty, credentials save with the null IDs and the wizard redirects to `/after-platform?warn=no_accounts` so the operator sees the warning and can fix manually instead of losing the refresh_token.
- `mureo auth setup --web` — wizard no longer quits after a single platform. Previously Google/Meta callback both redirected to `/done`, ending the wizard and preventing the user from configuring the second platform. Now the wizard redirects to a new `/after-platform` intermediate page after each platform completes, showing a "Configure [other] too" CTA (hidden when both are done) plus a Finish setup button. `/done` is reached only via explicit "Yes, finish" on a new `/done/confirm` confirmation page.
- `mureo auth setup --web` — Facebook "CSRF token invalid" 403 on re-submit from a cached page. Added `Cache-Control: no-store` and `Pragma: no-cache` to all wizard HTML responses so browsers never render a stale form with an outdated hidden token. Also removed premature `rotate_csrf()` from `/google-ads/submit` and `/meta-ads/submit` (OAuth-init steps that don't persist anything) — rotation now happens only at commit-point submits (`/google-ads/select-account`, `/meta-ads/select-account`). Back-button resubmissions and parallel tabs (one user on `/google-ads` while another is on `/meta-ads`) no longer wedge with a 403.
- `mureo auth setup --web` — "Continue to Facebook sign-in" button appeared unresponsive. Root cause: modern browsers enforce CSP `form-action` through the entire redirect chain, so a form posting to `/meta-ads/submit` that 302s to `facebook.com` was blocked by a `form-action` allow-list that only contained `accounts.google.com`. Widened the directive to `form-action 'self' https://accounts.google.com https://www.facebook.com`.
- `mureo auth setup --web` — Facebook "insecure connection" warning on Google/Meta OAuth redirect. The terminal flow used `http://localhost:<port>/callback`; the web wizard used `http://127.0.0.1:<port>/...`. Facebook treats the IP literal as non-dev and surfaces the scary "not secure" warning, while `localhost` is whitelisted as a dev origin. Switched all three web-wizard redirect_uri constructions to use `localhost` (host-header check already accepted both). Google accepts both transparently.
- `mureo auth setup --web` — `_configured_platforms` now requires `customer_id` / `account_id` to count a platform as "configured", not just `refresh_token` / `access_token`. Prevents the `/done` page from declaring a no-accounts partial save "complete". Paired with: `/after-platform` no longer bounces to `/` when `_configured_platforms` is empty, so a partial save reaches the Finish button instead of the user getting kicked home with no feedback.
- `mureo auth setup --web` — `_read_form` catches `UnicodeDecodeError` on malformed POST bodies and returns a 413 instead of a 500, matching the other boundary checks. Probe-error logging in the Google/Meta account-list paths no longer passes `exc_info=True` — the google-ads SDK occasionally embeds request arguments (developer_token / client_secret / access_token) in exception repr, and dumping the full traceback to stderr would leak them.
- `mureo auth setup --web` UX — Finish setup button is now green (`btn-finish`) and goes through a `/done/confirm` Yes/No confirmation page before terminating the wizard, so a user mistaking "Finish setup" for a primary continuation CTA (identical blue to "Configure X too" previously) can back out without losing the session.
- Skill-directory re-install now tolerates a symlink at the destination path. Previously, if an operator had symlinked `~/.claude/skills/<bundled>/` or `~/.codex/skills/<bundled>/` at their own dev copy (common during mureo development from an editable install), re-running `mureo setup claude-code` or `mureo setup codex` crashed with ``OSError: Cannot call rmtree on a symbolic link`` in `install_skills`, `install_codex_skills`, and `install_codex_command_skills`. The fix swaps the symlink for `unlink()` (the link itself is removed; the external target the link points at is left intact), then lays down a real copy of the bundled skill. Regular directories still go through `shutil.rmtree` unchanged.
- `install_commands` (`~/.claude/commands/*.md`) now also unlinks a symlinked destination before copying. Previously `shutil.copy2` followed the symlink and wrote through to the target file, so the symlink stayed in place. That silently broke **Claude Desktop slash commands** for any dev who had symlinked the bundled commands at their repo: Claude Desktop's sandboxed process tried to read the symlinked command file, followed the symlink into `~/Documents`, hit macOS TCC, got denied, and the slash command dispatched nothing. Real-file replacement fixes both Claude Desktop (no cross-sandbox read) and leaves the dev's external target file untouched.
- `mureo setup codex` now installs bundled workflow commands as Codex **skills** at `~/.codex/skills/<command>/SKILL.md` (with YAML frontmatter — `name:` / `description:`) instead of as custom prompts at `~/.codex/prompts/*.md`. Codex CLI 0.117.0 (2026-03) [stopped rendering the custom-prompts directory](https://github.com/openai/codex/issues/15941) in its slash-command menu, so `mureo setup codex` was silently installing ten files that Codex no longer picked up. Users invoke the workflows with `$daily-check` (explicit) or the `/skills` picker. Re-running `mureo setup codex` also removes the stale `~/.codex/prompts/<bundled>.md` files left behind by prior installs; user-authored prompts with names outside mureo's bundled set are preserved.

### Changed
- Internal documentation realigned to recent development. `docs/cli.md` now documents `mureo setup codex` (Codex skills migration + legacy-prompt cleanup, per [openai/codex#15941](https://github.com/openai/codex/issues/15941)), `mureo setup gemini`, and the per-platform `--google-ads/--meta-ads/--skip-auth` flags with their non-interactive auto-imply semantics. `docs/authentication.md` "Recommended Setup" lists all four setup subcommands plus `mureo auth setup --web`, and adds sections explaining `--skip-auth` TTY detection and the browser-based wizard (pointing at `SECURITY.md` → "Browser-based auth wizard"). `docs/architecture.md`, `AGENTS.md`, and `CONTEXT.md` code-tree / setup blocks now list `setup_codex.py`, `setup_gemini.py`, and the `--web` auth path. Bundled workflows skill (`mureo/_data/skills/mureo-workflows/SKILL.md` + top-level dev copy) gains a "Invocation syntax by host" note explaining that `/daily-check` on Claude Code reads as `$daily-check` on Codex CLI so a Codex user sees the same reference without confusion.
- Docs refreshed for the browser-based wizard: `README.md` / `README.ja.md` "Claude Code Desktop" section now points at `mureo auth setup --web` and no longer instructs operators to open `Terminal.app`. `docs/cli.md` documents both terminal and `--web` modes of `mureo auth setup`. `SECURITY.md` gains a "Browser-based auth wizard" section enumerating every hardening layer (localhost bind, DNS-rebinding guard, CSRF rotation, OAuth `state` validation, redirect-origin pinning, generic error surface, session zero-out, POST size cap, CSP/X-Frame-Options/Referrer-Policy headers, stdlib-only implementation).

### Added
- Meta Ads support in the browser-based OAuth wizard. The home page now offers both "Configure Google Ads" and "Configure Meta Ads" buttons; `/meta-ads` renders an App ID / App Secret form with inline deep links to Meta for Developers; the submit handler redirects to Facebook's OAuth dialog and the same wizard server receives the callback on `/meta-ads/callback`. After token exchange (short-token → 60-day long-lived token) the server writes `~/.mureo/credentials.json` (`meta_ads` block) and marks the wizard complete. All security guarantees from P2-2 apply symmetrically: CSRF rotation after submit, `state` validated with `secrets.compare_digest` on callback, Host-header check for DNS-rebinding, redirect origin pinned to `https://www.facebook.com/`, POST size capped at 16 KiB, session secrets zeroed after save, and graceful handling of Facebook's `error=` / `error_reason=` user-decline callbacks.
- Public `build_meta_auth_url(app_id, redirect_uri, state)` and async `exchange_meta_code(code, app_id, app_secret, redirect_uri)` helpers in `mureo/auth_setup.py` so the CLI and web paths share the same Facebook OAuth scaffolding. Localhost-only redirect URI validation mirrors the Google path. The legacy `_generate_meta_auth_url(port, state=None)` wrapper is kept for backward compatibility with existing tests.
- `mureo auth setup --web` — browser-based OAuth wizard for non-technical users. Starts a short-lived localhost HTTP server on a random port, opens the browser to a simple HTML form for Developer Token / OAuth Client ID / Client Secret, redirects to Google's own sign-in, receives the callback on the same wizard server, and writes `~/.mureo/credentials.json` — all without the operator opening a terminal. Inline deep links point at Google Cloud Console / Google Ads API Center so users know where to fetch each secret.
- Security hardening for the wizard: CSRF token rotates after every successful submit (replay guard); OAuth `state` parameter is stored at submit time and re-validated with `secrets.compare_digest` on callback; `Host` header must match `127.0.0.1:<port>` or `localhost:<port>` (DNS-rebinding guard); redirect URL is verified to start with `https://accounts.google.com/` before emitting 302 (open-redirect guard); session secret fields are zeroed after `save_credentials` succeeds; CSP includes `default-src 'none'`, `base-uri 'none'`, `frame-ancestors 'none'`, `object-src 'none'`, and `form-action 'self' https://accounts.google.com`; responses also set `X-Frame-Options: DENY` and `Referrer-Policy: no-referrer`; POST bodies over 16 KiB are refused with 413; exception messages from OAuth failures are logged server-side only — the browser sees a generic retry hint.

### Changed
- `mureo setup claude-code` / `cursor` / `codex` / `gemini` are now TTY-safe. When run from an AI agent's subprocess (Claude Code's Bash tool, Codex, etc.) without a controlling TTY, they auto-imply `--skip-auth` and print a banner instructing the operator to finish authentication in Terminal.app. Previously they hung forever on the first `typer.confirm` prompt.
- Each setup subcommand gained explicit `--google-ads/--no-google-ads` and `--meta-ads/--no-meta-ads` flags so choices can be specified without any prompt. Passing them together with `--skip-auth` (or under a non-TTY) emits a warning explaining they were ignored.
- New helper `mureo/cli/_tty.py` (`is_tty`, `confirm_or_default`) centralizes the TTY + fallback logic. Both stdin and stdout must be terminals for `is_tty()` to return true. `confirm_or_default` catches `EOFError` / `click.Abort` if the TTY disappears mid-prompt and falls back to the caller-supplied default.

### Added
- README + README.ja: "From inside Claude Code Desktop" section with a natural-language install phrase that non-technical users can paste into the Code tab. Notes that OAuth (Developer Token, App ID/Secret input) still requires a one-time Terminal.app session for safety.

## [0.5.0] - 2026-04-19

### Added
- `mureo setup codex` — new subcommand that installs the full mureo kit for OpenAI Codex CLI: tagged `[mcp_servers.mureo]` block in `~/.codex/config.toml` (append-only, idempotent, refuses to proceed if an untagged `[mcp_servers.mureo]` already exists), Read + Bash PreToolUse credential guard in `~/.codex/hooks.json`, workflow prompts in `~/.codex/prompts/*.md`, and skills in `~/.codex/skills/mureo-*/`. Config and hooks are written atomically via temp-file + rename so a mid-install crash cannot defeat the tag-based idempotency check. Corrupt `hooks.json` and non-list `PreToolUse` values are refused rather than silently clobbered.
- `mureo setup gemini` — new subcommand that registers mureo as a Gemini CLI extension at `~/.gemini/extensions/mureo/gemini-extension.json` with `mcpServers.mureo` and `contextFileName: CONTEXT.md`. Merges into an existing manifest instead of overwriting, so operator-added top-level keys (`excludeTools`, renamed `contextFileName`) and extra `mcpServers` entries are preserved across reinstall. Hooks and `.md` workflow commands are not installed because Gemini CLI does not support those surfaces.
- Rollback execution path — closes the rollback feature that previously landed as planner + CLI inspection only. Introduces `mureo/rollback/executor.py` (`execute_rollback`) and two MCP tools: `rollback_plan_get` (inspect the reversal plan for any `action_log` entry) and `rollback_apply` (execute the plan, re-dispatching through the same MCP handler used for forward actions so the reversal re-enters the forward-action policy gate — auth, rate limiting, input validation). `ActionLogEntry` gains an optional `rollback_of: int | None` field so the applied rollback is append-only in `STATE.json` and a second apply of the same index is refused. Safety: `confirm=True` must be the literal `True` (truthy integers / non-empty strings are refused), the planner is re-invoked at execution time rather than cached, destructive verbs are refused twice (planner allow-list + executor guard against `rollback.*` self-recursion), the appended rollback entry carries `reversible_params=None` so rollbacks of rollbacks do not chain by default, dispatch-time API failures never mutate `action_log`, `state_file` resolves strictly inside the MCP server's current working directory (no traversal or symlink escape), and downstream SDK exception messages are logged server-side only while the MCP response returns a generic message so tokens / account identifiers cannot leak into model context.
- Added `mureo/analysis/anomaly_detector.py`, a pure I/O-free module that compares a `CampaignMetrics` snapshot against a median-based baseline built from historical `action_log` entries and returns a prioritized list of `Anomaly` records with recommended actions. Detects zero spend on a previously-spending campaign (CRITICAL), CPA spike ≥ 1.5× baseline (critical at 2×, gated by 30+ conversions), and CTR drop ≤ 0.5× baseline (critical at 0.3×, gated by 1000+ impressions). Sample-size gates follow the `_mureo-learning` skill's statistical-thinking rules to suppress single-day noise. Baselines tolerate malformed `metrics_at_action` rows (string numerics, `"N/A"`, missing keys) so one bad entry cannot silently disable detection; CPA/CTR are medianed per-entry (never `median(cost) / median(conversions)`) so baseline values reflect a real historical day.
- Wired `anomaly_detector` behind the new MCP tool `analysis_anomalies_check` (`mureo/mcp/tools_analysis.py` + `_handlers_analysis.py`). The handler takes a `current` metrics payload plus an optional `state_file`, builds a median baseline from `action_log` history, and returns severity-ordered anomalies as JSON. `current.campaign_id` and `current.cost` are required so a zero-spend alert is always an intentional zero. Numeric fields accept int / float / numeric-string and reject the rest (`"N/A"`, bools, etc.), so a JSON client that stringifies numerics works but garbage does not silently pass. `state_file` is sandboxed against the MCP server's current working directory — paths that resolve outside or traverse a symlink are refused so a prompt-injected agent cannot redirect the read to an attacker-crafted STATE.json. A malformed history file does not silence live zero-spend detection; the response includes a `baseline_warning` so the agent can flag the unreliable baseline to the operator.
- Added `mureo/rollback/`, the data-model and planning half of the rollback feature. `ActionLogEntry` gains an optional `reversible_params` field (shape: `{"operation": "<allow-listed>", "params": {...}, "caveats": [...]}`); `STATE.json` round-trips it. `plan_rollback(entry) -> RollbackPlan | None` returns a concrete reversal plan, tagged `supported`, `partial` (reversible configuration but irreversible side effects like spend), or `not_supported`. The planner enforces an explicit operation allow-list (budget update + status toggles across Google/Meta Ads), refuses destructive verbs (`.delete` / `.remove` / `.destroy` / `.purge` / `.transfer`), and rejects unexpected parameter keys so a compromised agent cannot smuggle a privileged call through the rollback path.
- Added `mureo rollback list` and `mureo rollback show <index>` CLI commands for inspecting reversible actions in `STATE.json`. Intentionally read-only — execution continues to route through MCP so it re-enters the same policy gate as forward actions. String fields from the agent-writable STATE.json are sanitized of C0/C1 control characters before terminal output to prevent ANSI-escape injection.

### Security
- Added `mureo/google_ads/_gaql_validator.py`, a centralized whitelist-based validator for Google Ads Query Language (GAQL) string construction. Every ID, date, date-range constant, and string literal that enters a GAQL query now flows through one surface (`validate_id`, `validate_id_list`, `validate_date`, `validate_date_range_constant`, `escape_string_literal`, `validate_period_days`, `build_in_clause`). Hardened `_period_to_date_clause` so the `BETWEEN` branch pattern-matches and validates both dates instead of passing the raw caller string straight into GAQL, closing a trailing-injection path. Removed the unbounded `ALL_TIME` constant from the accepted date-range whitelist and added a 20-character length cap on numeric IDs to prevent absurd payloads reaching the upstream API.

## [0.4.3] - 2026-04-11

### Changed
- Google Ads mutate errors now include the specific API error detail in the raised `RuntimeError` message instead of the generic "An error occurred while processing X." Previously, when Google Ads rejected a mutation (e.g. a `cpc_bid_micros` value below the account's minimum bid, an invalid asset format, a policy rejection, etc.), the detail was only written to logs, so agents saw an opaque failure with no actionable information. The detail is now appended to the message for every tool decorated with `@_wrap_mutate_error` (campaigns.create/update/update_status/diagnose, ad_groups.create/update, ads.create/update/update_status, keywords, negative_keywords, budgets, conversions, extensions, and display ads). The existing RESOURCE_NOT_FOUND hint is preserved and now also carries the API detail.

## [0.4.2] - 2026-04-11

### Added
- `google_ads_ads_list` now returns Responsive Display Ad (RDA) text fields when the ad is a RDA: `headlines`, `descriptions`, `long_headline`, `business_name`, `marketing_images`, `square_marketing_images`, `logo_images`. Previously the response only populated these fields for Responsive Search Ads (RSA), so RDAs appeared with empty `headlines`/`descriptions` arrays.

### Fixed
- `google_ads_ad_groups_update` now rejects `cpc_bid_micros` with a clear error when the parent campaign uses an automated bidding strategy (MAXIMIZE_CLICKS / MAXIMIZE_CONVERSIONS / TARGET_CPA / TARGET_ROAS / etc.). Previously the Google Ads API returned the unhelpful "An error occurred while processing ad group update." message, and it was unclear that the root cause was a bidding-strategy mismatch. Manual ad-group-level bids are only supported under MANUAL_CPC, MANUAL_CPM, and MANUAL_CPV.
- `google_ads_ads_update` now fails fast with a clear message when the target ad is a Responsive Display Ad (RDA). The existing implementation only supports Responsive Search Ads (RSA), and attempting to update a RDA previously produced the cryptic "An error occurred while processing ad text update." error. RDA updates are still not implemented; recreate the ad via `create_display_ad` for text changes.

## [0.4.1] - 2026-04-11

### Fixed
- `google_ads_campaigns_create` silently ignored the `channel_type` parameter introduced in 0.4.0. The MCP handler forwarded only `name`, `bidding_strategy`, and `budget_id` to the client, so a request asking for a DISPLAY campaign produced a SEARCH campaign. `channel_type` is now passed through correctly, and `google_ads_ads_create_display` works end-to-end against live accounts.
- `google_ads_campaigns_list` / `google_ads_campaigns_get` now include the campaign's `channel_type` (SEARCH/DISPLAY/etc.) in the response, so agents can verify the channel type of an existing campaign without inferring it from other fields. `list_campaigns` and `get_campaign` GAQL queries were extended to select `campaign.advertising_channel_type`, and `map_campaign` surfaces it as `channel_type`.

## [0.4.0] - 2026-04-10

### Added
- Display Ads support: `google_ads_campaigns_create` now accepts a `channel_type` parameter (`"SEARCH"` or `"DISPLAY"`, defaults to `"SEARCH"`).
- New tool `google_ads_ads_create_display` for creating Responsive Display Ads (RDA). Marketing, square marketing, and (optional) logo image files are uploaded automatically from local paths before the ad is created.
- New module `mureo/google_ads/_rda_validator.py` with text and asset count validation for RDAs (headlines/long headline/descriptions/business name/image counts/URL).
- New `_DisplayAdsMixin` (`mureo/google_ads/_ads_display.py`) added to `GoogleAdsApiClient`.
- New exception type `RDAUploadError` that surfaces orphaned uploaded asset resource names when an RDA creation fails partway through, so callers can clean them up.
- Pre-check that the target ad group belongs to a DISPLAY campaign before any image upload happens, avoiding orphaned assets when the wrong ad group is selected.

### Changed
- `_AdsMixin` no longer contains display ad code; RSA-related operations remain in `_ads.py`, RDA operations live in `_ads_display.py`.

## [0.3.2] - 2026-04-10

### Added
- `customer_id` field in `GoogleAdsCredentials` (separate from `login_customer_id`) to support MCC-child account scenarios where the authentication context (MCC) and operation target (child account) differ.
- `GOOGLE_ADS_CUSTOMER_ID` environment variable support.

### Changed
- `mureo auth setup` now traverses the Manager (MCC) hierarchy to list child accounts. Previously it only showed accounts with direct login access, so users granted access only via an MCC would see only the MCC itself.
- When a child account reached via an MCC is selected, `login_customer_id` is set to the parent MCC while `customer_id` is set to the selected child.
- `mureo auth status` now shows both `customer_id` and `login_customer_id` when they differ.

## [0.3.1] - 2026-04-10

### Fixed
- Google Ads OAuth setup fails with "Address already in use" when port 8085 is occupied. The local OAuth callback server now picks an available port automatically (port=0), matching the Meta Ads setup behavior.

## [0.3.0] - 2026-04-06

### Added
- Evidence-based learning feedback loop with `_mureo-learning` skill
- `ActionLogEntry.metrics_at_action` field for recording metrics at action time
- `ActionLogEntry.observation_due` field for scheduling outcome evaluation
- Statistical thinking framework: observation windows, minimum sample sizes, evidence lifecycle (OBSERVING → CANDIDATE → VALIDATED)
- Noise guards in all 10 workflow commands to prevent premature optimization
- Google Search Console API client with 10 MCP tools, bringing total to 169
- Japanese README (README.ja.md)

### Changed
- All 10 workflow commands transformed to platform-agnostic orchestration (no hardcoded tool names)
- Commands now discover platforms at runtime from STATE.json `platforms` dict
- Search Console and GA4 integrated as data sources across all commands
- STATE.json format updated to v2 with multi-platform `platforms` dict
- SKILL.md version bumped to 0.3.0 with orchestration paradigm
- Workflow skill count increased from 5 to 6

### Fixed
- CONTEXT.md tool count corrected from 42 to 169
- Search Console tool reference pointed to correct documentation file

## [0.2.0] - 2026-03-31

### Added
- 78 new MCP tools (53 Google Ads + 25 Meta Ads), bringing total from 81 to 159
- Google Ads: sitelinks, callouts, conversion tracking, device/location/schedule targeting, recommendations, bid adjustments, change history, performance analysis, budget analysis, RSA asset analysis, B2B optimizations, creative research, landing page analysis, monitoring & goal evaluation, screenshot capture
- Meta Ads: campaign/ad set/ad pause & enable, audience get/delete/lookalike, creative list/create/dynamic, pixel management, performance analysis, audience analysis, placement analysis, cost investigation, ad comparison, creative improvement suggestions
- Handler tests for all 159 MCP tools (92 new tests, 1257 total)
- Project logo in README

### Changed
- Broadened project description from "Ad operations" to "Marketing operations" for multi-platform future
- Default branch renamed from `master` to `main`
- Split MCP tool definitions into category-based sub-modules for maintainability
- Split MCP handler files by domain (extensions, analysis, extended, other)

### Fixed
- All ruff lint errors (A002, F541, TC002, TC003, SIM102, SIM103, SIM105, SIM401, E402, E741, F811, F841, I001, UP037)
- All mypy type checking errors across Python 3.10 and 3.12
- Black formatting applied to all source files
- Bandit security warning (false-positive B104 on SSRF blocklist)
- Input validation: customer_id format, account_id prefix, file path traversal protection, URL scheme validation

### Security
- Added file extension validation on image/video upload handlers
- Added URL scheme validation on screenshot capture handler
- Added customer_id numeric format validation
- Added account_id `act_` prefix validation

## [0.1.0] - 2026-03-31

### Added
- Google Ads API client with 29 MCP tools (campaigns, ad groups, ads, keywords, budget, performance analysis, diagnostics, image upload)
- Meta Ads API client with 52 MCP tools (campaigns, ad sets, ads, creatives, audiences, pixels, insights, Conversions API, Lead Ads, Product Catalog, A/B testing, Ad Rules, videos, carousel, collection, Instagram, page posts)
- MCP server (stdio transport) for integration with AI agents
- CLI (Typer-based) with 15 commands for Google Ads, Meta Ads, and auth management
- Interactive setup wizard (`mureo auth setup`) with browser-based OAuth and account selection
- File-based strategy context (STRATEGY.md / STATE.json)
- Credential management (~/.mureo/credentials.json + environment variable fallback)
- Automatic MCP configuration placement (global or project-level)
- Comprehensive documentation (architecture, authentication, MCP server, CLI, strategy context, contributing)
- SKILL.md files for AI agent integration
