Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mureo",
"version": "0.10.43",
"version": "0.10.44",
"description": "Your local-first AI ad ops crew for Google Ads, Meta Ads, Search Console & GA4. mureo sits on top of the official ad-platform MCPs, gates every change against your strategy, correlates outcomes locally, and keeps an auditable decision log — credentials never leave your machine.",
"author": {
"name": "Logly Inc.",
Expand Down
172 changes: 86 additions & 86 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.10.44] - 2026-08-12

### Fixed

- **Every Meta lead-form create was a guaranteed 400, and duplicating a
Expand Down Expand Up @@ -251,6 +253,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the two guards: the path guard resolves symlinks in each direction, the
Bash guard never touches the filesystem and cannot.

- **Campaign flight dates are read from the fields v23 actually has.**
`map_campaign` read `campaign.start_date` / `campaign.end_date`. The v23
`Campaign` has neither — it spells the flight `start_date_time` /
`end_date_time` — and because both reads sat behind `hasattr` guards, the
keys were simply never emitted. No error, no empty string, just two absent
keys. The campaign date-range diagnosis in `_diagnostics.py` reads exactly
those keys, so "campaign start date is in the future" and "campaign end date
has passed" could never fire, and a finished flight was reported as a healthy
campaign that happened to have stopped spending.

Both layers were stale, so both were fixed: the mapper now reads
`start_date_time` / `end_date_time` and narrows each to `YYYY-MM-DD` (the
consumers compare whole days), and the two GAQL queries that feed it
(`list_campaigns`, `get_campaign`) now select those fields — neither selected
any date field, so correcting the attribute name alone would still have
yielded nothing. The output keys stay `start_date` / `end_date`. The same
narrowing is now shared with `_daily_delivery_row` rather than duplicated.

- **The mapper layer is now swept against the proto, not just the queries.**
`tests/test_gaql_field_names.py` validated every GAQL `SELECT` against the
live descriptors, which is why the flight-date bug survived it: a
`hasattr(campaign, "end_date")` is invisible to a query sweep, and
`MagicMock` answers `hasattr` for any name, so every test agreed with the
broken code. The file now also resolves the literal attribute reads in
`google_ads/mappers.py` against the proto, covering `x.field`,
`hasattr`/`getattr`, and the `_safe_str` / `_safe_int` / `_safe_float`
helpers that most of the file actually uses. The helper names are derived
from the source rather than listed, the extraction is pinned against an
independent count, and each read subject must be either bound to a message or
listed as unbindable with a reason — so the sweep cannot quietly decay into a
no-op, which is the failure mode it exists to prevent.

It immediately found a second instance: `map_tag_snippet` read
`snippet.page_header`, which `TagSnippet` does not have, so the conversion
tag's page-header snippet was always `""`. It now reads `global_site_tag`;
the response key stays `page_header` for compatibility with the documented
`google_ads_conversions_tag` contract.

- **A targeted STATE.json write no longer resets fields it does not own.**
All five mutators (`upsert_campaign`, `append_action_log`, `set_report`,
`set_platform_metrics`, `set_conversion_action_types`) rebuilt the
`StateDocument` — and three of them the `PlatformState` — by enumerating
every field. That works until a field is added, at which point every mutator
that forgot it silently resets it, and a reset field is indistinguishable
downstream from one that was never set.

They now use `dataclasses.replace` and change only the fields each call
actually owns, so preservation is structural rather than remembered. A new
field on either model is carried across by every mutator with no edit.

The same shape was found and fixed in `_merge_campaign_metrics`
(`mureo/analytics/builtin/_live_clients.py`), which folded two rows for one
campaign by enumerating five of `CampaignMetrics`'s seven fields and dropped
`cpa` / `ctr`. Inert today because nothing populates them — the exact state
the other instances were in before a field was added. The two duplicated
merge blocks are now one helper, and `cpa` / `ctr` are cleared *explicitly*
rather than by omission: they are ratios, cannot be summed, and carrying one
row's value across would report it as the total's.

A fifth was found in `preflight_tracking_consistency`
(`mureo/analysis/tracking/checks.py`) while rebasing onto the commit that
introduced it: narrowing a `TrackingConsistencyReport` to the planned ads
enumerated all five of its fields, so nothing is dropped today and the sixth
field added to that model would be. Two helpers beside it in the same file
already used `replace`.

This is the same defect that dropped `archived` from a renamed agency client
and `origin` from a batched `action_log` entry. Finding it four times did
not prevent the fifth, because the failure is an omission and omissions are
invisible in review — so the fix comes with tests driven off
`dataclasses.fields(...)`: each names only what its mutator declares it
changes and asserts everything else survived.

`state_codec` must keep enumerating (it maps to an external JSON schema, so
`replace` cannot help it), so it gets two guards with a clear division of
labour. Its field coverage is asserted **at module import**, so adding a
field without naming it there raises immediately — on any `import mureo`, a
REPL, or a `pytest -k` run that never collects the round-trip test. That
check asserts *declaration*; the round-trip tests assert the field actually
**survives**, and they now cover every model the codec touches — including
the nested `ActionLogEntry` and `AdState`, where a declared-but-unwired
field would previously have round-tripped to `None` unnoticed because the
fixtures left it at its default.

### Added

- **Delivery-collapse detection and diagnosis, across all platforms** (#546).
Expand Down Expand Up @@ -851,92 +937,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
campaign carrying the scheme on a single ad when the campaigns use different
landing pages.

### Fixed

- **Campaign flight dates are read from the fields v23 actually has.**
`map_campaign` read `campaign.start_date` / `campaign.end_date`. The v23
`Campaign` has neither — it spells the flight `start_date_time` /
`end_date_time` — and because both reads sat behind `hasattr` guards, the
keys were simply never emitted. No error, no empty string, just two absent
keys. The campaign date-range diagnosis in `_diagnostics.py` reads exactly
those keys, so "campaign start date is in the future" and "campaign end date
has passed" could never fire, and a finished flight was reported as a healthy
campaign that happened to have stopped spending.

Both layers were stale, so both were fixed: the mapper now reads
`start_date_time` / `end_date_time` and narrows each to `YYYY-MM-DD` (the
consumers compare whole days), and the two GAQL queries that feed it
(`list_campaigns`, `get_campaign`) now select those fields — neither selected
any date field, so correcting the attribute name alone would still have
yielded nothing. The output keys stay `start_date` / `end_date`. The same
narrowing is now shared with `_daily_delivery_row` rather than duplicated.

- **The mapper layer is now swept against the proto, not just the queries.**
`tests/test_gaql_field_names.py` validated every GAQL `SELECT` against the
live descriptors, which is why the flight-date bug survived it: a
`hasattr(campaign, "end_date")` is invisible to a query sweep, and
`MagicMock` answers `hasattr` for any name, so every test agreed with the
broken code. The file now also resolves the literal attribute reads in
`google_ads/mappers.py` against the proto, covering `x.field`,
`hasattr`/`getattr`, and the `_safe_str` / `_safe_int` / `_safe_float`
helpers that most of the file actually uses. The helper names are derived
from the source rather than listed, the extraction is pinned against an
independent count, and each read subject must be either bound to a message or
listed as unbindable with a reason — so the sweep cannot quietly decay into a
no-op, which is the failure mode it exists to prevent.

It immediately found a second instance: `map_tag_snippet` read
`snippet.page_header`, which `TagSnippet` does not have, so the conversion
tag's page-header snippet was always `""`. It now reads `global_site_tag`;
the response key stays `page_header` for compatibility with the documented
`google_ads_conversions_tag` contract.

- **A targeted STATE.json write no longer resets fields it does not own.**
All five mutators (`upsert_campaign`, `append_action_log`, `set_report`,
`set_platform_metrics`, `set_conversion_action_types`) rebuilt the
`StateDocument` — and three of them the `PlatformState` — by enumerating
every field. That works until a field is added, at which point every mutator
that forgot it silently resets it, and a reset field is indistinguishable
downstream from one that was never set.

They now use `dataclasses.replace` and change only the fields each call
actually owns, so preservation is structural rather than remembered. A new
field on either model is carried across by every mutator with no edit.

The same shape was found and fixed in `_merge_campaign_metrics`
(`mureo/analytics/builtin/_live_clients.py`), which folded two rows for one
campaign by enumerating five of `CampaignMetrics`'s seven fields and dropped
`cpa` / `ctr`. Inert today because nothing populates them — the exact state
the other instances were in before a field was added. The two duplicated
merge blocks are now one helper, and `cpa` / `ctr` are cleared *explicitly*
rather than by omission: they are ratios, cannot be summed, and carrying one
row's value across would report it as the total's.

A fifth was found in `preflight_tracking_consistency`
(`mureo/analysis/tracking/checks.py`) while rebasing onto the commit that
introduced it: narrowing a `TrackingConsistencyReport` to the planned ads
enumerated all five of its fields, so nothing is dropped today and the sixth
field added to that model would be. Two helpers beside it in the same file
already used `replace`.

This is the same defect that dropped `archived` from a renamed agency client
and `origin` from a batched `action_log` entry. Finding it four times did
not prevent the fifth, because the failure is an omission and omissions are
invisible in review — so the fix comes with tests driven off
`dataclasses.fields(...)`: each names only what its mutator declares it
changes and asserts everything else survived.

`state_codec` must keep enumerating (it maps to an external JSON schema, so
`replace` cannot help it), so it gets two guards with a clear division of
labour. Its field coverage is asserted **at module import**, so adding a
field without naming it there raises immediately — on any `import mureo`, a
REPL, or a `pytest -k` run that never collects the round-trip test. That
check asserts *declaration*; the round-trip tests assert the field actually
**survives**, and they now cover every model the codec touches — including
the nested `ActionLogEntry` and `AdState`, where a declared-but-unwired
field would previously have round-tripped to `None` unnoticed because the
fixtures left it at its default.

## [0.10.43] - 2026-08-07

### Changed
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mureo",
"version": "0.10.43",
"version": "0.10.44",
"description": "Your local-first AI ad ops crew. Works with Claude Code, Cursor, Codex & Gemini.",
"contextFileName": "CONTEXT.md"
}
2 changes: 1 addition & 1 deletion mureo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""mureo — your local-first AI ad ops crew. Works with Claude Code, Cursor, Codex & Gemini."""

__version__ = "0.10.43"
__version__ = "0.10.44"
2 changes: 1 addition & 1 deletion mureo/_data/skills/_mureo-amazon-ads/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _mureo-amazon-ads
description: "Amazon Ads (official MCP, bridged by mureo): query campaigns, ad groups, ads and targets, run reports, and manage account access under Amazon's own tool names."
metadata:
version: 0.10.43
version: 0.10.44
openclaw:
category: "advertising"
requires:
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/_mureo-google-ads/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _mureo-google-ads
description: "Google Ads: Manage campaigns, ad groups, ads, keywords, budgets, and performance analysis."
metadata:
version: 0.10.43
version: 0.10.44
openclaw:
category: "advertising"
requires:
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/_mureo-learning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _mureo-learning
description: "Evidence-based marketing decision framework: statistical thinking for AI agents operating ad accounts."
metadata:
version: 0.10.43
version: 0.10.44
openclaw:
category: "marketing"
requires:
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/_mureo-meta-ads/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _mureo-meta-ads
description: "Meta Ads: Manage campaigns, ad sets, ads, insights, and audiences on Facebook/Instagram."
metadata:
version: 0.10.43
version: 0.10.44
openclaw:
category: "advertising"
requires:
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/_mureo-shared/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _mureo-shared
description: "mureo: Shared patterns for authentication, security rules, and output formatting."
metadata:
version: 0.10.43
version: 0.10.44
openclaw:
category: "advertising"
requires:
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/_mureo-strategy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _mureo-strategy
description: "Strategy Context: Manage business strategy files (STRATEGY.md, STATE.json) for strategy-driven ad operations."
metadata:
version: 0.10.43
version: 0.10.44
openclaw:
category: "advertising"
requires:
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/ad-fatigue-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: ad-fatigue-check
description: "Detect creative fatigue across active ads — rising frequency, declining CTR week-over-week, and CPM drift — score each ad FATIGUED / WATCH / FRESH, and hand the evidence to a creative refresh. Use when the user asks whether creatives are worn out, why CTR is falling, if frequency is too high, when to rotate or refresh ads, or requests クリエイティブ疲弊チェック / 広告の疲弊を確認 / フリークエンシーが高い / CTRが落ちてきた / そろそろ差し替え時か. Reads active ads per platform, applies documented fatigue thresholds with noise guards, and routes fatigued ads to /creative-generate or /creative-refresh."
metadata:
version: 0.10.43
version: 0.10.44
---

# Ad Fatigue Check
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/audience-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: audience-review
description: "Audit who your ads actually target and where they run, compare it against the STRATEGY.md Persona, and surface exclusions, bid adjustments, lookalikes, and placement pruning tied to that Persona. Use when the user asks to review targeting, audiences, demographics, placements, or device performance, to check whether spend matches the Persona, to find wasted placements (e.g. Audience Network with no conversions), or requests オーディエンスレビュー / 配置レビュー / ターゲティング見直し / ペルソナと配信のズレを確認 / 除外設定を提案して. Reads Persona + Target Audience from STRATEGY.md and drives the read-only targeting tools."
metadata:
version: 0.10.43
version: 0.10.44
---

# Audience Review
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/budget-pacing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: budget-pacing
description: "Month-to-date spend vs the monthly budget target, projected month-end landing, and pace alerts across all configured platforms. Use when the user asks about pacing, burn rate, whether they will overspend or underspend this month, monthly-budget tracking, landing/forecast, 'are we on budget', or requests 予算ペーシング / 着地予測 / 予算消化ペース. DISTINCT from /budget-rebalance (which reallocates budget between campaigns) — this manages total-spend trajectory toward a monthly target. Reads STRATEGY.md Guardrails / a Monthly Budget section and STATE.json."
metadata:
version: 0.10.43
version: 0.10.44
---

# Budget Pacing
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/budget-rebalance/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: budget-rebalance
description: "Rebalance campaign budgets across all configured platforms based on strategy and performance signals. Use when the user asks to optimize budgets, redistribute spend, scale efficient campaigns, or cap overspending ones. Reads STRATEGY.md goals, analyzes campaign efficiency, and proposes budget changes with rationale. Also use when the user asks in Japanese (予算を最適化して / 予算の再配分 / 好調なキャンペーンに予算を寄せて / 予算リバランス)."
metadata:
version: 0.10.43
version: 0.10.44
---

# Budget Rebalance
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/competitive-scan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: competitive-scan
description: "Scan competitor activity using auction insights and market signals. Use when the user asks about competitors, market dynamics, impression share changes, competitor moves, or competitive positioning. Also use when the user asks in Japanese (競合の動きを調べて / 競合分析 / インプレッションシェアの変化を確認 / 競合にシェアを取られていないか)."
metadata:
version: 0.10.43
version: 0.10.44
---

# Competitive Scan
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/creative-generate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: creative-generate
description: "Generate creator-quality ad creatives — text-free key visuals plus composed banners — from a strategy-grounded brief. Use when the user asks to create ad creatives, generate ad images or banners, make banner variations, design display / social ad creatives, or asks in Japanese (クリエイティブ作成 / バナー作成 / 広告画像を生成 / バナーのバリエーションを作って). Runs a 6-step workflow (brief → copy → visuals → art-direction scoring loop → HTML/CSS composition → delivery) via the creative_studio_* MCP tools, then hands approved banners to the existing upload tools."
metadata:
version: 0.10.43
version: 0.10.44
---

# Creative Generate
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/creative-refresh/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: creative-refresh
description: "Refresh ad copy and creative assets based on performance signals and brand voice. Use when the user asks to refresh creative, propose new ad copy, A/B test creatives, update RSA assets, rotate underperformers, or visually evaluate / compare banner (image) creatives. Also use when the user asks in Japanese (クリエイティブを刷新して / 広告文の改善案がほしい / RSAアセットの入れ替え / バナーを比較評価して)."
metadata:
version: 0.10.43
version: 0.10.44
---

# Creative Refresh
Expand Down
2 changes: 1 addition & 1 deletion mureo/_data/skills/daily-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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.10.43
version: 0.10.44
---

# Daily Check
Expand Down
Loading