Weekly Brand MAA — canonical SOP

Skill packs › Skill

Weekly Brand MAA — canonical SOP

The canonical weekly MAA (Metrics → Analysis → Action) SOP every per-client brand agent runs. One source of truth for the MAA discipline, the Personal Brand Score, safe site-update policy, and delivery format. Each scheduled agent passes a PARAMETERS block and follows this file. Improve the method here once and every agent inherits it.

Skill file weekly-brand-maa.md · last updated Aug 12, 2026

How to run it. Download any pack from the skill-pack directory, unzip it into your Claude project folder, and this skill is one of the files inside. You do not paste it anywhere — the agent reads it when the job calls for it.

Use this when a scheduled per-client agent runs its weekly report. The calling task supplies a PARAMETERS block; you execute the steps below using those parameters. This file is the single source of truth for the MAA discipline so the per-client agents stay thin and consistent. Never improvise the method — deviations are data; if something’s missing, flag it and (per recursive-self-improvement-qa) propose the fix back into this file.

PARAMETERS the caller provides

` entity_name: # e.g. “Trenton Sandler” | or a roster file for agency mode mode: # personal-brand | agency-roster depth: # full | tracker-lite canonical_brief: # absolute path to the verified-facts brief (read FIRST). For agency-roster: the roster YAML. domains: # entity-home + owned domains (Ahrefs target; mode=subdomains) handles: # social handles to read live (YouTube/IG/TikTok/X/LinkedIn) gsc_property: # GSC URL-prefix property if verified (authoritative search signal) baseline_path: # prior snapshots/reports dir for week-over-week + vs-baseline deltas site_update_policy: # additive-auto | stage-only | none brand_score: # on | off delivery: # basecamp_url | gmail_draft:<addr> | file_only (always ALSO save a file copy) report_dir: # where to save the dated report + log extra_steps: # optional entity-unique steps (e.g. disavow append, content repurposing, target-keyword list) escalate_rule: # optional — a plain-language watch condition + what counts as “escalate” (e.g. “if organic traffic declines 3 consecutive weekly checks, say so explicitly to Dennis at the top of the report, don’t bury it in Analysis”) context_channels: # optional — other threads/boards to read for context before writing the report (e.g. a partner team’s own Basecamp thread or weekly report), when the entity has its own people/agents also reporting `

All 7 personal-brand/agency-roster client agents below call this file rather than re-deriving the loop. Ahrefs MCP tools referenced throughout are server mcp__ea56e910-0a35-4107-aeee-16d873278687__* (load via ToolSearch on first use each run).

Currently called by (update this list when you retrofit or retire a caller)

  • trenton-sandler-weekly-maa — mode=personal-brand, depth=full, site_update_policy=additive-auto, escalate_rule set. Cadence changed weekly → twice monthly (1st + 15th) on 2026-07-31 per Dennis (the task ID still says “weekly”; renaming it would break its run history, so the ID is now cosmetic — same situation as cxotalk’s delivery param above).
  • cxotalk-weekly-maa — mode=personal-brand, depth=tracker-lite (SOW-milestone framing instead of brand_score); client comms live in Basecamp bucket 48001656 / message 10075591082 (client-visible), redirected there by ops 7/17. Resolved 7/27: the delivery param still literally says gmail_draft:mkrigsman@cxotalk.com, but STEP 6.6 makes the weekly Basecamp post happen regardless, so the param is now cosmetic — the run posts to Basecamp and stages the internal Gmail draft to Dennis. Updating the param only removes a standing deviation note.
  • kingdom-broker-friday-maa — mode=agency-roster (multi-client CRM/GBP/ads roster)
  • family-law-leaderboard-weekly-checkin — mode=personal-brand, depth=tracker-lite, escalate_rule + context_channels set (reads the implementing team’s own Basecamp thread)
  • anthony-hilb-seo-tracker — mode=personal-brand, depth=tracker-lite, brand_score=off
  • wtp-monthly-seo-reaudit — mode=personal-brand, depth=tracker-lite, brand_score=off, site_update_policy=none
  • somba-weekly-maa — mode=personal-brand (agency-roster-flavored: one call fans out to ~78 members), delivery is bespoke (Elementor dashboard blob, not Basecamp/Gmail) — this file supplies the MAA/Funnel methodology only; see somba-weekly-maa‘s own SKILL.md for the Sigrun-specific publish mechanics, which are legitimately unique and should stay there.

Two more client cadence agents (igor-ivitskiy-monthly-brand-refresh, junks-above-daily-progress) are a different shape — relationship-maintenance + approval-gated site work, not a scored metrics loop — and call client-relationship-cadence.md instead. Don’t force them onto this file; see that SOP.

STEP 0 — Load context

Read canonical_brief (verified facts, IDs, handles, publish recipe) and the newest file in baseline_path (last week’s numbers). If you discover a NEW verified fact, update the brief. There is no memory between runs — the brief + last report ARE the memory.

  • If baseline_path is empty (first run): check whether the calling task’s extra_steps embeds an original baseline (numbers gathered when the account was first audited/onboarded, before this scheduled agent existed) — use that as the week-over-week comparison point instead of skipping deltas. Say explicitly in the report that this is the first tracked run and that it becomes the new baseline going forward.
  • Duplicate/same-day re-trigger guard: if report_dir already contains a report dated TODAY, this is a re-trigger (manual “Run now”, permission pre-approval click, or scheduler hiccup) — do NOT re-run the metrics loop and NEVER deliver twice. Instead: verify the earlier run completed every step (report file, log line, brief update, and the draft/post — confirm the draft is still DRAFT, or was legitimately sent by Dennis; check the thread for client replies while you’re there), fix only what’s genuinely missing, append anything new as a dated addendum to today’s report, and stop. A duplicate client-facing send is worse than a skipped run. (Added 2026-07-17 after the cxotalk evening re-trigger — the verify path also caught the client’s same-day reply and a channel-change instruction that would otherwise have waited a week.)

STEP 1 — METRICS (business first, then diagnostics)

Pull only what the parameters enable; note any source that’s unavailable rather than guessing.

  • Audience (live): read current follower/subscriber counts for each handle (Chrome JS from page data; Google snippet if walled). Record values + week-over-week deltas.
  • Authority/SEO (Ahrefs MCP, target = domains, mode=subdomains, today): domain-rating, site-explorer-metrics (org_keywords, org_traffic), top-pages, backlinks-stats, referring-domains (flag NEW domains; legit vs spam). Site Explorer’s date param rejects future/today’s dates on some plans — if you get "bad date", step back 1-2 days until it resolves.
  • Brand Radar / AI-citation tools specifically: these need either a saved report_id (a Brand Radar project already configured in the Ahrefs dashboard) or prompts: "ahrefs" premade prompts — and even then, each data_source (chatgpt, google_ai_overviews, grok, etc.) is a separate paid add-on that hard-errors (Missing addon: Brand Radar [...]) rather than degrading gracefully if the workspace’s plan doesn’t include it. Before spending calls on Brand Radar, call subscription-info-limits-and-usage (free, no units) to check the plan tier. If the addon’s missing, don’t retry with different data_source combos — note the gap plainly (per Step 6.5) and move on.
  • Fetching BlitzMetrics-fleet or other Cloudflare-protected client sites: raw curl/bash fetches return bot-challenge 403s (“Just a moment…”) even for plain static pages like sitemaps. Default straight to Chrome MCP (navigate + get_page_text, or browser_batch) for any client-site check — don’t burn a round-trip discovering the 403 first.
  • Search Console (AUTHORITATIVE when gsc_property set): clicks, impressions, avg CTR, avg position, top queries + top pages (last 28 days, W-o-W). Use as the primary search-presence signal; fall back to Ahrefs estimates only if GSC isn’t reachable.
  • Agency-roster mode also pulls business outcomes, per client, comparing the prior 7 days vs. the 7 before that: GA4/Google Ads/CallRail via the Windsor.ai MCP (sessions by channel, conversions, top landing pages, booked-call count by source, ad spend + conversions); GBP rank grid via the Local Falcon MCP (grid average rank, % top 3, % top 10, best/worst points) if a campaign_id is configured; CRM jobs via whatever crm.access_method the roster specifies (api = call it directly, csv = read the newest file in the configured dropbox path, chrome = log in via Claude in Chrome and pull the last-7-days report, unknown = skip and flag, ask Dennis to confirm). Skip+flag any connector that isn’t installed or configured — never abort the whole run over one missing source; note it as an explicit action item (“Connect X — Dennis, by next Friday”).
  • New results/press + Brand SERP: new mentions (past week), new content the entity published, and whether a Knowledge Panel renders and the entity-home ranks for the entity’s name.
  • If context_channels is set: before writing Analysis, read those threads/boards too (e.g. a partner team’s own weekly report or Basecamp thread) so you’re reacting to what they already said, not duplicating or contradicting it.

STEP 2 — ANALYSIS (the “why” — 10x more important than metrics)

For each meaningful change, explain WHY, tied to the entity’s goals (own the entity, trigger a Knowledge Panel, grow audience, monetize). Never optimize to a single metric — name the counterbalancing metric (publishing volume vs. indexation; followers vs. owned traffic). In agency-roster mode, correlate digital cause to business outcome in dollars (“calls down 18% because the Plano page fell #4→#9 after the core update ≈ 6 lost jobs at $X = $Y”). If nothing moved, explain what’s gating it. If the data looks wrong, say so — don’t smooth it over.

  • If escalate_rule is set: check it explicitly every run (e.g. “organic traffic down 3 consecutive weekly checks”). If it trips, put the escalation as the FIRST line of the report, addressed to Dennis by name, not folded into the middle of Analysis — the whole point of the rule is that it doesn’t get missed.

STEP 3 — ACTION (2–3 specific, assigned, due-dated items for next week)

Concrete and verifiable, each with an owner and a due day. Prefer “publish the rewritten /ac-repair-plano/ page by Tue — Eric” over “improve SEO.” Tie every action to a goal from Step 2.

STEP 4 — SAFE SITE UPDATES (governed by site_update_policy)

  • additive-auto: if logged in (check /wp-admin first; fleet WAF blocks Basic Auth → use cookie + X-WP-Nonce from window.wpApiSettings.nonce), apply low-risk additive updates only: add a new legit mention to /media/ + Person sameAs; refresh stats; publish a substantive repurposed SEO article (entity as author, verified content only, link to source, internal-link the cluster); set Rank Math title/desc where missing. Validate render after.
  • stage-only: write the changes as drafts/to-dos in report_dir; note that a wp-admin login is needed.
  • none: skip (tracker-lite).
  • Structural changes (layout/nav/schema overhaul): ALWAYS stage for human review, never auto-apply.
  • New spam backlinks: append to the entity’s disavow.txt if extra_steps names one.

STEP 5 — PERSONAL BRAND SCORE (when brand_score = on)

Re-score the 100-pt rubric: Entity Home 20, Knowledge Panel 15, Search Presence 15, Content 15, Audience 15, Schema 10, Social 10 (https://blitzmetrics.com/personal-brand-score/). Show total + per-component vs last week.

STEP 6 — DELIVER

  1. Save the report to report_dir/MAA-YYYY-MM-DD.md and append a one-line entry to MAA-LOG.md (create if missing). ALWAYS keep the file copy regardless of channel.
  2. Deliver per delivery: post to the Basecamp thread via Claude in Chrome (Dennis stays logged in; verify the post appeared) OR create a Gmail draft to the given address OR file_only. If a Basecamp URL is configured but unreachable, fall back to a Gmail draft and note it.
  • Basecamp two-thread rule (added 2026-07-19, Dennis’s explicit instruction): most client projects have both a client-visible thread and a separate internal-only “Updates” thread. Default to internal. Only post to the client-visible thread when the update is genuinely interesting/noteworthy to the client (a real win or milestone) — routine no-change verification passes, internal process/incident notes, and anything with internal-only commentary always go to the internal thread, never client-visible. Confirm which kind of thread you’re looking at via Basecamp’s “The client can see this” banner, don’t assume from the thread name.
  • Gmail-draft delivery is DRAFT-ONLY, always — no exceptions. Use the draft-creation tool; never a send-message tool. This applies even when the configured recipient is Dennis himself — a scheduled/autonomous run never has standing to put a message in someone’s inbox unreviewed. Before marking this step done, re-fetch the message (search_threads/list_drafts/get_message) and confirm its label is DRAFT, not SENT, and that the To: field matches the delivery parameter’s address exactly. Pulling the entity’s own contact info (athlete/client personal email) out of the canonical brief instead of the configured fallback address is a real, observed failure mode (2026-07-17, Trenton Sandler run: a report meant for gmail_draft:dennis@blitzmetrics.com was instead fully SENT to the athlete’s personal address + a teammate, skipping Dennis’s review entirely) — not a hypothetical one. If you ever discover a prior run sent instead of drafted, do not attempt to unsend or delete it; flag it plainly at the top of the next report and let Dennis decide on any follow-up.
  • Agency-roster mode: deliver per-client (one Basecamp post per client with a basecamp.project_url configured; skip+flag clients without one) AND always send one combined summary email to YOUR OWN inbox (the owner address configured for this agent — this skill ships in public packs, so it must never carry someone else’s address) with one bullet per client — headline metric delta, biggest action, link to that client’s Basecamp post or a note that posting was skipped. Never put internal-only figures (EBITDA, valuation, exit plans) in a client-facing Basecamp post — those stay in the Dennis-only summary email.
  1. Keep it tight: lead with business metrics + the 2–3 actions; diagnostics below. Plain English, encouraging, honest. Client-facing posts never include internal commentary (EBITDA, valuation, exit) — that goes only to Dennis.
  2. Run extra_steps (entity-unique work like content repurposing) where provided.
  3. If a connector or login needed this run isn’t working, don’t retry in bursts (WAF risk) — save what you have, note the gap plainly in the report, and say what Dennis needs to do to unblock next run.

REPORT FORMAT (personal-brand)

` {ENTITY} — WEEKLY MAA REPORT — {date} ★ Personal Brand Score: {n}/100 ({Δ}) (omit if brand_score off) METRICS: {audience w/ deltas} | DR {dr}; keywords {kw}; organic traffic {t}; new backlinks {legit}/{spam}; new mentions {list}; new content {list}; Brand SERP {KP? ranks for name?}. ANALYSIS: {why, tied to goals; counterbalancing metric}. ACTION: 1) … 2) … 3) … WHAT WE DID: {updates/articles/disavow}. WHAT WE NEED FROM {ENTITY}: {footage, login, approval}. ` Agency-roster mode uses the METRICS → ANALYSIS → ACTIONS sections per client (top-of-funnel + bottom-of-funnel + leading indicators), 500–800 words each, Basecamp-ready.

STEP 6.6 — DELIVERY IS NOT OPTIONAL (added 2026-07-24, Dennis’s explicit instruction)

“Update Basecamp every week with what’s going on without needing me to initiate.” A run that gathered perfect metrics and didn’t land in the channel is a failed run. Three rules make delivery self-healing:

  1. Post-always. The weekly post goes up whether or not anything changed, whether or not the client

replied, whether or not the news is good. “Nothing moved and here’s why” IS the update — silence from our side is indistinguishable from a dead agent, which is exactly the failure the client just had.

  1. Verify, then trust nothing. After posting, re-fetch the thread and confirm your comment is present

server-side (fresh navigation, not the optimistic in-page render). Only then mark delivery done. Degradation banners can be stale — Basecamp rendered a “database is in read-only mode” banner on 2026-07-24 while the status page read all-operational and the post in fact succeeded. Never let a banner alone stop you: check the vendor status page, then ATTEMPT the post and let the attempt be the verdict. Only a failed attempt is a real outage.

  1. Queue, never drop. If the post genuinely fails, write the exact post body to

report_dir/UNPOSTED/YYYY-MM-DD.md, note the failure in the report, and send the Gmail draft fallback. At STEP 0 of every subsequent run, check UNPOSTED/ first — if anything is queued, post it (labeled with its original date) before the current week’s report, then delete the queued file. A skipped post must resurface by itself; it may never depend on a human remembering.

Also: re-confirm the destination every run. Ops rotates Basecamp threads (Updates (Continuation-N)N+1) with only a pointer comment. Before posting, verify the configured thread is still the active one and that its “The client can see this” banner matches the intended audience — visibility does not carry over to the successor thread. If the thread moved, post to the new one and record the new bucket/message ID in the report so the next run inherits it.

STEP 6.7 — TRACK UNANSWERED ASKS WITH A COUNTER (added 2026-07-24)

When a client or partner goes quiet, the failure mode is that an ask gets re-asked politely forever and nobody notices it’s been dead for a month. Maintain report_dir/ASK-LEDGER.md: one row per open ask with owner, first-asked date, a miss counter, last status, and what it gates. Read it at STEP 0, rewrite it at STEP 6, and append a one-line counter history entry per run (never rewrite history).

Classify each ask every run as ANSWERED / PARTIAL / SILENT — PARTIAL and SILENT both increment. Then apply the ladder automatically, naming the rung in the report: 1–2 misses restate in one line · 3 misses name it as overdue with the count visible plus a yes/no for Dennis offering to route around the blocked person · 4 misses top of the report, addressed to Dennis by name, recommend off-channel contact or reassignment with a specific workaround · 5+ misses declare the channel dead for that ask, stop re-asking, state the assumption you’re proceeding under, execute the workaround, log it.

Two fairness rules that keep the counts credible: never inflate a count, and when someone partially delivers, say what they DID deliver in the same breath as the count. The counter is there to make drift visible, not to build a case against anyone.

Third rule, added 2026-08-01 — the “route around” rung has a hard exception: never route around your contact into their own relationships. The ladder’s rungs 3–4 offer to bypass a blocked person. That is correct when the blockage is organizational (a vendor, a shared inbox, an unstaffed queue) and wrong when the blocked person’s relationship IS the asset — a family member, their boss, their client, their co-founder. Going directly to that person to save a week costs your contact standing on their own project, permanently, in exchange for a scheduling win. It also reads as going over their head, because it is. Where the ask can only be closed through your contact, the escalation path is: restate briefly → offer concrete help that removes the work from them (draft it, run it, pre-fill it, do the pull yourself) → ask the principal to reach them directly, off-channel. Then proceed on a stated assumption. Encode the specific off-limits relationships in the calling task’s parameters so the ladder cannot re-derive the mistake next week — an agent that fires the same wrong rung every Friday is worse than one that never escalates. (Learned when this file’s own ladder told the family-law agent to book a meeting with the implementer’s father directly; Dennis countermanded it, the agent retracted it publicly in the client thread the same night, and the guardrail is now in that task’s parameters.)

NON-NEGOTIABLES

  • Metrics → Analysis → Action, in that order, every time. Analysis and Action are the value.
  • Deliver every week, unprompted, and verify the post landed. A report nobody received didn’t happen.
  • Business outcomes beat vanity metrics. Every number cited has a verifiable source; estimated/missing data is labeled as such. Never fabricate.
  • Posts read like Dennis wrote them — direct, confident, honest about what worked and what didn’t.
  • Self-improve: if you had to guess, the instruction was missing — note it for this file (see recursive-self-improvement-qa).

See also

  • recursive-self-improvement-qa.md (loop this run before moving on) · boil-the-ocean.md (operating principles)
  • dollar-a-day-strategist.md — for ad-amplification reports (e.g. the NaiL dollar-a-day update), which are NOT MAA reports and should use that skill instead.

Learned in the field

Appended automatically by the self-improvement loop (Skill-Learnings/): dated lessons from real runs. Newest at the bottom.

<!– learning:2026-07-28-a-shipped-skill-must-not-carry-your-own-address –> July 28, 2026 (from: skill-pack-propagation daily run, July 28, 2026)

A skill that ships must not carry your address, your staff, or your routing

client-access-checklist was mandated into every pack on July 27, 2026 and went live in all seven public downloads. Written for internal use, it told the reader to add access@localservicespotlight.com and 668sierra@gmail.com as Full users on the client’s Search Console, and to route blocked work to a named staff member and an internal team alias.

weekly-brand-maa was worse in effect: it instructed the agent to “always send one combined summary email to Dennis (668sierra@gmail.com).” Every workshop attendee who installed that pack had an agent whose weekly job was to email us about their clients.

That is not only a privacy leak; it is a functional bug. An instruction that names a specific person is correct in exactly one installation and wrong in every other one.

Rules:

  1. **Before a skill is mandated into distributed packs, read it as a stranger who just

downloaded it.* Every “we”, “our account”, named person and internal alias is a defect. Ask: *if a competitor installed this, what did I just hand them, and who would it email?

  1. Addresses, owners and destinations are CONFIGURATION, not content. Say “the owner

address configured for this agent”; keep the actual values in the internal runbook and the credentials file — the two places that never ship.

  1. Grep the built artifact, not the source folder. These files were fine in the folder they

were written for; the defect only exists once the mandate copies them somewhere else. Add the sweep to the run: fetch each LIVE download and search it for your own addresses. That check takes seconds and is the only one that reflects what a stranger actually receives.

  1. Generalising a skill for distribution is part of mandating it, not a follow-up. The

mandate that copies a file into ten packs is the moment its audience changes.

Learned July 28, 2026.

<!– learning:2026-08-01-decode-xml-entities-before-fetching-sitemap-children –> August 1, 2026 (from: wtp-monthly-seo-reaudit run (Western Trading Post, first tracked run))

Decode XML entities before fetching sitemap child URLs — or you will report a healthy sitemap as dead

Checking whether a client’s /sitemap.xml fix had shipped, the index at /xmlsitemap.php returned 200 and listed five child sitemaps. Fetching each child returned 404 with zero bytes, all five. The obvious read was that the sitemap fix was cosmetic — index alive, every child dead, zero URLs discoverable by Google. That was about to be the report’s headline finding, and it was completely wrong.

<loc> values are XML-escaped. The real URL is ?type=pages&page=1; the sitemap contains ?type=pages&amp;page=1. Fetching the raw captured string sends a literal &amp;, the parameters break, and the server 404s. Decoding entities first, all five children return 200 with 4,516 URLs.

Rules:

  1. Always entity-decode <loc> values before fetching them&amp; &lt; &gt; &quot; &apos;.

This bites hardest on sitemaps with query-string pagination, which is the norm on BigCommerce, Shopify and most hosted carts.

  1. A 100% failure rate across every child is a smell, not a finding. Real breakage is

usually partial. When every single item in a set fails identically, suspect the harness before the target — the same instinct that max_crawl_pages taught on the SERP side.

  1. Never report an infrastructure catastrophe from a single method. Confirm with a second

path (browser navigation to one child URL, or Search Console’s sitemap report) before telling a client their sitemap is dead. The credibility cost of a false alarm this size is far higher than the minute it takes to check.

Same run, same discipline, two more times: a robots.txt parser that reported “zero crawlers blocked” was prove-red tested against a synthetic blocking file first (it correctly caught 2/2) before its zero on the live file was trusted, and cross-checked against a raw count of bare Disallow: / lines. And a +46% referring-domain jump — exactly the shape of a mode/measurement artifact — was confirmed as real by pulling refdomains-history and seeing a steady 13-week climb before it was narrated as growth.

General form of all three: when a check returns the answer you were hoping for, or an answer too dramatic to be ordinary, make it prove itself before it reaches the client.

Learned August 1, 2026.

<!– learning:2026-08-01-read-the-channel-before-reporting-a-missing-data-source –> August 1, 2026 (from: wtp-monthly-seo-reaudit run (Western Trading Post) — GSC reported as “not configured” while a teammate posted GSC data weekly in the same thread)

A task parameter that names a missing data source is a claim with an expiry date — check the client’s own channel first

This monthly audit’s parameters said gsc_property: not configured — Ahrefs + direct crawl only. The run believed it, wrote “No Google Search Console property is configured” into the client-facing report as a finding with an owner, and listed “get GSC verified” as an action.

Then the run opened the client’s Basecamp thread to post — and found our own operations teammate posting Search Console data in that thread every single week: ~120K impressions, 3.5% CTR, average position 8.4, top queries with click and impression counts. The property existed. It had existed the whole time.

Two costs, and the second is worse than the first:

  1. We nearly asked a client for access they had already granted — the exact move that burns an ask and makes

the retainer look inattentive.

  1. We did the analysis without the best data we had. Ahrefs estimates rankings; Search Console reports

what actually happened. The GSC query table turned out to contain the single most valuable finding of the engagement — 4,419 monthly impressions on one dead craftsman’s name, landing on a sold lot page. That insight was sitting in a teammate’s weekly report for six weeks and the “authoritative” monthly audit never opened it.

Rules:

  1. Before reporting any data source as missing or unavailable, read the client’s own channel — the

Basecamp thread, the shared drive, the weekly report someone else files. A per-client agent’s parameters are a snapshot of what was true when the task was written; access changes and nobody edits the task.

  1. When you find the parameters wrong, fix the parameters, not just the report. File it as an ask against

yourself in the ledger. A correction that lives only in one month’s write-up gets re-derived — and re-published as a false finding — next month.

  1. Sibling reporting is a data source, not just context. The existing 2026-07-20 learning already says

“check sibling scheduled tasks’ outputs before declaring a metric blocked.” Extend it: check what humans on the account are already reporting, in the channel you are about to post into. Read the channel before you write to it.

  1. Corollary on credit: when you use a teammate’s numbers, say whose they are. The client should see one team,

and the teammate should see their work being built on rather than quietly re-derived.

This is the same family as the 2026-07-31 lesson that “blocked is a claim that needs evidence” — but a rung earlier. There, a real blocker was misdiagnosed. Here, a non-existent blocker was inherited from a config file and published without anyone testing it once.

Learned August 1, 2026.

<!– learning:2026-08-02-same-origin-required-before-trusting-an-empty-search –> August 2, 2026 (from: WTP auction-tracking investigation — five Basecamp searches returned zero because they ran cross-origin from a client site)

An in-page fetch to another origin fails silently — and an empty search result looks exactly like “no history exists”

Asked to mine years of Basecamp history for prior conversations about a client’s auction platform, the run issued five in-page fetch calls to Basecamp’s search endpoint and got zero results for every query. The obvious conclusion was that the team had never discussed it.

The tab was sitting on auction.westerntradingpost.com. Every one of those fetches was cross-origin and was rejected by the browser before it left. The catch block swallowed it. Zero results was never an answer about Basecamp; it was an answer about CORS.

Run properly, the same searches returned 11 hits, and the history contained the single most valuable fact of the whole investigation: the client’s tag stack was already installed on the auction platform, and a 9-month-old access request had dissolved into an unrecorded phone call.

Rules:

  1. Check location.host before trusting any in-page fetch result. If you are not on the origin you are

querying, the result is meaningless. Navigate first, then query.

  1. A search that returns zero needs a positive control before you report “nothing exists.” Run a query you

know has hits through the identical code path. If the control also returns zero, the harness is broken, not the archive. This is the same prove-red discipline used for the robots.txt parser — extend it to every negative finding, because a negative finding is the easiest kind to fake.

  1. A second failure mode stacked on the first here: even same-origin, Basecamp’s search results are

client-rendered, so fetch + DOMParser returned a shell with zero result anchors while the live page showed 53. When a fetch of a modern web app returns structurally empty results, read the rendered DOM after navigation instead. Two different mechanisms, one identical symptom: a confident, wrong “nothing found.”

  1. **”No prior discussion” is a claim about an archive, and archives are exactly where an agent’s memory

advantage lives.** Getting it wrong does not just lose a fact — it wastes the institutional knowledge the client already paid for, and re-asks colleagues questions they answered months ago.

Learned August 2, 2026.

<!– learning:2026-08-02-one-message-for-two-opposite-facts –> August 2, 2026 (from: sigrun.com security monitor — a paid plugin alerted every morning forever because “not listed” and “unreachable” printed the same line)

When one code path can produce a message for two opposite facts, the message is wrong in both cases

The sigrun.com monitor verifies each plugin version against api.wordpress.org. Its lookup returned None for two situations that have nothing in common:

  • wordpress.org answered, and it does not distribute this plugin — true of every paid add-on (Elementor

Pro, Yoast Premium, WPConsent Premium) and of the site’s own custom plugin.

  • wordpress.org could not be reached at all — a timeout, a 5xx, a WAF interstitial.

Both printed upstream UNVERIFIABLE ... lookup failed. One sentence, two opposite meanings, and the failure runs in both directions:

  1. It never clears. A paid plugin nobody had hand-added to the PREMIUM_SLUGS allowlist alerted every

single morning, forever, and the only way to silence it was for a human to edit a hardcoded set. That is alert fatigue attached to a scheduled job. This monitor exists to catch the next infection on day one — and a daily alert everyone learns to skim rebuilds the exact condition it was built to remove.

  1. It hides the real thing. During a wordpress.org outage, every ordinary plugin bump prints that same

“UNVERIFIABLE” line. A genuinely tampered plugin folder arriving in that window would have been visually identical to the routine noise. The one line a human most needs to trust said the same thing whether the news was “nothing to see” or “someone edited your plugins.”

Rules:

  1. Distinguish “answered no” from “did not answer.” A 404 is data. A timeout is the absence of data. Any

function that collapses them into one return value has thrown away the more important half. Return a three-state result, not a nullable one.

  1. A hand-maintained allowlist is a clock that runs slower than the thing it describes. PREMIUM_SLUGS

had two entries and the site had four unlisted plugins. Derive the answer from the authority (wordpress.org already knows) instead of restating it locally.

  1. Retry before you alarm; vary time before you vary anything else. A one-second network blip should not

be able to manufacture a security alert. Backoff-retry the unreachable case, then report it.

  1. Dispatch on type and fail CLOSED. The sentinel chain if known is UNREACHABLE ... elif nv in known

would substring-match on a sentinel ("1.1" in "NOT_LISTED") or raise TypeError on None if identity ever missed. Check the shape of the good case first and let everything unexpected fall through to the alert branch — a security check must never be able to pass by accident.

  1. Test the path that only runs during the emergency. The new retry code called time.sleep() with time

unimported. It executes only when wordpress.org is down — i.e. only when the monitor matters — so no live run would ever have caught it. Any branch that fires only under failure conditions needs a test that simulates those conditions, because production will never rehearse it for you.

  1. Prove red before you trust green. Reconstructing the pre-change code and running the new suite against

it produced 17 failures and a TypeError. Without that step, 116 passing assertions prove only that the tests agree with the code that was just written.

Learned August 2, 2026.

<!– learning:2026-08-03-a-compromised-site-must-not-outscore-a-clean-one –> August 3, 2026 (from: weekly-fleet-hub-audit v2, fleet-wide proof enrichment)

Rankings are evidence about someone’s work — check whose before you score them

The fleet scoreboard rates every site on PROVE: Domain Rating, organic traffic, and the breadth of keywords it ranks for. On August 3, 2026 the two sites whose keyword breadth looked strongest were philmershon.com (15 ranking keywords) and theathletespotlight.com (5). Both readings were the attacker’s, not the client’s.

Pulling the keywords themselves rather than the count showed philmershon.com — a speaker coach — ranking for hollymoviehd, borat thong, nintendo store, jupiter 125 black colour, silver aranjanam for baby boy. Fourteen of its fifteen keywords were junk. On theathletespotlight.com it was five of five: activa 6g best colour, bici decathlon, charola de unicel. Selecting best_position_url alongside the keyword named the cause — every junk term ranked on an injected path:

/product-similar-image/?<digits> /product/category/<digits> /shop/manufacturer-site?&transition=top<digits>

with a per-site numeric suffix (…1310 on one, …1760 on the other): one kit, two of our sites. Uncorrected, philmershon.com scored impact 40; netting the injected rankings out drops it to 21 — an eight-point BIS swing. A compromised site was being rewarded for being compromised, and would have been reported as a fleet-best performer.

Rules:

  1. Never score a ranking you have not attributed to a URL. org_keywords is a count of

things Google associates with the domain, not a count of the client’s wins. Select best_position_url and read the paths before any keyword number reaches a score or a report.

  1. Net hostile rankings out of the score and raise them as an action instead. Traffic

attributed to injected URLs gets discounted in the same proportion. Infection is a dispatch item, never a credit.

  1. Judge the keywords by fit with the person, not by how spammy they look. `nintendo

store` is a fine keyword — for a games retailer. The tell is a speaker coach ranking for it. The GCT already states who each site is for; compare against that.

  1. A clean sitemap and a clean REST API do not mean a clean site. Both sites’ sitemaps

and post lists were entirely legitimate, and their real content is real. The injection lives beside WordPress, in URL space the CMS never enumerates — so any check that walks the sitemap or /wp-json/wp/v2/posts is structurally unable to find it. What Google has indexed is a separate source of truth from what the CMS will admit to.

  1. 404 today does not mean clean. These URLs now return 404 to human and Googlebot

alike from a datacenter IP, while still ranking. That is consistent with cleaned-but- still-indexed and with a cloak keyed to something the probe can’t reproduce. Say which of those you have ruled out; removal still has to be requested in Search Console either way, because the junk keeps ranking after the files are gone.

Companion to the same day’s classify-the-metric-dont-just-count-it (referring domains, same disease one metric over): fleet median referring domains is 368 against a median of 26 dofollow, because a .shop/.store link-spam blast hits every site daily. Report refdomains_dofollow; refdomains is noise. billybatt.com reads as 324 referring domains and is actually 2 dofollow, both of them ours — the authority problem the number appears to have solved is entirely intact. Ahrefs exposes an is_spam flag; use it.

Learned August 3, 2026.

<!– learning:2026-08-03-classify-the-metric-dont-just-count-it –> August 3, 2026 (from: anthony-hilb-seo-tracker run, week 7 after publishing 16 guides)

A tracker only sees what is on its checklist — so derive the checklist from the goal, not from the last run

Seven weekly runs of this tracker reported Domain Rating as the headline movement metric. Four of them credited a DR lift (9 → 12 → 11 → 10) to the 16 guides published on June 14,

  1. This run classified the backlink profile for the first time and found that **385 of

the site’s 398 live referring domains — 96% — are link-farm spam (178 .store, 158 .shop), that the flood began in mid-April, two months before the publish date those reports credited, and that all 30 referring domains gained in the last week were spam and zero were legitimate.** DR is a function of referring domains. The metric four reports narrated as our content working was link farms.

The July 27, 2026 learning had already added site-explorer-backlinks-stats to this tracker — so the run counted 371 referring domains and moved on. Counting a metric is not inspecting it. One extra call with history=live and Ahrefs’ own is_spam flag, split into spam and not-spam, turned a number into the run’s headline finding and retired DR as a progress metric for the site.

The same blindness showed up twice more in the same run, which is what makes this a rule rather than an anecdote:

  • The /watch-and-learn/ hub had never been index-checked in seven weeks — every run

checked “the 16 guides” because that was the list it inherited. The hub is not indexed either.

  • The byline had never been checked at all. All 16 guides on a personal brand site

are published under a different person’s name, with two conflicting Article schema nodes per page and an indexed /author/<someone-else>/ archive. On a site whose entire purpose is establishing one person as the authority, authorship is arguably the primary metric, and it was on nobody’s list.

Rules:

  1. Classify every metric you report, don’t just size it. For backlinks that means

splitting the profile on the vendor’s spam flag and reporting the ratio, from run one. A count with no composition can move for reasons that are the opposite of progress, and it will be narrated as progress because the number went up.

  1. **When a metric’s movement is about to be credited to our work, check whether anything

else could have moved it** — and check the dates line up. The DR lift landed three weeks after the publish, in the middle of a spam flood. The timing alone should have stopped the claim.

  1. Rebuild the checklist from the goal each time, rather than inheriting last run’s.

“Is the content working” is not “are these 16 URLs ranking” — it also covers the hub that links them, the byline that earns them E-E-A-T, and the profile that funds their crawl budget. An inherited list silently defines what the agent is able to notice.

  1. Retract, in writing, any prior conclusion the new evidence kills. This run withdrew

its own previous week’s third action (a “distinctiveness pass” on six unindexed pages) after measuring that those pages average 2,058 words against 2,042 for the indexed ones — indistinguishable, so there was nothing to fix and the recommendation had been inference dressed as an action. A tracker that never contradicts itself is not being read carefully enough.

  1. **A classification is a vendor’s opinion — cross-check it against a second index before

it drives an action. This is the half that nearly shipped wrong. Having found “96% spam” in Ahrefs, the run was one step from recommending a 385-domain disavow. A two-minute check against DataForSEO returned 37 referring domains against Ahrefs’ 398, and zero .store/.shop referrers against Ahrefs’ 336** — and not from ignorance, since DataForSEO scores those same domains 45–68 for spam when queried directly. It simply doesn’t record them linking to this site.

The discipline that makes a cross-check useful is deciding, explicitly, which conclusions survive it and which don’t, rather than letting the disagreement wash out everything at once:

  • Survived: retiring DR as the progress metric. Domain Rating is computed from Ahrefs’

own index, so if Ahrefs sees 385 spam referring domains, Ahrefs’ DR is driven by them no matter what another crawler sees. Airtight, and independent of the disagreement.

  • Weakened: the disavow. A network only one crawler can see is not obviously a network

Google counts, and disavowing on one vendor’s index is acting on the weaker half of the evidence.

Generalise it: when two sources disagree, don’t average them and don’t pick the one that makes the better story — partition your conclusions by which ones depend on the disputed data. Some usually don’t, and those are the ones you can still act on today.

Corollary on delivering the fix rather than the finding: the spam list was still enumerated into a ready-to-upload disavow.txt (385 domains, with the 15 legitimate domains listed as excluded-and-why), but staged, not uploaded, with the vendor disagreement written into its own header so whoever opens it inherits the doubt along with the file. Build the artifact so the decision costs five minutes; never make the decision on the client’s behalf from data that cannot answer it.

Learned August 3, 2026.

<!– learning:2026-08-03-prefer-the-instruction-that-cannot-go-stale –> August 3, 2026 (from: Ahrefs free-DR deprecation follow-up after the anthony-hilb-seo-tracker run)

A conditional built on a vendor’s date rots in every copy except the one you corrected

The anthony-hilb report flagged that Ahrefs’ public-domain-rating-free endpoint stops accepting unauthenticated calls on August 10, 2026 — seven days out, and the date of the tracker’s own next run — and recommended a “5-minute free API key registration.” Chasing that down produced two findings, and the second is the one that generalises.

1. The registration was never necessary, and checking took one call. The workspace MCP already authenticates with a paid Lite key. The “free API key” in Ahrefs’ warning is aimed at callers who hold no key at all. site-explorer-domain-rating returns the identical number on the key we already have — verified across two domains the same day (anthonyhilb.com DR 10 from both endpoints, michaelkrigsman.com DR 1.0 from both) — for ~50 units against a 100,000/month allowance. It is also more accurate: the free endpoint lags the authenticated series by about a day, which is precisely what wrote a wrong DR 11 into the 2026-07-20 snapshot. So the “deprecation fix” was really a defect fix that had been available all along.

Rule: before scheduling work to satisfy a vendor’s new requirement, check whether the credential you already hold satisfies it. A deprecation notice describes the vendor’s default caller, not your setup.

2. The instruction had already rotted in five of six copies. The block telling agents to prefer the free endpoint lived in six skill files. On July 27 the cutoff moved from August 1 to August 10, and exactly ONE file — weekly-brand-maa.md — received the correction. On August 3 the other five still read “Until August 1, 2026 keep using public-domain-rating-free first”: a deadline that was both wrong and two days expired, still actively instructing agents toward the dying endpoint. Nobody noticed, because each file was individually plausible and nothing compares them.

The fix was not to propagate the new date. It was to delete the date: the rule is now unconditional — use site-explorer-domain-rating, always — with the dated version struck through beneath it as the record, plus a pointer at the top of the section so an agent reading top-to-bottom cannot hit the stale narrative first.

Rules:

  1. Prefer the instruction that cannot go stale over the one that is merely correct today. “Use X”

survives indefinitely. “Use X until DATE, then Y” is a maintenance obligation in every copy, forever, and it fails silently and invisibly — an expired conditional reads exactly like a live one.

  1. **When a correction lands on a duplicated instruction, grep for the other copies in the same

breath.** This is the same shape as the standing rule in Skill-Learnings/README.md that “a standing contract recorded in one file is not a standing contract,” and the same shape as the July 29→31 gap between a rebuild gate being learned and the runner being changed. Three independent recurrences means the default is wrong: assume duplication until a grep proves otherwise.

  1. A date copied out of a vendor’s warning is the least durable thing in a skill file. Where one

must be written down, write it as “as of <Month D, YYYY> the API said X” so the staleness is visible on the page — and pair it with a dateless instruction that stays correct if nobody ever revisits it.

  1. Check the whole fleet of callers, not the one that surfaced the problem. Of 31 scheduled task

prompts, only two named the dying endpoint and two others were already on the authenticated one. Grepping the mirrored prompt set answered in one call what would otherwise have been six file reads and a guess.

Learned August 3, 2026.

<!– learning:2026-08-03-crawlability-must-be-tested-on-served-html –> August 3, 2026 (from: cxotalk-weekly-maa run)

Never diagnose crawlability from rendered text — the client who corrected us was right for four weeks

For four consecutive reports, this engagement’s #1 technical recommendation was: “~40M words of CXOTalk transcripts are client-rendered and invisible to the crawlers ChatGPT and Perplexity depend on — server-render them.” It was the headline of the strategy, it was in every ANALYSIS section, and it was false.

The client disputed it in writing on 7/28: “I checked this carefully and I do not believe that transcripts on cxotalk.com are hidden by JS.” Tested properly, he was right:

Check Result
Raw same-origin fetch, no JS executed, /episode/{slug} HTTP 200, 660KB HTML, 59,761 chars body text after stripping <script>, 97 speaker turns
Same, /episode/{slug}/transcript Identical — 59,761 chars, 97 turns
robots.txt 916 bytes; only Baiduspider is Disallow: /. The file explicitly names “Googlebot, bingbot, Applebot, ClaudeBot, GPTBot, etc.” as covered by the permissive User-agent: * group

The mechanism of the error. Reading the rendered page returns ~349 words from document.body.innerText, because the transcript sits inside a collapsed/tabbed element. innerText returns only visible text. The content was fully present in the served HTML the entire time. A visible-text read was mistaken for a crawler’s view, and nobody re-tested it for a month because each week’s report inherited the previous week’s premise.

Rules:

  1. Test crawlability against the SERVED HTML, never the rendered view. Same-origin

fetch(url)DOMParser → strip script/style/noscripttextContent. Never innerText, never get_page_text, never a screenshot. Those three answer “what can a human see,” which is a different question and frequently the opposite answer.

  1. innerText vs textContent is the whole bug. Collapsed accordions, inactive tabs,

height:0 containers and offscreen panels are invisible to innerText and perfectly visible to a crawler. Any conclusion of the form “the content isn’t in the HTML” that was reached via innerText is unsupported.

  1. Check robots.txt in the same pass, and read it verbatim. A JS thesis and a blocking thesis

are two different claims; we asserted both and both were wrong. Quote the actual User-agent groups rather than characterising them.

  1. **A recommendation that survives four reports without being re-tested is a belief, not a

finding.** Standing recommendations need a re-verification cadence, because the cost of being wrong compounds: every week it went unchallenged, it displaced whatever the real fix was.

  1. When a client contradicts your technical claim, test their version first, not yours. The

instinct is to defend. The client here had checked something we hadn’t, and one raw fetch settled it in under a minute. Being corrected and saying so plainly is cheaper than four more weeks of confident wrongness — and it upgraded the diagnosis (the citation gap is an authority/entity problem, not a plumbing one), which is a better answer than the one we lost.

Same run, same family, caught before it shipped: the week’s first Ahrefs metrics pull passed country=us where prior runs passed no country filter, returning 394 keywords vs 464 — a false 17% “collapse” that would have been the report’s lead. Re-pulling the prior week’s date under today’s exact config reproduced last week’s numbers exactly, proving config drift rather than decline. Any alarming metric move must be re-pulled at the prior date under the current config before it is narrated. Two near-misses in one run, both of the same shape: the measurement apparatus changed, the world did not.

Learned August 3, 2026.

<!– learning:2026-08-08-basecamp-lexxy-editor-silent-blank-post –> August 8, 2026 (from: family-law-leaderboard-weekly-checkin — hit mid-delivery; affects every agent that posts to Basecamp)

Basecamp has replaced Trix with Lexxy, a Lexical-based editor, and the old posting method now fails silently by posting an empty comment. trix-editor no longer exists in the comment form. The composer is <lexxy-editor id="comment_content"> wrapping a contenteditable #comment_content-content. Three approaches all failed without throwing: execCommand('insertHTML'), assigning innerHTML on the contenteditable, and dispatching a synthetic ClipboardEvent('paste') carrying text/html. Each one updates the visible DOM while Lexical’s internal model — the thing actually serialized on submit — stays <p><br></p>.

That failure mode is the dangerous part: an agent that verifies by reading the editor’s innerText sees its full post sitting on screen and confidently submits a blank comment. STEP 6.6’s “verify server-side” catches it after the fact, but only if the check counts characters rather than just confirming a new comment exists.

Working method: `js document.getElementById(‘comment_content’).value = ‘<p>…</p><p>…</p>’; // form-associated, real setter document.querySelector(‘form[action*=”comments”] input[type=submit]’).click(); ` Gate on new FormData(form).get('comment[content]').length before clicking — never on the editor’s innerText. If richer manipulation is needed, le.contents exposes insertHtml, insertDOM, insertText, and le.clear() resets the model.

Two related verification fixes from the same run. (1) section.thread--comments article includes non-comment nodes — the composer and the notification footer — so a naive count overstates by two; filter on the presence of a time[datetime] child to count real comments. (2) The generalizable lesson: when a vendor swaps out an editor, the old write path degrades to a no-op rather than an error. Any skill that fills a third-party rich-text field should assert on the value the form will actually submit, not on what the UI displays.

<!– learning:2026-08-08-ahrefs-shared-cap-preflight –> August 8, 2026 (from: family-law-leaderboard-weekly-checkin — run hit API units limit reached. Expected usage: 66, API units left: 0 mid-report)

The Ahrefs Lite allowance is one shared pool across all seven trackers, and no caller currently checks it before spending. This run found the workspace 4,072 units over a 100,000/month cap (104,072 used; ~86.7k through this API key, ~17k from other seats; resets on the 19th). The failure lands mid-run: earlier calls succeed, a later one errors, and the report is left half-populated.

Add to STEP 1: call subscription-info-limits-and-usage (free, zero units) before any paid Ahrefs call. If remaining units are below the run’s expected cost, degrade deliberately rather than discovering the wall halfway — pull DR via batch-analysis (~18 units/target vs ~50) and drop optional keyword pulls, noting the omission per STEP 6.5.

The meta-lesson, and it is the same shape as the hard-coded-deprecation-date lesson above: a headroom figure has a shelf life, so never record one without its reset date. The 2026-07-31 run wrote “Lite plan has 58k units of headroom” into a report and a ledger row; nine days later the pool was empty and both statements were quietly wrong. Either record usage_reset_date alongside any quota number, or re-derive the quota each run and don’t cite the old one at all.

<!– learning:2026-08-09-recurring-jobs-date-by-cadence-not-today –> August 9, 2026 (from: somba-weekly-maa catch-up run, 9 Aug 2026)

A recurring job does not always run on its cadence day. When a host sleeps through its window, overdue tasks fire later in a catch-up burst — on 9 Aug 2026 three tasks fired within one second, 2, 4 and 2 days late. So a weekly Friday report can arrive on Sunday, after a recovery run has already delivered that week’s work.

Two rules follow, and a client-facing job needs both.

Date by the cadence day, never by today. Snap to the most recent Friday (or the 1st, or the 18th) and say so in the log: “today is Sunday 9 Aug; dating this MAA Friday 7 Aug.” Taking today on trust also mis-dates any run that starts near midnight and crosses it while publishing.

Be idempotent for that date. Dating correctly is not enough — the second fire must detect that the entry already exists and change nothing, or a catch-up posts a duplicate week to every client dashboard.

When both hold, the correct output of a catch-up fire is nothing published, and that is a success, not a skipped run. Prove it rather than assuming it: hash the artifact before and after, and verify the live state independently. Then say plainly in the report that you published nothing and why. Do not re-push a byte-identical payload to a WAF-rate-limited host to feel productive — it risks a ban that costs a real publish later and changes nothing a client can see.

Also: do not write that run’s record under the filename the weekly ledger globs. A catch-up report named like a weekly report enters the ledger as a Friday MAA dated on a Sunday. Give it a different prefix and verify the ledger’s hash is unchanged after regenerating it.

<!– learning:2026-08-10-a-stable-set-that-moves-is-a-different-finding –> August 10, 2026 (from: anthony-hilb-seo-tracker, week 8 — a page left Google’s index and the tracker could only see it because it re-checks the whole set, not the known-bad subset)

Track state history, not current state — “6 pages unindexed” and “a page just fell out” are different diagnoses wearing the same number

For seven weeks this tracker reported the same fact: 10 of 16 published guides are in Google’s index, 6 are not. On week 8 it reported 9 and 7. One page — indexed on 27 July and again on 3 August — had been dropped.

The number moved by one. The diagnosis moved much further than that.

  • “Six pages Google never selected” is a story about pages. It invites page-level fixes:

more words, better internal links, a distinctiveness pass. (This tracker recommended exactly that on 27 July, then withdrew it on 3 August after measuring that the unindexed pages were no thinner than the indexed ones.)

  • “A page Google indexed and then dropped” is a story about the site. Google evaluated

this domain, made a decision, and reversed it. No page-level edit explains a reversal on a page that is HTTP 200, index, follow, self-canonical and 1,777 words.

The second reading only exists because the run had a prior observation of that specific URL in a specific state. Had the tracker checked only the six known-bad URLs — the efficient thing to do, and the tempting thing after five identical weeks — the drop would have been invisible, and the report would have said “no change” for the eighth time on the week the account’s story actually changed.

Rules:

  1. Re-check the whole tracked set every run, never the subset that was failing. The known-bad

items are the ones least likely to teach you something new; the known-good ones are where regressions hide. An inherited “problem list” silently becomes the definition of what the agent can notice.

  1. Record per-item state history, not just the current tally. A snapshot that says

indexed: 9, not_indexed: 7 cannot answer “which one changed, and when?” — the question that distinguishes one page wobbling from the start of a slide. Store the per-URL verdict with a date so the next run can diff it, rather than re-deriving the diff from prose in last week’s report.

  1. **A metric that has been flat for N weeks is not evidence that it is stable. It is evidence

that you have been measuring one thing.** Five flat weeks here preceded an indexation finding; two more preceded a backlink-classification finding; the eighth produced a deindexation. Each time, “nothing moved” was true of the number being reported and false of the account.

Corollary, from the same run: a forced degradation can be an upgrade — check before you apologise for it

The Ahrefs unit pool was exhausted mid-month (104,072 of 100,000 workspace units, shared across seven client trackers), so DR, organic keywords and organic traffic were simply unavailable. The reflex is to treat that as a diminished run and say so apologetically.

Two things were true instead:

  • The lost metric was already worthless here. DR had been formally retired as a progress

metric for this site the week before, once 96% of its referring domains turned out to be link farms. Losing a number you had already stopped trusting costs nothing.

  • The substitute was better than the original. The tracker normally answers “do we rank for

these 7 target topics?” from Ahrefs’ organic-keywords estimate. With Ahrefs down, it asked Google, live, pages 1–2, testing the SERP HTML for a scheme-qualified URL. That is the search engine’s own answer rather than a vendor’s model of it. The method is being kept after the quota resets.

Rule: when an outage forces a substitute method, evaluate the substitute on its merits before labelling the run degraded. Vendor APIs are convenient proxies for questions the primary source will often answer directly and better. An outage is a free, forced experiment in whether the convenient path was also the correct one — and sometimes the fallback should become the default.

And the half that must not be softened: where a metric genuinely could not be measured, write null and say “unavailable.” Do not carry the prior week’s value forward into the slot, and do not let it appear in a table beside measured numbers. A tracker whose whole job is detecting movement cannot afford a number that looks measured and is actually a memory. This is the same family as “distinguish ‘answered no’ from ‘did not answer'” — one code path, two opposite facts.

<!– learning:2026-08-10-redirect-hides-a-dead-site –> August 10, 2026 (from: weekly-fleet-hub-audit 2026-08-10)

Resolve the FINAL host of every site you audit, and compare it to the domain you asked for. HTTP libraries follow redirects silently, so a domain that has been pointed at a different site returns a clean 200 with a full, healthy page — and every downstream check then grades the DESTINATION while printing the SOURCE’s name.

Found 2026-08-10: archiepadley.com 200s straight through to dennisyu.com. The weekly foundation audit had been scoring Dennis Yu’s homepage as Archie Padley’s site — green on homepage_up, has_title, entity_schema and sameas_links — for a personal brand site that does not exist. The audit had no way to notice, because none of its checks ever asked “whose page is this?”

Two rules fall out of this, and they generalize past redirects:

  1. Compare the answer’s identity to the question’s identity. urlopen(...).geturl() / curl -w '%{url_effective}' costs nothing. Any fetch whose final host differs from the requested host is a different subject: report it as REDIRECTED and stop, rather than scoring it. The same applies to a canonical tag pointing off-domain, an OG:url naming another site, and a Person schema whose name is not the site’s person.
  1. A green foundation score is not an identity check. Title, meta description, schema presence and sameAs count all pass on a page about the wrong human. On the same run azuifeachor.com — Azu Ifeachor’s site — was publishing a page at slug about-felix-fagbuyi, all foundation checks green. For an entity home, serving a second person’s name is the most damaging on-page defect there is, because it teaches Google and every LLM the wrong entity, and it survives an all-green report indefinitely.

When you add the identity check, make it precise before you ship it. The first version compared token sets, which reads the concatenated domain ayeshafarrukh.com and the hyphenated slug about-ayesha-farrukh as two different people — 2 real hits inside 4, a 50% false-positive rate on the one signal meant to be exact. Compare on the squashed letter-stream as well as tokens. And keep the noisy corroborating signal subordinate to the precise one: flagging any foreign person-name in homepage copy put 83 of 92 sites on the list, because podcast sites legitimately name their guests — a list that long is indistinguishable from no list at all.

One more trap in the same check: a consuming regex for capitalised name pairs reads “About Felix Fagbuyi” as (About, Felix), discards it as a stop word, resumes past “Felix”, and can never form (Felix, Fagbuyi) — the exact name the scanner exists to catch becomes invisible. Use a lookahead so matches overlap.

Showing the 14 most recent field lessons of 28. This skill is one of the most-used in the system, so it collects a lesson from almost every run. The complete history ships inside the skill file itself — download any pack and open weekly-brand-maa.md.


Other skills: ai-search-visibility · boil-the-ocean · client-access-checklist · client-relationship-cadence · content-agent · content-factory

The full run order is on the skill-pack directory. Every skill here is one task from the Task Library — the library is the catalogue of what can be done; a pack is the subset you install; an agent is who runs it.

Where this sits in the system

A skill is a document. A pack is a folder of documents. Neither one does any work. Work happens when a job runs those skills on a schedule, checks its own output, and keeps its files somewhere it can read them again tomorrow. That is the whole difference between owning skills and having an agent.

  1. Skill — one task, written down to a standard, so an agent can run it without you in the room. There are 239 of them.
  2. PackYOU ARE HERE — those skills bundled into a download you install in one paste.
  3. Agent — a named role with a job description — not a chat window you retype every morning.
  4. Job — a schedule, a QA cycle, and somewhere to keep working files. Miss any of the three and nothing runs twice.
  5. Proof — every finished run written up in public, and the lesson pushed back into the skill.

The map: The System · every asset: Asset Tracker · next door: Asset Tracker.

Scroll to Top