Evidence Verification

Skill packs › Skill

Evidence VerificationIN EVERY PACK

Establish what is actually TRUE about a client and prove it with records a stranger can check. Find the hard anchors hiding in public registries (Secretary of State, bar admission, ISBN, patents, ORCID, authority files), verify identity by matching a second fact rather than a matching name, and never let “we haven’t looked yet” render as “they scored badly.” The upstream skill every authority skill depends on — knowledge-panel-entity-seo, grokipedia-authority and ai-search-visibility all fail the same way when the underlying facts are wrong.

Skill file evidence-verification.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. This one ships in every pack.

Use this when you are about to assert something about a client in public — a schema field, an encyclopedia submission, an audit score, a bio, a Knowledge Panel claim — and you want to be certain it is true, and provable by someone who does not trust you.

This skill exists because of a specific, repeatable failure. In July 2026 our records described a client as a style and image coach in Austria. She is a sewing author in South Tyrol, Italy. The error had been flagged in a research note five weeks earlier and never actioned. It was one submission away from putting a false description of a real person into an encyclopedia — which would then be scraped by every AI assistant that reads it.

Nothing about that failure was exotic. A field was wrong, a note said so, nobody carried it through, and the next system downstream treated the field as fact.

The one idea

A fact about a client is not established until a stranger can check it without asking you.

Your client’s own website is not evidence of anything except what your client wants to say. A testimonial is not evidence. A follower count is not evidence. What counts is a record held by someone with no stake in the outcome: a government registry, a national library, a court, a standards body, a publisher, a newsroom.

Everything below is machinery for finding those records and refusing to fake them.


Part 1 — Find the anchors nobody looked up

Most clients already have hard, verifiable public records. Nobody has searched for them. Work this list in order of how cheap the win is.

The business registry — the anchor almost every client has

Every LLC, corporation and partnership is registered with a state or national authority, and those records are free, public, and name the officers. In the US it is the Secretary of State (or Corporation Commission); in the UK, Companies House; most countries have an equivalent.

What you get: legal entity name, charter or company number, formation date, status, registered address, and — the part that matters — named officers, managers and registered agents.

Worked example. A client was known to us only as “Roland.” No surname in eleven email threads, none in the client tracker. His business was a registered Louisiana company:

JUNKS ABOVE LLC · charter 42116285K · filed 30 December 2015 · Active, in good standing ·
Officer: ROLAND LEBLANC, title Manager · domicile 4626 D’Hemecourt St, New Orleans

Two minutes in a public database produced a surname, a title, a formation date, and a verifiable charter number. Nearly every local-service client on a roster is an LLC owner, which means most clients sitting at zero corroboration have an anchor like this waiting, unclaimed.

The rest of the ladder, by profession

If the client is… Look here What it proves
Any business owner Secretary of State / Companies House Legal name, role, entity, formation date
An attorney State bar admission; reported case citations Licensure, jurisdiction, practice history
A doctor, dentist, therapist State licensing board; NPI registry Credential, specialty, standing
Any author ISBN; national library (Library of Congress, DNB, BnF) Authorship, publisher, date
An academic ORCID, Google Scholar, institutional page Publications, citations, affiliation
An inventor Patent office (USPTO, EPO) Named inventor, filing date
A contractor or tradesperson State contractor licence board Licence number, class, standing
A restaurateur or retailer Health permits, liquor licence, local press Operating record
A speaker Conference programmes and archives Named role at a named event
A nonprofit officer IRS Form 990 (public) Named officer, compensation, role
Anyone with a Wikipedia article Wikidata QID + authority files (GND, VIAF, ISNI) Everything, instantly

Weighting: not all anchors are equal

A registration proves someone runs a business. It does not prove anyone independent found them notable. Weight accordingly, and say so out loud when you report:

  1. Strongest — Wikipedia article, national-library authority record, book with an ISBN from a real publisher, sustained national press where they are the subject
  2. Strong — a single national press piece as subject, a named award, an academic publication record, a patent
  3. Real but modest — business registration with a charter number, professional licence, named speaking role
  4. Not an anchor — their own website, their own bio, follower counts, testimonials, directory listings they submitted themselves, press releases

Never let a category-3 anchor alone trigger a public submission. It is enough to establish identity; it is not enough to establish notability.


Part 2 — Verify identity with a second fact

A matching name is not a match. This is the single most expensive mistake in entity work, because the output looks completely normal when it is wrong.

Rule: confirm on a second, independent attribute — an address, a formation date, a brand name, a co-founder, a licence number, a photograph.

In the Roland example the identity proof was not the name. It was that the registry’s domicile address and the address on the shop’s website were the same street address. Same name in the same city would have been a guess. Same name and same address is an identification.

The inverse error is just as costly. A client was filed as a “namesake trap” — the Wikidata item matching her name was assumed to be somebody else. Five weeks later the record was followed properly through its authority file to the national library, which showed it was her own item all along. The stale warning had been suppressing her score and excluding her from work for over a month. A warning that has been disproved is worse than no warning, because nobody re-examines someone the file says is unsafe to touch.

So:

  • Two identifiers agreeing = identified.
  • One identifier = a hypothesis, and it must be labelled as one.
  • A namesake flag must carry the evidence and the date it was established, so it can be re-tested rather than inherited forever.

Part 3 — Read negation

Research prose records absence as often as presence. Your own notes will say:

“Confirmed to have NO Knowledge Panel, NO Wikipedia, NO Wikidata and NO Grokipedia.”

A keyword matcher scanning that sentence finds Wikipedia, Wikidata and Knowledge Panel and records three anchors the client does not have. That is exactly what happened to a client audit in August 2026: the sentence documenting that he had nothing was read as proof that he had everything, and it promoted him to “ready to submit.”

Any automated anchor detection must check for negation before the matchno, not, never, without, lacks, missing, zero, none, absent. Scan a short window to the left and stop at a clause break so a negation in one sentence cannot suppress a real anchor in the next.

And when you add a new anchor type later, add it to the negation guard in the same edit. This is the kind of check that silently stops applying the moment someone extends the system.


Part 4 — “Not researched” is not “failing”

This is the rule that protects the client relationship, and it is easy to get wrong in a way nobody notices until a client sees the report.

If nobody has researched a client yet, that is a gap in our knowledge. It is not a judgement about them. A system that scores unresearched entities as zero produces a report where “we never looked” and “we looked and they have nothing” are visually identical — and the client cannot tell which one you meant.

Build the distinction into the data model, not the wording:

  • UNKNOWN — no research done. No score at all, not a zero. Goes to a research queue with the specific questions to answer.
  • BUILD — researched, no anchor found yet. A real, honest finding.
  • NEARLY / READY — researched, anchors found.

Then enforce it with a test that fails if any scored entity has a score of zero, and another that checks the UNKNOWN action text describes our next step rather than the client’s deficiency. Wording drifts; tests do not.


Part 5 — Carry the evidence into the artifact

Research that stays in a research file is wasted. Whatever you produce at the end — a submission, an audit, a schema block, a pitch — must carry the specific verifiable anchors, not a summary of them.

A generator that says “active professional presence; any press, podcast appearances, books” has thrown away everything that mattered and produced filler that reads as unsourced. Compare:

Before: “Documented independently; entity home with structured data; active professional presence.”
After: “Independently verifiable: published by Springer Nature; ISBN 978-3-662-62443-2; authority records GND 130452106, VIAF, ISNI; Wikidata Q108866818; existing German Wikipedia article.”

The second one a reviewer can check in four clicks. The first one is a shrug.

And when there is no hard anchor, say so loudly in the artifact rather than dressing up the absence. A package that admits “no third-party anchor found — expect rejection, surface one proof first” is more useful than one that bluffs and gets bounced.


Part 6 — Fix at the source, in the same run

Every failure in this document has the same shape: someone found the problem, wrote it down, and moved on.

  • The wrong niche was recorded in a research note five weeks before it nearly shipped.
  • The disproved namesake trap was resolved in one file and left standing in another.
  • The “does this have any hard anchor?” check existed in two places; adding a new anchor type updated one and not the other.

So:

  1. Fix the source record, not just the output. If the roster is wrong, fix the roster.
  2. Make the contradiction self-detecting. If two records disagree about the same identifier, that is a testable condition. Write the test; do not rely on someone noticing.
  3. One authoritative definition. If a rule is expressed in two places, they will diverge — usually at the moment someone extends the system, which is the worst possible time.
  4. A note saying “X is wrong and should be corrected” is not a correction. Either fix it in the same run or file it somewhere that fails until it is fixed.

How to run it

  1. Inventory — list every client, partner and entity. Mark active/inactive from the authoritative source, and record a reason for every inactive one.
  2. Probe what already exists — before researching anything, check whether the entity already has a Wikipedia page, Wikidata item, Knowledge Panel or Grokipedia page. Run controls first: a known-good lookup and a deliberately nonsense one. A check that cannot fail is not a check. (Our first fleet run found pages already existed for a client and for one of our own companies — nobody knew, and nobody was checking them for accuracy.)
  3. Hunt anchors — work Part 1’s ladder, cheapest first. Business registry before anything else.
  4. Verify identity — second attribute, always. Record which two facts agreed.
  5. Score honestly — UNKNOWN where you have not looked.
  6. Carry evidence into the artifact — specific records, and a loud warning where there are none.
  7. Report the corrections you made, not just the findings. A run that fixed two wrong records delivered more value than one that found three new anchors.

What good looks like

  • Every assertion traces to a record a stranger can pull up.
  • Every identification names the two facts that agreed.
  • Nobody is scored on evidence they do not have, in either direction.
  • The count of unresearched entities is stated plainly and never disguised as a low score.
  • Corrections to source records are reported in the same run they were found.

Related skills

  • knowledge-panel-entity-seo — consumes these verified facts as schema. Wrong facts here become wrong structured data there, which Google then believes.
  • grokipedia-authority — the encyclopedia submission. Its “Insufficient Citations” rejections are this skill’s absence, showing up downstream.
  • ai-search-visibility — what ChatGPT and Perplexity say about a client is assembled from exactly these records.
  • positive-mentions-harvester — finds the press; this skill decides which of it is a real anchor.
  • client-access-checklist — access is the precondition for measuring; verified identity is the precondition for publishing.
  • recursive-self-improvement-qa — the discipline in Part 6 is that skill applied to facts instead of code.

Learned in the field

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

<!– learning:2026-08-02-a-row-in-a-table-is-not-a-page –> August 2, 2026 (from: grokipedia-fleet)

August 2, 2026 (from: grokipedia-fleet)

We had 24 skills and nowhere to send someone who asked what one of them was.

Each skill existed in three places — a markdown file in the repo, a file inside every pack zip, and a single row in a table on /skill-packs/. All three are real. None of them is a page. There was no URL for “what is evidence-verification and why would I run it,” which means there was nothing for a search engine to rank, nothing for an AI assistant to cite, and nothing to link to in a client email.

That is the same mistake we diagnose in clients every week. A capability that exists but has no citable address does not exist to anything that reads the web.

Fixed by generating one page per skill from the master .md files — 24 pages at /skills-<slug>/, each carrying the same five-rung ladder block as the rest of the system tree, each linking up to the pack directory and across to the Task Library.

Three things worth carrying forward:

Generate, never hand-write. The pages are built from the skill files that already exist, so a skill and its page cannot describe different things. The moment someone edits a page in wp-admin, the next run overwrites it and nobody finds out for weeks. Say GENERATOR-OWNED in the file header, and mean it.

A new tier of pages needs a new line in the verifier, the same day. Twenty-four pages went live at once, entirely outside the daily link-graph check. That is precisely how aibuilderspotlight.com/skill-pack linked to nothing for weeks inside green reports. The verifier now checks that every master skill on disk has a page linked from the directory, and samples three live pages per run on a rotating index so all of them get covered over time.

A page tier needs a line in the runner too, or it becomes a slower clock than its source. The daily job rewrites the master skill files every morning. Without a regeneration step the published pages would keep describing whatever the skill said the day they were generated, while the directory printed a fresh “last updated” date next to them — a stale page wearing a current timestamp, which is worse than an obviously old one. This is the third time the same defect has appeared in this system (Dorine’s mirror pack, the cloud runtime mirror, now the skill pages). Any artifact derived from a source that changes daily needs its own step in the daily job. Look for the pattern rather than waiting to be bitten by it a fourth time.

One smaller thing, worth its own note: the page generator imported propagate_all_packs just to read its MANDATED list. That module parses sys.argv at import time, so the generator’s own argument errors came out under the propagator’s name and usage text. Never import a module that acts at import time in order to read one constant from it — parse the constant out of the source instead.

<!– learning:2026-08-02-a-placeholder-that-reads-as-a-sentence-will-be-printed-as-one –> August 2, 2026 (from: SOMBA audit-cover regeneration, second pass — 30 live audits still carried the placeholder nine hours after the fix was declared closed)

2026-08-02 (second pass, same day)

What happened

Earlier on 2 Aug we found that gct.py‘s keyword archetypes had been printing invented positioning lines on audit covers — “personal authority” → author → “helps aspiring authors finish their book” — and that 27 members whose niche matched nothing were shown the raw fill-in-the-blank: “Helping your ideal client a specific, nameable result.”

We fixed the cover, regenerated all 104 audits, verified every cover line was distinct and grounded, added tools/test_audit_grounding.py to block a recurrence, and told the client it was closed.

It was not closed. Nine hours later, while browser-testing an unrelated new feature on Claudius Krucker’s dashboard, the same string appeared on screen. Thirty of the live audits still contained it — in the “a sharper version to try” card and on the agent-team page. It was also one build away from Jane Omorogbe’s personal-brand website, because site.py reads the same triple, and Agnieszka Figielek’s site was set to go live describing her zero-emission building academy as helping “families planning a build or renovation” — the same substring collision that mislabelled Claudius, on a public site rather than a PDF.

Three of the thirty unmatched members are Sigrun’s mentors: Ina, Jagoda, Katrin. So the three people who raised the complaint would have opened their own audits and found the complaint still true.

Why the first fix missed it

The test asserted the right property (no fabricated line) against the wrong scope (the cover element). It parsed <h1 class="mission serif"> out of each audit and checked that one string. That is exactly where we had been looking when we found the bug, and looking there again is not verification — it is re-reading your own patch.

grep -c "a specific, nameable result" audits/*.html would have taken four seconds and returned

  1. Nobody ran it, because the fix felt complete.

The two lessons

1. Scope a regression test to the string, not to the place you found it. If the defect is “this text must never describe a member”, the test is a scan of every built document — audits, dashboard, team board, member websites — not an assertion about one element. tools/test_receipts.py now scans 113 built files for four forbidden strings.

2. A placeholder that reads as a sentence will be rendered as a sentence. The deeper mistake was ever having a fallback value that is grammatical, first-person and member-shaped. Every caller was supposed to check matched(p) first. Callers forget; that is what callers do. The durable fix is for the value itself to be harmless:

  • UNMATCHED is now ordinary English that asserts nothing and reads naturally in every

sentence template we own — ("the people you serve", "get the result you're known for", "your method").

  • gct() returns suggested=None when nothing matched, so “here is a better line for you”

cannot be fabricated at all. A forgetful caller now gets a None — loud — rather than a plausible sentence about a person who does not exist.

Design rule to carry forward: make the unsafe value unusable, not merely conditional. A guard you must remember is a guard that will be forgotten. This is the same shape as the git_askpass bug from this morning (a branch that could never fire) and the frozen team-board roster (a hand-maintained list beside a generated one) — in all three, correctness depended on a human remembering something at the call site.

Related

  • receipts.py — every claim now carries its inputs and its rule; a claim with no basis is not

returned at all, so “unsourceable” is a structural impossibility rather than a test finding.

  • tools/test_receipts.py — the whole-document scan, plus a check that receipts.py’s restated

score arithmetic still equals score.py for all 104 members.

<!– 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-buckets-must-partition-the-thing-they-explain –> August 3, 2026 (from: weekly-fleet-hub-audit v2, phase 1 down-site triage)

If a report splits a set into buckets, assert that the buckets add up

The fleet audit deliberately splits unreachable sites two ways so a WAF block is never reported as an outage: genuinely_down (DNS/TLS/connection failure) and waf_suspect (403 from our crawler’s IP). The runbook says to read those two lists rather than the raw “Homepage NOT reachable” line, because the raw line conflates them.

On August 3, 2026 the raw line said 4 and the two buckets said 1 + 2.

The missing site was owenhemsath.com, returning a real HTTP 500 — a genuine outage on our own AWS fleet host, up and healthy the week before. The classifier had always produced a third kind, http_NNN, for real 4xx/5xx; nothing ever consumed it. So a site could be hard-down and appear in no dispatch list, while every summary line in the report stayed true. Following the runbook exactly would have made a live outage invisible for a week.

Fixed in audit_fleet.py and _combine_batches.py: an http_error bucket plus an explicit down_unclassified = down − (genuinely_down ∪ waf_suspect ∪ http_error) that prints a loud warning when non-empty. Proven able to fail before being trusted — injecting a bogus _home_kind into a scratch copy put the site in down_unclassified and printed the warning.

Rules:

  1. Every partition gets a residual bucket and an assertion. Whenever a report explains

a total by splitting it into categories, compute total − Σ(categories) and surface it loudly. Categories that came from an enum will silently drop members the day the enum grows a value.

  1. A value the producer emits and no consumer reads is a latent hole, not dead code.

Grep the consumer for every value the producer can return.

  1. The conflated line is the honest one. When a summary offers both a raw total and a

nicer breakdown, treat any disagreement between them as the finding.

  1. Read deltas for artifacts before narrating them. The same run’s needs_hub went

84 → 85 with “1 resolved: owenhemsath.com” — which reads as progress and was the outage: needs_hub requires homepage_up == yes, so a site leaves the list by going down. Any queue gated on reachability shrinks when sites break. State that in the report rather than counting it as work completed.

Learned August 3, 2026.

<!– learning:2026-08-03-a-timed-out-tool-call-is-not-a-stopped-process –> August 3, 2026 (from: skill-pack-propagation daily run, August 3, 2026 — the runner was launched twice against live production sites)

A timeout is a fact about the observer, never about the observed. When a tool call that started a long job returns “Request timed out”, the job is still running; what ended was the wait, not the work. On August 3, 2026 the daily propagation runner was started, its tool call timed out mid-step-3, the session’s process list showed no active sessions because the wrapper had detached, and the run was relaunched as though it had died. It had not: the original was three minutes in and already uploading zips. For ninety seconds two copies of a pipeline that publishes to eight live WordPress sites ran concurrently, interleaving their output into one log — one at step 3 rebuilding agent pages while the other at step 4 swapped the zip URLs underneath it. The duplicate was killed by PID and the original finished clean, but nothing in the tool’s error had said which was true.

Before relaunching anything that mutates shared state, confirm the previous attempt is actually dead — by process table (ps -eo pid,ppid,etime,command | grep <script>), by lock file, or by a heartbeat the job itself writes. A session-level “no active sessions” answers only whether this session is still attached, which is a different question. The same discipline applies to any probe: a fetch that times out means the request did not complete, not that the site is down; a publish call that times out means the response was lost, not that the write was — re-read the target’s state before retrying, or an idempotent retry becomes a double write. Vary time before you vary anything else, and prove the prior run is stopped before you start another.

<!– learning:2026-08-03-a-check-that-can-quietly-not-check-reports-green-either-way –> August 3, 2026 (from: skill-pack-propagation, August 3, 2026 — adding a concurrency gate exposed two ways a gate can disable itself)

A guard that can silently decline to run is worse than no guard, because it still prints the green line. Adding one concurrency gate to the daily runner on August 3, 2026 exposed two instances within ten minutes. First, the runner invoked it as [[ -x ./tools/x.sh ]] && run it — so a lost executable bit, from a zip round-trip or a clone or a copy that did not preserve mode, would skip the gate and say nothing. Gate on existence, invoke through the interpreter (zsh ./tools/x.sh), and make ABSENT a hard failure, not a skip. Second, the runtime-completeness test derived its required-file list by scanning the runner for python3 <path>.py invocations — correct, and blind to the ./tools/x.sh the runner had just gained. It printed “COMPLETE — all 31 referenced paths are present” while the new gate was absent from the cloud runtime entirely. A derivation that understands only one of the languages its source is written in is a hand-maintained list wearing a derivation’s clothes; it drifts exactly like one, but with more credibility.

The general form: for every automated check, ask what makes it a no-op — a missing file, a permission bit, a resource already held, an empty input, a regex that matches nothing — and make each of those loud and distinguishable from a pass. The self-test added that day correctly declines to run against a live lock, which is right; the runner then reported “guard OK ()” with an empty count, which was not. “NOT CHECKED” and “PASSED” must never render the same. When you extend a pipeline, extend the thing that verifies the pipeline in the same change, and confirm the verifier actually fails before you trust it passing.

<!– learning:2026-08-05-a-disabled-check-must-not-print-the-same-word-as-a-passing-one –> August 5, 2026 (from: skill-pack-propagation daily run)

“date OK” was printed by a check that had switched itself off — and the machine-readable twin certified the date the page did not show

blitzmetrics.com/task-library-dashboard served August 3, 2026 through a run whose own log said:

EDITED blitzmetrics.com/task-library-dashboard (id 104693): 1 url swap(s), dates -> August 5, 2026 VERIFY rendered (HTTP 200): urls OK, date OK

Both lines were true statements about what the code intended and false about the page. The mechanism was one line:

ok_d = (TODAY_HUMAN in hay) if dated else True

dated was bool(old_dates) — whether the date pattern had matched anything. The page’s badge phrasing was unrecognised, so nothing matched, so dated was False, so the check was skipped and printed the identical word it prints when it passes. A reader cannot tell the two apart, which means the green line is not weak evidence, it is manufactured evidence.

Rules:

  1. A check’s disabled state must never be spelled like its passing state. “OK”,

“not applicable”, and “NOT CHECKED” are three different facts and must read as three different facts. If a check can decline to run, its output has three values, not two.

  1. Prefer no gate to a conditional gate. The fix was to look every time and to describe

what was found — OK, OK (no badge pattern matched, but today's date is present), NOT STAMPED. The condition that used to skip the check now only changes the wording.

  1. Fixing a drift in one direction can create it in the other. On August 1, 2026

data-kept-current was found lagging a visible badge that was correct daily, and the fix made the attribute follow the badge. On this page there was no badge the code could see, so the attribute became the only thing that updated: it read 2026-08-05 while the human-readable page read August 3. The machine twin went from trailing the truth to asserting something the page visibly contradicted — strictly worse, because that attribute exists precisely to be trusted by machines that will never read the prose.

  1. Two representations of one fact need an equality assertion, not two writers. The

durable guard is: if the machine field says today and the human text never does, fail. It needs no knowledge of how the badge is worded, so it is the check that will still work on the surface nobody has thought about yet.

  1. **When two verifiers disagree about one page, the louder one is not automatically right —

but the disagreement is always a defect.** Here step 4 said OK and step 7 said MISSING on the same URL. Step 7 was correct. The fact that a later stage caught it is not a reason to leave the earlier stage lying: a lie at step 4 costs the diagnosis time of every future failure, and step 4 is the one that decides what gets published.

Diagnostic that settled it in one fetch: the same anonymous, cache-busted response contained the new zip filename and the old date. A stale cache cannot do that — it would have to miss both. One request separated “the page did not update” from “the page updated and the date logic never fired,” and pointed straight at the badge pattern.

Learned August 5, 2026.

<!– learning:2026-08-05-a-rotation-announced-is-not-a-rotation-applied –> August 5, 2026 (from: skill-pack-propagation daily run)

The push failed with a 403 and the fix had been sitting in the thread for two days — we were presenting a superseded credential

The daily run’s ledger push to git.adectra.com/blitzmetrics/agent-runtime returned 403 “You are not allowed to upload code.” The instinct — and the first draft of the escalation — was “the remote changed, ask a human to restore our access.”

That was wrong, and one rule caught it: an auth error tells you the credential presented was refused, never that it was the credential you meant to present. Proving which one went over the wire took three checks and about a minute:

Check Result What it ruled out
git ls-remote succeeds not a network/host/outage issue
GET /personal_access_tokens/self active, not revoked, write_repository, expires 2027-07-20 not an expired or revoked token
push to a non-master ref same 403 not branch protection — receive-pack is refused outright
token created_at 2026-07-21 17:27 ← the answer

That creation timestamp is three minutes before the email that originally delivered it. Two days earlier, in the same thread, the person who owns those tokens had written: “I have created new GitLab tokens and shared them with you via LastPass. Let me know when you’ve swapped those and I’ll remove the old tokens.” He had begun narrowing the old ones. We were still presenting the July token, so it read (leftover role) and could not push.

Rules:

  1. A rotation announced in prose is not a rotation performed. “New credentials are in

the vault” is a message, not a change. Until someone swaps the value and a real call succeeds with it, the system is running on the old one.

  1. Make the swap a task with an owner and a due date, not a sentence in a thread. The

half-completed rotation is the dangerous state: the old credential still half-works, so nothing fails loudly until something does, days later, in a scheduled job at 4am.

  1. Ask the vendor’s API who you are before you ask a human what changed. Name, scopes,

created_at, revoked — four fields that convert “the remote is broken” into “our copy is stale,” and they are readable with the very credential that is failing.

  1. created_at on a credential is a diagnostic, not trivia. Compare it against the last

rotation you were told about. If it predates that conversation, stop debugging the remote.

  1. Escalate with the answer, not the symptom. The draft that went out says “we never

applied the rotation you told us about, don’t revoke yet, I’ll swap and confirm” — which costs the other person nothing. The draft that nearly went out asked him to investigate a permissions regression that did not exist.

The pattern underneath, which has now happened twice on this exact thread: in July a reply containing both tokens sat unopened for eleven days while our own notes said “waiting on GitLab.” In August a rotation sat unapplied for two days while the job reported a remote failure. Both times the answer had already arrived and nothing was watching the channel it arrived on. A plan that names an external blocker is only as fresh as the last time someone checked the place the unblock would appear — and a job that reports “they refused us” when the truth is “we never updated” sends everyone looking in the wrong direction.

Learned August 5, 2026.

<!– learning:2026-08-05-an-enumeration-cannot-be-fixed-with-a-longer-enumeration –> August 5, 2026 (from: skill-pack-propagation daily run)

Two hard-coded lists in one pipeline were outrun on the same day — both had already been “fixed” by making the list longer

On August 5, 2026 the daily propagation run hit the same defect twice, in two unrelated files, within an hour of each other:

Where The list What walked past it Cost
harvest_learnings.py ("targets","target","skill","skill-slugs","skill_slugs") a note written with target_skills: a correct lesson sat unapplied for 2 days
pack_republish_lib.py `_LABEL = “Kept current\ Last updated\ kept current\ last updated”` a badge phrased Registry-reported update — <date> a live page served a 2-day-stale date under a run that reported success

Both lists carried a comment, written by an earlier run, correctly diagnosing the problem: “a guard that hard-codes the single instance of a class catches that instance and nothing else.” Both times the remedy chosen was more entries. An enumeration of the spellings we have seen cannot cover the spellings we have not; lengthening it moves the failure date, it does not change the failure.

What to do instead — match the shape, or assert the invariant:

  1. Match on shape, not on membership. The harvest fix stopped listing aliases and

started asking whether the key normalizes to something containing “skill” — so target_skills, skillSlugs, skills_to_update and spellings nobody has written yet all land. Keep the literal list only as a deterministic preference order for notes carrying two candidate keys, never as the gate.

  1. Where shape-matching is unsafe, assert a cross-invariant that needs no list. Badge

phrasings can’t be pattern-matched safely — too close to ordinary prose, and a greedy rule would rewrite historical dates the page states as fact. So the durable guard became: if data-kept-current says today and the visible page never says today, fail. That catches any unrecognised phrasing on any surface, present or future, without knowing a single one of them.

  1. Enumerate for the known case, cross-check for the class. Do both. The list fixes

today’s page; the invariant is what survives the next author’s wording.

  1. **When you add an entry to a list like this, that is the signal to ask what the list is

standing in for.** The second entry is a coincidence. The third is a design defect. Both of these were on their third.

The tell that generalises: any guard whose correctness depends on someone remembering to extend it will be correct until the first person who does not know it exists. The people writing target_skills: and Registry-reported update were not doing anything wrong — they were writing what read naturally. A guard that requires the rest of the world to phrase things our way is a guard that has outsourced its own reliability.

Learned August 5, 2026.

<!– learning:2026-08-09-verify-target-is-real-and-crawlable –> August 9, 2026 (from: weekly-fleet-interlinking-agent (2026-08-09 run))

Before treating a URL as a link TARGET, and before treating a page as a link SOURCE, prove two separate things over the network. A 200 answers neither.

1. A target that is live is not a target that is built. Byte count is not evidence. chuckthokey.com had been correctly rejected on four consecutive runs as “200 but 114 empty bytes — a parked lander.” It then began returning HTTP 200 with 67,915 bytes and the title “Chuck Thokey”, which reads as finally built by every metric previously used. It was a bare WordPress install: Twenty Twenty-Five default theme, the “Hello world!” starter post, a “Sample Page”, stock WordPress footer. The 68KB was theme weight. Four articles would have been pointed at a Hello World page.

Fetch the body text and check for the starter-content fingerprint before promoting a domain from parked to linkable: Hello world!, Sample Page, Designed with WordPress, twentytwentyfour|twentytwentyfive/style.css as the only theme, a lone post dated at install time. Prefer positive proof of identity — the person’s real name in prose, Person/ProfilePage schema, a real nav — over any size threshold. Liveness is not identity, and neither is size.

2. A page that publishes successfully is not a page that renders. Check content.protected. A WordPress page with a password returns content.rendered as an EMPTY STRING to the public, and the public URL serves a small stable password form that never contains the content — while REST still accepts the write, returns status: publish and a fresh modified_gmt, and context=edit reads the saved anchors back perfectly. Every success signal fires and Google sees nothing, so an SEO link filed there passes zero equity and logs as a win.

Exclude content.protected == true from candidate scans, and assert it false at verify time. Note this cuts against the standing “never call a fleet publish failed on one short response” rule: that rule is right, but a short response is sometimes real. Distinguish the two with content.protected and the length of content.rendered, never with retry count — a retry loop alone concludes “flaky CDN” and files a link that does not exist.

3. Verify with the exact href written, not the domain it came from. A verifier searching href="https://andrewpickettlaw.com reported FAIL / 0 anchors on a link written as https://www.andrewpickettlaw.com/. A www prefix or a trailing slash is enough to make a live link look dead and trigger a pointless “fix” on a page that was already correct. Build the needle from the string actually inserted, and assert the anchor appears exactly once with the intended visible text.

<!– learning:2026-08-09-test-what-wins-the-chain-not-your-entry-in-it –> August 9, 2026 (from: skill-pack-propagation (2026-08-09 run))

When a system resolves something through a priority chain, verifying YOUR entry in the chain proves nothing. Verify what wins.

Step 9 pushes the jobs ledger to GitLab. It exports GIT_ASKPASS so the token is read from .credentials.json and never written into a remote URL, .git/config or ~/.git-credentials. That design is right and it was implemented correctly. It still sent the wrong credential for four days.

Git asks every configured credential.helper FIRST and only falls through to GIT_ASKPASS when they all come up empty. macOS ships credential.helper = osxkeychain in the Command Line Tools system gitconfig, so on this machine the keychain is permanently in front of us. At 2026-08-05 02:09 UTC an internet password for git.adectra.com was written to the keychain under the account claude-audit. From that minute git_askpass.py was never executed — not once — and every push presented claude-audit.

Why three days of evidence pointed at an innocent party. claude-audit has read access, so every diagnostic we owned came back green:

check result what it actually tested
git ls-remote origin 200, refs listed that SOME identity can read
GET /personal_access_tokens/self active, not revoked, write_repository the token we MEANT to send
POST .../info/refs?service=git-receive-pack with our token HTTP 200 the token we MEANT to send
git push 403 “You are not allowed to upload code” the token git CHOSE

Only the last line observes the credential that left the machine. The escalation block printed next to the failure said “403 on push is usually OUR stale copy — compare the token’s created_at against the last rotation you were told about,” which sent the reader after a teammate’s token handoff. That is the second time this job has blamed a colleague for a defect on our side (cf. 2026-08-02, where an askpass sniffed a prompt for a project path the prompt never carries). Both share one root: an auth error tells you the credential presented was refused, never that it was the one you intended to present.

Read the failure message literally, and let it partition the causes. GitLab returns 401 HTTP Basic: Access denied for a credential it cannot authenticate, and 403 You are not allowed to upload code for one it authenticates but that lacks push rights. A 403 therefore rules out “our token was revoked” — a revoked token cannot produce it. The distinction was sitting in the error text on 08-05 and we read past it three times.

The fix, and why it is env and not -c:

export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=credential.helper GIT_CONFIG_VALUE_0=””

An empty credential.helper value resets the inherited chain, so askpass becomes the only source. GIT_CONFIG_* applies to every git process in the subshell — including git invoked from inside a script — which -c on one command would miss. It stores no secret anywhere new, so the “one store, one reader” rule is intact.

Generalize past git. The same shape is everywhere we work: PATH (a shim earlier in it wins over the interpreter you configured), Python sys.path, WordPress filter priority, DNS resolver order, ~/.aws/credentials vs AWS_* env vars vs instance metadata, MCP tool name collisions. In every case the honest question is not “is my entry correct?” but “whose entry is consulted first, and does it answer?” Before debugging a resource you configured, print the resolved chain and find your position in it.

Deleting the bad entry is NOT the fix — the fix is the guard, and here is the proof. Cleaning up afterwards, the stale claude-audit entry was removed and the keychain left empty for the host. One unguarded git push --dry-run later, the keychain held a BRAND NEW entry for git.adectra.com under the account oauth2, created that minute — our project token, copied out of .credentials.json and written to a second location by git itself.

That is standard behaviour and it is the whole trap: on a successful authentication git calls credential approve, and every configured helper stores what worked. So a credential helper is not only a reader that outranks you, it is a WRITER that manufactures the next stale entry. Delete the bad row and the very next successful push re-creates one, which will in turn go stale at the next token rotation and shadow you all over again. Manual cleanup is a fix with a half-life.

The same GIT_CONFIG_VALUE_0="" that stops the helper being READ also stops it being WRITTEN — an empty chain has nowhere to store. Verified: three guarded pushes in a row, the keychain still empty afterwards. So the guard is what keeps “one store, one reader” true, and it is also why no extra test was added for “is the keychain clean?” — that assertion is machine-specific, would be meaningless in the cloud runtime, and is already implied by the helper-list assertion that test_credential_source.py makes.

Generalizes to every cache-on-success credential layer: aws configure writing ~/.aws/credentials, docker login writing ~/.docker/config.json, gh auth login, kubectl context tokens, browser password managers. Ask not only “which store answers first?” but “which store is silently recording my answer for next time?” — because that is the one that will be wrong later, long after you have forgotten it exists.

Coda, same day, same shape, caught before it cost anything. Re-verifying the runner’s unknown-flag contract, zsh run_skill_pack_propagation.sh --bogus 2>&1 | head -6; echo $? printed the refusal and then 0 — which reads as “the guard refuses but reports success,” the exact republish_daily.py al defect. It was not. $? after a pipeline is the exit status of the LAST command, so that was head‘s 0, not the script’s. Unpiped, the script returns 2 correctly. Reading a status through a pipe measures the pipe — the identical mistake as cmd | tee || exit N testing tee, and the identical mistake as testing the credential you configured rather than the one git chose. Whenever you read a value, ask which component actually produced it. One near-miss false alarm is cheap; a false alarm filed as a defect would have sent someone to “fix” a guard that was already correct.

Test contract this produced (tools/test_credential_source.py, gated at exit 99 before the push): assert the effective helper list for the repo is EMPTY under the runner’s exact environment; prove the assertion is not vacuous by planting a helper and watching the same check detect it; and confirm askpass returns the project-correct token by digest, never by printing it. A green run now means what wins the chain is what we configured, which is a claim the previous checks could not make.

<!– learning:2026-08-11-a-gate-run-only-in-clean-conditions-tests-nobody –> August 11, 2026 (from: skill-pack-propagation daily run)

A gate that only ever runs in cleaner conditions than production is testing a program nobody executes

The daily skill-pack job did not run on 2026-08-11. Not “ran and failed a step” — it refused during pre-flight, published nothing, wrote no log, took no lock, and left a 45-byte console containing one line. Meanwhile every gate the pipeline owns was green, on that same machine, that same minute, when run by hand.

Auto-detach had shipped at 05:58 the previous day. 08-11 was its first scheduled morning.

The chain. The launcher exports SPP_DETACHED_CHILD=1 into its child — correctly, to stop the child (whose stdout is a file, which is also “not a terminal”) from detaching forever. The child’s pre-flight then runs tools/test_detach.sh, and two of that file’s cases assert the no-flag default by spawning the runner and reading its answer. The spawned runner inherited the fence. It reported MODE: foreground, which is correct for that environment and wrong for the assertion, and the pre-flight refused to start the pipeline.

Why nobody saw it. The same test file politely backed up _daily-run-<date>.console at entry and mv‘d the copy back at exit — a file it also let its --detach children truncate. That file is the live run’s fd 1. mv replaces the inode, so from that moment the running pre-flight was appending to an unlinked inode. Its refusal message — twenty-five lines naming the exact failing assertion — went nowhere. What stayed on disk was the 45-byte copy taken before the failure: a run that reads as though it simply stopped after one line.

The rules.

  1. A gate must be exercised in production’s environment, not a cleaner one. Every gate here

was run attached, from a terminal, with a clean env — the one way the real job never runs. If the real invocation differs from the tested invocation in any dimension (detached, piped, cron, different cwd, inherited exports), that dimension is untested. New gate: tools/test_preflight_under_detach.sh launches the runner through the real auto-detach path and asserts the whole pre-flight comes back clean.

  1. **A test that asserts a DEFAULT must set every input that default depends on — including the

ones it would otherwise inherit.* An inherited variable is an input nobody wrote down, so every reader assumes the clean value they see in their own shell. The case is really measuring its caller. Unset it at the top; let the one case that is *about the variable set it explicitly. (Written into the new gate too — which then made the identical mistake on its first run, inside the hour, and was caught by its own “am I really driving the path I claim to be driving?” assertion. Include that assertion in anything that spawns its subject.)

  1. Stdout is not an artifact. It is a handle to whatever the caller chose; it can be closed,

truncated, or substituted underneath you, and the process cannot tell. Anything that must survive to be read tomorrow gets appended to a file the process names itself. Append, never rewrite — >> cannot orphan another writer’s descriptor the way mv just did.

  1. Never back up and restore a file you do not own exclusively. Copy-then-mv-back looks

considerate and is the opposite: it silently substitutes the inode under every process holding it open. If a helper needs scratch space, give it scratch space (SPP_CONSOLE), and assert afterwards that the caller’s file kept the same inode and the same size.

  1. Anchor structural checks on the construct, not on a line window. grep -A 3 became

-A 6 after a comment pushed the target line out of range; on 08-11 an eight-line comment blew past -A 6 and re-armed the identical false alarm in the same file. Widening a window is a longer fuse, not a fix. Ask for “the first executable line inside this block that does X” and let people write as many comments as the code deserves.

  1. A derivation is only as good as the claim that its source is the population. A new check

derived “slugs that must resolve” from the scheduled-task inventory and flagged 29 of 31 tasks — all false, because a task files under the skill slugs it exercised, not its own name. The real population was on disk elsewhere (the notes’ own skills: history). A false alarm wearing the words of a real regression is the most expensive kind: ship it as a blocking gate and it trains its readers to ignore the pipeline. Scope the hard assertion to what is actionable today (can the inbox drain?) and demote the rest to advisory.

  1. Rank a failure by DIRECTION. This one removed output. No error, no log, no lock — the

artifact left behind looked like a short, quiet, successful morning. Anything that makes output disappear deserves more scrutiny than anything that adds output, because the reader’s instinct on seeing less is relief.


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