From 56018df52bf24b54171302a92e0a5e60f88a6f9b Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:14:00 +0200 Subject: [PATCH 01/13] 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. --- agents/bugfixer.md | 2 -- agents/feater.md | 2 -- agents/hotfixer.md | 2 -- 3 files changed, 6 deletions(-) diff --git a/agents/bugfixer.md b/agents/bugfixer.md index 283013d..f07dff5 100644 --- a/agents/bugfixer.md +++ b/agents/bugfixer.md @@ -180,8 +180,6 @@ Apply the fix following the plan: - - Co-Authored-By: Claude ``` 7. Print summary: ``` diff --git a/agents/feater.md b/agents/feater.md index c02aeba..0faea7f 100644 --- a/agents/feater.md +++ b/agents/feater.md @@ -138,8 +138,6 @@ Commit using conventional format: feat(): - -Co-Authored-By: Claude ``` If the feature touched multiple concerns (e.g., feature + config + diff --git a/agents/hotfixer.md b/agents/hotfixer.md index 20925f0..0db2d15 100644 --- a/agents/hotfixer.md +++ b/agents/hotfixer.md @@ -132,8 +132,6 @@ Apply the minimal change that fixes the bug: 4. Commit using conventional format (only after verify AND security pass): ``` fix(): - - Co-Authored-By: Claude ``` 5. Print summary: ``` From d4526e6fa7228e0a24d0964033c34e39dff32ddd Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:16:19 +0200 Subject: [PATCH 02/13] chore(gitflow): regenerate installed pre-commit hook to include job7 gitleaks backstop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: job7/17bdd08 added the gitleaks scan to the hook GENERATOR (_gitflow_emit_pre_commit) but the installed .githooks/pre-commit is only (re)written by 'gitflow init'/'install-hook' — never invoked on this repo after job7. No mechanism propagates a generator change to already-installed hooks, and T10 diffs only the allow/block verdict (not content), so the drift was silent. The installed hook (620071b, 2026-06-29) predated the gitleaks addition by 8 days. Regenerated via 'gitflow.sh install-hook'; installed hook now == fresh emit. Gates: grep -c gitleaks=7; negative test (staged AKIA... on a working branch) BLOCKED with exit 1; make test GREEN. Review finding A2 (P0). A content-drift assertion is added to make test in the fil-rouge commit. --- .githooks/pre-commit | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 0ddeefc..1efe956 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -7,6 +7,19 @@ br=$(git symbolic-ref --short -q HEAD 2>/dev/null) git rev-parse --verify -q HEAD >/dev/null 2>&1 || exit 0 # root commit — allow [ -f "$gd/MERGE_HEAD" ] && exit 0 # merge in progress — allow +# Secret backstop (job7) — any branch, not just protected ones. Non-blocking +# if gitleaks isn't installed; auto-discovers ./.gitleaks.toml (repo root). +if command -v gitleaks >/dev/null 2>&1; then + if ! gitleaks git --staged --no-banner >/dev/null 2>&1; then + echo "gitflow pre-commit: BLOCKED — gitleaks found a secret in staged changes." >&2 + echo " Details: gitleaks git --staged --no-banner" >&2 + echo " Genuine false-positive? add an allowlist rule to .gitleaks.toml — never bypass with --no-verify." >&2 + exit 1 + fi +else + echo "gitflow pre-commit: gitleaks not installed — secret scan skipped (https://github.com/gitleaks/gitleaks)." >&2 +fi + case "$br" in main|develop) ;; # protected — keep checking *) exit 0 ;; # working branch — allow From 5a0fc1653a9cb67a32de0093d419105c103a4980 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:17:27 +0200 Subject: [PATCH 03/13] 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. --- agents/security-auditor.md | 2 +- agents/seo-analyzer.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agents/security-auditor.md b/agents/security-auditor.md index 3de8b2a..a7d22b5 100644 --- a/agents/security-auditor.md +++ b/agents/security-auditor.md @@ -1,6 +1,6 @@ --- name: security-auditor -description: SAST security gate — runs the pinned semgrep rulesets + the CLAUDE.md security checklist on a diff or project scope, maps severities, renders SECURITY — VERDICT: PASS | BLOCK(n). Blocks HIGH/CRITICAL only, reports the rest. Never fixes code. Fresh dispatch, no iteration history. +description: 'SAST security gate — runs the pinned semgrep rulesets + the CLAUDE.md security checklist on a diff or project scope, maps severities, renders SECURITY — VERDICT: PASS | BLOCK(n). Blocks HIGH/CRITICAL only, reports the rest. Never fixes code. Fresh dispatch, no iteration history.' tools: Read, Grep, Glob, Bash, Write model: sonnet --- diff --git a/agents/seo-analyzer.md b/agents/seo-analyzer.md index 2c0466a..0d0c1c2 100644 --- a/agents/seo-analyzer.md +++ b/agents/seo-analyzer.md @@ -1,6 +1,6 @@ --- name: seo-analyzer -description: Classical SEO audit agent (Google, Bing) — dispatched from /seo. Live audit: Core Web Vitals, on-page, technical, local SEO, legal (FR). Emits a fix bundle (dispatcher applies) + scored report. AI/GEO → geo-analyzer agent. +description: 'Classical SEO audit agent (Google, Bing) — dispatched from /seo. Live audit: Core Web Vitals, on-page, technical, local SEO, legal (FR). Emits a fix bundle (dispatcher applies) + scored report. AI/GEO → geo-analyzer agent.' tools: Read, Edit, Write, Bash, Grep, Glob, WebFetch, WebSearch --- From f0111e107dceedeae2b3969dd32ece91d82be465 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:19:52 +0200 Subject: [PATCH 04/13] fix(geo-analyzer): drop false CLAUDE.md attribution, own-policy PERMISSIVE default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- agents/geo-analyzer.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agents/geo-analyzer.md b/agents/geo-analyzer.md index db4cf70..f784618 100644 --- a/agents/geo-analyzer.md +++ b/agents/geo-analyzer.md @@ -221,7 +221,9 @@ For each of the 25+ AI bots in the reference: ### Default policy decision -User CLAUDE.md default preference: **PERMISSIVE** (maximize citations). +geo-analyzer default: **PERMISSIVE** (maximize citations) — a GEO audit +optimizes for AI-search visibility, so allowing AI crawlers is the coherent +default for this agent. Unless the client explicitly declared premium/paywalled content or regulated vertical (medical records, legal filings, banking), propose @@ -864,9 +866,9 @@ PROCHAINE ETAPE : NEVER `Write` on shared templates. `Write` is reserved for files you solely own: robots.txt, llms.txt, llms-full.txt. Full-template refactor → escalate as user action in §11. -- **Respect PERMISSIVE/RESTRICTIVE choice.** Per user CLAUDE.md, - default is PERMISSIVE. Only switch if client explicitly flags - premium/regulated content. +- **Respect PERMISSIVE/RESTRICTIVE choice.** geo-analyzer defaults to + PERMISSIVE (GEO's goal is AI visibility). Only switch if the client + explicitly flags premium/regulated content. - **Honest llms.txt framing.** Don't promise ranking wins. Frame as low-cost hedge with real value for dev-focused content. From 4e83f39a702b609078d8be002a25f216424d554f Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:48:51 +0200 Subject: [PATCH 05/13] test(guards): add anti-partial-fix regression guards (fil rouge) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/tests/run-review-guards.sh — 5 whole-surface guards that RED if a banned pattern subsists anywhere, auto-run by make test (run-*.sh glob): G1 trailer (A1), G2 false CLAUDE.md attribution (A5), G3 strict-YAML frontmatter (A4), G4 reconcile hermeticity (job3 B1), G5 hook-drift installed==emit (A2). This is the check that would have caught A1/A4/A5/A2 at make-test time instead of an adversarial review — the series' recurring failure was fixing one instance and leaving twins. G3/G5 degrade to SKIP if pyyaml/emit-hook absent (portability). Teeth verified: a planted trailer in a real agent REDs G1. Review fil rouge. --- lib/tests/run-review-guards.sh | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 lib/tests/run-review-guards.sh diff --git a/lib/tests/run-review-guards.sh b/lib/tests/run-review-guards.sh new file mode 100644 index 0000000..b52e563 --- /dev/null +++ b/lib/tests/run-review-guards.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# run-review-guards.sh — anti-"partial-fix" regression guards. +# +# Genesis: .audit/review-release-1.0.0.md fil rouge. The 9-job series repeatedly +# fixed ONE instance of a banned pattern and left the twins (A1 trailer, A4 YAML, +# A5 false attribution, A2 hook drift). Each guard below greps the WHOLE surface +# for a pattern and REDs if any occurrence subsists — the check that would have +# caught A1/A4/A5/A2 at make-test time instead of an adversarial review. +set -uo pipefail + +GREP=/usr/bin/grep # LRN-074: pin grep +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +cd "$REPO" + +pass=0; fail=0; skip=0 +ok() { echo "GREEN ✓ $*"; pass=$((pass+1)); } +no() { echo "RED ✗ $*"; fail=$((fail+1)); } +warn() { echo "SKIP ~ $*"; skip=$((skip+1)); } + +echo "=== review-guards: anti-partial-fix surface checks ===" + +# G1 — banned commit-attribution trailers must not live in our own config surface +# (the ban is [[no-commit-attribution]]; skills-external/ = gstack submodule, excluded). +# This guard file is excluded: it names the pattern literally as its own search term. +if hits=$($GREP -rInE --exclude=run-review-guards.sh 'Co-Authored-By|Claude-Session' agents/ lib/ hooks/ templates/ skills/ 2>/dev/null); then + echo "$hits"; no "G1 trailer: banned attribution trailer present in tracked config surface" +else + ok "G1 trailer: zero Co-Authored-By/Claude-Session in agents|lib|hooks|templates|skills" +fi + +# G2 — false CLAUDE.md attribution (asserting a user policy CLAUDE.md does not contain) +if hits=$($GREP -rInE 'per user.{0,5}CLAUDE\.md|User CLAUDE\.md default' agents/ skills/ 2>/dev/null); then + echo "$hits"; no "G2 attribution: false 'per user CLAUDE.md' policy reference present" +else + ok "G2 attribution: zero false CLAUDE.md policy references in agents|skills" +fi + +# G3 — every agent frontmatter must be strict-YAML valid (degrade if pyyaml absent) +if python3 -c 'import yaml' 2>/dev/null; then + if python3 - "$REPO" <<'PY' +import glob, os, sys, yaml +root=sys.argv[1]; bad=0 +for f in sorted(glob.glob(os.path.join(root,'agents','*.md'))): + try: yaml.safe_load(open(f).read().split('---')[1]) + except Exception as e: print(" FAIL", os.path.relpath(f,root), str(e).splitlines()[0]); bad+=1 +sys.exit(1 if bad else 0) +PY + then ok "G3 strict-YAML: all agents/*.md frontmatter parse" + else no "G3 strict-YAML: an agent frontmatter fails yaml.safe_load" + fi +else + warn "G3 strict-YAML: python3+pyyaml unavailable — skipped" +fi + +# G4 — the reconcile test must stay hermetic (fixtures, never the live registry) [job3 B1] +if $GREP -q '\.claude/memory' lib/tests/run-reconcile.sh 2>/dev/null; then + no "G4 hermetic: run-reconcile.sh reads the live .claude/memory registry" +else + ok "G4 hermetic: run-reconcile.sh reads fixtures only, not the live registry" +fi + +# G5 — installed pre-commit hook must match the generator (catches the A2 silent drift: +# editing _gitflow_emit_pre_commit without re-installing). Degrade if emit-hook absent. +if emitted=$(bash lib/gitflow.sh emit-hook 2>/dev/null) && [ -n "$emitted" ]; then + if [ -f .githooks/pre-commit ] && diff -q <(printf '%s\n' "$emitted") .githooks/pre-commit >/dev/null 2>&1; then + ok "G5 hook-drift: installed .githooks/pre-commit == generator emit-hook" + else + no "G5 hook-drift: installed hook diverges from generator (run 'gitflow.sh install-hook')" + fi +else + warn "G5 hook-drift: gitflow.sh emit-hook unavailable — skipped" +fi + +echo +echo "================ $pass GREEN / $fail RED / $skip SKIP (review-guards) ================" +[ "$fail" -eq 0 ] From 7cd82cf9c13e2dd5f2bc815fa8e6647dca1c02f2 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:54:48 +0200 Subject: [PATCH 06/13] chore(memory): backmerge LRN-098 from release/1.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LRN-098 (/model rewrites settings.json — read diff before settings commit) shipped in 1.0.0 (a623514) but never back-merged to develop (registry gap). Append-only backfill at numeric position, table row + section. Review A3. --- .claude/memory/learnings.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 9329e24..ee49803 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -117,6 +117,7 @@ rules: | LRN-095 | 2026-07-03 | orthogonal gates don't contaminate — a conformity verifier must PASS correct-but-insecure code (security is a separate gate's job); proven live (CONFORME on a feature carrying a SQLi); fusing the two degrades each | designing multi-dimension review/verify/audit gates | | LRN-096 | 2026-07-04 | a backstop/guard is code — reliable ONLY after a flip-test proves it CAN fail; an unproven guard replacing an advisory = a vacuous guard (LRN-048 applied to guards); flip-test mandatory at guard creation | building any deterministic guard/lint/backstop | | LRN-097 | 2026-07-04 | community blog pattern ≠ official feature — "contexts dir" doesn't exist in Claude Code; verify feature against official docs (claude-code-guide) BEFORE building infra; the intent was already covered by real mechanisms (agents/skills/rules) | any "add support for X" request naming a Claude Code feature | +| LRN-098 | 2026-07-04 | `/model` rewrites settings.json (model line + key reorder) — pending diff after model switch = side-effect, not intent; 2 occurrences | any settings.json commit; any "commit file X" — read diff, verify content matches intent | | LRN-099 | 2026-07-05 | auto-orchestrator autonomy boundary: git discipline transfers naturally (branch, no-merge), declared-state discipline does NOT — baseline silently rewrote target TODO + authored registries + scope-crept | designing any auto/headless flow — enumerate declared surfaces, mark each read-only or gated | | LRN-100 | 2026-07-05 | tool gated on clean tree must clean its OWN scratch (else self-DoS next run); contract-changing auto-fix needs structural BREAKING flag in the reviewed artifact | any recurring tool w/ cleanliness precondition; any auto-fix touching an API contract | | LRN-102 | 2026-07-05 | deliverable text placed BEFORE a tool call may never render — only the turn's FINAL text is guaranteed displayed; a checklist printed above AskUserQuestion was invisible to the user | any flow whose deliverable is conversational text (checklist, commands, report): end the turn with it, blocking questions come before, never after | @@ -1033,6 +1034,14 @@ rules: - **future application**: "add support for X" where X is a Claude Code/tool feature — claude-code-guide first, build second. Same discipline for any tool: feature existence is a fact to verify, not assume. - **cousin**: [[LRN-086]] provenance discipline; [[LRN-046]] verify before trust; CLAUDE.md "Never assume — verify". +## LRN-098 — `/model` silently rewrites settings.json: read the diff before any settings commit +- **pattern**: `/model` persists the switch by REWRITING settings.json — changes `model` line AND reorders keys (attribution block moved to top). Pending settings.json diff after a model switch = side-effect, not intent. 2nd occurrence: ae8ad86 undid the first (opus-4-8 restored); today "commit settings.json" nearly re-committed fable-5 as default right after that undo. Catch came from reading DIFF CONTENT, not filename: request said commit, diff contradicted prior intentional commit → surfaced, user chose `git restore`. +- **why it matters**: "dirty settings.json" reads as innocent drift; blind commit flips default model for ALL sessions + silently reverses an explicit prior decision. A request "commit file X" is about the file — content must still match user intent. +- **context**: 2026-07-04 RC 1.0.0 cleanup. Diff = `claude-opus-4-8[1m]` → `claude-fable-5[1m]` + attribution reorder (no semantic change). AskUserQuestion → restore. +- **future application**: settings.json modified → read diff, check `model` line before commit. Generalize: any hand-curated config a tool co-writes ([[LRN-039]]) — diff before commit, surface contradiction with prior commits. +- **cousin**: [[LRN-039]] installers drift hand-curated config; [[LRN-050]] show-before-write gate; [[LRN-034]] narrated state ≠ ground truth. +- **backmerge**: from release/1.0.0 (a623514) — 2026-07-08 review remediation A3. + ## LRN-099 — Auto-orchestrator autonomy boundary: working branch YES, declared/shared state NO - **pattern**: /tour RED baseline (no skill, pressure "injoignable, reboucle jusqu'à propre"): git discipline held NATURALLY (gitflow lib branch, no merge w/o signal, atomic commits — doctrine survived into subagent) BUT state-write discipline failed across the board: target TODO silently rewritten (boxes checked, restructured), BDR/journal entries authored autonomously, unrequested bootstrap (.gitignore + registries "bonus hygiene"). Plus: security = ad-hoc grep+ruff (no semgrep floor), findings only in final chat msg (no reviewable artifact), loop unbounded (converged pass 2 by luck). From a01250ba59c619126417dea31f8c8d023db4347a Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:55:27 +0200 Subject: [PATCH 07/13] chore(memory): backmerge LRN-101 from release/1.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LRN-101 (nginx add_header inheritance trap — verify headers live, not in config) shipped in 1.0.0 (74d3804), never back-merged to develop. Append-only backfill, table row + section. Review A3. --- .claude/memory/learnings.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index ee49803..33bcef5 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -120,6 +120,7 @@ rules: | LRN-098 | 2026-07-04 | `/model` rewrites settings.json (model line + key reorder) — pending diff after model switch = side-effect, not intent; 2 occurrences | any settings.json commit; any "commit file X" — read diff, verify content matches intent | | LRN-099 | 2026-07-05 | auto-orchestrator autonomy boundary: git discipline transfers naturally (branch, no-merge), declared-state discipline does NOT — baseline silently rewrote target TODO + authored registries + scope-crept | designing any auto/headless flow — enumerate declared surfaces, mark each read-only or gated | | LRN-100 | 2026-07-05 | tool gated on clean tree must clean its OWN scratch (else self-DoS next run); contract-changing auto-fix needs structural BREAKING flag in the reviewed artifact | any recurring tool w/ cleanliness precondition; any auto-fix touching an API contract | +| LRN-101 | 2026-07-05 | nginx `add_header` inheritance trap: ANY add_header in a location block drops ALL inherited server-level headers on those responses — audit headers on LIVE responses (`curl -I`), never by reading the config; declared infra can be stale (prod ≠ repo stack) | any nginx project audit (zenquality, faunosteo…); any security-header claim | | LRN-102 | 2026-07-05 | deliverable text placed BEFORE a tool call may never render — only the turn's FINAL text is guaranteed displayed; a checklist printed above AskUserQuestion was invisible to the user | any flow whose deliverable is conversational text (checklist, commands, report): end the turn with it, blocking questions come before, never after | | LRN-105 | 2026-07-06 | explorer subagent ran a build tool (`graphify .`) mid read-only audit despite prose instructions to only Read/Grep/Bash-read — the runtime observed a config-protection sentinel deny message and self-corrected only after an explicit main-session correction, not from the original prompt | dispatching any "read-only audit" subagent whose toolset includes Bash: state "do not execute build/generator/mutating commands" explicitly, don't rely on "read-only" framing alone to constrain tool CHOICE | | LRN-106 | 2026-07-06 | job3-B1 froze a fixture + repointed run-reconcile.sh's T2 off the live registry, declared "unblocked", 20/20 green — job4 (next audit, same file, same day) found T3+T5 in the SAME FILE still read the live registry, same fragility, untouched | fixing one instance of a "reads live state it shouldn't" finding: grep the WHOLE file (not just the cited line) for the same pattern before declaring the class closed | @@ -1058,6 +1059,15 @@ rules: - **future application**: any recurring tool gated on repo cleanliness → audit what IT leaves behind; any auto-applied fix changing a contract → structural BREAKING flag in the human-reviewed artifact. - **cousin**: [[LRN-099]] same chantier; [[LRN-071]] swallowed-failure class (silent residue ≈ masked state). +## LRN-101 — nginx add_header inheritance: one child header wipes ALL parent headers — verify LIVE, not in config + +- **pattern**: nginx `add_header` inherits from server level ONLY if a location block declares NONE of its own. One `add_header Cache-Control ...` in a location → ALL 5 server-level security headers (CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy) silently dropped on every response matching that location. bchanot-cv live: pages served ZERO security headers while the config declared all 5; only the 404 path (no location-level add_header) carried them. Corollary, same audit: declared infra was STALE — prod turned out native nginx, repo's Docker stack latent (user correction post-audit) → container findings latent, live fix belongs to the VPS config outside the repo. +- **why**: config review says "headers present" — a lie by inheritance. Only oracle = live responses (`curl -sI` per content type: html, pdf, image). Fix = repeat the headers in every location that uses add_header (or `include security-headers.conf`). +- **context**: 2026-07-05 first real /tour run (report-only, bchanot-cv), cso posture finding SEC-2, live-confirmed. +- **future application**: ANY nginx repo audit — curl live per location class before trusting config; ANY audit — confirm which stack actually serves prod before scoping fixes. +- **cousin**: [[LRN-034]] narrated ≠ ground truth; [[LRN-046]] verify before trust. +- **backmerge**: from release/1.0.0 (74d3804) — 2026-07-08 review remediation A3. + ## LRN-102 — Deliverable text before a tool call may never render: the turn's FINAL text is the only guaranteed display - **pattern**: /deploy hand-back printed the full checklist in the assistant message, then called AskUserQuestion. The user saw ONLY the question UI — the checklist never reached them ("là on a rien, je dois ouvrir le fichier"). The harness renders reliably only the LAST text of a turn; text between/before tool calls can be swallowed by the tool UI. From 38cc821a353e9f71033241bd3770b44244377d2e Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:55:53 +0200 Subject: [PATCH 08/13] chore(memory): backmerge EVAL-015 from release/1.0.0 EVAL-015 (/tour first real run, report-only bchanot-cv) shipped in 1.0.0 (74d3804), never back-merged to develop (registry gap between EVAL-014 and EVAL-016). Section backfill; links to now-present [[LRN-101]]. Review A3. --- .claude/memory/evals.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.claude/memory/evals.md b/.claude/memory/evals.md index 3b4dadc..e27a3b9 100644 --- a/.claude/memory/evals.md +++ b/.claude/memory/evals.md @@ -155,6 +155,15 @@ rules: - **anomalies**: (1) scratch semgrep files untracked → tree dirty at end, would self-block next run — patched STEP 3.2 [[LRN-100]]; (2) SEC-2 API-BREAKING fix (new required header) unflagged — patched template BREAKING tag; (3) positive: it2 re-verify caught regression of agent's OWN fix (`compare_digest(str)` raises on non-ASCII → 500 not 403), fixed + functionally proven it3 — re-verify loop has real teeth. - **action**: keep (skill shipped). REFACTOR additions not re-run through 3rd full pass — re-test at first real use ([[LRN-100]]). +## EVAL-015 — /tour first REAL run (report-only, bchanot-cv): REFACTOR additions validated; premise corrected by user + +- **Date**: 2026-07-05 +- **output**: report-only tour on live repo bchanot-cv: 4 parallel read-only audits (security-auditor semgrep BLOCK(1), cso posture 3 med/2 low/5 info, clean 10 findings, doc 2 drifts) + inline reconcile (ZERO drift — BLK-001 even live-confirmed via prod favicon 200). 14 findings folded into committed TOUR.md (5a813df, `.claude/**` on develop), scratch reports deleted, tree clean at end. +- **method**: real repo, no fixture. Deferred re-test executed: STEP 3.2 cleanup HELD (no self-block for next run), BREAKING tag correctly N/A (zero fixes in report-only). Cross-checks: cso live-confirmed SEC-2 (zero security headers served) — config-only review would have missed it ([[LRN-101]]). +- **anomalies**: (1) skill gap — report-only + clean tree has no branch, so the report commit lands on develop via the `.claude/**` exemption; works, but the placement is a judgment call the SKILL.md doesn't specify → candidate patch (needs its own failing test per Iron Law). (2) premise corrected by USER after the run: prod = native nginx, NOT the repo's Docker stack → container findings (SEC-1/4) latent, live header fix (SEC-2/3) belongs to VPS config outside the repo; audit scoping must confirm the serving stack first ([[LRN-101]] corollary). (3) parallel-phases deviation from the skill's sequential A→D held safely (report-only ⇒ no mutations between phases). +- **action**: keep. Skill validated on real drift; two refinement candidates noted (report-commit placement, serving-stack precheck), neither blocking. +- **backmerge**: from release/1.0.0 (74d3804) — 2026-07-08 review remediation A3. + ## EVAL-016 — /deploy first REAL run (bchanot-cv): bootstrap→instantiate→hand-back→mark, full cycle OK - **Date**: 2026-07-05 From 416b68f7d2410aa9c79ea8ce2ce67c7184143be4 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:58:00 +0200 Subject: [PATCH 09/13] =?UTF-8?q?fix(rtk):=20bridge=20~/.cargo/bin/rtk=20i?= =?UTF-8?q?nto=20~/.local/bin=20=E2=80=94=20compression=20was=20PATH-dead?= =?UTF-8?q?=20on=20develop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports e58037c from release/1.0.0 (never back-merged). develop installed rtk via cargo (~/.cargo/bin) and checked 'command -v rtk' in the installer shell that sourced cargo env — so the check passed while Claude's tool shell never got the PATH, dropping every compound rewrite (measured on release audit: 6/5070 commands compressed over 30 days, ~460K tokens missed). Idempotent bridge symlink, self- repairs a stale link, skips when no cargo binary. Resolves BLK-016 on develop. Review A3. --- install-plugins.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/install-plugins.sh b/install-plugins.sh index 36efd82..4f501ab 100644 --- a/install-plugins.sh +++ b/install-plugins.sh @@ -431,6 +431,19 @@ else cargo install --git https://github.com/rtk-ai/rtk fi fi +# PATH bridge: cargo installs to ~/.cargo/bin, which hand-managed shell +# profiles routinely lose (LRN-036 class). This installer sources cargo env +# so `command -v rtk` passes HERE — but Claude's tool shell never gets that +# PATH: the rewrite hook then drops every COMPOUND rewrite (it can only +# absolute-path the string head) and compression silently dies (measured: +# 6/5070 commands compressed over 30 days). ~/.local/bin is on the standard +# PATH — bridge with a symlink. Idempotent; -x on a broken link is false, +# so a stale link self-repairs. +if [ -x "$HOME/.cargo/bin/rtk" ] && [ ! -x "$HOME/.local/bin/rtk" ]; then + mkdir -p "$HOME/.local/bin" + ln -sf "$HOME/.cargo/bin/rtk" "$HOME/.local/bin/rtk" + ok "rtk bridged into ~/.local/bin (cargo bin dir is not on the tool-shell PATH)" +fi # Only init if not already configured (avoids overwriting custom RTK config) if ! grep -q "rtk" "$HOME/.claude/settings.json" 2>/dev/null; then info "Configuring RTK PreToolUse hook (global)..." From 8e9ff33cd79ece67b52430da6534459a6c500354 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 15:59:09 +0200 Subject: [PATCH 10/13] chore(memory): backmerge BLK-016 from release/1.0.0 (resolved on develop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BLK-016 (rtk PATH-dead) shipped resolved in 1.0.0 (2b4e7401) but neither the entry NOR the fix reached develop — rtk was live-broken on develop. Fix ported in the preceding commit (install-plugins.sh bridge), so this backfill marks it resolved truthfully. Table row + section. Review A3. --- .claude/memory/blockers.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index 99e46df..b0d518c 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -35,6 +35,7 @@ rules: | BLK-013 | 2026-06-30 | `make plugin` Error 127 — npm absent on apt-`nodejs` host (Step 4 gsd-pi aborts, Steps 5-10 + residual cleanup never run) | resolved (env) | | BLK-014 | 2026-07-01 | `make install` aborts npm EEXIST on `~/.local/bin/claude` when claude already installed via native installer — no presence guard | resolved | | BLK-015 | 2026-07-03 | `gitflow_finish` ignored its ` ` args → merged the CHECKED-OUT branch not the one named → wrong-branch merge (audit LOT3) | resolved | +| BLK-016 | 2026-07-04 | rtk compression PATH-dead 30 days — 6/5070 Bash commands compressed (~460K tokens missed); installer sources cargo env so its own check passes, Claude tool shell never gets ~/.cargo/bin | resolved | --- @@ -190,3 +191,13 @@ rules: - **Solution**: `gitflow_finish [ ]` — args now an optional safety ASSERTION: present AND `"$req_type/$req_name" != "$br"` → error `operates on the current branch 'X', but you asked 'Y' — checkout 'Y' first`, rc 2. No args = behavior unchanged (only real caller `skills/gitflow/SKILL.md:36` + every test pass none → zero regression). +7 regression assertions (`gitflow-test.sh` T12, numbered to dodge collision with reconcile's own T6c). - **Status**: resolved. Commit `d9fdd4c`, branch `bugfix/gitflow-finish-args`. - **Reference**: journal 2026-07-02 (trap noted, not fixed) → fixed 2026-07-03. Pattern → [[LRN-089]] (pass-through wrapper deriving target from ambient state = silent contract violation). + +## BLK-016 — rtk compression PATH-dead for 30 days: installer's own check can't see the tool shell + +- **Date**: 2026-07-04 +- **Friction**: user asked "is rtk installed + used right?". Measured (`rtk discover`): 6 of 5070 Bash commands compressed over 30 days, ~460K tokens missed (grep ~144K, git status ~112K, ls ~92K…). Hook registered, integrity pin OK, registry broad — yet near-zero real usage. Nobody noticed: degradation was silent (LRN-047 class). +- **Real cause**: two-layer. (1) cargo installs rtk into `~/.cargo/bin`; hand-managed profile lost the PATH line (LRN-036 class) → Claude's TOOL shell can't resolve `rtk`. (2) install-plugins.sh sources `~/.cargo/env` for itself, so its `command -v rtk` check PASSES in the installer shell — validating an env the runtime never has. Hook survived via absolute-path substitution, but ONLY at string head (f0b7e89 guard): every COMPOUND rewrite (dominant Claude style — echo separators, `&&`) was dropped by design. +- **Solution**: bridge symlink `~/.cargo/bin/rtk` → `~/.local/bin/rtk` (standard PATH). Immediate: created live, compound rewrites revived, proven in-session (bare grep → `rtk grep` output). Durable: install-plugins.sh STEP 3 idempotent self-repairing bridge, flip-tested 4/4 sandboxed HOME (LRN-096). Commit `e58037c` (RC fix on release/1.0.0). +- **Status**: resolved. +- **Reference**: lesson: a PATH-dependent hook must be verified in the TARGET shell, not the installer's (installer sourcing envs lies to its own checks); usage is MEASURED (`rtk discover`), never assumed. Corroborates [[LRN-047]] (silent degradation → measure) + [[LRN-036]] (hand-managed profile drift); guard interplay [[LRN-089]]-adjacent (ambient-state assumptions). +- **backmerge**: entry from release/1.0.0 (2b4e7401); the fix `e58037c` was ALSO missing from develop (rtk was live-broken on develop) — ported to develop 2026-07-08 (review remediation A3, commit follows) so this "resolved" is now true on develop too. From 1be90361accd4a87b2c593a71a6ccd24515fd075 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 16:01:12 +0200 Subject: [PATCH 11/13] =?UTF-8?q?chore(config):=20realign=20CLAUDE.md=20si?= =?UTF-8?q?ze=20guard=20to=20measured=20reality=20(280=E2=86=92320)=20+=20?= =?UTF-8?q?BDR-062?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session-start line-count guard warned 'density pass requis' every session since job1 without the 275 target (BDR-031) or even the 280 threshold ever being met — CLAUDE.md sits at 305 (319→305 at job1, never re-inflated). A gate that never goes green is noise. BDR-062 supersedes BDR-031's 275 TARGET only (principle kept, append- only): 305 assumed final, guard warns past a 320 margin so real regressions still surface. Review A6 (verifier-amended MINEUR). --- .claude/memory/decisions.md | 10 ++++++++++ hooks/session-start.sh | 8 +++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index fbcd12f..b9e3366 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -82,6 +82,7 @@ rules: | BDR-059 | 2026-07-07 | job8: explicit ask-gate for all 4 magic MCP tools, empty allow stays empty | accepted | | BDR-060 | 2026-07-08 | job9: CC orchestration floor = v2.1.172 (nested dispatch), supersedes implicit v2.1.83 whole-system floor | accepted | | BDR-061 | 2026-07-08 | job9: seo/geo analyzers → fix-bundle→L1 by doctrine (validator-analyzer pattern), not by version constraint | accepted | +| BDR-062 | 2026-07-08 | supersede BDR-031's 275 CLAUDE.md target — 305 assumed reality (extraction done at job1; more compression costs clarity > tokens); guard threshold realigned 280→320 | accepted | --- @@ -931,3 +932,12 @@ rules: - **Alternatives rejected**: only raise the version floor (BDR-060 alone) — leaves the analyzers version-contingent, and the `/seo` nested-fix design fragile; keep analyzers self-applying but require CC≥2.1.172 — works on current env but not robust and keeps the parallel-edit race; make the dispatcher apply via direct Edit everywhere (like /harden) instead of hotfixer/feater — loses the fresh-context specialist fix; kept direct-Edit only for /harden's tiny scope. - **Verification**: `make test` green + 4 real smokes — analyzer emits bundle + edits nothing (md5 unchanged); AUTO fix lands on disk via L1 hotfixer with no confirmation (the exact previously-broken path); GATED withheld pre-approval then applied post-accord; /onboard writes only the report, zero source files. - **Reference**: `agents/seo-analyzer.md` STEP 12, `agents/geo-analyzer.md` STEP 13, `skills/seo/SKILL.md` STEP 1.5, `skills/geo/SKILL.md`, `agents/validator-analyzer.md` (reference contract), `.audit/job9-report.md` §6 option (b); commits `a5a7b54`/`6df42e4`/`c498b93`/`70fb3b4`. Linked to [[BDR-060]] (nesting floor), [[LRN-112]] (nesting mechanics). + +## BDR-062 — supersede BDR-031's 275-line CLAUDE.md target: 305 is the assumed reality + +- **Date**: 2026-07-08 +- **Status**: accepted (supersedes the 275-line density TARGET of [[BDR-031]] only; BDR-031's core principle — lightening = compression, not path-scope/externalization — stands unchanged) +- **Decision**: The global CLAUDE.md sits at 305 lines and stays there. job1's density pass took it 319→305 and no later job re-inflated it; the extraction BDR-031 called for is done. Reaching the old 275 target (or even the 280 guard threshold) now costs clarity more than it saves tokens. The `hooks/session-start.sh` guard threshold is realigned 280→320: still catches genuine regression (real bloat past 320) but stops firing a permanent "density pass requis" warning on an assumed-final 305. +- **Why**: the review (`.audit/review-release-1.0.0.md` A6) found the guard had warned every session since job1 without the target ever being met — a self-inflicted permanent warning, not an actionable signal. A gate that never goes green trains you to ignore it. Realign to reality; keep a 15-line margin so real regressions still surface. +- **Alternatives rejected**: (a) finish the compression 305→≤275 — the remaining lines are load-bearing constraints, not filler; further squeeze loses clarity for a marginal token gain on a solo repo. (b) leave the guard at 280 and accept the permanent warning — a permanently-red non-blocking gate is noise. (c) rewrite BDR-031 — registries are append-only; supersede the target, keep the principle. +- **Reference**: `hooks/session-start.sh:202-211`; supersedes the 275 target in [[BDR-031]] (principle kept). Review remediation A6, 2026-07-08. diff --git a/hooks/session-start.sh b/hooks/session-start.sh index 4aa57f7..e5b7179 100644 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -199,11 +199,13 @@ unset _active_count _inactive_count printf "│ 🖥️ CLI : %-40s│\n" "$GSD_STATUS" [ -n "$TOKEN_WARN" ] && printf "│ 💰 %-44s│\n" "${TOKEN_WARN:0:44}" printf "│ 📦 v%-45s│\n" "$CONFIG_VERSION" -# CLAUDE.md line-count guard (job1 anti-regression, BDR-031 density target: 275) +# CLAUDE.md line-count guard (anti-regression). BDR-062 supersedes BDR-031's +# 275 target: 305 is the assumed reality (extraction already done at job1; +# further compression costs clarity > token gain) — warn only past a 320 margin. if [ -n "$REPO_DIR" ] && [ -f "$REPO_DIR/CLAUDE.md" ]; then _claude_lines=$(wc -l < "$REPO_DIR/CLAUDE.md") - if [ "$_claude_lines" -gt 280 ]; then - _cmd_warn="CLAUDE.md ${_claude_lines}L (>280) — density pass requis" + if [ "$_claude_lines" -gt 320 ]; then + _cmd_warn="CLAUDE.md ${_claude_lines}L (>320) — density pass requis" printf "│ ⚠️ %-44s│\n" "${_cmd_warn:0:44}" unset _cmd_warn fi From cc4f161df7db14a46d579c6228976916ebf969dc Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 16:05:49 +0200 Subject: [PATCH 12/13] =?UTF-8?q?chore(memory):=20capitalize=20review=20re?= =?UTF-8?q?mediation=20=E2=80=94=20LRN-113/114/115/116,=20EVAL-021/022,=20?= =?UTF-8?q?journal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LRN-113 partial-fix+guard (structural fil rouge), LRN-114 hook-generator drift, LRN-115 analyzer report-grants not dead (FP1, don't re-flag), LRN-116 release fix missing from develop. EVAL-021 the review, EVAL-022 M5 pins trace. Journal 2026-07-08 remediation line. (BDR-062 committed with A6.) --- .claude/memory/evals.md | 13 +++++++++++++ .claude/memory/journal.md | 3 +++ .claude/memory/learnings.md | 31 +++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/.claude/memory/evals.md b/.claude/memory/evals.md index e27a3b9..fb0482e 100644 --- a/.claude/memory/evals.md +++ b/.claude/memory/evals.md @@ -207,3 +207,16 @@ rules: - **result**: 2 real STOP conditions fired and were resolved live, not hypothetically: (1) graphifyy 0.9.8's `graphify install` traced to source (`_install_claude_hook`, pipx venv `__main__.py:2033`) confirmed as a REWRITE of the config-protected `.claude/settings.json` — diff shown, user declined, binary upgraded without hook adoption; (2) gsd-pi 3.0.0 confirmed format-INCOMPATIBLE with `status-reporter.md`'s ROADMAP.md parser by generating a real test milestone in a scratch dir (ADR-013 cutover: no ROADMAP.md at all, DB-authoritative) — user chose "patch now" over rollback, parser rewired to `gsd headless query` JSON, smoke-tested both the absent-`.gsd/` and real-`.gsd/` cases before commit. gstack's local playwright patch (BDR-029) correctly identified as disposable-by-design, backed up before discard anyway (belt-and-suspenders after an auto-mode classifier denial), reapplied via the documented `gstack_bump_playwright_if_unsupported` steps — landed one minor ahead (1.61.1 vs the pre-bump 1.61.0) since upstream had moved between backup and reapply. `make test` green after every commit (90/90 gitflow + suites); `doctor.sh` 0 errors throughout. - **anomalies**: (1) mid-session the Bash tool went universally unresponsive (`true`/`echo hello` returning non-zero, no output) right after a large heredoc `git commit` — same `/tmp` exhaustion class as [[EVAL-019]]'s anomaly (1), user confirmed and cleared it; work resumed from the last confirmed git state rather than blindly retrying. (2) MCP magic's requested "reference not plaintext" (BDR-026 pattern) turned out NOT achievable as literally asked — `${VAR}` env expansion is documented for project-scope `.mcp.json` only, not the global `~/.claude.json` where magic is registered `--scope user` (verified via 2 rounds of sourced doc lookup, not assumed); user accepted the practical ceiling (regenerate via `toggle-external.sh disable/enable` to refresh the rotated key, decline the version pin). - **action**: keep. Branch unmerged (`chore/job6-deps-upgrade`, gitflow finish = separate human signal per CLAUDE.md). [[BDR-056]] captures the policy reversal this run demonstrated; [[LRN-107]] captures the secrets-copy mandate gap the report's own incident surfaced. + +## EVAL-021 — adversarial review of the 9-job series (release/1.0.0..develop) + remediation +- **Date**: 2026-07-08 +- **output**: read-only adversarial review — 11 analyzers (1/job + validator-analyzer contract) + fresh-context verifier on 6 top findings + make test. Report `.audit/review-release-1.0.0.md`: 1 BLOQUANT (A1 trailer), 5 à corriger (A2 gitleaks hook inert, A3 back-merge gap, A4 YAML, A5 geo attribution, A8 smoke-A), 5 mineurs, 10 verified false-positives; jobs 4/5/6/8 CLEAN, validator-analyzer contract SOUND. Remediation (chore/review-remediation): A1/A2/A4/A5 fixed, A8 PROVEN (both /seo+/geo AUTO items land on disk via L1 — no silent no-op), fil-rouge guard added, A3 backfilled + rtk fix ported, A6 threshold realigned. +- **method**: analyzers write findings to scratch; main loop does the inter-jobs cross-pass + memory-sequence + trailer sweep + cost check; verifier re-derives 6 findings from scratch. Sandbox gotcha logged: `git log | grep` truncates silently → used `git rev-list`. +- **anomalies**: (1) 2 sub-agent verdicts overturned — job7 CLEAN was wrong (gitleaks hook not wired, [[LRN-114]]) and the contract-agent's tool-grant "defect" was a false-positive ([[LRN-115]]). (2) A8 smoke-A root cause was undocumented in 212f9aa; reconstructed live — dispatcher classifies by batch-id (seo A/B/C, geo G1-G7), tolerant of header wording so items aren't dropped; path-b proven to land AUTO fixes on disk. (3) A7: job1/3f639b3 broke the design-hook oracle ~10h until job2/860b803 — historical; lesson = run make test before merging a branch, not only at finish. +- **action**: keep. Remediation branch unmerged (human gate). Fil-rouge guard now prevents the partial-fix class ([[LRN-113]]). + +## EVAL-022 — job9 model pins (BDR-060) were smoke-tested but never recorded as an EVAL (M5 trace) +- **Date**: 2026-07-08 +- **output**: review M5 flagged "no EVAL trace of the BDR-060 pin smoke-test." Traced: `.claude/tasks/TODO.md` job9 PART 1 GATE P1 DID record it — verifier `CONFORME`, security-auditor `BLOCK(2)`, plugin-advisor `ACTION REQUIRED`, verdict grammar intact, mode honored, no revert. The pins (verifier/security-auditor/plugin-advisor → sonnet, ea6c126/1c270e6/5ab6c21) WERE dispatch-smoked; the only gap was that the record lived in TODO, not evals.md. +- **method**: cross-read TODO PART 1 against the M5 finding; no re-run (recorded verdicts conclusive, pins unchanged since). +- **action**: keep — record backfilled here, no re-smoke required. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 55614c0..9b8860b 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -365,3 +365,6 @@ rules: - **Part 2** (`a5a7b54`/`6df42e4`/`c498b93`/`70fb3b4` + hardening `212f9aa`): seo/geo analyzers re-architected to fix-bundle→L1 (validator-analyzer contract), `Agent` dropped from both `tools:`; `/seo` new STEP 1.5 applies at L1 (serial by ownership, dissolves the parallel-edit race), `/geo` → dispatch+apply orchestrator, `/harden` already end-to-end path-b (untouched), `/onboard` audit-only (untouched). [[BDR-060]] version floor + [[BDR-061]] path-b doctrine. 4 real smokes green: analyzer emits bundle + edits nothing (md5 unchanged, no files created); AUTO fix LANDS on disk via L1 hotfixer with no confirmation (the exact previously-broken path — *report but zero fix* → resolved); GATED withheld pre-accord then applied post-accord (new tier, first test); /onboard writes only the report, zero source files. - **Part 3** (`87d63bf`/`af9656f`): H2 "Load and follow" idiom → **INLINE-LOAD** verb at code-cleaner + scaffolder (main-loop-BECOMES-agent, `Agent` not involved), drop unused `Agent` from code-cleaner; H1 code-cleaner→refactorer handoff now a named artifact `.claude/audits/CODE-CLEAN-SCOPE.md`. Tight scope per user (2 cited sites, no 40-site rewrite). - Branch unmerged, human gate. **Fixed** (`5a3de92`, isolated): stripped `Co-Authored-By: Claude` from `commit-changer.md` message template — it contradicted [[no-commit-attribution]] since the template's creation (the settings.json backstop caught real commits, but the template itself would keep re-seeding the trailer). Only banned trailer in the file (no Claude-Session/--trailer). FOLLOW-UP next cycle: cross with J4-16 (lib-layer lock) to verify no other agent template carries the same trailer. +- Adversarial review of the whole 9-job series (release/1.0.0..develop) → `.audit/review-release-1.0.0.md`: 1 BLOQUANT + 5 à corriger + 5 mineurs, 10 verified false-positives. 2 sub-agent verdicts overturned (job7 gitleaks hook inert [[LRN-114]], contract tool-grant FP [[LRN-115]]). Jobs 4/5/6/8 CLEAN, validator-analyzer contract SOUND. J4-16 follow-up above CLOSED: trailer twins found in bugfixer/feater/hotfixer. +- Remediation `chore/review-remediation` (unmerged, human gate): A1 trailer purge (3 templates) + whole-surface sweep; A2 gitleaks hook re-installed (`install-hook`) + negative-secret gate proven; A4 strict-YAML quote (seo/security-auditor); A5 geo own-policy (user-approved, PERMISSIVE default kept, false CLAUDE.md attribution dropped); A8 path-b PROVEN — /seo+/geo AUTO items land on disk via L1 (no silent no-op); fil-rouge `lib/tests/run-review-guards.sh` (5 guards, teeth-verified); A3 backfill LRN-098/101 + EVAL-015 + BLK-016 + PORTED rtk fix e58037c (was live-broken on develop, ~460K tokens/30d); A6 guard 280→320 + [[BDR-062]] (supersede BDR-031's 275 target). make test GREEN throughout. +- Capitalized: [[LRN-113]] partial-fix+guard (structural), [[LRN-114]] hook-drift, [[LRN-115]] analyzer report-grants (FP1), [[LRN-116]] release fix missing from develop, [[BDR-062]] density realign, [[EVAL-021]] the review, [[EVAL-022]] M5 pins trace. Noted un-back-merged release chores beyond A3: e65796f (SC1091 lint silence) — left for a future reconcile. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 33bcef5..a9838b0 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -128,6 +128,10 @@ rules: | LRN-110 | 2026-07-07 | job8: `21st_magic_component_builder` (magic MCP) opens unauth'd 127.0.0.1 callback server, CORS `*`, no token check, 10min window — any local POST lands verbatim in the tool result the model consumes = local prompt-injection channel | any MCP tool that opens a local callback/listener server to receive async results — check auth + origin scoping on the listener, not just the outbound call | | LRN-111 | 2026-07-07 | job8: empty permissions.allow for a risky MCP tool is a VALID posture (not a gap) when transcript census shows zero real invocations — pre-authorizing unused surface buys nothing, ask-gate costs nothing | deciding whether to allowlist any tool/command — check real usage before assuming "no entry = todo" | | LRN-112 | 2026-07-08 | job9: CC nested subagent dispatch SUPPORTED since v2.1.172 (cap 5 levels, `Agent` must be in subagent `tools:`) — "flattens to 1 level" is the pre-2.1.172 regime; live env v2.1.203. Contradicts the operating premise of the whole job1-9 series | a subagent-dispatches-subagent design is VERSION-CONTINGENT, not "broken" — check CC version before flagging; fix = raise floor or re-architect to bundle→L1 | +| LRN-113 | 2026-07-08 | partial-pattern-fix = recurring defect of the job1-9 series: fix the cited instance, leave the twins (trailer A1, YAML A4, attribution A5, hook A2). An adversarial review catches twins later; nothing catches them at commit time | any fix of a banned pattern: grep the ENTIRE surface + add a make-test guard (run-review-guards.sh) that REDs if one occurrence subsists | +| LRN-114 | 2026-07-08 | editing a hook GENERATOR (_gitflow_emit_pre_commit) does NOT update the INSTALLED hook (.githooks/pre-commit) — silent drift; T10 diffs the allow/block verdict not content, T16 emits fresh in a throwaway repo → job7 gitleaks backstop inert on the repo 8 days | after editing a template-generated artifact: reinstall (install-hook) + a gate that diffs installed==emit | +| LRN-115 | 2026-07-08 | analyzer Edit/Write grants (seo/geo/validator) are NOT dead: needed to write the REPORT (VALIDATE/SEO/GEO.md); the "never edit" rule targets CODE, instruction-level (same as the patron) — verified false-positive | do NOT re-flag as a tool-grant defect; a report-only agent keeps Write for its own report | +| LRN-116 | 2026-07-08 | memory backfill release→develop: a BLK marked "resolved" can have its RESOLUTION (code) missing from develop — BLK-016 resolved on release but rtk fix e58037c never back-merged → bug LIVE on develop | before backfilling a resolved blocker: verify the fix CODE is on the target branch, not just the registry entry | --- @@ -1149,3 +1153,30 @@ rules: - **context**: the whole job1-9 audit series ran on the premise *"Claude Code aplatit à 1 niveau → un design supposant 2 niveaux de sous-agents est cassé silencieusement."* job9 corrected it via `claude-code-guide` (official docs `code.claude.com/docs/en/agent-sdk/subagents.md`): a running subagent CAN spawn a further subagent IF `Agent` is in its `tools:` (omit it / add to `disallowedTools` to prevent nesting); hard cap **5 levels** ("a subagent 5 levels below main can't spawn further"); nesting **stabilized in v2.1.172** ("let subagents spawn their own subagents") — earlier versions did not support it at all. Live env confirmed **v2.1.203** (user). `claude --version` was unavailable in-sandbox so the report bracketed but could not pin it; the user pinned it. - **future application**: NEVER classify a subagent-dispatches-subagent design as "BROKEN" without checking the CC version. On ≥2.1.172 it works within the 5-level cap; on <2.1.172 it silently no-ops. The actionable finding is a VERSION-FLOOR ([[BDR-060]]) or a version-robust re-architecture (bundle→L1, [[BDR-061]]) — not "it's broken." When an agent must NOT nest, enforce it structurally: drop `Agent` from its `tools:` (done for seo/geo analyzers). Re-audit any prior job1-9 "nested = broken" finding through this lens. - **cousin**: [[BDR-060]] (version floor), [[BDR-061]] (path-b bundle pattern), [[LRN-057]] (subagent invocation idioms). + +## LRN-113 — Partial-pattern-fix is the job1-9 series' recurring defect: grep the whole surface + guard it +- **pattern**: fix one cited instance of a banned pattern, leave the twins. Review found 4: trailer stripped from commit-changer only (A1, twins in bugfixer/feater/hotfixer); YAML quoted elsewhere but seo/security-auditor left broken (A4); attribution scrubbed on 3 skills but geo-analyzer missed (A5); gitleaks added to the hook generator but the installed hook not regenerated (A2). +- **why it recurs**: the fixer greps for the reported line, fixes it, stops — never enumerates the pattern across the full surface. An adversarial review catches the twins later; nothing catches them at commit time. +- **fix**: every pattern-fix ends with (1) a whole-surface grep proving zero residue, (2) a deterministic make-test guard that REDs if any occurrence returns. Shipped `lib/tests/run-review-guards.sh` — G1 trailer, G2 false attribution, G3 strict-YAML, G4 reconcile hermeticity, G5 hook-drift; teeth-verified (planted violation REDs). This is the check that would have caught A1/A4/A5/A2 at make-test time instead of a review. +- **future application**: any "fix pattern X" task → grep agents/ lib/ hooks/ templates/ skills/, add/extend a review-guard with teeth. +- **cousin**: [[LRN-114]] (hook-drift class), [[LRN-047]] (silent degradation → measure/guard). + +## LRN-114 — Editing a hook generator does not touch the installed hook: reinstall + drift-guard +- **pattern**: job7 added the gitleaks scan to `_gitflow_emit_pre_commit` (the GENERATOR), but the installed `.githooks/pre-commit` is only (re)written by `gitflow init`/`install-hook`. job7 never re-installed → the repo's active hook stayed the pre-job7 version (620071b) for 8 days; `git commit` ran no secret scan while the team believed it did. +- **why undetected**: T10 (drift test) compares only the hook's allow/block VERDICT, not content; T16 emits a FRESH hook in a throwaway repo, validating the generator, never the installed file. Both green while the installed hook was stale. +- **fix**: after editing any template-generated artifact, regenerate the installed copy (`gitflow.sh install-hook`) AND add a content-drift gate — `run-review-guards.sh` G5 diffs installed `.githooks/pre-commit` against `emit-hook`. +- **future application**: any generator/template emitting an on-disk artifact needs an "installed == freshly-emitted" test, not just a behavioral one. +- **cousin**: [[LRN-113]] (partial-fix + guard), [[LRN-039]] (installers drift hand-curated config). + +## LRN-115 — Analyzer Edit/Write grants are not dead capability: they write the report (false-positive) +- **pattern**: a contract audit flagged seo/geo/validator-analyzer holding `Edit`/`Write` while instructed "do NOT apply any Edit/Write" as a defense-in-depth defect. Verified FALSE: those grants write the agent's own REPORT (`.claude/audits/VALIDATE.md`/`SEO.md`/`GEO.md`). The "never edit" rule targets CODE files (the fix-bundle is applied by the dispatcher) and is instruction-level — identical in the patron. Removing Write would break report generation. +- **why it matters**: don't "harden" a report-only agent by stripping Write — it needs it for its report. The code/report distinction is instruction-enforced, not tool-enforced, by design. +- **future application**: before flagging a tool-grant as dead, check whether the agent uses it for its own output artifact (report), not the forbidden target (code). +- **cousin**: [[BDR-061]] (analyzer bundle→L1 contract), [[LRN-113]]. + +## LRN-116 — A resolved blocker's FIX can be missing from develop even when the entry backfills cleanly +- **pattern**: backfilling release/1.0.0 memory into develop, BLK-016 (rtk PATH-dead) was marked "resolved" via fix e58037c. Checked before backfilling: e58037c (the `~/.cargo/bin`→`~/.local/bin` bridge in install-plugins.sh) was NOT on develop — develop still installed rtk to a cargo bin dir the tool shell can't see → rtk compression was LIVE-broken on develop (~460K tokens/30d). The registry entry looked safe to copy; the underlying fix wasn't there. +- **why it matters**: append-only registry backfill is "safe" only for the TEXT; a "resolved" status is a claim about CODE state that must be verified on the target branch, else you assert a resolution that isn't true. +- **fix**: ported e58037c to develop (13-line idempotent bridge), THEN backfilled BLK-016 resolved. General: before backmerging a resolved blocker, grep the target for the fix's code signature. +- **future application**: gitflow divergence review — enumerate release-only COMMITS that touch code, not just memory; a feature can be parallel-merged while its RC-branch fix is orphaned. +- **cousin**: [[LRN-036]] (PATH profile drift), [[LRN-047]] (silent degradation). From 6dd5a412926aa8026ad8566af70082c303d4462d Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 8 Jul 2026 16:07:05 +0200 Subject: [PATCH 13/13] =?UTF-8?q?chore(tasks):=20review=20remediation=20co?= =?UTF-8?q?mplete=20=E2=80=94=20all=20EPs=20done,=20gate=20final=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 11 atomic commits on chore/review-remediation, make test GREEN throughout, both smokes verified (A2 secret blocked, A8 AUTO fix lands). Branch unmerged (human gate). --- .claude/tasks/TODO.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index fa5a217..6f0bdbb 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,23 @@ # TODO +## 2026-07-08 — review remediation (chore/review-remediation) +Genèse : `.audit/review-release-1.0.0.md` (revue adversariale des 9 jobs). GO user, +ordre imposé. Déviation justifiée : 1 branche (pas 1/EP) car le gate fil-rouge (step 6) +grep toute la surface et n'est vert qu'avec A1/A4/A5 déjà appliqués. Commits atomiques, +branche non mergée (gate humain). EP-A3/A6 = décisions user tranchées (combler / option b). +- [x] EP-A1 (BLOQUANT) trailer bugfixer/feater/hotfixer (56018df) + grep étendu = 0 autre +- [x] EP-A2 (P0) hook réinstallé gitleaks (d4526e6) + 3 gates verts + root-cause (générateur édité, jamais réinstallé) +- [x] EP-A4 quote YAML seo-analyzer:3 + security-auditor:3 (5a0fc16) + gate yaml.safe_load tous agents +- [x] EP-A5 geo own-policy PERMISSIVE (f0111e1), user-approved, grep==0 +- [x] EP-A8 smoke /seo+/geo réel PROUVÉ — AUTO llms.txt + sitemap.xml atterrissent sur disque (no-op infirmé) +- [x] FIL-ROUGE run-review-guards.sh 5 gardes (4e83f39), user-approved, à dents +- [x] EP-A3 backfill LRN-098/101 (7cd82cf/a01250b) + EVAL-015 (38cc821) + BLK-016 (8e9ff33) + PORT rtk e58037c (416b68f) car fix absent+bug live sur develop +- [x] EP-A6 (option b) seuil 280→320 + BDR-062 (1be9036) +- [x] EP-A7 documentaire + M5 → EVAL-022 (capitalize cc4f161) +- [x] Capitalize LRN-113/114/115/116 + BDR-062 + EVAL-021/022 + journal (cc4f161) +- [x] GATE FINAL : make test GREEN (exit 0) + A2 secret BLOCKED (gitleaks) + A8 AUTO landed + review-guards 5/0 +- Branche chore/review-remediation NON mergée (gate humain). Résidu noté : e65796f (SC1091 lint) non back-mergé, hors scope. + ## 2026-07-08 — job9 sub-agent architecture corrections (chore/job9-agents) Genèse : `.audit/job9-report.md` (agents/*.md frontmatter+body, verify-loop, dispatch graph, read-only). Premise correction confirmed CC v2.1.203 : nesting