21 Commits
Author SHA1 Message Date
Bastien Chanot c127aef1d9 Merge chore/backmerge-release-full into develop 2026-07-08 18:05:18 +02:00
Bastien Chanot 8397354caa chore(memory): backmerge — LRN-117 fork orphans code + consolidated B journal + backlog 2026-07-08 17:22:52 +02:00
Bastien Chanot fcdb157cdd chore(lint): silence SC1091 info notes — shellcheck health stack exits clean 2026-07-08 17:13:33 +02:00
Bastien ChanotandClaude Opus 4.8 82ce02cf28 chore(skills): sync design-motion-principles from upstream (make update)
Vendored-skill content refreshed by update-all.sh step 8 during the soak
update runs: demo-shell + output-format reworked upstream, new
report-template.html reference. Content-only, no wiring change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS
2026-07-08 17:13:07 +02:00
Bastien ChanotandClaude Opus 4.8 3049250150 fix(update): rtk step — source cargo env + install-by-tag version guard
RC fix (soak day 1, #4). Two stacked defects: (1) update-all.sh never
sourced ~/.cargo/env (unlike install-plugins.sh), so on a profile that
lost the cargo PATH line the rtk step printed "Cargo not available" forever
— rtk never updated via make update (BLK-016 class). (2) once cargo was
found, the "latest" branch ran a bare `cargo install --git` (default-branch
HEAD) and would have recompiled Rust on EVERY update: upstream's HEAD
Cargo.toml (0.42.4) trails its newest stable tag (v0.43.0), so any
tag-vs-installed comparison never converges against a HEAD build.

Fix: source cargo env before concluding cargo is absent; resolve the newest
STABLE tag by name (sed anchored on refs/tags/v?N.N.N$ — dev-N.N.N-rc.*
pre-releases excluded; a naive version grep had picked dev-0.44.0-rc.308),
install BY TAG, and skip when installed == target. Proven live both ways:
run 1 compiled v0.43.0 (?tag=v0.43.0#5a7880d4), run 2 skipped
("already at latest tag (0.43.0)"). Pinned-version branch gets the same
skip-on-match guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS
2026-07-08 17:12:40 +02:00
Bastien ChanotandClaude Opus 4.8 ce07e55e98 fix(update): TTY-guard the gstack prompt — non-interactive runs died at EOF
RC fix (soak day 1, #3). `read -r` on the "Proceed with GStack update?"
prompt hits EOF in any non-TTY run (cron, CI, background shell) and kills
the whole update under set -e — every later step (rtk, gsd, ctx7, semgrep,
npx skills) silently never ran. Guard on [ -t 0 ]: interactive behavior
unchanged, non-TTY defaults to the safe N and continues. Proven end-to-end:
before = Error 1 at the prompt; after = full run exit 0 through step 7.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS
2026-07-08 17:11:47 +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 28026d8403 Merge chore/review-remediation into develop 2026-07-08 16:32:55 +02:00
Bastien Chanot 6dd5a41292 chore(tasks): review remediation complete — all EPs done, gate final green
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).
2026-07-08 16:07:05 +02:00
Bastien Chanot cc4f161df7 chore(memory): capitalize review remediation — LRN-113/114/115/116, EVAL-021/022, journal
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.)
2026-07-08 16:05:49 +02:00
Bastien Chanot 1be90361ac chore(config): realign CLAUDE.md size guard to measured reality (280→320) + BDR-062
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).
2026-07-08 16:01:12 +02:00
Bastien Chanot 8e9ff33cd7 chore(memory): backmerge BLK-016 from release/1.0.0 (resolved on develop)
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.
2026-07-08 15:59:09 +02:00
Bastien Chanot 416b68f7d2 fix(rtk): bridge ~/.cargo/bin/rtk into ~/.local/bin — compression was PATH-dead on develop
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.
2026-07-08 15:58:00 +02:00
Bastien Chanot 38cc821a35 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.
2026-07-08 15:55:53 +02:00
Bastien Chanot a01250ba59 chore(memory): backmerge LRN-101 from release/1.0.0
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.
2026-07-08 15:55:27 +02:00
Bastien Chanot 7cd82cf9c1 chore(memory): backmerge LRN-098 from release/1.0.0
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.
2026-07-08 15:54:48 +02:00
Bastien Chanot 4e83f39a70 test(guards): add anti-partial-fix regression guards (fil rouge)
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.
2026-07-08 15:48:51 +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 d4526e6fa7 chore(gitflow): regenerate installed pre-commit hook to include job7 gitleaks backstop
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.
2026-07-08 15:16:19 +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
25 changed files with 1762 additions and 544 deletions
+11
View File
@@ -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 `<type> <name>` 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 [<type> <name>]` — 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.
+10
View File
@@ -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.
+22
View File
@@ -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
@@ -198,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.
+5
View File
@@ -365,3 +365,8 @@ 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.
- Full back-merge release/1.0.0→develop (`chore/backmerge-release-full`, unmerged): the RC fork had left ~6 functional fixes orphaned on develop, silently. PORTED via cherry-pick, make test green each: `095d881` drop find-skills, `a1093ca` make-update TTY-guard (proven: EOF-die exit1 → guarded exit0), `4c5e862` rtk update-path version-guard (complements the `e58037c` install bridge already ported), `c76479f` design-motion sync, `e65796f` SC1091 lint. B soak journal (find-skills day1 / TTY #3 / rtk-update #4) folded here, not cherry-picked — divergent journal tails conflict (STOP-on-conflict honored, extract-consolidate fallback). C all covered/skip: `93e43c0` attribution + `ae8ad86` model already on develop; `188a9a7` docs → /doc backlog (README missing semgrep/scan-secrets/verify+secure/ctx7). Registry (LRN-098/101, EVAL-015, BLK-016) already backfilled in the review run. Gate: 23/23 release-only commits classified, 0 orphan functional, 0 missing registry; make test GREEN, review-guards 5/0. version.txt stays 4.0.0 (fork intentional, D — `eb93050`).
- [[LRN-117]]: the fork silently orphaned functional CODE on develop (not just memory); the review back-merge caught ~half. Detecting it needs a code-level drift check (advisory, backlogged) — registry-sequence gaps alone miss it.
+58
View File
@@ -117,8 +117,10 @@ 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-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 |
@@ -126,6 +128,11 @@ 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 |
| LRN-117 | 2026-07-08 | a release/develop fork silently orphans FUNCTIONAL code on develop, not just memory — RC soak fixes (find-skills, make-update TTY, rtk version-guard) lived only on release for the fork's duration; the review's memory back-merge caught only ~half | at release-finish/reconcile: list develop..release commits touching non-registry code (excl. merges/version) for back-merge review — a registry-gap check alone misses code |
---
@@ -1033,6 +1040,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).
@@ -1049,6 +1064,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.
@@ -1130,3 +1154,37 @@ 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).
## LRN-117 — A release/develop fork silently orphans functional CODE on develop, not just memory
- **pattern**: cutting release/1.0.0 and continuing on develop, the RC-branch bug fixes (find-skills drop `095d881`, make-update TTY guard `a1093ca`, rtk update-path version-guard `4c5e862`, rtk install bridge `e58037c`, SC1091 lint `e65796f`) landed ONLY on release. They were live-broken on develop for the whole fork duration (rtk compression dead, `make update` dies non-interactively). The review's memory back-merge caught the registry gaps and one code fix (rtk bridge); a full back-merge found ~5 more functional commits.
- **why it hides**: registry-sequence gaps (missing LRN/BLK/EVAL ids) are easy to detect; orphaned CODE has no sequence to check. A feature can be parallel-merged to both branches while an RC-branch fix commit is never back-merged, and nothing flags it.
- **fix**: at release-finish / in /reconcile, list `develop..release/*` commits touching functional files (exclude merges, `.claude/**`, version.txt/CHANGELOG) and present them for back-merge review. Advisory, NOT a hard make-test gate — cherry-picks land with new SHAs so the source commit stays in the range; automatic "already-ported?" equivalence is unreliable and would false-positive. Backlogged.
- **future application**: any long-lived fork (release/*, long feature) — audit CODE divergence, not just declared/registry state ([[LRN-034]] narrated ≠ ground truth, applied to branches).
- **cousin**: [[LRN-116]] (a resolved blocker's fix can be missing from develop), [[BDR-054]] (supersession-trace discipline).
+44
View File
@@ -1,5 +1,49 @@
# TODO
## 2026-07-08 — full back-merge release/1.0.0→develop (chore/backmerge-release-full)
Genèse : la revue avait porté ~5/19 commits ; back-merge complet demandé. Cherry-pick par
catégorie, 1 commit atomique/item, make test après chaque code. Branche non mergée (gate humain).
- [x] A CODE (5 cherry-picks, make test GREEN chacun) : 095d881 drop find-skills (5a1fff5),
a1093ca TTY-guard make-update (ce07e55, prouvé EOF exit1→exit0), 4c5e862 rtk version-guard
(3049250, complète le pont e58037c déjà porté — fichiers/concerns distincts), c76479f
design-motion sync (82ce02c), e65796f SC1091 lint (fcdb157, shellcheck 0 SC1091).
- [x] B JOURNAL : cherry-pick direct conflicte (tails journal divergents) → STOP honoré,
fallback note consolidée sous journal 2026-07-08. TODO /deploy ca9fa8f skip (release-specific).
- [x] C DÉCISION/DOUBLON tous skip vérifiés : 93e43c0 attribution + ae8ad86 model (opus[1m]=Opus4.8)
déjà sur develop ; a623514/74d3804/2b4e740 registres déjà backfillés (run revue) ;
188a9a7 docs → backlog /doc ci-dessous.
- [x] D fork version 1eb5b08/eb93050 intouchés — version.txt reste 4.0.0.
- [x] GATE FINAL : 23/23 commits release-only classifiés, 0 code orphelin, 0 entrée registre
manquante ; make test GREEN + review-guards 5/0. Capitalize [[LRN-117]] structurel.
### Backlog (issu du back-merge)
- [ ] **/doc** — README develop ne documente pas semgrep / scan-secrets / verify+secure pipeline /
ctx7 (delta de 188a9a7, non porté car base README divergente job3 + CHANGELOG version-entangled).
Une passe /doc doit combler ces sujets sur le README réécrit de develop.
- [ ] **release-drift advisory** ([[LRN-117]]) — check qui liste les commits `develop..release/*`
touchant du CODE fonctionnel (exclut merges, `.claude/**`, version.txt/CHANGELOG) pour revue
de back-merge. Advisory, PAS un gate make-test dur : les cherry-picks landent avec de nouveaux
SHA → le commit source reste dans le range → équivalence "déjà porté ?" non fiable automatiquement
(faux positifs). Cible : étape release-finish ou /reconcile, pas run-review-guards.
## 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
+13
View File
@@ -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
-1
View File
@@ -68,7 +68,6 @@ skills/impeccable
# External skills installed via `npx skills add` — auto-created by link.sh
skills/darwin-skill
skills/find-skills
# Context7 docs-lookup skill — installed by `ctx7 setup --claude --cli`
# (install-plugins.sh Step 6, when absent) into ~/.claude/skills (a symlink to
+3
View File
@@ -25,6 +25,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
- `gitflow_finish` ignored its `<type> <name>` arguments and always merged the checked-out branch — naming a different branch silently merged the wrong one. The arguments are now an optional safety assertion: if given and not equal to the current branch, `finish` refuses with a clear error instead of merging. No-argument calls (the only real caller) are unchanged.
- `doctor.sh` false-warnings removed (a check that cries wolf is one you learn to ignore): `cargo` absence no longer claims "RTK unavailable" (RTK ships as a prebuilt binary); `check_symlink` no longer flags files reached through directory-level symlinks (e.g. `hooks/session-start.sh`); the GStack check counts the per-skill symlinks instead of a `skills/gstack` link that `link.sh` deliberately removes; the token-budget estimate is measured against the ~200k context window instead of a mis-framed "~11k session budget" that produced a false "92% CRITICAL".
### Removed
- **find-skills** (alchaincyf) — skill-discovery helper dropped from the toolchain (install/update/link/toggle/advisor). Never used, and its `make update` refresh step had started failing on clone timeouts. The discovery use case stays reachable manually: `npx -y skills find <query>`.
## [4.0.0] — 2026-06-30
### Added
-2
View File
@@ -180,8 +180,6 @@ Apply the fix following the plan:
<what was wrong and why>
<what the fix does>
Co-Authored-By: Claude <noreply@anthropic.com>
```
7. Print summary:
```
-2
View File
@@ -138,8 +138,6 @@ Commit using conventional format:
feat(<scope>): <what was added>
<brief description of the feature>
Co-Authored-By: Claude <noreply@anthropic.com>
```
If the feature touched multiple concerns (e.g., feature + config +
+6 -4
View File
@@ -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 : <highest-priority>
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.
-2
View File
@@ -132,8 +132,6 @@ Apply the minimal change that fixes the bug:
4. Commit using conventional format (only after verify AND security pass):
```
fix(<scope>): <what was wrong>
Co-Authored-By: Claude <noreply@anthropic.com>
```
5. Print summary:
```
+3 -3
View File
@@ -19,7 +19,7 @@ Detect active plugins and project signals. Recommend enable/disable. Apply compa
claude plugin list 2>/dev/null || echo "plugin-list-unavailable"
# External (non-marketplace) tools status — gstack, emil-design-eng,
# darwin-skill, find-skills. Managed by lib/toggle-external.sh since
# darwin-skill. Managed by lib/toggle-external.sh since
# `claude plugin enable|disable` does not apply to them.
bash "$HOME/.claude/lib/toggle-external.sh" list 2>/dev/null || echo "toggle-external-unavailable"
@@ -353,8 +353,8 @@ RULE: IF `complex-arch` signal (multiple services, event bus, distributed system
## TOGGLING EXTERNAL TOOLS
Marketplace plugins toggle via `claude plugin enable|disable <name>@<marketplace>`.
Non-marketplace tools (gstack per-skill symlinks, emil-design-eng, darwin-skill,
find-skills) toggle via `bash $HOME/.claude/lib/toggle-external.sh enable|disable <tool>`.
Non-marketplace tools (gstack per-skill symlinks, emil-design-eng, darwin-skill)
toggle via `bash $HOME/.claude/lib/toggle-external.sh enable|disable <tool>`.
When a recommendation flips the state of one of those tools, emit the exact
command — never write files directly.
+1 -1
View File
@@ -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
---
+1 -1
View File
@@ -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
---
+6 -4
View File
@@ -36,7 +36,7 @@ fi
# ── Load shared detection library ──
_lib="$(dirname "${BASH_SOURCE[0]}")/../lib/detect-plugins.sh"
if [ -f "$_lib" ]; then
# shellcheck source=../lib/detect-plugins.sh
# shellcheck source=../lib/detect-plugins.sh disable=SC1091
source "$_lib"
else
echo "⚠️ lib/detect-plugins.sh not found — config broken, run: bash link.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
+15 -3
View File
@@ -197,6 +197,7 @@ if command -v cargo &>/dev/null; then
else
info "Installing Rust (rustup)..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
# shellcheck source=/dev/null
source "$HOME/.cargo/env"
ok "Rust installed: $(cargo --version)"
fi
@@ -431,6 +432,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)..."
@@ -819,7 +833,6 @@ echo ""
NPX_SKILLS=(
"alchaincyf/darwin-skill"
"alchaincyf/find-skills"
)
# `skills add` resolves its target (.agents/skills/, skills-lock.json) RELATIVE
@@ -972,7 +985,7 @@ echo ""
# STEP 10 — REFRESH SYMLINKS (final, so this script is self-sufficient)
# ============================================================
# Steps 2/8/8.5 INSTALL skills (gstack submodule, emil/frontend/motion, npx
# darwin/find-skills) that link.sh must symlink into ~/.claude/skills/. Since
# darwin-skill) that link.sh must symlink into ~/.claude/skills/. Since
# link.sh runs BEFORE this script in install.sh, those symlinks would be missing
# on a fresh run until link.sh is run again by hand. Re-run it here so
# `make plugin` (and `make install`) finish complete — nothing left to do.
@@ -1010,7 +1023,6 @@ echo " 🔄 frontend-design — distinctive frontend interfaces, anti-AI-
echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)"
echo " 🔄 find-skills — skill discovery helper (npx skills, ~/.agents/skills/)"
echo " 🔄 magic MCP — 21st-dev UI generation MCP (toggle: lib/toggle-external.sh enable magic)"
echo ""
echo " All plugins installed at: user scope (~/.claude/plugins/)"
+77
View File
@@ -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 ]
+5 -6
View File
@@ -20,7 +20,6 @@
# gstack — per-skill symlinks populated by gstack's own setup
# emil-design-eng — single symlink → skills-external/emil-design-eng
# darwin-skill — single symlink → ~/.agents/skills/darwin-skill
# find-skills — single symlink → ~/.agents/skills/find-skills
# magic — 21st-dev Magic MCP server (API key in .env)
#
# For fine-grained activation (only design skills, only qa skills, only
@@ -41,7 +40,7 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; }
err() { echo -e "${RED}✗${NC} $1"; }
# All non-plugin tools this script can toggle.
MANAGED_TOOLS=(gstack emil-design-eng darwin-skill find-skills magic)
MANAGED_TOOLS=(gstack emil-design-eng darwin-skill magic)
# Load MAGIC_API_KEY (and any other secrets) from $REPO/.env if present.
# Called only by the magic branch — other tools don't need env vars.
@@ -81,7 +80,7 @@ status_tool() {
[ -d "$REPO/skills-external/emil-design-eng" ] || { echo "missing"; return; }
[ -e "$SKILLS_DIR/emil-design-eng" ] && echo "enabled" || echo "disabled"
;;
darwin-skill|find-skills)
darwin-skill)
[ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; }
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
;;
@@ -116,7 +115,7 @@ disable_tool() {
done < <(gstack_skills)
ok "gstack disabled ($moved symlinks moved)"
;;
emil-design-eng|darwin-skill|find-skills)
emil-design-eng|darwin-skill)
if [ -e "$SKILLS_DIR/$tool" ]; then
rm -rf "${DISABLED_DIR:?}/${tool:?}"
mv "$SKILLS_DIR/$tool" "$DISABLED_DIR/$tool"
@@ -158,11 +157,11 @@ enable_tool() {
ok "gstack enabled ($moved symlinks restored)"
fi
;;
emil-design-eng|darwin-skill|find-skills)
emil-design-eng|darwin-skill)
local src
case "$tool" in
emil-design-eng) src="$REPO/skills-external/$tool" ;;
darwin-skill|find-skills) src="$HOME/.agents/skills/$tool" ;;
darwin-skill) src="$HOME/.agents/skills/$tool" ;;
esac
if [ -e "$DISABLED_DIR/$tool" ]; then
rm -rf "${SKILLS_DIR:?}/${tool:?}"
+1 -1
View File
@@ -90,7 +90,7 @@ done
# absolute paths so the link stays valid regardless of where the
# repo is cloned (relative ../../ paths broke on repos deeper than
# one level below $HOME).
NPX_EXTERNAL_SKILLS=(darwin-skill find-skills)
NPX_EXTERNAL_SKILLS=(darwin-skill)
for _ext in "${NPX_EXTERNAL_SKILLS[@]}"; do
_target="$HOME/.agents/skills/$_ext"
_link="$REPO/skills/$_ext"
@@ -1,63 +1,77 @@
<!--
Demo Shell — design-motion-principles v2.1
===========================================
Demo Shell — design-motion-principles
=====================================
This file is a template the audit agent reads during STEP 3 of the
audit workflow (see ../SKILL.md). The agent embeds one .demo-card per
Critical or Important finding (per R4 in the plan — Opportunities do
not get demo cards).
Minimal, isolated reference for a single demo card. The agent reads this
during STEP 3 of the audit workflow (see ../SKILL.md) and uses it as the
per-finding template — one .demo block per Critical or Important finding
(Opportunities don't get demo cards).
How to use this file as the agent
---------------------------------
The full worked example with five demo cards in context lives in
references/report-template.html. This file is intentionally minimal:
one card, every contract visible, no report scaffolding.
How the agent uses this file
----------------------------
1. Copy the entire <style> block into the report's <head>. The shell's
CSS variables, .demo-card layout, loop indicator, and the
prefers-reduced-motion guard are shared across all demo cards in
the report — they are not duplicated per finding.
tokens, .demo layout, stage colors, segmented control, and the
prefers-reduced-motion guard are SHARED across every demo card in the
report and must not be duplicated per finding.
2. For each finding {n} (1-indexed across the whole report):
a. Replace the MOTION-CODE-SLOT-{n} comment in <style> with the
per-finding @keyframes block AND any .demo-card-{n}__motion-target
selector rules. Use the suffix {n} so multiple findings in one
report do not collide on keyframe names or target selectors.
b. Replace the DEMO-CARD-MOTION-SLOT-{n} comment in the .demo-card
a. Replace the MOTION-CODE-SLOT-{n} comment with the per-finding
@keyframes m{n} block AND the .demo-{n}__mt selector rule.
Suffix {n} so multiple findings cannot collide on keyframe
names or selectors.
b. Replace the DEMO-CARD-MOTION-SLOT-{n} comment in the .demo__stage
markup with the actual motion-target element. Its class must be
.demo-card-{n}__motion-target so it matches the rules above.
c. Set the .demo-card__header text to a short title for the
recommended motion (e.g., "Subtle enter: opacity + translateY + blur").
d. Set the .demo-card__subhead text to the duration plus easing or
other relevant values (e.g., "300ms · ease-out"). The subhead
ALWAYS renders — the agent populates it for every demo so card
heights stay consistent across the report.
.demo-{n}__mt so it matches the rules above.
c. Set .demo__title to a short title for the recommended motion
(e.g., "Quick tab crossfade", "Sheet enter (mirror for exit)").
d. Set .demo__timing to the duration plus easing (e.g.,
"180ms · ease-out", "300ms · ease-out-quint"). The subhead
ALWAYS renders — populate it for every demo so card heights
stay consistent across the report.
e. Renumber the four radio ids from st1-* to st{n}-*, and the
labels' for= attributes to match.
3. Per-finding code MUST honor these contracts:
- Do not redefine the shell's CSS variables (--bg, --fg, --border,
--accent, --loop-dim, --card-radius, --card-padding, --gap,
- Do NOT redefine the shell's tokens (--ink, --paper, --surface-2,
--line, --line-strong, --st-bg, --st-fg, --st-line, --st-dim,
--sans, --mono). Use them via var().
- Do not modify the prefers-reduced-motion block. The shell's
guard collapses all .demo-card-{n}__motion-target animations to
none. The per-finding @keyframes 100% state must match the
motion-target's default rendered state so the reduce-motion
fallback shows the correct final visual.
- Per-finding @keyframes use the 0% / 66% / 100% cadence:
0% = start state, 66% = motion complete (~2s in), 100% = hold
(~1s). The shell uses animation-duration: 3s.
- Do NOT modify the prefers-reduced-motion block. The shell's
guard collapses all .demo-{n}__mt animations to none. The
per-finding @keyframes 100% state MUST match the motion-target
element's default static rendering so the reduce-motion fallback
shows the correct final visual.
- Per-finding @keyframes use the 0% / ~60% / 100% cadence:
0% = start state, ~60% = motion complete (~1.8s in),
100% = hold (~1.2s). The shell uses animation-duration: 3s.
4. Demo cards are non-interactive. They have no hover or focus state
beyond the default outline suppression. tabindex="-1" keeps them
out of keyboard nav order — readers tab through findings, not
through demo cards.
4. Motion targets and ANY UI primitives inside the stage (.ui-btn,
.ui-card, .ui-row, .ui-check, .ui-num, .ui-label, badge, etc.)
use --st-bg / --st-fg / --st-line / --st-dim — NEVER --accent or
other page tokens. This guarantees the demo contrasts correctly
when its stage is locked to a different theme than the report.
5. Demo cards are non-interactive. The stage toggle radios are the
only interactive element. tabindex on the radios is fine; the
stage itself stays out of focus order.
Loop pacing
-----------
animation-duration: 3s. Keyframes 0% / 66% / 100%. Motion 0-66% = ~2s,
hold 66-100% = ~1s, then the animation restarts. Per-finding code
imitates this cadence so all demos in a report share the same rhythm.
animation-duration: 3s. Keyframes 0% / ~60% / 100%. Motion 0–60% ≈ 1.8s,
hold 60–100% ≈ 1.2s, then restart. Every demo in a report uses this
cadence so all cards share the same rhythm.
Empty state (this file rendered standalone)
-------------------------------------------
Opening this file directly in a browser shows one .demo-card with the
loop indicator and placeholder content. No motion plays — the agent
injects motion per finding when this template is embedded in a report.
Standalone preview
------------------
Opening this file directly in a browser shows one .demo card with the
loop indicator, the segmented Auto/Light/Dark stage toggle, and a
placeholder motion target (a simple shape that fades in). No real
per-finding motion plays — the agent injects motion per finding when
this template is embedded in a report. The shell also honors the
viewer's prefers-color-scheme for standalone rendering.
-->
<!DOCTYPE html>
<html lang="en">
@@ -65,144 +79,197 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Demo Shell — design-motion-principles</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@400;500;600;700&family=Public+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #ffffff;
--fg: #111111;
--border: #e5e5e5;
--accent: #111111;
--loop-dim: rgba(0, 0, 0, 0.45);
--card-radius: 12px;
--card-padding: 20px;
--gap: 12px;
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, monospace;
/* Cool slate-graphite neutrals (hue 255). In the full report these
are overridden by a global theme toggle; here the shell flips via
prefers-color-scheme for standalone preview. */
--ink: oklch(0.155 0.006 255);
--surface: oklch(0.195 0.007 255);
--surface-2: oklch(0.235 0.008 255);
--paper: oklch(0.945 0.004 255);
--paper-dim: oklch(0.800 0.006 255);
--muted: oklch(0.680 0.008 255);
--faint: oklch(0.505 0.008 255);
--line: oklch(0.95 0.01 255 / 0.09);
--line-strong:oklch(0.95 0.01 255 / 0.16);
--display: "Familjen Grotesk", "Hanken Grotesk", system-ui, sans-serif;
--sans: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
--mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
}
@media (prefers-color-scheme: dark) {
@media (prefers-color-scheme: light) {
:root {
--bg: #1a1a1a;
--fg: #f0f0f0;
--border: #333333;
--accent: #f0f0f0;
--loop-dim: rgba(240, 240, 240, 0.45);
--ink: oklch(0.985 0.003 255);
--surface: oklch(0.965 0.004 255);
--surface-2: oklch(0.940 0.005 255);
--paper: oklch(0.180 0.006 255);
--paper-dim: oklch(0.330 0.008 255);
--muted: oklch(0.475 0.010 255);
--faint: oklch(0.640 0.009 255);
--line: oklch(0.180 0.010 255 / 0.12);
--line-strong:oklch(0.180 0.010 255 / 0.22);
}
}
* { box-sizing: border-box; }
body {
background: var(--bg);
color: var(--fg);
background: var(--ink);
color: var(--paper);
font-family: var(--sans);
margin: 0;
padding: 32px;
}
.demo-card {
position: relative;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
padding: var(--card-padding);
max-width: 360px;
min-width: 280px;
font-family: var(--sans);
outline: none;
}
.demo-card:focus-visible {
outline: none;
}
.demo-card__header {
font-size: 0.875rem;
font-weight: 600;
color: var(--fg);
margin-bottom: 4px;
padding-right: 72px;
}
.demo-card__subhead {
font-family: var(--mono);
font-size: 0.75rem;
color: var(--loop-dim);
margin-bottom: var(--gap);
}
.demo-card__stage {
background: var(--bg);
border: 1px dashed var(--border);
border-radius: 8px;
padding: 24px;
min-height: 100px;
padding: 48px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.demo-card__loop-indicator {
position: absolute;
top: 12px;
right: 12px;
font-family: var(--mono);
font-size: 0.7rem;
color: var(--loop-dim);
letter-spacing: 0.02em;
user-select: none;
/* visually-hidden (stage-toggle radios) */
.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* ───────────── Demo card ───────────── */
.demo { position: relative; width: 380px; }
.demo__bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.demo__meta { min-width: 0; }
.demo__title { display: block; font-size: 13.5px; font-weight: 600; color: var(--paper); line-height: 1.3; }
.demo__timing { display: block; font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 2px; font-variant-numeric: tabular-nums; }
.demo__controls { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.demo__loop { font-family: var(--mono); font-size: 12px; color: var(--faint); }
/* segmented Auto / Light / Dark stage toggle (pure CSS).
Auto = follow ambient theme via --surface-2 + --paper.
Light / Dark = locked overrides (hardcoded values, ignore ambient). */
.seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: 100px; overflow: hidden; }
.seg label { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); padding: 4px 9px; cursor: pointer; user-select: none; transition: background 0.15s ease, color 0.15s ease; }
.demo input[id$="-a"]:checked ~ .demo__bar .seg label[for$="-a"],
.demo input[id$="-l"]:checked ~ .demo__bar .seg label[for$="-l"],
.demo input[id$="-d"]:checked ~ .demo__bar .seg label[for$="-d"] {
background: color-mix(in oklch, var(--paper) 12%, transparent);
color: var(--paper);
}
.demo__stage {
/* AUTO default — stage uses the ambient elevated surface */
--st-bg: var(--surface-2);
--st-fg: var(--paper);
--st-line: var(--line-strong);
--st-dim: color-mix(in oklch, var(--paper) 55%, transparent);
background: var(--st-bg); color: var(--st-fg);
border: 1px solid var(--line); border-radius: 10px;
padding: 32px 24px; min-height: 168px;
display: flex; align-items: center; justify-content: center; overflow: hidden;
}
/* Locked Light: hardcoded light, regardless of ambient theme */
.demo input[id$="-l"]:checked ~ .demo__stage {
--st-bg: oklch(0.985 0.003 255);
--st-fg: oklch(0.180 0.006 255);
--st-line: oklch(0.180 0.010 255 / 0.18);
--st-dim: oklch(0.180 0.010 255 / 0.45);
}
/* Locked Dark: hardcoded dark, regardless of ambient theme */
.demo input[id$="-d"]:checked ~ .demo__stage {
--st-bg: oklch(0.180 0.006 255);
--st-fg: oklch(0.985 0.003 255);
--st-line: oklch(0.985 0.010 255 / 0.18);
--st-dim: oklch(0.985 0.010 255 / 0.50);
}
/* Generic UI primitives for use inside motion targets. All theme via
stage tokens, NEVER page tokens — so they contrast correctly when a
stage is locked to a different theme than the report. */
.ui-btn { display: inline-flex; align-items: center; justify-content: center; padding: 9px 18px; border-radius: 8px; background: var(--st-fg); color: var(--st-bg); font-family: var(--sans); font-size: 13px; font-weight: 600; }
.ui-card { background: var(--st-bg); border: 1px solid var(--st-line); border-radius: 10px; padding: 14px 16px; min-width: 180px; box-shadow: 0 8px 24px oklch(0 0 0 / 0.12); }
.ui-row { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--st-fg); }
.ui-check { width: 22px; height: 22px; border-radius: 7px; background: var(--st-fg); display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.ui-check svg { width: 13px; height: 13px; stroke: var(--st-bg); }
.ui-num { font-family: var(--display); font-size: 34px; font-weight: 700; color: var(--st-fg); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.ui-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--st-dim); }
/* prefers-reduced-motion — disables every per-finding animation and
hides the loop indicator. The per-finding @keyframes 100% state
must match the motion-target's default rendering so the reduce
fallback shows the correct final visual. */
@media (prefers-reduced-motion: reduce) {
.demo-card__loop-indicator {
display: none;
}
[class*="__motion-target"] {
animation: none !important;
transition: none !important;
}
.demo__loop { display: none; }
[class*="__mt"], [class*="__mt"] * { animation: none !important; transition: none !important; }
}
/* MOTION-CODE-SLOT-{n}
Per-finding @keyframes and .demo-card-{n}__motion-target rules
go here. {n} = finding's 1-indexed position across the report.
Per-finding @keyframes and .demo-{n}__mt rules go here. {n} is the
finding's 1-indexed position across the report.
Example shape (replace per finding):
Example shape (replace per finding — wrap in
@media (prefers-reduced-motion: no-preference) { ... }):
@keyframes motion-1-enter {
@media (prefers-reduced-motion: no-preference) {
@keyframes m1 {
0% { opacity: 0; transform: translateY(8px); filter: blur(4px); }
66% { opacity: 1; transform: translateY(0); filter: blur(0); }
60% { opacity: 1; transform: translateY(0); filter: blur(0); }
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
}
.demo-card-1__motion-target {
animation: motion-1-enter 3s infinite;
display: inline-block;
padding: 8px 16px;
border-radius: 6px;
background: var(--accent);
color: var(--bg);
font-family: var(--sans);
.demo-1__mt {
animation: m1 3s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
}
*/
/* Placeholder motion for standalone preview only — agent strips this
when composing the report. Demonstrates the 0 / 60 / 100 cadence. */
@media (prefers-reduced-motion: no-preference) {
@keyframes m-placeholder {
0% { opacity: 0; transform: translateY(8px); filter: blur(4px); }
60% { opacity: 1; transform: translateY(0); filter: blur(0); }
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
}
.demo-1__mt {
animation: m-placeholder 3s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
}
</style>
</head>
<body>
<article class="demo-card" tabindex="-1">
<div class="demo-card__loop-indicator">↻ looping</div>
<div class="demo-card__header">Recommended motion title</div>
<div class="demo-card__subhead">300ms · ease-out</div>
<div class="demo-card__stage">
<div class="demo">
<input class="vh" type="radio" name="st1" id="st1-a" checked>
<input class="vh" type="radio" name="st1" id="st1-l">
<input class="vh" type="radio" name="st1" id="st1-d">
<div class="demo__bar">
<div class="demo__meta">
<span class="demo__title">Recommended motion title</span>
<span class="demo__timing">300ms · ease-out</span>
</div>
<div class="demo__controls">
<div class="seg">
<label for="st1-a">Auto</label>
<label for="st1-l">Light</label>
<label for="st1-d">Dark</label>
</div>
<span class="demo__loop">↻</span>
</div>
</div>
<div class="demo__stage">
<!-- DEMO-CARD-MOTION-SLOT-{n}
Per-finding motion-target element goes here. Its class must
match the rules in MOTION-CODE-SLOT-{n} above:
<div class="demo-card-{n}__motion-target">...</div>
<div class="demo-{n}__mt">...content...</div>
The element's contents are agent-determined (a button shape,
a card, an icon, a list of items for stagger demos, etc.). -->
<span style="color: var(--loop-dim); font-family: var(--mono); font-size: 0.75rem;">
(motion preview renders here per finding)
</span>
a card, an icon, a row of items for stagger demos, a number,
a badge, etc.). All inner UI primitives use the stage tokens
(--st-fg / --st-bg / --st-line / --st-dim) — NEVER page tokens. -->
<div class="demo-1__mt">
<div class="ui-card">
<div class="ui-label" style="margin-bottom: 6px;">Placeholder</div>
<div class="ui-row">
<span class="ui-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 13l4 4L19 7"/></svg></span>
<span>(motion preview renders here per finding)</span>
</div>
</div>
</div>
</div>
</div>
</article>
</body>
</html>
@@ -1,19 +1,28 @@
# Output Format
This file defines the audit's two output modes:
The audit produces one of two outputs:
- **HTML mode (default)** — a self-contained `.html` file written to the audited project's `motion-audits/` directory and opened in the user's default browser. Each Critical or Important finding gets an auto-looping CSS demo card beside it.
- **Terminal mode (flag-triggered)** — the decorated-markdown report rendered inline in the conversation. Use when the user passes `--terminal`, `--inline`, "show the full report inline," "skip the HTML," or any natural-language equivalent. No HTML file is written.
- **HTML mode (default)** — a self-contained `.html` file written to the audited project's `motion-audits/` directory and opened in the user's default browser. Each Critical or Important finding gets a live, looping CSS demo card beside it.
- **Terminal mode (flag-triggered)** — a decorated-markdown report rendered inline in the conversation. Use when the user passes `--terminal`, `--inline`, `--no-html`, "show the full report inline," or any natural-language equivalent. No HTML file is written.
The two modes contain the same audit content; only the rendering differs. Do not summarize — users want full per-lens perspectives.
Both modes carry the same audit content; only the rendering differs. Do not summarize — users want full per-lens perspectives.
---
## HTML mode
### Canonical references
| File | Role |
|---|---|
| `references/report-template.html` | **Source of truth.** Full worked example (fictional "Tally" habit tracker, React + Framer Motion). Every section, every token, every pattern. When in doubt about layout, structure, or styling, READ this file. |
| `references/demo-shell.html` | Minimal isolated example of a single demo card with the per-finding slot pattern. Used as a per-finding template snippet. |
The agent builds the report by reading these two files and adapting them to the audited project — same architecture, audit-specific content.
### File structure
The HTML output is a single self-contained `.html` document with everything inlined — no external CSS, no external JS, no external fonts (fonts may degrade gracefully if a CDN reference is used). The file scaffolds:
Single self-contained `.html`. All CSS inlined. No external JS. Fonts loaded via Google Fonts CDN (Familjen Grotesk / Public Sans / Geist Mono) with full system-stack fallbacks so the file degrades gracefully offline.
```
<!DOCTYPE html>
@@ -22,310 +31,233 @@ The HTML output is a single self-contained `.html` document with everything inli
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{project-name} motion audit — {ISO date}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@400;500;600;700&family=Public+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* 1. Demo shell tokens, .demo-card layout, prefers-reduced-motion */
/* — copied from references/demo-shell.html */
/* 2. Report layout tokens: hero, finding-row, perspective-section,
severity-table, recommendation-summary */
/* 3. Per-finding @keyframes and .demo-card-{n}__motion-target
rules, generated per audit — one block per Critical or
Important finding, suffixed by the finding's 1-indexed
position across the report */
/* 1. :root token block (neutrals, accent aliases, severity, timing ramp, spacing, fonts)
2. :root:has(#theme-light:checked) light-mode token override
3. Layout + component CSS (header, lens-table, timing-figure, lens-sec, finding-row, demo, rec, ref-summary)
4. Per-finding @keyframes m{n} + .demo-{n}__mt rules, one block per Critical or Important finding,
{n} = 1-indexed across the whole report (collision-free) */
</style>
</head>
<body>
<!-- Hero -->
<!-- Global theme switch (Dark/Light radios) -->
<!-- Header (eyebrow, title, lede, meta, stats) -->
<!-- Overall Assessment -->
<!-- Per-lens sections (primary, secondary, selective) -->
<!-- Combined Recommendations tables -->
<!-- Lens Reference Summary -->
<!-- 01 · Lens summary table -->
<!-- 02 · Where the timings land (duration-budget diagram) -->
<!-- 03–05 · Per-lens sections (Jakub, Emil, Jhey — ordered by weighting for the audited context) -->
<!-- 06 · Combined recommendations tables -->
<!-- 07 · Lens reference summary -->
<!-- footer -->
</body>
</html>
```
### Report's own motion posture
### Design system
The report itself has **no** entrance, scroll, or mount animations. No staggered reveals, no fade-in-on-scroll, no motion-on-mount outside the demo cards. The demo cards are the only animated elements in the document — anything else would reproduce the AI-slop patterns the skill audits against.
Neutral-default, dual-mode, severity-driven.
### Hero header
- **Neutrals.** Cool slate-graphite at hue 255, very low chroma (0.003–0.010). `--ink` is the page background; `--paper` is the foreground text. In light mode the two swap values via the `:root:has(#theme-light:checked)` override — every other token derives from these two and flips automatically.
- **Severity (FIXED, never adaptive).** Red `oklch(0.655 0.185 25)` (critical) · Amber `oklch(0.805 0.125 78)` (important) · Green `oklch(0.745 0.135 152)` (opportunity). Light-mode counterparts deepen L for contrast on white; hues stay constant.
- **Timing-budget ramp (FIXED).** Same hues as severity; used in section 02 only. Instant + responsive = green, deliberate = amber, sluggish = red.
- **Accent (NEUTRAL by default).** `--accent`, `--accent-soft`, `--accent-tint` alias to `--paper`, `--paper-dim`, and a low-alpha paper tint. The report has no chromatic primary color — severity is the only color in the document. An individual audit MAY repoint these three to a sampled brand color, but ONLY if the brand has at least ~40° hue clearance from each of the severity hues and is verified not to fall in the AI-cliché zone (neon cyan, purple-to-blue gradients).
- **Fonts.** Display = Familjen Grotesk, body = Public Sans, mono = Geist Mono. The mono carries timing values (`240ms · ease-out`) and all small labels — never substitute a more generic mono for the timing values.
Top of the document. Project name + ISO date + severity counts row + primary lens label.
### Dual theme
```html
<header class="report-hero">
<h1>{project-name} motion audit</h1>
<p class="report-hero__date">{ISO date}</p>
<p class="report-hero__counts">
<a href="#critical-findings">🔴 Critical: {N}</a> ·
<a href="#important-findings">🟡 Important: {N}</a> ·
<a href="#opportunity-findings">🟢 Opportunities: {N}</a>
</p>
<p class="report-hero__primary">Primary: {Designer Name} — {Perspective Handle}</p>
</header>
Pure-CSS toggle. Two radios (`#theme-dark` default-checked, `#theme-light`) live inside `.theme-switch` at the top of `.wrap`. `:root:has(#theme-light:checked)` overrides every theme-dependent token. No JS. Selector compatibility: `:has()` is Baseline 2023, supported by all modern browsers.
The global toggle's visual control is a segmented `Dark / Light` pill, top-right of the page, styled to match the per-demo stage segmented control.
### The report's motion posture
**The report itself has no entrance, scroll, or mount animation.** No staggered reveals. No fade-in-on-scroll. No motion on mount outside the demo cards. The demo cards are the only animated elements in the document — anything else would reproduce the AI-slop patterns this skill audits against.
The one allowed transition: `border-color 0.2s ease` on lens-table rows and finding-rows for hover feedback. That's it.
### Sections (in render order)
#### Global theme switch
First element inside `.wrap`, right-aligned segmented `Dark / Light` pill.
#### Header
```
.eyebrow ("MOTION AUDIT · DESIGN-MOTION-PRINCIPLES")
h1.title ({project name} — {one-line audit framing})
p.lede ({1–2 sentence project description})
.meta-row (what it is · stack)
.stats (Findings · Critical · Important · Opportunities — each is an anchor link to its rec table)
```
The severity counts pair each emoji with a text label (`Critical: N`, not just `🔴 N`) so the severity signal is readable under red-green color vision deficiency. Each count is an anchor link to the corresponding section in the body — this is the navigation affordance for long audits with many findings.
Each severity count pairs the number with a text label so the signal is readable under red-green color vision deficiency. Each count is an anchor link (`#rec-crit`, `#rec-imp`, `#rec-opp`) to the corresponding recommendation table.
### Overall Assessment
#### Overall Assessment
One short paragraph in larger display type. Does this feel polished? Too much? Too little? What's working, what's not? Wraps in `<section class="assessment">` with a `mono-label` "OVERALL" eyebrow.
One short paragraph in larger type. Does this feel polished? Too much? Too little? What's working, what's not?
#### 01 · Lens summary
3-row table, one row per practitioner. Columns: Lens (with name and weight chip) · Verdict (`Strong` / `Concern` / `Problem` / `Mixed` with a colored dot) · One-line read. Weight chips indicate `Primary` / `Secondary` / `Selective` per audit context.
```html
<section class="report-assessment">
<p>{one-paragraph assessment}</p>
</section>
#### 02 · Where the timings land — duration-budget diagram
Motion-native analog of thumb-first's thumb-zone diagram. A horizontal SVG (`viewBox="0 0 660 300"`) plots Tally's animations as numbered dots on a 0–600ms scale with four zone bands:
| Zone | Range | Color |
|---|---|---|
| Instant | 0–100ms | green (`--t-good`) |
| Responsive | 100–300ms | green (`--t-good`) |
| Deliberate | 300–500ms | amber (`--t-mid`) |
| Sluggish | 500ms+ | red (`--t-slow`) |
Animations with NO transition are plotted as hollow dashed circles at `x=40` (= 0ms). The paired key list to the right carries the action names and durations. A "What's off" block below explains the misalignments.
The SVG uses CSS-class-driven fills (via an inline `<style>` block) so the diagram re-tones with the global theme. Dot label color flips per theme (dark text on lighter dots in dark mode, light text on deeper dots in light mode).
#### 03–05 · Per-lens sections
Three sections, ordered by weighting (primary first). Each section:
```
.lens-sec__head (h3 "Designer — Perspective" + .lens-sec__weight chip)
p.lens-sec__verdict (verdict dot)
.lens-block "What's working well" (ul.lens-list.good with ✓ markers + file refs)
.lens-block "Issues to address" (one .finding-row per Critical/Important finding)
.lens-block "Opportunities" (ul.lens-list.opp with 💡 markers + file refs)
.lens-take ("Through {Designer}'s lens: {1–2 sentence summary}")
```
### Per-lens sections
Section heading: `Designer Name — Perspective Handle` (em-dash). Lens take is the documented lens summary, NOT a quote from the person — render as `Through {Designer}'s lens` (apostrophe-s).
Three sections in weighting order: primary, secondary, selective. Each section header pairs the designer name with the perspective handle using an em-dash (`Designer Name — Perspective Handle`):
Three perspective handles:
```html
<section class="perspective-section" id="perspective-emil">
<h2>Emil Kowalski — Restraint &amp; Speed</h2>
<div class="perspective-section__working-well">
<h3>What's Working Well</h3>
<ul>
<li>✓ {observation} — <code>{file.tsx:line}</code></li>
</ul>
</div>
<div class="perspective-section__issues" id="emil-issues">
<h3>Issues to Address</h3>
<!-- One .finding-row per Critical or Important finding under this lens -->
</div>
<div class="perspective-section__opportunities">
<h3>Opportunities</h3>
<ul>
<li>💡 {idea} — <code>{file.tsx:line}</code></li>
</ul>
</div>
<p class="perspective-section__quote"><strong>Through Emil's lens:</strong> {1-2 sentence summary}</p>
</section>
```
The three perspective handles:
| Designer | Perspective handle |
| Designer | Handle |
|---|---|
| Emil Kowalski | Restraint & Speed |
| Jakub Krehel | Production Polish |
| Jhey Tompkins | Experimentation & Delight |
Always render section headers as `Designer Name — Perspective` (em-dash). Always close each section with the `Through {Designer}'s lens:` summary — it's a documented lens, not a quote from the person.
#### Finding rows (Critical + Important only)
Each Critical or Important finding renders as a `.finding-row` inside its lens's "Issues to address" block:
### Finding rows (Critical + Important only)
```
.finding-row[data-sev="crit|imp"]
.finding-row__prose
.find-tags (severity chip + 1–2 lens chips)
h4.find-title
.find-body
<p><span class="label">What</span>{prose}</p>
<p><span class="label">Why it matters</span>{prose}</p>
<div class="fix"><p><span class="label">Recommended motion</span>{prose}</p></div>
<p class="find-loc"><code>{file:line}</code></p>
.demo
{radios + bar + stage with motion-target}
```
Each Critical or Important finding inside an `Issues to Address` block renders as a `.finding-row` with the issue prose on the left and the demo card on the right (two-column at desktop, stacked at narrow widths):
Two-column at desktop (1fr 380px), stacks at narrow widths (≤860px).
Opportunities never render a `.finding-row` and never get a demo card. They appear in the per-lens `.lens-block "Opportunities"` as a `.lens-list.opp` bulleted list.
#### 06 · Combined recommendations
Three severity-grouped tables, in order: `Critical · must fix` (`#rec-crit`) → `Important · should fix` (`#rec-imp`) → `Opportunities · could enhance` (`#rec-opp`). Each has a `.tier-label` with severity-colored mono label, a horizontal rule, and a count. Columns: Issue · File · Fix (or Enhancement · Where · Impact for opportunities).
#### 07 · Lens Reference Summary
Closing `.ref-summary` block. Which lens was referenced most + why + how to lean differently (one line per lens). No new findings here.
#### Footer
Mono micro-row: project name + finding counts.
### Demo cards (the centerpiece)
Each Critical or Important finding gets one demo card. The card is the ONLY animated element in the report.
#### Markup pattern
```html
<div class="finding-row" id="finding-{n}">
<div class="finding-row__prose">
<p class="finding-row__severity">🔴 Critical</p>
<h4>{finding title}</h4>
<p>{finding explanation}</p>
<p class="finding-row__location"><code>{file.tsx:line}</code></p>
<div class="demo">
<input class="vh" type="radio" name="st{n}" id="st{n}-a" checked>
<input class="vh" type="radio" name="st{n}" id="st{n}-l">
<input class="vh" type="radio" name="st{n}" id="st{n}-d">
<div class="demo__bar">
<div class="demo__meta">
<span class="demo__title">{recommended motion title}</span>
<span class="demo__timing">{duration} · {easing}</span>
</div>
<article class="demo-card" tabindex="-1">
<div class="demo-card__loop-indicator">↻ looping</div>
<div class="demo-card__header">{recommended motion title}</div>
<div class="demo-card__subhead">{duration} · {easing}</div>
<div class="demo-card__stage">
<div class="demo-card-{n}__motion-target">{motion target markup}</div>
<div class="demo__controls">
<div class="seg">
<label for="st{n}-a">Auto</label>
<label for="st{n}-l">Light</label>
<label for="st{n}-d">Dark</label>
</div>
<span class="demo__loop">↻</span>
</div>
</div>
<div class="demo__stage">
<div class="demo-{n}__mt">{motion target markup}</div>
</div>
</article>
</div>
```
`{n}` is the finding's 1-indexed position **across the whole report** (not per-section). This guarantees `@keyframes motion-{n}-...` and `.demo-card-{n}__motion-target` selector names are unique across the document, so concatenating multiple findings' CSS in one `<style>` block does not produce keyframe-name collisions.
`{n}` is the finding's 1-indexed position across the whole report (not per-section). This guarantees `@keyframes m{n}` and `.demo-{n}__mt` selectors are unique across the concatenated `<style>` block.
Opportunities never render a demo card. They appear in the per-lens section's `Opportunities` block as a plain bulleted list.
#### Stage 3-state toggle
### Demo-shell embedding pattern
| State | Behavior |
|---|---|
| **Auto** (default) | Stage colors follow the global theme via `--surface-2` + `--paper` tokens. Matches the rest of the report. |
| **Light** | Hardcoded light stage (`--st-bg: oklch(0.985 0.003 255)`, `--st-fg: oklch(0.180 0.006 255)`). Overrides global. |
| **Dark** | Hardcoded dark stage (inverted). Overrides global. |
The agent reads `references/demo-shell.html` and uses it as a template. For each Critical or Important finding:
Pure CSS. Radio inputs precede `.demo__bar` and `.demo__stage` so sibling-combinator selectors (`#stN-l:checked ~ .demo__stage`) work.
1. **Generate the per-finding motion code.** Read the audited code, the relevant lens reference (`emil-kowalski.md`, `jakub-krehel.md`, `jhey-tompkins.md` — matching the lens this finding lives under), and `references/motion-cookbook.md` for the concrete recipe (easing, spring config, enter/exit shape). Write a CSS keyframe block + selector rules that demonstrate the recommended motion. Use the 0% / 66% / 100% cadence with `animation-duration: 3s` (~2s motion, ~1s hold, then loop).
#### Stage tokens (used by motion targets)
2. **Inject the per-finding code into the report's `<style>` block.** Append a `@keyframes motion-{n}-...` block and a `.demo-card-{n}__motion-target { animation: ...; }` rule. The shell's CSS variables (`--bg`, `--fg`, `--border`, `--accent`, `--loop-dim`, `--sans`, `--mono`) are available — use them via `var()`. Do not redefine them.
| Token | Role |
|---|---|
| `--st-bg` | Stage background color |
| `--st-fg` | Foreground / text / "ink" color on the stage |
| `--st-line` | Border / divider color on the stage |
| `--st-dim` | Dimmed text color on the stage |
3. **Inject the motion-target element into the `.demo-card__stage`.** The element's class must be `.demo-card-{n}__motion-target` so it matches the rules from step 2.
Motion-target elements (`.ui-btn`, `.ui-card`, `.ui-row`, `.ui-check`, `.ui-num`, `.ui-label`, milestone badge, etc.) use `--st-fg` / `--st-bg` instead of `--accent` / page colors. This guarantees correct contrast even when a stage is locked to a different theme than the page.
4. **Set the demo card's header and subhead.** Header = short title for the recommended motion (e.g., "Subtle enter: opacity + translateY + blur"). Subhead = duration + easing in monospace (e.g., "300ms · ease-out"). The subhead always renders — populate it for every demo.
#### Per-finding motion code
5. **Honor the prefers-reduced-motion guard.** The shell's `@media (prefers-reduced-motion: reduce)` block disables all `[class*="__motion-target"]` animations. The per-finding `@keyframes` 100% values MUST match the motion-target element's default static rendering so the reduce-motion fallback shows the correct final visual. Do not write per-finding overrides inside the reduce-motion block.
For each Critical or Important finding `{n}`:
### Combined Recommendations tables
After the three per-lens sections, render severity-grouped tables for quick scanning:
```html
<section class="recommendations" id="critical-findings">
<h2>🔴 Critical (Must Fix)</h2>
<table>
<thead>
<tr><th>Issue</th><th>File</th><th>Action</th></tr>
</thead>
<tbody>
<tr>
<td>{issue}</td>
<td><code>{file:line}</code></td>
<td>{fix}</td>
</tr>
</tbody>
</table>
</section>
<section class="recommendations" id="important-findings">
<h2>🟡 Important (Should Fix)</h2>
<!-- same shape -->
</section>
<section class="recommendations" id="opportunity-findings">
<h2>🟢 Opportunities (Could Enhance)</h2>
<!-- same shape -->
</section>
```
The hero's severity counts link to these section IDs (`#critical-findings`, `#important-findings`, `#opportunity-findings`).
### Lens Reference Summary (closing)
```html
<section class="reference-summary">
<h2>Lens Reference Summary</h2>
<p><strong>Which lens was referenced most:</strong> {Designer} — {Perspective}</p>
<p><strong>Why:</strong> {one-line context reason}</p>
<p><strong>If you want to lean differently:</strong></p>
<ul>
<li>To follow Emil more strictly: {specific actions}</li>
<li>To follow Jakub more strictly: {specific actions}</li>
<li>To follow Jhey more strictly: {specific actions}</li>
</ul>
</section>
```
1. **Generate motion code.** Read the audited code, the relevant lens reference (`emil-kowalski.md` / `jakub-krehel.md` / `jhey-tompkins.md`), and `references/motion-cookbook.md` for the concrete recipe. Author a `@keyframes m{n}` block and a `.demo-{n}__mt { animation: m{n} 3s {easing} infinite; }` rule.
2. **Loop pacing.** `animation-duration: 3s`. Keyframes at `0%` / `~60%` / `100%`. Motion completes by ~60% (~1.8s), then holds until `100%` (~1.2s) before looping. The `100%` state MUST match the motion-target's default (no-animation) static rendering — this is the `prefers-reduced-motion` fallback contract.
3. **Inject into `<style>`.** Append the `@keyframes m{n}` + `.demo-{n}__mt` block to the report's `<style>`, after the layout CSS, inside a `@media (prefers-reduced-motion: no-preference) { ... }` guard.
4. **Inject demo-card markup.** Append the `.demo` block to the finding's `.finding-row`. Set `.demo__title` to a short motion title (e.g., "Quick tab crossfade"). Set `.demo__timing` to duration + easing (e.g., "180ms · ease-out").
5. **Honor reduced-motion.** The shell's `@media (prefers-reduced-motion: reduce)` block disables all `[class*="__mt"]` animations and hides the `↻` loop indicator. The per-finding `100%` keyframe state must match the motion-target's default static rendering. Do NOT write per-finding overrides inside the reduce-motion block.
### Empty-state behavior
When the audit produces zero Critical + zero Important findings:
- The hero still renders with the severity counts row (showing `Critical: 0 · Important: 0 · Opportunities: N`).
- Each per-lens section's `Issues to Address` block still renders its header, but the body shows a dimmed-italic line:
- Header still renders with the severity counts (showing `Critical: 0 · Important: 0 · Opportunities: N`).
- Each per-lens "Issues to address" `.lens-block` still renders its `.mono-label`, but the body shows a dimmed-italic line:
```html
<div class="perspective-section__issues" id="emil-issues">
<h3>Issues to Address</h3>
<p class="perspective-section__empty">No issues found at this severity level.</p>
</div>
<p class="lens-empty">No issues found at this severity level.</p>
```
Style: `font-style: italic; color: var(--loop-dim); padding: 12px 0;`. Communicates absence without looking broken.
- No `.finding-row` markup, no demo cards.
- Opportunities still render in text as usual.
- Opportunities still render as `.lens-list.opp` lists.
- Combined recommendations tables render with empty `<tbody>` containing a single dimmed-italic row, OR are omitted entirely if their tier has zero findings.
### Responsive behavior
The `.finding-row` two-column layout (prose left, demo right) needs a breakpoint for narrow viewports:
- `.finding-row` 2-col → 1-col at ≤860px (demo stacks below prose).
- `.timing-grid` 2-col → 1-col at ≤860px (key list stacks below SVG).
- `.lens-table` → stacked blocks at ≤600px (each row becomes a block, headers hidden).
- `.ref-lean` 2-col → 1-col at ≤600px.
```css
.finding-row {
display: grid;
grid-template-columns: 1fr 360px;
gap: 24px;
align-items: start;
margin: 24px 0;
}
### Absolute bans
@media (max-width: 768px) {
.finding-row {
grid-template-columns: 1fr;
}
.demo-card {
max-width: 100%;
}
}
```
Below 768px the demo card stacks below the finding prose. The shell's `min-width: 280px` keeps the card from compressing past usability.
### Report layout tokens
These extend the shell's tokens for report-level structure. Append to the `<style>` block after the shell's variables:
```css
body {
max-width: 960px;
margin: 0 auto;
padding: 48px 24px;
}
.report-hero h1 {
font-size: 1.75rem;
margin: 0 0 4px;
}
.report-hero__date {
font-family: var(--mono);
color: var(--loop-dim);
font-size: 0.875rem;
margin: 0 0 16px;
}
.report-hero__counts {
font-size: 1rem;
margin: 0 0 8px;
}
.report-hero__counts a {
color: inherit;
text-decoration: none;
border-bottom: 1px dashed var(--border);
}
.report-hero__counts a:hover {
border-bottom-style: solid;
}
.report-hero__primary {
font-family: var(--mono);
color: var(--loop-dim);
font-size: 0.875rem;
margin: 0;
}
.report-assessment {
margin: 32px 0;
font-size: 1.05rem;
line-height: 1.6;
}
.perspective-section {
margin: 48px 0;
padding-top: 24px;
border-top: 1px solid var(--border);
}
.perspective-section h2 {
font-size: 1.25rem;
margin: 0 0 16px;
}
.perspective-section__quote {
margin-top: 24px;
padding: 12px 16px;
background: var(--bg);
border-left: 3px solid var(--accent);
font-size: 0.95rem;
}
.recommendations {
margin: 48px 0;
}
.recommendations table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.recommendations th,
.recommendations td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
```
- **NO `border-left` or `border-right` >1px as a colored accent stripe** on cards, list items, callouts, or alerts. Use full borders, leading numbers, tinted backgrounds, or no visual indicator instead. (The previous version of this spec had `border-left: 3px solid var(--accent)` on the lens-take block — removed.)
- **NO gradient text** (`background-clip: text` + gradient).
- **NO pulsing UI** in any demo (looping scale/opacity on status indicators, "live" pulse rings, breathing CTAs). Demo motion is structured one-shot enters that loop, not attention-getting pulses.
- **NO chromatic accent in the default system.** The neutral-default is the supported configuration. Repointing `--accent` to a sampled brand color is permitted only when severity-hue clearance is verified.
---
@@ -333,148 +265,73 @@ body {
When the user passes `--terminal` / `--inline` / a natural-language equivalent, do not write an HTML file. Render the decorated-markdown report inline in the conversation.
### Quick Summary (Show First)
### Quick Summary (show first)
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 AUDIT SUMMARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 [X] Critical | 🟡 [X] Important | 🟢 [X] Opportunities
Primary perspective: [Designer(s)] ([context reason])
Primary lens: [Designer] ([context reason])
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Overall Assessment
One paragraph: Does this feel polished? Too much? Too little? What's working, what's not?
---
### Per-Designer Sections
#### Emil's Section
For each designer (Emil, Jakub, Jhey — ordered by weighting), use a horizontal-rule header and the body format:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚡ EMIL KOWALSKI — Restraint & Speed
⚡ EMIL KOWALSKI — Restraint & Speed (Secondary)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
*Weight based on context. Heavy for productivity tools, light for creative/kids apps.*
**What to Check:**
- High-frequency interactions that might not need animation
- Keyboard-initiated actions that animate (generally shouldn't)
- Durations **if this is a productivity context** (Emil prefers under 300ms)
- Animations starting from scale(0) (should be 0.9+)
- Transform-origin on dropdowns/popovers
- CSS keyframes that should be transitions (for interruptibility)
**Body format:**
**What's Working Well**
What's Working Well
- ✓ [Observation] — `file.tsx:line`
**Issues to Address**
Issues to Address
- ✗ [Issue] — `file.tsx:line`
[Brief explanation]
Recommended: [Brief recommendation]
**Through Emil's lens**: [1-2 sentence summary]
---
#### Jakub's Section
Through Emil's lens: [1–2 sentence summary]
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 JAKUB KREHEL — Production Polish
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
**What to Check:**
- Enter animations (opacity + translateY + blur?)
- Exit animations (subtler than enters? Or missing entirely?)
- **Motion gaps** — Conditional renders without AnimatePresence (from gap analysis)
- **Layout transitions** — Size/position changes that snap instead of animate
- Shadow vs border usage on varied backgrounds
- Optical alignment (buttons with icons, play buttons)
- Hover state transitions (150-200ms minimum)
- Icon swap animations (opacity + scale + blur)
- Spring usage (bounce: 0 for professional, higher for playful)
**Body format:**
**What's Working Well**
- ✓ [Observation] — `file.tsx:line`
**Issues to Address**
- ✗ [Issue] — `file.tsx:line`
[Brief explanation]
**Through Jakub's lens**: [1-2 sentence summary]
---
#### Jhey's Section
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✨ JHEY TOMPKINS — Experimentation & Delight
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
**What to Check:**
- Could @property enable smoother animations?
- Could linear() provide better easing curves?
- Are stagger effects using optimal techniques?
- Could scroll-driven animations improve the experience?
- What playful touches would enhance engagement?
- Are there celebration moments that need more delight? (streaks, achievements, etc.)
**Body format:**
**What's Working Well**
- ✓ [Observation] — `file.tsx:line`
**Opportunities**
- 💡 [Idea] — `file.tsx:line`
[Brief explanation]
**Through Jhey's lens**: [1-2 sentence summary]
---
### Combined Recommendations
**Critical (Must Fix)**
| | Issue | File | Action |
|-|-------|------|--------|
Three severity tables:
```
Critical · Must Fix
| | Issue | File | Fix |
|-|-------|------|-----|
| 🔴 | [Issue] | `file:line` | [Fix] |
**Important (Should Fix)**
| | Issue | File | Action |
|-|-------|------|--------|
Important · Should Fix
| | Issue | File | Fix |
|-|-------|------|-----|
| 🟡 | [Issue] | `file:line` | [Fix] |
**Opportunities (Could Enhance)**
Opportunities · Could Enhance
| | Enhancement | Where | Impact |
|-|-------------|-------|--------|
| 🟢 | [Enhancement] | `file:line` | [Impact] |
---
```
### Lens Reference Summary
End every terminal audit with:
> **Which lens was referenced most**: [Designer Name] — [Perspective]
```
> Lens referenced most: [Designer Name] — [Perspective]
>
> **Why**: [Explanation based on the project context]
> Why: [Explanation based on the project context]
>
> **If you want to lean differently**:
> If you want to lean differently:
> - To follow Emil more strictly: [specific actions]
> - To follow Jakub more strictly: [specific actions]
> - To follow Jhey more strictly: [specific actions]
```
---
@@ -486,4 +343,4 @@ Default to HTML mode. Trigger terminal mode only when the user explicitly signal
- Natural-language equivalent: "show the full report inline," "skip the HTML," "no HTML," "terminal only"
- Any headless or CI environment where opening a browser doesn't apply
When in doubt, render HTML and mention the terminal-mode flag in the 3-line summary (see `workflows/audit.md` STEP 3) so the user knows the alternative exists.
When defaulting to HTML, mention in the 3-line confirmation summary (see `workflows/audit.md`) that `--terminal` is the alternative — so the user knows it exists.
File diff suppressed because it is too large Load Diff
+44 -7
View File
@@ -65,8 +65,16 @@ echo ""
echo "── Updating GStack submodule..."
warn "GStack tracks branch = main (no commit hash). Review upstream commits before updating."
echo ""
printf " Proceed with GStack update? [y/N] "
read -r _gstack_confirm
# TTY guard: in a non-interactive run (cron, CI, background shell) `read`
# hits EOF and dies under set -e — the whole update aborted mid-script.
# Default to the safe N and keep going; interactive behavior unchanged.
if [ -t 0 ]; then
printf " Proceed with GStack update? [y/N] "
read -r _gstack_confirm
else
info "Non-interactive run — skipping GStack update (run in a terminal to be prompted)"
_gstack_confirm="n"
fi
if [[ "$_gstack_confirm" =~ ^[Yy]$ ]]; then
# Capture gstack state before the update so we can restore it after
# ./setup runs (setup re-creates every symlink; without this, an
@@ -117,6 +125,13 @@ fi
# ── 3. Update RTK (if pinned version available) ──
echo ""
echo "── Updating RTK..."
# cargo lives in ~/.cargo/bin, which hand-managed profiles lose (BLK-016
# class) — source cargo env, as install-plugins.sh does, before concluding
# cargo is absent. Without this the step silently never updated rtk.
if ! command -v cargo &>/dev/null && [ -f "$HOME/.cargo/env" ]; then
# shellcheck disable=SC1091
source "$HOME/.cargo/env"
fi
if command -v cargo &>/dev/null; then
RTK_VERSION=""
if [ -f "$REPO/plugins.lock.json" ] && command -v python3 &>/dev/null; then
@@ -128,23 +143,46 @@ print(d.get('rtk',{}).get('version',''))
" 2>/dev/null || true)
fi
# Version-jump guard: a cargo build takes minutes — only pay it when the
# target (pin, or the newest remote tag for "latest") differs from what is
# installed. Same pin-honored/skip-on-match shape as the semgrep step.
RTK_CUR=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
[ -z "$RTK_CUR" ] && RTK_CUR=$("$HOME/.cargo/bin/rtk" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
if [ -n "$RTK_VERSION" ] && [ "$RTK_VERSION" != "latest" ]; then
info "Pinned version: $RTK_VERSION"
if [ "${RTK_VERSION#v}" = "$RTK_CUR" ]; then
ok "rtk already at pinned $RTK_CUR"
else
info "Pinned version: $RTK_VERSION (installed: ${RTK_CUR:-none})"
info "Compiling from source — this may take a few minutes..."
if cargo install --git https://github.com/rtk-ai/rtk --tag "$RTK_VERSION" --force; then
ok "RTK updated to $RTK_VERSION"
else
warn "RTK update failed"
fi
fi
else
info "No pinned version — installing latest"
# "latest" = newest release TAG, resolved by name and installed BY TAG.
# (A bare `cargo install --git` builds the default-branch HEAD, whose
# Cargo.toml version can trail the newest tag — the guard would then
# never converge and recompile on every run.)
RTK_TIP_TAG=$(git ls-remote --tags https://github.com/rtk-ai/rtk 2>/dev/null \
| sed -n 's|.*refs/tags/\(v\{0,1\}[0-9][0-9.]*\)$|\1|p' | sort -V | tail -1 || true)
RTK_TIP="${RTK_TIP_TAG#v}"
if [ -n "$RTK_TIP" ] && [ "$RTK_TIP" = "$RTK_CUR" ]; then
ok "rtk already at latest tag ($RTK_CUR)"
else
info "No pin — latest tag: ${RTK_TIP_TAG:-unknown} (installed: ${RTK_CUR:-none})"
info "Compiling from source — this may take a few minutes..."
if cargo install --git https://github.com/rtk-ai/rtk --force; then
ok "RTK updated (latest)"
if [ -n "$RTK_TIP_TAG" ] && cargo install --git https://github.com/rtk-ai/rtk --tag "$RTK_TIP_TAG" --force; then
ok "RTK updated to $RTK_TIP_TAG"
elif [ -z "$RTK_TIP_TAG" ] && cargo install --git https://github.com/rtk-ai/rtk --force; then
ok "RTK updated (latest HEAD — no tag resolvable)"
else
warn "RTK update failed"
fi
fi
fi
else
warn "Cargo not available — skipping RTK"
fi
@@ -388,7 +426,6 @@ echo "── Updating external skills (npx skills)..."
if command -v npx &>/dev/null; then
NPX_SKILLS=(
"alchaincyf/darwin-skill"
"alchaincyf/find-skills"
)
for _src in "${NPX_SKILLS[@]}"; do
_name="${_src##*/}"