Commit Graph
683 Commits
Author SHA1 Message Date
Bastien Chanot 02c7a6fe6d Merge bugfix/seo-geo-integrity-phase1 into develop 2026-07-17 13:09:08 +02:00
Bastien Chanot acd452b92f fix(seo): I8 — drop the phantom .claude/audits/external/ precondition
STEP 0 told the user to run `mkdir -p .claude/audits/external` themselves
before handing over an external report. Three things wrong with that:

- The skill runs dozens of bash commands but outsourced this one to a human.
- The timing was impossible: to "drop the export in" that directory the user
  needed it to already exist, so the instruction arrived after the moment it
  would have been useful.
- The directory is not needed at all. `:218` already reads "File path given
  → Read it" — any path works — and nothing in skills/ or agents/ ever
  writes to that path. Grep confirms it is referenced by exactly these two
  lines and known to nothing else: a convention the skill invented, asked
  the user to create, and never used.

Fix removes the precondition instead of automating it: give a path from
anywhere, the tidy location stays a suggestion.

Verified: make test 35 GREEN / 0 RED.
2026-07-16 20:36:03 +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 e70e1d6c71 fix(seo): I4 — stop double-counting security headers; /harden owns them
Headers were scored three ways: seo-analyzer priced them into the Technical
axis at both depths (:619 FULL, :635 LOCAL), depth-matrix.md:29 said drop
them, and /harden re-audits them 0-100 against three external validators.
The dedup rule and the agent spec contradicted each other; the agent won by
default, so the same finding moved two scores in two reports.

Arbitrated (user): /harden keeps them, /seo drops them. That confirms the
rule that already existed — seo-analyzer was the violator.

Constraint: /harden REUSES seo-analyzer, so the capability cannot be
deleted, only scoped. Reading is not scoring:
- Technical axis definitions no longer name security headers.
- STEP 4 still curls them — needed for X-Robots-Tag, canonical/redirect
  coherence, and the §14 observed-list — but they earn no points under /seo.
- Dispatched from /harden: unchanged, headers ARE the job (verified: its
  scope spec untouched, 16 header references intact).

Carve-out: X-Robots-Tag stays in /seo under indexability. It is an indexing
directive wearing a header's clothes — `noindex` there deindexes as surely
as a meta robots tag. That is what depth-matrix.md:29 means by "unless it
directly affects indexability"; the security headers do not.

Drop is not silence: mandatory §14 line on FULL naming what was observed
live plus a "run /harden <url>" pointer. A user who never runs /harden must
not read a clean Technical score as clean headers — same principle as the
mandatory COVERAGE line (I5).

Verified: make test 35 GREEN / 0 RED.
2026-07-16 16:56:32 +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 4ea2fb8c37 fix(seo): I2 — remove VSI, an SEO-blog fiction, from CWV thresholds
seo-analyzer.md:278 listed "VSI (Visual Stability Index) — new 2026 signal,
Google Core Web Vitals 2.0" as a threshold, stated as fact, no hedge, in
client-facing audits. It does not exist.

Verified against two primary sources:
- developer.chrome.com/docs/crux/api — complete metric list carries no
  visual_stability_index. Blogs claimed "Google is actively collecting VSI
  through CrUX": flatly false.
- web.dev/articles/vitals — three stable CWV (LCP, INP, CLS). No VSI, no
  "Core Web Vitals 2.0". Thresholds change with prior notice on an annual
  cadence.

Ten SEO blogs cross-cited each other into an apparent consensus. WebSearch
returns that consensus, which is why the resources README rule "agents MUST
cross-check via WebSearch on FULL" did not catch it — that mitigation
launders blog misinformation into apparent verification.

Fix removes the metric and states the sourcing rule where a future rumour
would land: primary sources only (web.dev / Chromium blog / CrUX API list,
the last being decisive — a metric CrUX cannot return is one we cannot
score). Incident documented inline so it is not re-added.

Verified: make test 35 GREEN / 0 RED.
2026-07-16 16:46:52 +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 57c67f2f75 fix(seo): I1 — scope Off-page axis to what is actually measured
Axis was defined "backlinks, mentions, authority" (10% local / 15%
national of the FULL score) but only mentions have a data source
(STEP 6 web_search "<name>" -site:<domain>). Backlinks and authority
have no index, no API — the agent had to invent 2/3 of the number, and
that number reaches a client via /client-handover.

- Axis label names what is measured + points at §14.
- Off-page axis note: score mentions ONLY; never price in unmeasured
  sub-components; a low mention count is NOT evidence of a weak backlink
  profile. Mandatory verbatim §14 line naming the gap + the nearest free
  source (Common Crawl) so the omission is legible, not silent.
- LOCAL N/A label: was `N/A — requires FULL audit`, a promise FULL cannot
  keep for backlinks. Now states FULL covers brand mentions only.

Weights deliberately unchanged: re-deriving now and again when a backlink
source lands would churn historical scores twice. Revisit when the axis
widens back (Common Crawl, phase 5).

Note: initial plan was to mark the axis N/A in FULL and redistribute the
weight. Reading the real spec (seo-analyzer.md:622 + STEP 6) showed that
over-corrects — it discards the mentions data, which IS gathered. Narrowed
the definition instead; composes with the Common Crawl work later.

Verified: make test 35 GREEN / 0 RED.
2026-07-16 16:14:12 +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 3bc6506332 Merge chore/fix-inert-write-deny-rules into develop 2026-07-16 15:04:00 +02:00
Bastien Chanot 56aa3c8a17 chore(memory): BDR-069 + LRN-130 + EVAL-024 — deny-list design pass
- BDR-069: keep broad Edit(**/.env.*), keep .env.example name (option A).
  Rename rejected (~30 refs); glob narrowing rejected (fails open on
  .env.production outside the Next.js convention).
- LRN-130: a deny glob is absolute — allow, `!` negation and PreToolUse
  hooks all fail to exempt it (permissions.md :33/:35/:361, verbatim).
  Only lever = the glob's own shape.
- EVAL-024: the pass shipped one unauthorized weakening (scope inversion +
  framework parochialism) on my own permission boundary, caught by the
  auto-mode classifier rather than self-caught. Reverted pre-commit. Also
  logs a false-positive automated review and a bad subagent glob claim.
2026-07-16 15:02:00 +02:00
Bastien Chanot 960d3f33ea chore(graphify): sync vendored skill 0.9.6 -> 0.9.15
Upstream skill refresh, present in the working tree before this session —
committed here rather than left dangling. Not authored work.

- uv invocation fix: `uv tool run graphifyy python` -> `uv tool run --from
  graphifyy python`. Without --from, uv resolved the command name against
  the package instead of running the interpreter.
- default output is now HTML viz; --obsidian opts into the vault.
- description reworded to trigger on codebase questions generally, not
  only when graphify-out/ already exists.
2026-07-16 14:45:37 +02:00
Bastien Chanot 07ca738b3f fix(settings): Write() deny rules inert — convert to Edit(), close write gaps
Startup emitted 15 warnings: "Write(**/.env) is not matched by file
permission checks — only Edit(path) rules are."

Write(path) rules never matched. The 5 secret-file write bans were dead
config — .env, secrets/**, *.pem, *.key were freely writable. Converting
to Edit() makes them enforced: permissions.md:242 "Edit rules apply to all
built-in tools that edit files", and :244 prescribes exactly this ("add an
Edit deny rule for paths no tool may change").

- settings.json: Write(...) -> Edit(...) on the 5 patterns.
- Mirror the 9 secret patterns Read denied but Edit did not: *.p12, *.pfx,
  id_rsa*, id_ed25519*, .ssh/**, credentials, credentials.json,
  .aws/credentials, .azure/**. Read/Edit parity now 14/14. Claude could
  previously overwrite an SSH private key or ~/.aws/credentials.
- New read-allowed/write-denied class: lockfiles (*.lock,
  package-lock.json, pnpm-lock.yaml, go.sum) + node_modules/**. Reading
  aids diagnosis; hand-editing is always wrong — the package manager
  regenerates them via Bash, which Edit deny does not block.
- templates/settings/SETTINGS.md taught the broken Write() pattern; fixed
  at the source so /onboard stops propagating it.

Rule syntax has no negation and deny beats allow, so deny globs cannot
carry exceptions — see the .env.example conflict noted in the follow-up.
2026-07-16 14:45:26 +02:00
Bastien Chanot 83eba36ac7 chore(memory): journal — v1.1.0 cut + v4.0.0 stale-tag watch-item 2026-07-16 14:06:58 +02:00
Bastien Chanot 21b1e21a2c Merge release/1.1.0 into develop 2026-07-16 13:57:08 +02:00
Bastien Chanot 0543dafa2d chore(release): 1.1.0 — version.txt + CHANGELOG 2026-07-16 13:56:02 +02:00
Bastien Chanot 1b13bac652 Merge feature/close-auto-persist into develop 2026-07-16 13:52:12 +02:00
Bastien Chanot 096418c3e7 feat(capitalize): auto-persist memory to develop on /close + /capitalize (STEP 5C, BDR-068) 2026-07-16 13:51:15 +02:00
Bastien Chanot d36d4d0a58 Merge chore/session-close into develop 2026-07-16 13:42:31 +02:00
Bastien Chanot c41aac6975 chore(memory): LRN-128 LRN-129 EVAL-023 — close ritual 2026-07-16 13:37:43 +02:00
Bastien Chanot fdbe168ad8 chore(memory): BDR-067 — v1.0.0 first public release (versioning reset) + journal + TODO 2026-07-16 13:29:54 +02:00
Bastien Chanot 6c23d6f925 Merge release/1.0.0 into develop 2026-07-16 13:22:29 +02:00
Bastien Chanot b0e2ebc31a chore(release): 1.0.0 — first public release (versioning reset from internal 4.x lineage) 2026-07-16 13:12:10 +02:00
Bastien Chanot 5f159f38d2 Merge bugfix/model-routing-edge-fixes into develop 2026-07-16 12:50:01 +02:00
Bastien Chanot 890e55f789 fix(model-routing): ronde edge fixes — feater applier carve-out, /refactor→sonnet dispatch, /analyze gate, audit-pin guards (F1-F5) 2026-07-16 12:45:07 +02:00
Bastien Chanot d8917bff4c Merge feature/client-handover-dispatch into develop 2026-07-16 12:17:11 +02:00
Bastien Chanot c43f89cede docs(memory): LRN-126 (split severs implicit data paths) + LRN-127 (SDD implementer git-ops discipline) 2026-07-16 12:17:01 +02:00
Bastien Chanot 1947a21237 fix(model-routing): wave-4 review fixes — forward DEPLOY_HINTS + SKIP_SEO in PACKAGE, realign §7/§8 annex numbering (I1/I2/I3) 2026-07-16 12:12:53 +02:00
Bastien Chanot fe1d60fccb chore(model-routing): wave-4 census + docs + BDR-066 (client-handover doc-gen → sonnet) 2026-07-16 11:56:54 +02:00
Bastien Chanot 1dcda2702b feat(model-routing): client-handover MODEL GATE (pipeline orchestrates audits = reflection) 2026-07-16 11:51:57 +02:00
Bastien Chanot 1ec032d2af feat(model-routing): client-handover-writer trimmed to pipeline + delegates doc-gen to sonnet doc-writer
STEP 1-8 preserved byte-for-byte; STEP 9-16 replaced by a doc-gen orchestration
that resolves all interaction (questions, NAP, precheck, overwrite, client-name),
assembles the PACKAGE, and dispatches handover-doc-writer. Dropped the inert
model: opus pin (inherits the big session model via inline-load).
2026-07-16 11:51:10 +02:00
Bastien Chanot a98610f676 feat(model-routing): handover-doc-writer — sonnet gate-free deliverable generator (wave 4) 2026-07-16 11:36:58 +02:00
Bastien Chanot 872225f7d1 docs(model-routing): wave-4 plan (client-handover redaction-only doc-gen dispatch) + TODO 2026-07-16 11:26:42 +02:00
Bastien Chanot e5c7c516d2 Merge feature/model-routing into develop 2026-07-16 11:03:25 +02:00
Bastien Chanot 30f732c08f chore(memory): LRN-125 — no dual-use agent across model tiers (wave-3 lesson) 2026-07-16 11:02:32 +02:00
Bastien Chanot 4294bc2af5 docs(model-routing): USAGE onboard STEP 6 audit → general-purpose (wave-3 consumer sweep, review finding) 2026-07-16 10:41:23 +02:00
Bastien Chanot bed695a6c6 chore(model-routing): wave-3 census + docs + BDR-066 update (bugfix/code-clean split) 2026-07-16 10:34:12 +02:00
Bastien Chanot a7d4df8704 feat(model-routing): /code-clean split — audit+gate inline, code-cleaner = sonnet PHASE-2 executor
code-cleaner is now a pure fix executor (was audit+gate+execute). Reroute the two
read-only-audit consumers (onboard STEP 6, tour Phase B) to a big-model agent
(general-purpose/analyzer) — an audit must stay on the big model, never the sonnet
executor. Refactor now runs on sonnet inside the executor (inline-load pin was inert).
2026-07-16 03:24:51 +02:00
Bastien Chanot 1f7afc1d49 feat(model-routing): /bugfix split — reflection inline, bugfixer = sonnet executor (supersedes BDR-050 bugfix carve-out)
Reroute hotfix's deeper-bug escalation to the /bugfix skill (bugfixer is now a
pure executor, not loadable standalone). loops-light locks repointed to the
bugfix orchestrator + bugfixer-executor shape.
2026-07-15 23:50:34 +02:00
Bastien Chanot 152da63624 docs(model-routing): wave-3 plan (bugfix + code-clean split) + wave-4 stub + TODO 2026-07-15 23:16:52 +02:00
Bastien Chanot 2136953b2a fix(model-routing): commit-changer resolves step→hash refs on apply; edge-case resume notes (release NEED-DECISION, commit-change skip) 2026-07-15 22:05:23 +02:00
Bastien Chanot bc8eede090 chore(model-routing): wave-2 census + docs + BDR-066 update 2026-07-15 21:49:34 +02:00
Bastien Chanot dd7868fc1c fix(model-routing): release-candidate title back to 'orchestrator' to match preserved Overview doctrine 2026-07-15 21:37:23 +02:00
Bastien Chanot da50c38be9 feat(model-routing): /release-candidate dispatches sonnet release-executor, human gates in dispatcher 2026-07-15 21:31:54 +02:00
Bastien Chanot 4217fcfe35 fix(model-routing): keep commit grouping on the sonnet subagent (edit<n> re-dispatches; /feat routes to /commit-change) 2026-07-15 21:24:48 +02:00
Bastien Chanot ab0fafc0ef feat(model-routing): /commit-change dispatch to sonnet commit-changer, gates relocated to dispatcher 2026-07-15 21:09:15 +02:00
Bastien Chanot 29fa962e43 fix(model-routing): /feat Rule 1 downgrade routes to /hotfix skill, not the bare executor agent 2026-07-15 19:46:56 +02:00
Bastien Chanot 45cd86810a feat(model-routing): /hotfix split — reflection inline + gate, hotfixer = sonnet executor (dual-use applier preserved) 2026-07-15 19:28:58 +02:00
Bastien Chanot f36aec370b feat(model-routing): doc/status dispatch their agent (sonnet/haiku pins take effect) 2026-07-15 12:38:12 +02:00
Bastien Chanot 89093a7835 docs(plan): model routing wave 2 — pure-execution + reflection-split skills (doc/status/hotfix/commit-change/release-candidate) 2026-07-15 12:34:20 +02:00