Two tools on the maskan_crm server: one to list the fields that exist, one
to define a new one. Asking in MaskanX chat for a field now creates it in
the CRM, where it is usable on the next record and visible in the CRM's
own screens.
The list tool exists mainly so the create tool has something to check
against — without it an agent invents a near-duplicate of a field that is
already there under a slightly different name, and its description says
so.
Nulls are stripped from the definition before it is sent, so the CRM's
defaults apply rather than being overwritten with None. A rejected
definition comes back as the CRM's own message, which is what lets an
agent correct itself and retry rather than reporting a constraint
violation to the user.
_request now accepts a list response behind an explicit flag. Every
endpoint returns an object except the collection reads, and an unexpected
array is more likely a proxy's error page than data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the three things most likely to be broken by a well-meant change:
insights are re-fetched and upserted rather than appended, breakdown rows
duplicate the spend they break down and must be filtered out of
aggregates, and both dashboard windows end yesterday because today is
partial.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the insights reader, a pure guardrail evaluator, and the sweep that
applies them to live campaigns.
Three things carry the weight here:
Units. Meta reports spend in major units ("12.34") while budgets and
guardrails are in minor units (1234). normalise_row converts, rounding
half up rather than using round(), which rounds halves to even and can
record an exact half-unit of spend as nothing.
Leads. Meta has no leads field; leads live in the actions array, under
several action types depending on whether the lead came from a Facebook
form or a pixel. Cost per lead is computed from spend and leads over the
same window rather than read from cost_per_action_type, so the two can
never disagree.
Order. The campaign is paused on Meta before the local record changes. A
campaign recorded as paused but still delivering is the outcome this
exists to prevent. One campaign's failure never aborts the sweep, so a
rate limit on the third does not leave the fourth unguarded.
Cost rules are skipped until the first lead or click: no leads yet is not
an infinite cost per lead, and pausing for that would kill every campaign
in its first hour.
Deliberately a deterministic loop rather than a maskanx_cron_jobs entry.
That scheduler runs prompts through an agent, and asking a language model
whether a budget has been exceeded would make an arithmetic guarantee
probabilistic. Follows reconcile.py's lifespan-task pattern instead, and
warns every cycle if Meta is unconfigured — a safety system that cannot
run should be loud.
require_approval_for_budget_increase is enforced at the API layer, where
the budget is actually edited: raising it while a campaign awaits approval
returns 409, since it would change what the approver is reviewing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Launch set only the campaign to ACTIVE. Sync creates the ad set and ad
PAUSED, and Meta delivers only when the ad, its ad set and its campaign
are all active — so Launch reported "live" while nothing ran. This was
the central promise of Phase 2 and it did not work.
The chain is now activated children-first, campaign last. Nothing under a
paused campaign delivers, so a failure part-way leaves the campaign unable
to spend. That ordering is also why pause and stop only flip the campaign.
An imported campaign has no stored ad set or ad ids, so launching it still
touches only the campaign and its children keep the statuses set in Ads
Manager.
Deleting an imported campaign is now refused rather than silently
pointless: deleting it on Meta would destroy work MaskanX did not author,
and deleting only the local row achieved nothing because the reconciler
re-imported it on the next cycle. Deleting it in Ads Manager is what
sticks, after which the reconciler archives the record.
Also corrects the README's reconciler interval: it is 120s, not 300s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README still described Phase 1's "creates nothing in Meta" contract,
which is no longer true. It now covers the lifecycle, that sync creates
only paused objects and why that is enforced in three places, what launch
requires, adopt/delete semantics for imported campaigns, and the
reconciler interval.
Also documents the separate campaign_test database. Without it the
repository integration tests skip and the suite still passes, so the skip
count matters as much as the exit code.
The live smoke test is the only thing that proves Meta accepts what the
client sends: every other test runs against a fake transport. It syncs a
real campaign, reads back from Graph that all three objects are PAUSED,
and deletes them in a finally block. Opt-in via MASKANX_LIVE_TESTS=1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records what Phase 1 does and deliberately does not do: no Meta writes, no
launch or sync endpoint, previews rendered without creating objects, budget
minimums enforced locally, and the fact that a payment method can only be
attached in Meta Business Manager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Independent FastAPI backend for the MaskanX agentic growth platform.
Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn,
HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage
for chats and cron jobs, provider and secret management, and the CLI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>