Commit Graph
124 Commits
Author SHA1 Message Date
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 2de58faa38 feat(seo-data): C1b — sitemap verb, the denominator COVERAGE never had
I5 made a COVERAGE line mandatory in STEP 9 and told the agent to "count the
URLs in sitemap.xml" without giving it a command. STEP 4 only ever did
`curl … | head -50` — a preview, not a count. This closes that.

fetch.sh sitemap --url … → {count, urls[], index, dropped}. Stdlib only
(urllib + xml.etree + gzip): no auth, no Google, no venv, so it runs wherever
the mock/degrade paths run. Follows <sitemapindex> one level, dedupes, strips
whitespace, handles .xml.gz. Every cap REPORTS what it cut (children_skipped,
truncated) rather than truncating silently — same rule as COVERAGE itself.

PLAN CORRECTION: the proposal said the verb would "validate each URL via the
H1 guard". Wrong. urllib fetches these, so nothing here reaches a shell and
there is no injection surface to guard. The guard belongs at the point of
use, where seo-analyzer interpolates a URL into curl — which is the contract
the sameAs check already established. A second copy of url-guard here would
only drift from the first. The module carries a garbage filter, named as such.

SECURITY: the security-guidance hook asked for defusedxml. Taken seriously,
not obeyed — it would drag a venv into a module whose whole point is being
stdlib-only. Split the threat instead: xml.etree does NOT expand external
entities (XXE is not the vector), but it IS billion-laughs-vulnerable, and
the 20 MB read ceiling bounds the input, not the expansion. A sitemap NEVER
has a DTD — sitemaps.org is <?xml?> then <urlset xmlns=> — so any
doctype/entity is refused BEFORE parsing, with its own reason
(unsafe_xml_dtd, distinct from parse_failed: it is a finding, not a glitch).
Refusing the construct beats depending on parser internals. Fixture is a real
billion-laughs payload.

Verified against the live target, not just fixtures: zenquality's sitemap
returns count=86, dropped=0, matching `grep -c '<loc>'` on the raw XML
exactly. Dead URL → {"status":"degraded","reason":"fetch_failed"}, exit 0.
seo-data 95 -> 110 pass, 0 fail; full suite green; shellcheck + py_compile
clean.

Note: no config-edit sentinel was needed after all — config-protection guards
lib/tests, not lib/seo-data. I posted one, found it uncommitted-and-unconsumed
afterwards, and removed it rather than leave an open one-shot gate lying
around. Worth knowing: seo-data.test.sh is 110 assertions and is NOT covered
by that hook, while lib/tests/*.test.sh is.
2026-07-17 11:28:00 +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 a6d423b940 feat(seo-data): W1 — surface rich_results, the data inspect already threw away
google_seo.py:129 read only indexStatusResult out of the URL Inspection
response and discarded the rest. richResultsResult was already on the wire:
same call, same OAuth scope (webmasters.readonly), same quota. Google's own
structured-data verdict on the live indexed URL was being downloaded and
binned.

Plan correction: the TODO said "richresults verb". Wrong — a new verb means
a second POST to the same endpoint for a payload already received, on a
per-site quota, and nobody wants rich results without index status. Extended
inspect() instead; fetch.sh unchanged, no new verb, no new scope.

Design driven by the published schema, not by guesswork — two details I
would have got wrong:
- richResultsResult is OMITTED when Google detects none ("absent if none
  found"). Surfaced as synthetic verdict ABSENT rather than a missing key: a
  caller cannot tell an absent key from a check that never ran. ABSENT means
  "none detected", never "invalid". The KeyError path is the real risk here,
  so it has its own fixture dir (fixtures-norich/) and its own tests.
- PARTIAL is "Reserved, unused" per the API docs. The draft emitted it. It
  never emits it now, and a test asserts the absence.

issues[] deduped (the same issueMessage repeats across every affected item),
errors/warnings count instances — scale from the counter, cause from the
message.

seo-analyzer STEP 4 consumes it as the system's only programmatic JSON-LD
validation, bounded honestly: index:inspect is per-URL, quota'd, and needs a
verified property, so its reach is the STEP 9 COVERAGE ratio, not the site.
Replacing a fake validator with a fake coverage promise would be no better.

This is what beats claude-seo: their README's "dual validator (Rich Results
Test + Schema Markup Validator)" is two hyperlinks a human clicks — grep of
their .py finds zero calls. This is Google's verdict, via auth already held.

Note: the new dedupe assertion trips SC2015 (A && B || C), same as the
pre-existing line 27; ok() ends on an assignment so it cannot fail. Kept for
house-style consistency — lib/seo-data/*.sh is outside the lib/*.sh
shellcheck glob anyway.

Verified: seo-data 85 -> 95 pass, 0 fail; both paths exercised end-to-end
and output inspected by hand; make test 35 GREEN / 0 RED; py_compile clean.
2026-07-16 20:51:30 +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 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 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 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 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 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 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 da50c38be9 feat(model-routing): /release-candidate dispatches sonnet release-executor, human gates in dispatcher 2026-07-15 21:31:54 +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 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 56c451ea25 feat(model-routing): /feat re-architecture — reflection inline, feater = sonnet executor (partial supersede BDR-050) 2026-07-15 11:33:18 +02:00
Bastien Chanot 1ed77cb3fb feat(model-routing): pin hotfixer sonnet (executor), un-pin analyzer (inherits session) 2026-07-15 11:16:33 +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 b6bde8f4ee feat(seo): NAP guardrails + audit-end deliverables
- STEP 0 collects user-confirmed CANONICAL NAP (LRN-032 zenquality:
  duplicated-seed trap — source majority is not truth)
- Both dispatch prompts carry the canonical NAP + no-majority rule;
  seo-analyzer spec forbids directional NAP fix without confirmation
- STEP 2 now emits .claude/audits/HUMAN-ACTIONS.md (checklist from §11)
  and NAP-KIT.md (local business) in BOTH modes — audit-only runs leave
  the user immediately actionable; /client-handover §4 consumes NAP-KIT
2026-07-10 17:03:53 +02:00
Bastien Chanot 15962fcd90 feat(seo): wire GSC+CrUX data into /seo FULL (STEP 0 account select, CWV field, GSC perf) 2026-07-10 02:19:57 +02:00
Bastien Chanot 9d9c55e87c fix(commit-change): add gitflow aiguillage before commit
commit-changer committed code autonomously with no branch precondition
(gitflow-conformity §2a, verified MEDIUM CONFIRMED) — on develop/main it
attempted a direct code commit, backstopped only by the pre-commit hook.
Adds Phase 0: the same `gitflow-aiguillage.md` mechanism hotfixer/bugfixer/
feater already use (TYPE=chore) — branches to chore/* on a protected base,
no-op on a working branch — plus a report-only fallback (no develop / no
lib -> ask human, don't auto-branch). Commit-plan gate + scoped staging
untouched. SKILL.md pre-flight notes the aiguillage.

Dry-run (throwaway repos, REAL lib + REAL pre-commit hook) — both paths:
  CASE 1 on develop: aiguillage -> chore/commit-pending, code commit
    SUCCEEDS, hook never blocks; contrast: same commit direct on develop
    is BLOCKED -> aiguillage is what avoids it. PASS
  CASE 2 no develop: fallback -> no auto-branch, changes uncommitted,
    no chore/* created, ask human. PASS
2026-07-09 11:31:59 +02:00
Bastien Chanot 2741e8b239 fix(client-handover): gate push behind explicit GO + report-only fallback
STEP 5 previously ran `git push origin "$CURRENT_BRANCH"` autonomously
after the fix loops (gitflow-conformity §2b, verified HIGH). Now:
- gitflow precondition: no develop / no lib -> skip commit+push, note in
  summary (report-only fallback).
- push gated behind an explicit-GO AskUserQuestion (A push / B defer)
  BEFORE the push; red-flag STOP on push without GO / finish / merge.
Core untouched: SEO/GEO/HARDEN/VALIDATE loops, scoring, doc + PDF branding.

Dry-run (throwaway repos) — both sides of each fallback:
  CASE 1 report-only (no develop): DEVELOP_OK=no -> NO commit/push. PASS
  CASE 2 gate answer A: -> RUN git push origin feature/handover. PASS
  CASE 3 gate answer B: -> SKIP, push deferred; HEAD unchanged. PASS
2026-07-09 11:30:58 +02:00
Bastien ChanotandClaude Opus 4.8 5a1fff5030 chore(skills): drop find-skills — unused, and its update step began timing out
RC fix (soak day 1): `make update` step 7.5 failed on a 300s clone timeout
of alchaincyf/find-skills. The skill (search the skills.sh registry from
Claude) was never used; the discovery case stays reachable manually via
`npx -y skills find <query>`. Removed from install-plugins.sh (install list
+ summary + comment), update-all.sh (refresh list), link.sh
(NPX_EXTERNAL_SKILLS), lib/toggle-external.sh (MANAGED_TOOLS + case arms),
plugin-advisor.md, .gitignore; local skills/find-skills symlink deleted.
Memory-registry and test-fixture mentions kept — append-only history.
toggle-external `list` verified post-removal; suites 8/8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS
2026-07-08 17:11:11 +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 5a0fc1653a fix(agents): quote strict-YAML descriptions (seo-analyzer, security-auditor)
Both line-3 descriptions contained an unquoted ': ' (and '|') that fails
python3 yaml.safe_load ('mapping values are not allowed here'). seo-analyzer's
line was last rewritten by job9/a5a7b54 AFTER job2's F7 strict-YAML rule
(git blame); security-auditor's dates to job6. Single-quote wrap, no internal
apostrophes. Gate: yaml.safe_load over ALL agents/*.md now clean. Review A4.
2026-07-08 15:17:27 +02:00
Bastien Chanot 56018df52b fix(agents): strip banned Co-Authored-By trailer from bugfixer/feater/hotfixer templates
Completes job9/5a3de92 (which only cleaned commit-changer). These 3 execution
agents still emitted the banned trailer into their commit-message templates;
the settings.attribution backstop does not filter agent-authored message bodies.
Extended sweep of agents/ lib/ hooks/ templates/ for Co-Authored-By|Claude-Session|
--trailer now returns zero. Review finding A1 (BLOQUANT), J4-16 follow-up.
2026-07-08 15:14:00 +02:00
Bastien Chanot 5a3de923ac job9: strip banned attribution trailer from commit-changer template 2026-07-08 13:09:29 +02:00
Bastien Chanot af9656faee job9: H1+H2 code-cleaner→refactorer — INLINE-LOAD verb + named handoff contract, drop unused Agent 2026-07-08 12:38:42 +02:00
Bastien Chanot 87d63bfa93 job9: H2 mark scaffolder→doc-syncer as INLINE-LOAD (idiom disambiguation) 2026-07-08 12:38:42 +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 a5a7b54f28 job9: re-architect seo-analyzer to fix-bundle→L1 (path b, no nested dispatch) 2026-07-08 12:24:22 +02:00
Bastien Chanot 5ab6c21e38 job9: D pin plugin-advisor to sonnet 2026-07-08 02:50:08 +02:00
Bastien Chanot 1c270e6537 job9: C pin security-auditor to sonnet 2026-07-08 02:50:08 +02:00
Bastien Chanot ea6c126f73 job9: B pin verifier to sonnet 2026-07-08 02:50:08 +02:00
Bastien Chanot 0ede52c0ea job9: A commit-changer drop unused Agent tool 2026-07-08 02:50:08 +02:00
Bastien Chanot 00c97bcacb job6: supply-chain documentation pass (F-X1, semgrep caveat)
- README + plugins.lock.json graphifyy note: pipx/PyPI install only, never
  npm/npx — a different publisher (rhanka/graphify) squats the same
  'graphifyy' name on npm as a version-shadowing shim with its own
  conflicting 'graphify' bin (F-X1).
- agents/security-auditor.md: one-line caveat that p/* semgrep packs are
  fetched from the registry at runtime — the CLI version pin does not
  freeze ruleset content, so a new BLOCK can appear on unchanged code.

MCP magic (F-X3): version pin declined by user call (stays @latest in
lib/toggle-external.sh). ${VAR} env expansion confirmed unsupported at
~/.claude.json user scope (Claude Code docs — expansion is .mcp.json
project-scope only), so the BDR-026 reference-not-plaintext pattern
doesn't transfer here; existing mitigations (canonical ~/.claude/.env,
gitignore, audit env-field filtering) remain the practical ceiling.
~/.claude.json regenerated out-of-repo via toggle-external.sh disable+
enable magic to pick up the already-rotated MAGIC_API_KEY (no repo diff,
no commit for that file — traced in the job6 final report).
2026-07-07 03:42:15 +02:00
Bastien Chanot b4896c9ae1 job6: gsd-pi 2.64.0→3.0.0 — adapt status-reporter parser to ADR-013 cutover
Upgrade confirmed format-incompatible before use (job6 gate, BATCH-2):
gsd-pi 3.0.0 no longer writes .gsd/ROADMAP.md (verified by generating a
real test milestone in a scratch project) — state moved to .gsd/STATE.md,
.gsd/gsd.db (authoritative DB), and one .gsd/milestones/<ID>/<ID>-ROADMAP.md
per milestone, all in a different markdown shape. Every grep/awk in
status-reporter.md PHASE 3 would silently print 0/blank against the old
path instead of erroring.

Rewired PHASE 3 to read `gsd headless query` (stable JSON snapshot, no LLM
call) instead of scraping markdown — smoke-tested against both the absent
case (this repo, no .gsd/) and a real gsd-managed scratch project.

plugins.lock.json pin bumped deliberately to 3.0.0 (update-all.sh honors
the pin; this is the required manual bump).
2026-07-07 03:23:19 +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 30c5803453 job2: F6+F12 status-reporter — version path + subordination 2026-07-06 12:19:16 +02:00
Bastien Chanot 18c1b327c3 job2: F12 subordinate 6 agent descriptions to their skills 2026-07-06 12:19:08 +02:00
Bastien Chanot 112714fafa job2: F11 compress 3 agent descriptions 2026-07-06 12:18:31 +02:00
Bastien Chanot 73c765aa08 feat(install): wire impeccable into the toolchain — deterministic design floor + /impeccable verbs
Complementary to frontend-design (kept: build-time aesthetic direction).
impeccable adds what the chain lacked: 45 deterministic anti-slop rules
(npx impeccable detect, exit 0/2, --json) — the design counterpart of the
semgrep gate — plus 23 design verbs under one /impeccable skill and
persistent per-project design context.

- plugins.lock.json: CLI pinned 3.2.0 (rules update = audit output change
  on unchanged code, LRN-077 class); skill dist = its own release track
- install-plugins.sh Step 8d: staged npx install (tmpdir) -> moved to
  skills-external/impeccable (machine-owned, gitignored, ctx7 pattern);
  never writes through the ~/.claude/skills symlink into the tracked tree
- update-all.sh: pin-honored refresh, Node<24 or failure -> dist kept
- Node >= 24 required (host at 22): steps skip gracefully, activation
  deferred to a deliberate Node bump
- link.sh EXTERNAL_SKILLS, profiles (design/web/web-full/full),
  plugin-advisor, CLAUDE.md design routing, design-gate, README, CHANGELOG
- NOT in design GATE-BLOCK yet: promotion after first dogfood
2026-07-05 14:42:42 +02:00