Merge develop into release/2.0.0

This commit is contained in:
bchanot
2026-10-06 16:07:22 +02:00
38 changed files with 788 additions and 244 deletions
+25 -2
View File
@@ -38,13 +38,16 @@ rules:
| 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 |
| BLK-017 | 2026-07-17 | Bing Webmaster API unusable for a multi-client agency: OAuth swamp (localhost redirect refused, rotated single-use refresh tokens race our parallel dispatch), API key = wrong model (client-owned sites) | open/deferred |
| BLK-018 | 2026-07-20 | release-executor finish span blocked by permission classifier (human signal invisible to subagent) — 2026-07-… | open |
| BLK-019 | 2026-09-01 | notify-attention bell silent, toast OK (VS Code client default) — 2026-09-01 | resolved |
| BLK-020 | 2026-09-02 | notify-attention: both channels dead on one VS Code client — 2026-09-02 | resolved |
| BLK-019 | 2026-09-01 | notify-attention bell silent, toast OK (VS Code client default) — 2026-09-01 | superseded by BLK-028 |
| BLK-020 | 2026-09-02 | notify-attention: both channels dead on one VS Code client — 2026-09-02 | superseded by BLK-028 |
| BLK-021 | 2026-09-22 | Bash tool dead mid-session ("every command exits 1"): /tmp usrquota blown by a dead session's probe HOMEs — 2… | open |
| BLK-022 | 2026-09-22 | `hooks/guard-bash.sh` withheld by the safety classifier; executable spec shipped instead — 2026-09-22 | open |
| BLK-023 | 2026-09-28 | floor-guard SKIP pattern `xit(` (Jasmine) matches any `exit(` in python/JS test helpers → false ECARTS; workaround: no `exit(` in inline python, bash derives rc from output — 2026-09-28 | resolved |
| BLK-024 | 2026-09-29 | update-all.sh re-fetched vendored skills but never re-applied the effort pins (lost until next `make plugin`); my first fix placed the re-apply BEFORE the late 21st refresh — rtk-truncated grep read as complete — 2026-09-29 | resolved |
| BLK-025 | 2026-09-30 | deny rule `Bash(npm install -g *)` bypassed unknowingly by the alias `npm i -g` (pasted user instruction ran as typed); deny patterns are literal prefixes — 2026-09-30 | resolved (partial) |
| BLK-026 | 2026-10-06 | `make test` red on macOS: 13 suites, GNU-only idioms in suite + 7 libs (SIGPIPE under pipefail, `sed -i`, `wc` padding, `stat -c`, `realpath -m`, bare `timeout`, `grep -oP`) | resolved |
| BLK-027 | 2026-10-06 | this machine never ran `make link`/`make plugin`: no global `core.hooksPath` → post-commit push never fired, branches landed ahead of upstream; 11 vendored skills + `~/.claude/.env` missing | resolved (link) / open (plugin) |
| BLK-028 | 2026-10-06 | notify-attention on a VS Code client: bell + toast silent-degradation faults (merge of BLK-019 + BLK-020): terminalBell sound default off, ext hooks only terminals born after activation, Code muted in Windows mixer | resolved |
---
@@ -282,3 +285,23 @@ rules:
- **Real cause**: deny entries are literal patterns; `i` alias and `--global` spelling do not match. No refusal fired, so nothing signalled the guardrail. Not a deliberate reroute, same effect.
- **Solution**: deny += `npm i -g *`, `npm install --global *`, `npm i --global *` (3dad33e, user go). Disclosed to the user at the design gate. Rule for me: before a global install, grep settings.json `deny` for the verb family, not the exact spelling.
- **Status**: resolved (partial) 2026-09-30 — flag-after-package forms (`npm i <pkg> -g`, `npm add -g`, `npm -g i`) still pass; pattern grammar for a mid-string wildcard unverified. Open question left to the user. [[BDR-109]]
## BLK-026 — `make test` red on macOS: GNU-only idioms — 2026-10-06
- **Friction**: release prep for 2.0.0 ran `make test`: 13 suites red, ~45 FAIL. gitflow-test 15 "merged into" FAIL while merge commit present.
- **Real cause**: suite + 7 libs written on Ubuntu. `cmd | grep -q` under `set -o pipefail`: grep exits at 1st match, producer SIGPIPE rc 141 (5/5 repro on `git log | grep -q`). Plus `sed -i` no suffix (BSD reads file as script), `wc -l` padded, `stat -c`, `touch -d`, `realpath -m` (gstack write-guard never fired), bare `timeout` off sanitized PATH (design gate UNVERIFIED), `grep -oP` (update-all `_plugins` empty), empty array under `set -u` on bash 3.2, extractor false positive in doctrine-citers. Effort pins also dropped on 2 gitignored SKILL.md (cause not established, re-applied by hand).
- **Solution**: [[BDR-110]] forms at every site, 23 files, commit 0efdff0; regression tests T4b, Alphabet/Alpha flip, profile-set-managed T18, portability-census suite.
- **Status**: resolved 2026-10-06. Open: Linux `make test` deferred (TODO); effort-pins re-red trigger (TODO).
## BLK-027 — machine never onboarded: no global hooksPath, push hook silent — 2026-10-06
- **Friction**: fix commit 0efdff0 and release/2.0.0 prep commit stayed `[ahead 1]`; nobody noticed until `git status -sb`.
- **Real cause**: `make link` never run on this Mac → `core.hooksPath` unset (local + global), `~/.claude/githooks` absent. `gitflow start` pushes explicitly so branch creation looked fine; commits rely on the post-commit hook. `make link` also reports `make plugin` never ran (11 vendored skills absent) and `~/.claude/.env` missing.
- **Solution**: pushed both branches by hand; `make link` run (user go) → `core.hooksPath=~/.claude/githooks`, 4 hooks installed. `make doctor` "Git hooks" section flags this; run it first on a new machine.
- **Status**: resolved for hooks; open: `make plugin` + `.env` on this machine (user action).
## BLK-028 — notify-attention on VS Code client: three client-side faults, one probe order (merge BLK-019 + BLK-020) — 2026-10-06
- **Friction**: hook fires, server side clean, yet bell and/or toast silent on a VS Code client over SSH. Looks like half-broken hook. Two machines, three distinct faults.
- **Real cause (3 faults, all client-side)**: (1) VS Code `accessibility.signals.terminalBell` defaults `"auto"` = sound OFF unless screen reader active (BLK-019). (2) ext `wenbopan.vscode-terminal-osc-notifier` parses only terminals created AFTER its activation: claude terminal born before install never hooked, toast dead (BLK-020 fault A). (3) Windows per-app volume mixer, Code entry at 0: toast still audible because Windows shell emits that sound, not Code → masked plain app mute (BLK-020 fault B). Not a hook bug; not dtach (dtach broadcasts to every attached client, zero session loss).
- **Solution**: client settings.json `"accessibility.signals.terminalBell": { "sound": "on" }`; install ext THEN start or re-attach claude (`dtach -a ~/.dtach/<sess>` from a fresh terminal); raise Code volume in Windows mixer (mixer lists app only after it tried playback → hit preview first). Per-client-machine, not repo-portable.
- **Probe order (do FIRST, before server archaeology)**: fresh VS Code terminal, `printf '\a\a\033]777;notify;Test;hello\033\\'` → splits terminal path from client renderer; palette `Help: List Signal Sounds` → Terminal Bell preview bypasses terminal/BEL/hook/dtach/ext, isolates renderer audio in one step.
- **Status**: resolved (BLK-019 2026-09-01, BLK-020 A+B 2026-09-02/03). Sources superseded by this entry; bodies kept for history.
- **Reference**: `~/.claude/hooks/notify-attention.sh` header documents the setting; [[LRN-145]] terminalSequence-not-/dev/tty; silent-degradation class [[LRN-047]]; sources [[BLK-019]], [[BLK-020]].
+10 -2
View File
@@ -131,6 +131,7 @@ rules:
| BDR-107 | 2026-09-28 | Effort tiering: session high, effort pins on 20 agents (BDR-077 second axis), entry level on 30 skills, five paired shifter skills, max at loop caps + ship-feature 4b | accepted |
| BDR-108 | 2026-09-29 | Effort round: level on every skill next to its model pin (3 repo + 25 vendored via `lib/effort-pins.txt` re-applied after the LAST vendoring step of install + resync), design stack ONE level (high), model pins stay tier aliases: quality/price trade-off = tier × effort, never version | accepted |
| BDR-109 | 2026-09-30 | Higgsfield pack: npm CLI `latest` + 8 upstream skills git-cloned into gitignored `skills-external/higgsfield-*`, OFF by default, in no profile; two toggles (`higgsfield` = allowlist of 7 media skills, `higgsfield-websites` = landing-page aid, never website create/deploy/publish); CLI presence by probe; routing on explicit ask | accepted |
| BDR-110 | 2026-10-06 | Shell portability doctrine: native userland on macOS AND Linux, no Homebrew GNU tools on PATH; `lib/tests/portability-census.test.sh` locks deterministic GNU-only idioms | accepted |
---
@@ -981,9 +982,9 @@ rules:
## 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**: global CLAUDE.md sits at 305 lines, 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. Old 275 target (or the 280 guard) 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.
- **Decision**: global CLAUDE.md sits at 305 lines, 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. Old 275 target (or 280 guard) now costs clarity more than tokens saved. `hooks/session-start.sh` guard threshold realigned 280→320: still catches genuine regression (bloat past 320), stops firing permanent "density pass requis" warning on 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; 15-line margin keeps real regressions visible.
- **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 append-only; supersede the target, keep the principle.
- **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 guard at 280, accept permanent warning — permanently-red non-blocking gate is noise. (c) rewrite BDR-031 — registries append-only; supersede target, keep principle.
- **Reference**: `hooks/session-start.sh:202-211`; supersedes the 275 target in [[BDR-031]] (principle kept). Review remediation A6, 2026-07-08.
## BDR-063 — GSC multi-account: OAuth2 installed-app flow + label-keyed token store
@@ -1356,3 +1357,10 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Alternatives rejected**: `npx skills add` (relinks all 8 into skills/ on every refresh, breaks off-by-default); `creative` profile (`profile apply` overwrites the active label, next `make plugin` runs exclusive `set`, parks the design stack); `+creative` profile modifier (parser + statusline + census for a label); commit pin (user: track main like 21st; security gate reports 2 MEDIUM, accepted); glob "every higgsfield-* except websites" (renamed upstream skill linked with no review).
- **Gates**: plan challenge 3 lenses + 1 confirmation, 0 BLOCKER, 7 MAJOR closed by named changes; SDD 7 tasks (sonnet), 1 fix round; GATE 0 MET; verifier ECARTS(6) (2 shellcheck suppressions of mine + 4 plan copies) → CONFORME 14/14, again CONFORME after the fix wave; security PASS ×2; final review (opus): 1 Important (drift never reported once enabled) + 5 minors fixed in one wave, 3 deferred with rulings; `make test` 45 suites rc 0.
- **Refs**: contract `.claude/tasks/contracts/2026-09-30-higgsfield-pack-1412.md`, commits 3dad33e..5350221 (feature/higgsfield-pack), [[BDR-093]], [[BDR-079]], [[BDR-108]], [[LRN-183]], [[LRN-184]], [[LRN-185]], [[LRN-186]], [[LRN-187]], [[LRN-188]], [[BLK-025]], [[EVAL-039]].
## BDR-110 — Shell portability: native userland both OS, census on deterministic idioms [accepted] (2026-10-06)
- **Decision**: every tracked `*.sh` + hook runs on BSD (macOS) and GNU userland as installed. Portable forms: producer captured out of `| grep -q` pipelines under pipefail (`grep -q PAT < <(cmd)` tests, `if grep -q PAT <<<"$(cmd 2>&1)"` prod); `sed -i.bak` tests, temp-sibling copy on user dotfiles; `wc -l | tr -d ' '`; python3 for perms; `touch -t`; `realpath -m` emulated (refuses `..`); `perl -e 'alarm shift; exec @ARGV'` for bounds; `sed -n` not `grep -P`; `/usr/bin/grep` pin ([[LRN-074]]). `lib/tests/portability-census.test.sh` greps tracked shell for `sed -i` no-suffix, `stat -c`, `realpath -m`, `touch -d`, `grep -P`, bare `/bin/grep`; file:line allowlist with reason; NO `grep -q` rule (not decidable by text, fix structural).
- **Why**: suite built on Ubuntu, repo now also lives on macOS. 13 suites red here; 7 prod scripts broken silently (update-all plugin list empty, design gate UNVERIFIED, gstack write-guard dead). Brew GNU on PATH rejected: brew dependency + two behaviours by PATH.
- **Alternatives rejected**: prepend coreutils/gnu-sed to PATH in Makefile+hooks; `grep X >/dev/null` (GNU grep treats /dev/null stdout like `-q`, race stays); broad `| grep -q` census (119 hits, fixtures, false confidence).
- **Gates**: 3 lenses (FATAL 6 / CONCERNS 4 / FATAL 2) + confirmation CONCERNS(2), all closed r3; GATE 0 MET 8/8 ×2; verifier CONFORME 9/9; security PASS (1 MEDIUM hardened). Linux run `[deferred]`.
- **Refs**: contract `.claude/tasks/contracts/2026-10-06-macos-portability-1105.md`, plan `.claude/tasks/plans/2026-10-06-macos-portability-1030.md`, commit 0efdff0 (bugfix/macos-portability), [[BLK-026]], [[LRN-189]], [[LRN-190]].
+6 -6
View File
@@ -235,9 +235,9 @@ rules:
## 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`.
- **method**: analyzers write findings to scratch; main loop does 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]]).
- **action**: keep. Remediation branch unmerged (human gate). Fil-rouge guard now prevents 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
@@ -293,10 +293,10 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
## EVAL-029 — 4-agent plan challenge: 6 BLOCKERs, and the fix round produced 3 of them
- **Date**: 2026-09-15
- **Method**: 3 blind lenses (correctness / robustness / simplicity) on plan rev 1, then 1 confirmation lens on rev 2. Subject = the gstack Playwright lib plan ([[BDR-088]]).
- **Result**: rev 1 → 3 BLOCKER + 12 MAJOR. Rev 2, written specifically to close them → 3 NEW BLOCKERs, and 2 of the 3 were INTRODUCED BY the fixes: the new "every public function returns 0" rule contradicted the new "return rc", and the printer-name clause came verbatim from my own contract criterion 9. Rev 3 dropped the recovery branch entirely at the human gate — 6 findings closed by deletion instead of code.
- **Anomaly**: my first user-facing answer asserted ~654 MB of orphan Playwright revisions. FALSE — `.links` showed every dir referenced, 0 reclaimable. Caught only while designing the guard, not while asserting the number. Worse, the guard I proposed would itself have deleted gsd-pi's rev 1243.
- **Action**: (1) never state a disk-reclaimable figure before reading the registry that owns it ([[LRN-151]]). (2) A fix round deserves the same challenge as the original plan — 3/3 confirmation BLOCKERs came from fixes, not from the original. (3) The confirmation pass earned its cost: without it the printer override would have shipped and silently disconnected doctor's counters ([[LRN-150]]).
- **Method**: 3 blind lenses (correctness / robustness / simplicity) on plan rev 1, then 1 confirmation lens on rev 2. Subject = gstack Playwright lib plan ([[BDR-088]]).
- **Result**: rev 1 → 3 BLOCKER + 12 MAJOR. Rev 2, written to close them → 3 NEW BLOCKERs, 2 of 3 INTRODUCED BY the fixes: new "every public function returns 0" rule contradicted new "return rc"; printer-name clause came verbatim from my own contract criterion 9. Rev 3 dropped recovery branch entirely at human gate — 6 findings closed by deletion instead of code.
- **Anomaly**: first user-facing answer asserted ~654 MB orphan Playwright revisions. FALSE — `.links` showed every dir referenced, 0 reclaimable. Caught only while designing the guard, not while asserting the number. Worse, proposed guard would itself have deleted gsd-pi's rev 1243.
- **Action**: (1) never state a disk-reclaimable figure before reading the registry that owns it ([[LRN-151]]). (2) A fix round deserves the same challenge as the original plan — 3/3 confirmation BLOCKERs came from fixes, not from the original. (3) Confirmation pass earned its cost: without it printer override would have shipped, silently disconnected doctor's counters ([[LRN-150]]).
- **Status**: keep.
- **Reference**: `.claude/tasks/plans/2026-09-13-gstack-playwright-lib-2220.md` (rev 3). Links [[BDR-088]], [[LRN-150]].
+5
View File
@@ -562,3 +562,8 @@ rules:
- User go "pour tout le reste tu peux merger": feature/higgsfield-pack merged into develop via `gitflow finish` → df6dbce, spec + plan purged (BDR-065), pushed (develop == origin/develop), local + origin copies removed by the lib. Open: user wants npm global installs on `ask` instead of `deny` (supply-chain caution, installs themselves fine); deny removal = hand edit by the user (hard_deny on weakening guardrails), `ask` tier abandoned under auto mode ([[BDR-090]]) → soft_deny entry proposed.
- User go "merge tout ça": chore/npm-global-soft-deny merged into develop via `gitflow finish` (soft_deny "Global npm installs" entry + CHANGELOG), pushed, copies removed. The four `permissions.deny` npm lines are still in settings.json: user's hand edit pending, they override the classifier until removed.
## 2026-10-06
- /release-candidate 2.0.0 (user: MAJOR): prep DONE on release/2.0.0 (9d421e4), suite RED → user hold. Root cause = GNU-only idioms on new macOS machine ([[BLK-026]]). /bugfix on bugfix/macos-portability: 4 challenge passes → r3, bugfixer, GATE 0 MET, verifier CONFORME 9/9, security PASS, hardening (sed_profile keeps tmp on failed write), commit 0efdff0 + CHANGELOG docs commit; [[BDR-110]] [[LRN-189]] [[LRN-190]]. Found [[BLK-027]]: no global hooksPath here, `make link` run (user go), `make plugin` + `.env` still missing. Next: reconcile, prune-memory, doc-sync, resume release at STEP 4 after merging develop into release/2.0.0.
- Reconcile 2026-10-06: TODO:15 + TODO:10 closed (npm soft_deny covers), TODO:892 re-verified open; 6 BLK external/open unchanged; BLK-018 due at the running release.
- /prune-memory 2026-10-06: A none, D none; B BLK-019+020 → BLK-028 (merge); C bounded 37 entries ≥9% filler → 37 edited, 1 untouched (LRN-075 all-negation), cuts 1-11% only (negation guard protects "X not Y" lessons); fidelity + index OK. 110 bloated entries left for a later run.
- /doc global audit 2026-10-06 (opus): 45 items, 6 docs. Applied 34 (24 AUTO + 9 HUMAN drafts + clone URL → Gitea): README components/slash/flow, Makefile help (11 profiles), USAGE /health→make doctor + GSD 3.0.0, ARCHITECTURE layout, MIGRATION retitled + "Upgrading to 2.0.0", SETTINGS package-install guard, CHANGELOG SemVer + default model + upgrade pointer → c6fb2e4. 10 deferred logged in TODO (LICENSE, Known-residual vs release, README restructure, USAGE narrative, templates/settings.json ask inert).
+94 -84
View File
@@ -208,6 +208,8 @@ rules:
| LRN-186 | 2026-09-30 | `GIT_TERMINAL_PROMPT=0` does not stop credential prompts: editor terminals export `GIT_ASKPASS`; empty `GIT_ASKPASS` short-circuits core.askPass + SSH_ASKPASS | unattended `git clone` of a repo that may vanish or go private |
| LRN-187 | 2026-09-30 | Vacuous fixtures: git drops empty dirs; a symlink to a surviving target is needed to test a symlink guard; a multi-call coreutils binary (uutils) dispatches on argv[0], so a renamed symlink fails | hermetic bash suites building git or PATH fixtures |
| LRN-188 | 2026-09-30 | Contract oracle tied to code shape (`grep -A6` line window) breaks on the first refactor while the property still holds; assert the property over the whole unit | writing CHECK oracles |
| LRN-189 | 2026-10-06 | `cmd \| grep -q` under pipefail = SIGPIPE false negative (rc 141); `>/dev/null` is NOT a fix (GNU grep treats it like -q); portable = producer out of the pipeline | any bash under `set -o pipefail`, tests and prod |
| LRN-190 | 2026-10-06 | Contract oracles: join `\\` line continuations before regex; "lint clean" = no finding beyond base, not zero; never `rm -rf "$var"` inside a CHECK | writing CHECK: lines |
---
@@ -679,20 +681,20 @@ rules:
## LRN-037 — Verify the load-bearing scenario on the REAL subject in REAL context, not a stub or a logic argument
- **Date**: 2026-06-21
- **Context**: design-gate chantier. 4 successive plausible claims each REFUTED only by running the real thing: (1) .env read path was `$REPO/.env`, not `~/.claude/.env` (read the actual script); (2) fail-open — unknown folded into silent READY (saw it in live output); (3) "alias dies in subshell = cause" (refuted: real binary on inherited PATH → `command -v` succeeds); (4) real cause = PATH carrying nvm bin (proven by `PATH=/usr/bin:/bin` run). Logic/stub never caught any. The DISCRIMINATING magic-OFF-under-stripped-PATH → exit 10 is what proved the gate truly runs `claude mcp list` vs. defaulting to READY.
- **Pattern**: for the load-bearing scenario, run it on the REAL subject in the REAL invocation context (prod path `$HOME/.claude/lib/...`, prod-like PATH), not a stub or a "the code path is correct" argument. A stub proves branch coverage; only the real subject proves the integration. Always add a DISCRIMINATING case — force the failure state; the check must REPORT it, not pass by default (a check that only ever passes proves nothing).
- **Context**: design-gate chantier. 4 successive plausible claims each REFUTED only by running the real thing: (1) .env read path was `$REPO/.env`, not `~/.claude/.env` (read the actual script); (2) fail-open — unknown folded into silent READY (saw it in live output); (3) "alias dies in subshell = cause" (refuted: real binary on inherited PATH → `command -v` succeeds); (4) real cause = PATH carrying nvm bin (proven by `PATH=/usr/bin:/bin` run). Logic/stub never caught any. DISCRIMINATING magic-OFF-under-stripped-PATH → exit 10 proved gate truly runs `claude mcp list` vs. defaulting to READY.
- **Pattern**: for the load-bearing scenario, run it on the REAL subject in the REAL invocation context (prod path `$HOME/.claude/lib/...`, prod-like PATH), not a stub or a "the code path is correct" argument. Stub proves branch coverage; only real subject proves integration. Always add a DISCRIMINATING case — force the failure state; the check must REPORT it, not pass by default (a check that only ever passes proves nothing).
- **Future application**: any "fixed/works" claim on a critical path → produce the real run output (command + lines + exit code) before capitalizing or shipping; don't summarize ("condition met") in place of the output. Stub/logic = necessary for branch coverage, never sufficient for the integration claim. Most rentable discipline of the whole segment: every refutation came from execution, none from reasoning.
- **Reference**: design-gate chantier, the `PATH=/usr/bin:/bin` matrix (magic-on → READY/0, magic-off → INCOMPLETE/10), commits 4d19135 / f963318. Linked to [[LRN-036]] (the concrete instance: the PATH cause surfaced only by the real run), [[LRN-034]] (its twin — 034 = don't trust a narrated *claim*; 037 = don't trust a *stub/logic argument* as proof; both demand execution against ground truth).
- **Reference**: design-gate chantier, `PATH=/usr/bin:/bin` matrix (magic-on → READY/0, magic-off → INCOMPLETE/10), commits 4d19135 / f963318. Linked to [[LRN-036]] (the concrete instance: the PATH cause surfaced only by the real run), [[LRN-034]] (its twin — 034 = don't trust a narrated *claim*; 037 = don't trust a *stub/logic argument* as proof; both demand execution against ground truth).
---
## LRN-038 — Playwright host-platform override for distros newer than its hardcoded support list
- **Date**: 2026-06-23
- **Context**: fresh Ubuntu 26.04. gstack `./setup` aborted: "Playwright does not support chromium on ubuntu26.04-x64". Playwright 1.58.2's registry hardcodes `ubuntu20.04/22.04/24.04` only; a newer release → no matching build → hard error. gstack is a pinned submodule (must not edit).
- **Pattern**: `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntuXX.04-<arch>` forces a fallback build. MUST include arch (`x64`/`arm64`) — bare `ubuntu24.04` fails ("does not support … ubuntu24.04"). Set from the WRAPPER: `export` before the submodule's setup (install-time download) AND persist to the shell profile (runtime launch) — both paths call `getHostPlatform`. No submodule edit. Gate on real OS version (`sort -V`) → supported distros untouched. Test with the LOCAL `./node_modules/.bin/playwright` — `bunx playwright` pulls the LATEST playwright (different browser revision than the local import), masks the result.
- **Future application**: pinned tool hardcoding an OS allowlist breaks on a fresh OS upgrade. Look for a host-platform override env before bumping/forking the dep. Prove the fallback binary actually runs (`ldd` = no missing libs + a real headless render), not just that the download resolves.
- **Pattern**: `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntuXX.04-<arch>` forces fallback build. MUST include arch (`x64`/`arm64`) — bare `ubuntu24.04` fails ("does not support … ubuntu24.04"). Set from WRAPPER: `export` before submodule's setup (install-time download) AND persist to shell profile (runtime launch) — both paths call `getHostPlatform`. No submodule edit. Gate on real OS version (`sort -V`) → supported distros untouched. Test with LOCAL `./node_modules/.bin/playwright` — `bunx playwright` pulls LATEST playwright (different browser revision than local import), masks result.
- **Future application**: pinned tool hardcoding OS allowlist breaks on fresh OS upgrade. Look for host-platform override env before bumping/forking dep. Prove the fallback binary actually runs (`ldd` = no missing libs + a real headless render), not just that the download resolves.
- **Reference**: `install-plugins.sh` `playwright_platform_override()`, commit 211c7d4. Linked to [[BLK-008]].
- **2026-06-23 CORRECTION (override REVERTED, commit b9c3937)**: the override is NOT a usable fix on Ubuntu 26.04. It makes `playwright install` switch to the ubuntu24.04 fallback build, which downloads to 100% then HANGS at extraction (chrome binary never materializes; real machine + sandbox). 0.5s fast-fail → install-blocking hang. Isolated proof (`ldd` + headless render) PASSED on an already-extracted sibling build (rev 1228) — masked the install-path hang in the real flow (rev 1208). **Sharpened lesson**: proving the binary launches in isolation is NOT proving the install path works — run the ACTUAL install command end-to-end (it must COMPLETE, not just "download resolves" nor "a binary launches"). Override technique stays valid in general; the EXTRACTION/COMPLETE step is part of "does it work".
- **2026-06-23 CORRECTION (override REVERTED, commit b9c3937)**: the override is NOT a usable fix on Ubuntu 26.04. It makes `playwright install` switch to the ubuntu24.04 fallback build, which downloads to 100% then HANGS at extraction (chrome binary never materializes; real machine + sandbox). 0.5s fast-fail → install-blocking hang. Isolated proof (`ldd` + headless render) PASSED on already-extracted sibling build (rev 1228) — masked install-path hang in real flow (rev 1208). **Sharpened lesson**: proving the binary launches in isolation is NOT proving the install path works — run the ACTUAL install command end-to-end (it must COMPLETE, not just "download resolves" nor "a binary launches"). Override technique stays valid in general; EXTRACTION/COMPLETE step is part of "does it work".
---
@@ -709,8 +711,8 @@ rules:
## LRN-040 — OS newer than a pinned tool supports = TWO distinct layers (version build + security policy)
- **Date**: 2026-06-23
- **Context**: gstack browser on fresh Ubuntu 26.04. Layer 1 = Playwright 1.58.2 ships no browser build for 26.04 → install errors (the host-platform override "fixes" the error but its fallback build HANGS at extraction — dead end, [[BLK-008]]). Layer 2 = even with Playwright 1.61 (native 26.04 build that launches fine in isolation), the real browse path aborts "No usable sandbox" because Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
- **Pattern**: (a) bump the tool PAST the OS-support threshold — don't force the OS to look older (overrides/fallbacks are fragile; prove the install COMPLETES, not just that a binary launches). Pinned submodule dep: `bun add X@latest` in the submodule, automatable in the installer, idempotent via grep of the dep's support list for the running OS tag before bumping. (b) SEPARATELY handle OS security hardening: Chromium needs `--no-sandbox` where `sysctl kernel.apparmor_restrict_unprivileged_userns=1`; gstack exposes `GSTACK_CHROMIUM_NO_SANDBOX=1` (#1562). Gate persistence on the sysctl, not an OS-version guess.
- **Future application**: "tool X broke after an OS upgrade" → check BOTH (1) does X ship a build / support entry for the new OS (bump if not), and (2) does the new OS's hardening (userns/AppArmor/SELinux) block X at runtime (needs an opt-out flag). Fix one without the other → still fails. Verify the FULL runtime path (drive a real page) — isolated `chromium.launch()` PASSED while the real `browse` path failed on the sandbox.
- **Pattern**: (a) bump the tool PAST the OS-support threshold — don't force the OS to look older (overrides/fallbacks are fragile; prove the install COMPLETES, not just that a binary launches). Pinned submodule dep: `bun add X@latest` in submodule, automatable in installer, idempotent via grep of dep's support list for running OS tag before bumping. (b) SEPARATELY handle OS security hardening: Chromium needs `--no-sandbox` where `sysctl kernel.apparmor_restrict_unprivileged_userns=1`; gstack exposes `GSTACK_CHROMIUM_NO_SANDBOX=1` (#1562). Gate persistence on the sysctl, not an OS-version guess.
- **Future application**: "tool X broke after an OS upgrade" → check BOTH (1) does X ship a build / support entry for the new OS (bump if not), and (2) does the new OS's hardening (userns/AppArmor/SELinux) block X at runtime (needs an opt-out flag). Fix one without the other → still fails. Verify FULL runtime path (drive real page) — isolated `chromium.launch()` PASSED while real `browse` path failed on sandbox.
- **Reference**: `install-plugins.sh`, `.bashrc` `GSTACK_CHROMIUM_NO_SANDBOX=1`, gstack `browse/src/browser-manager.ts` `shouldEnableChromiumSandbox()`, commit 3b8ffb1. Linked to [[BDR-029]], [[BLK-008]], [[LRN-038]].
---
@@ -718,8 +720,8 @@ rules:
## LRN-041 — A check reading a symlink an EARLIER install step makes → false negative if that step's precondition wasn't met
- **Date**: 2026-06-23
- **Context**: install warned "MAGIC_API_KEY not found in ~/.claude/.env" though the key WAS set there. Root: the check grep'd `$REPO/.env` — a symlink → `~/.claude/.env` ([[BDR-026]]) created by `link.sh`'s `link_env`. On a fresh machine `~/.claude/.env` is created AFTER `link.sh` runs (install first warns "create it"), so the symlink was never made and the key was unreachable via `$REPO/.env`. `make plugin` also never runs `link.sh`. The warning misleadingly blamed `~/.claude/.env`.
- **Pattern**: a check that reads a path PRODUCED by an earlier setup step silently fails when that step's precondition wasn't met yet (target absent → symlink skipped). Fix: read the CANONICAL source and/or self-heal (create the missing symlink when the canonical exists). Env-key greps must tolerate `export `/leading whitespace and require a non-empty value: `^[[:space:]]*(export[[:space:]]+)?KEY=.` — and the message must name the real gap (symlink missing vs key absent), with an actionable hint (`run make link`).
- **Context**: install warned "MAGIC_API_KEY not found in ~/.claude/.env" though the key WAS set there. Root: check grep'd `$REPO/.env` — symlink → `~/.claude/.env` ([[BDR-026]]) created by `link.sh`'s `link_env`. On a fresh machine `~/.claude/.env` is created AFTER `link.sh` runs (install first warns "create it"), so the symlink was never made and the key was unreachable via `$REPO/.env`. `make plugin` also never runs `link.sh`. Warning misleadingly blamed `~/.claude/.env`.
- **Pattern**: a check that reads a path PRODUCED by an earlier setup step silently fails when that step's precondition wasn't met yet (target absent → symlink skipped). Fix: read CANONICAL source and/or self-heal (create missing symlink when canonical exists). Env-key greps must tolerate `export `/leading whitespace and require non-empty value: `^[[:space:]]*(export[[:space:]]+)?KEY=.` — and message must name real gap (symlink missing vs key absent), with actionable hint (`run make link`).
- **Future application**: any "X not found in FILE" where FILE is a symlink/derived path → verify the producing step ran with its precondition, prefer the canonical source, self-heal or give an actionable message. Sandbox note: `.env*` reads were blocked — diagnosed via directory listing + regex tests on SYNTHETIC lines, never reading the secret.
- **Reference**: `install-plugins.sh` magic check (self-heal symlink + tolerant regex), `link.sh` `link_env`, commit 1b028cb. Linked to [[BDR-026]].
@@ -758,9 +760,9 @@ rules:
## LRN-045 — Renaming a command: audit exact-name leak-guard / forbidden-token regexes
- **Date**: 2026-06-25
- **Context**: rename `/validate` → `/web-validate`. A client-deliverable leak-guard in `agents/client-handover-writer.md:1462` greps generated docs for internal tool names via `grep -niE '/(seo|harden|validate|cso|...)\b'`. The `web-` prefix means `/web-validate` no longer matches the `/validate` branch (the `/` must sit immediately before `validate`; post-rename a `-` sits there) → renamed command leaks SILENTLY into client-facing output. No error — the gate just stops catching it.
- **Pattern**: any rename of a command/skill/identifier must sweep regexes/allowlists/denylists that match the OLD name by exact token — leak guards, forbidden-token gates, routing dispatchers, CI greps. A prefix/suffix rename breaks anchored matches (`/oldname\b`) with zero error. Fix = alternation covering BOTH names (`web-validate|validate`), NOT replacement — old artifacts (already-shipped client docs, logs) still carry the legacy name and must stay caught.
- **Future application**: when renaming, grep the BARE old token inside regex/test/gate files, not just `/oldname` command refs. A blind `replace_all '/old' '/new'` MISSES these because the guard stores the name inside an alternation (`|old|`), not as `/old`. For each guard found, extend to `new|old`; verify the gate line shows both names.
- **Context**: rename `/validate` → `/web-validate`. Client-deliverable leak-guard in `agents/client-handover-writer.md:1462` greps generated docs for internal tool names via `grep -niE '/(seo|harden|validate|cso|...)\b'`. The `web-` prefix means `/web-validate` no longer matches the `/validate` branch (the `/` must sit immediately before `validate`; post-rename a `-` sits there) → renamed command leaks SILENTLY into client-facing output. No error — the gate just stops catching it.
- **Pattern**: any rename of command/skill/identifier must sweep regexes/allowlists/denylists matching OLD name by exact token — leak guards, forbidden-token gates, routing dispatchers, CI greps. Prefix/suffix rename breaks anchored matches (`/oldname\b`) with zero error. Fix = alternation covering BOTH names (`web-validate|validate`), NOT replacement — old artifacts (already-shipped client docs, logs) still carry the legacy name and must stay caught.
- **Future application**: when renaming, grep the BARE old token inside regex/test/gate files, not just `/oldname` command refs. A blind `replace_all '/old' '/new'` MISSES these because the guard stores the name inside an alternation (`|old|`), not as `/old`. For each guard found, extend to `new|old`; verify gate line shows both names.
- **Reference**: `agents/client-handover-writer.md:1462`, rename commit `e5e673a`. Linked to [[BDR-032]].
## LRN-046 — Destructive skill: deterministic oracle > semantic judge
@@ -838,8 +840,8 @@ rules:
## LRN-055 — Body `## ID —` headings are a drift-immune index; the maintained `## Index` table is not
- **Date**: 2026-06-26
- **Pattern**: When a registry keeps both per-entry `## ID — title` headings AND a hand-maintained `## Index` table, the Index DRIFTS (entries land in the body, the manual update lapses) while headings cannot (an entry IS its heading — 100% coverage by construction). Measured: decisions 11/34 (32%), learnings 21/52 (40%), blockers 2/9 (22%) missing from the Index — scattered in large blocks (e.g. decisions BDR-024–033 unindexed while the newer BDR-034 is), not an old/new split. Manual Index-update step unreliable. Key any selector/scan off `grep '^## <PREFIX>-'`, never the convenience Index. Backfill (prune-memory passe D) = human-TOC hygiene, NOT a selector dependency.
- **Context**: analyze-before-plan ([[BDR-035]]) two-pass. First instinct "reuse the Index capitalize maintains"; measuring the drift killed it — convenient artifact unreliable, guaranteed one (headings) free.
- **Future application**: choosing a substrate to index/select over: prefer what the STRUCTURE guarantees over what a step PROMISES to maintain. Verify maintained-artifact completeness before depending on it.
- **Context**: analyze-before-plan ([[BDR-035]]) two-pass. First instinct "reuse the Index capitalize maintains"; measuring drift killed it — convenient artifact unreliable, guaranteed one (headings) free.
- **Future application**: choosing substrate to index/select over: prefer what STRUCTURE guarantees over what a step PROMISES to maintain. Verify maintained-artifact completeness before depending on it.
- **Reference**: `lib/analyze-before-plan.md` (PASS 1). `skills/prune-memory` passe D. See [[BDR-035]].
## LRN-056 — `grep PAT dir/*.md` on an absent dir ERRORS (exit 2), it does not no-op → guard with `[ -d ]`
@@ -853,8 +855,8 @@ rules:
## LRN-057 — Match the consumption mechanism to the consumer (mechanical / external-cognitive / inline-cognitive)
- **Date**: 2026-06-26
- **Pattern**: When a produced artifact must be CONSUMED downstream, the mechanism depends on the consumer: (a) MECHANICAL (git merge integrating a branch) — production on the shared substrate = consumption, automatic ([[BDR-034]]'s "commit before FINISH"); (b) EXTERNAL-COGNITIVE (an unmodifiable skill like `superpowers:brainstorming`) — "produced before" ≠ "consumed"; INJECT the artifact into the consumer's INPUT at the invocation boundary (orchestrator = adapter) + a RECONCILIATION gate that EXPOSES the disposition for review (not auto-detect); (c) INLINE-COGNITIVE (same agent reads then plans) — reader=planner, same context → natural consumption, just force the trace ([[LRN-053]]). Don't import (b)'s machinery where (c) suffices, nor assume (a)'s automatism when the consumer is cognitive.
- **Context**: analyze-before-plan ([[BDR-035]]). ship-feature brainstorm = external-cognitive → STEP 0d injection + STEP 3 expose-for-review gate; feat/bugfix = inline-cognitive → natural + trace, no injection. Asymmetry vs [[BDR-034]] (mechanical merge) = the chantier's hardest point.
- **Future application**: wiring ANY produce→consume invariant: classify the consumer first (mechanical / external-cognitive / inline-cognitive), pick the lightest sufficient mechanism. Stops reflexive import of orchestrator-grade injection+gate where an inline trace would do.
- **Context**: analyze-before-plan ([[BDR-035]]). ship-feature brainstorm = external-cognitive → STEP 0d injection + STEP 3 expose-for-review gate; feat/bugfix = inline-cognitive → natural + trace, no injection. Asymmetry vs [[BDR-034]] (mechanical merge) = chantier's hardest point.
- **Future application**: wiring ANY produce→consume invariant: classify consumer first (mechanical / external-cognitive / inline-cognitive), pick lightest sufficient mechanism. Stops reflexive import of orchestrator-grade injection+gate where inline trace would do.
- **Reference**: `skills/ship-feature/SKILL.md` STEP 0d/1/2/3, `agents/bugfixer.md`+`feater.md`. Contrast [[BDR-034]] (mechanical). See [[BDR-035]], [[LRN-053]].
## LRN-058 — Same bug-class ≠ same fix: verify the twin shares the fix's PRECONDITION before replicating
@@ -876,15 +878,15 @@ rules:
## LRN-060 — A fail-closed guard is proven by what it REFUSES (loudly); pass dynamic lists as argv, not a separator-string
- **Date**: 2026-06-27
- **Pattern**: Two robustness lessons from doc-commit. (a) The inverse-`.claude/` exclusion is a SECURITY guard (BDR-022) → test it by what it must REFUSE (forbidden path ALONE, and MIXED with legit), not only what it accepts; and refuse LOUDLY (dedicated exit 4, names the offender, refuse-ALL on mixed) — silent-filtering would MASK an upstream violation (doc-syncer surfaced a `.claude/` it must never patch). The refusal IS the alarm. (b) Pass a dynamic file list as ARGV, never a separator-joined string: argv has no in-band delimiter → a path with spaces survives as one element (proven, T7); newline is only the producer's text format the agent maps to argv. Space-join-then-resplit would mis-split + the `[ -e ]` filter then silently drops it.
- **Pattern**: Two robustness lessons from doc-commit. (a) The inverse-`.claude/` exclusion is a SECURITY guard (BDR-022) → test it by what it must REFUSE (forbidden path ALONE, and MIXED with legit), not only what it accepts; and refuse LOUDLY (dedicated exit 4, names the offender, refuse-ALL on mixed) — silent-filtering would MASK an upstream violation (doc-syncer surfaced a `.claude/` it must never patch). Refusal IS the alarm. (b) Pass a dynamic file list as ARGV, never a separator-joined string: argv has no in-band delimiter → a path with spaces survives as one element (proven, T7); newline is only the producer's text format the agent maps to argv. Space-join-then-resplit would mis-split + `[ -e ]` filter then silently drops it.
- **Context**: doc-commit.sh ([[BDR-036]]), T1a/b/c (refuse paths) + T7 (argv space-safe), all real-exec.
- **Future application**: any automated scoped-commit / destructive guard — test the REFUSAL path + refuse loud; pass lists as argv. Same family as [[LRN-046]] (deterministic oracle for a destructive guard).
- **Future application**: any automated scoped-commit / destructive guard — test REFUSAL path + refuse loud; pass lists as argv. Same family as [[LRN-046]] (deterministic oracle for destructive guard).
- **Reference**: [[BDR-036]], [[LRN-051]] (changed-paths filter), [[LRN-046]].
## LRN-061 — Runtime net proposed for an unwired skill → check the wiring first
- **Date**: 2026-06-27
- **Pattern**: Tempted to build a runtime guard/hook/monitor that watches for a bad OUTCOME (memory written but uncommitted)? First ask if the outcome is a MISSING WIRING, not a behavioral lapse. A per-turn Stop-hook was proposed to catch "dirty memory" — but the cause was `/capitalize`+`/close` not calling the commit include (they predate it). Fix for an unwired skill = WIRE it (deterministic, zero-noise, at source); a monitor over a wiring hole pays RECURRING cost for a ONE-TIME omission; a frequent ignored nag is itself a risk ([[LRN-047]]). **NOT "runtime nets are bad"** — the split is by DETERMINISM: a MISSING WIRING is deterministic → repair structurally; a genuinely NON-DETERMINISTIC aléa → a runtime net IS the right tool. Good counter-example: [[BDR-033]] anim-lib nudge — "will the user want motion?" is unknowable statically → a stateless 1-line suggestion is correct. Same determinism test as [[LRN-046]]/[[LRN-049]], applied to the build-or-not question.
- **Context**: deferred "v2 capitalize hook" ([[BDR-037]]). Read-phase killed it before code: git proved skills predate the include (oubli), memory committed by hand 35×, orphans self-heal via `commit_memory`. Hook would've been disabled within an hour (frequent ignored nag).
- **Pattern**: Tempted to build runtime guard/hook/monitor watching for bad OUTCOME (memory written but uncommitted)? First ask if the outcome is a MISSING WIRING, not a behavioral lapse. A per-turn Stop-hook was proposed to catch "dirty memory" — but the cause was `/capitalize`+`/close` not calling the commit include (they predate it). Fix for unwired skill = WIRE it (deterministic, zero-noise, at source); monitor over a wiring hole pays RECURRING cost for ONE-TIME omission; frequent ignored nag is itself a risk ([[LRN-047]]). **NOT "runtime nets are bad"** — the split is by DETERMINISM: a MISSING WIRING is deterministic → repair structurally; a genuinely NON-DETERMINISTIC aléa → a runtime net IS the right tool. Good counter-example: [[BDR-033]] anim-lib nudge — "will the user want motion?" is unknowable statically → stateless 1-line suggestion is correct. Same determinism test as [[LRN-046]]/[[LRN-049]], applied to the build-or-not question.
- **Context**: deferred "v2 capitalize hook" ([[BDR-037]]). Read-phase killed it before code: git proved skills predate include (oubli), memory committed by hand 35×, orphans self-heal via `commit_memory`. Hook would've been disabled within an hour (frequent ignored nag).
- **Future application**: any "build a hook/watcher/lint to catch when X isn't done" — first grep whether X is even WIRED at its source. Deterministic/structural gap (missing include/call) → fix structurally; reserve runtime nets for non-deterministic lapses, never to complete a rollout. Classify by determinism BEFORE building.
- **Reference**: [[BDR-037]], [[BDR-034]] (rollout this completes), [[BDR-033]] (the GOOD net — contrast). Conditions [[LRN-047]], [[LRN-049]], [[LRN-054]].
@@ -912,14 +914,14 @@ rules:
- **future application**: any helper relying on `git status --porcelain` to detect changes — add a `git check-ignore` guard; a path that must persist but is ignored has to fail loud, not no-op.
## LRN-067 — a pipeline that looks 2-level can finish at the SAME level; a human-mediated step masks the collision until automated
- **pattern**: an orchestrator delegating to a sub-skill can LOOK two-level (sub assembles, orchestrator integrates) yet the sub's TERMINAL node operates at the SAME level as the orchestrator's finish → double-integration. `subagent-driven-development` assembles tasks on ONE branch (no per-task sub-branches — true) BUT its last flowchart node IS `finishing-a-development-branch` = feature→base merge, the SAME act as the orchestrator's FINISH. init-project (STEP 8 SDD + STEP 11 finish) AND ship-feature (STEP 4 SDD + STEP 9 finish) BOTH invoked finish TWICE. Latent, not visibly broken: SDD's terminal finish is INTERACTIVE (menu → human picks "keep as-is"), so the human SILENTLY de-duplicated. Collision SURFACES when the orchestrator's finish becomes DETERMINISTIC (gitflow finish) → real double-merge. Fix = scope the sub-skill by instruction to stop before its terminal step (NO fork — the finish is a flowchart node the controller follows, not a script; verified by reading SDD's scripts). Pressure-test: RED agent chained the finish ("literal next node in the flowchart"); GREEN with the scope instruction stopped + returned.
- **pattern**: orchestrator delegating to a sub-skill can LOOK two-level (sub assembles, orchestrator integrates) yet sub's TERMINAL node operates at SAME level as orchestrator's finish → double-integration. `subagent-driven-development` assembles tasks on ONE branch (no per-task sub-branches — true) BUT its last flowchart node IS `finishing-a-development-branch` = feature→base merge, the SAME act as the orchestrator's FINISH. init-project (STEP 8 SDD + STEP 11 finish) AND ship-feature (STEP 4 SDD + STEP 9 finish) BOTH invoked finish TWICE. Latent, not visibly broken: SDD's terminal finish is INTERACTIVE (menu → human picks "keep as-is"), so the human SILENTLY de-duplicated. Collision SURFACES when orchestrator's finish becomes DETERMINISTIC (gitflow finish) → real double-merge. Fix = scope the sub-skill by instruction to stop before its terminal step (NO fork — the finish is a flowchart node the controller follows, not a script; verified by reading SDD's scripts). Pressure-test: RED agent chained finish ("literal next node in the flowchart"); GREEN with scope instruction stopped + returned.
- **context**: gitflow chantier, wiring orchestrators onto `gitflow finish`. Mapping (premise #6) caught it by READING the real (SDD `SKILL.md` + `scripts/`) BEFORE coding — seam-bug class `deploy` hit, caught earlier this time. Two human-gate backstops survive a missed instruction: SDD's interactive menu + the `gitflow finish` human gate ([[LRN-054]] — no oracle; deterministic layer carries the dangerous case).
- **future application**: before replacing an interactive/human-mediated step with a deterministic one, check whether a delegated sub-skill's TERMINAL step operates at the same level — the human gate may have silently de-duplicated a double-action. Read the sub-skill's real flow (nodes + scripts), don't assume "distinct levels".
- **future application**: before replacing interactive/human-mediated step with deterministic one, check whether delegated sub-skill's TERMINAL step operates at same level — human gate may have silently de-duplicated a double-action. Read the sub-skill's real flow (nodes + scripts), don't assume "distinct levels".
## LRN-068 — enforcement-bootstrap must be transactional: activate the guard LAST and gate it on the bootstrap commit succeeding
- **pattern**: a routine that BOTH installs an enforcement guard (pre-commit hook, branch protection, lock) AND makes a bootstrap commit must be transactional, else a partial run strands it. Two teeth: (a) precheck preconditions (git identity, clean tree) and fail LOUD before ANY mutation; (b) the guard-activation step must NOT run if the guarded bootstrap commit failed — order activation LAST and gate it on commit success. A `cmd_a || cmd_b` form SWALLOWS cmd_b's failure when a later stmt returns 0 → the failure never propagates; use explicit `if ! …; then … || return 1; fi`.
- **context**: `gitflow_init` ([[BLK-012]]). Existing-repo path swallowed the socle-commit failure (`git diff --cached --quiet || git commit`, then `git branch develop` returned 0 masking it) → init CONTINUED and ran `gitflow_activate_hook` though the socle was never committed → every re-run self-blocked (commit on main blocked by the hook just installed). Fresh-repo path already propagated → the asymmetry was the bug. Fix: fatal socle commit + identity precheck; verified on an identity-less repo → aborts rc1 with ZERO mutation, 57/57 tests green.
- **future application**: any init/bootstrap installing enforcement (hooks, protection, immutability) + committing — activate LAST, gate on the commit, precheck identity/clean-tree up front, make every link propagate (no `||` swallow). TEST the partial-failure path (identity-less / commit-blocked repo) → must abort with zero mutation and stay re-runnable.
- **pattern**: routine that BOTH installs enforcement guard (pre-commit hook, branch protection, lock) AND makes bootstrap commit must be transactional, else partial run strands it. Two teeth: (a) precheck preconditions (git identity, clean tree) and fail LOUD before ANY mutation; (b) the guard-activation step must NOT run if the guarded bootstrap commit failed — order activation LAST and gate it on commit success. A `cmd_a || cmd_b` form SWALLOWS cmd_b's failure when a later stmt returns 0 → the failure never propagates; use explicit `if ! …; then … || return 1; fi`.
- **context**: `gitflow_init` ([[BLK-012]]). Existing-repo path swallowed the socle-commit failure (`git diff --cached --quiet || git commit`, then `git branch develop` returned 0 masking it) → init CONTINUED and ran `gitflow_activate_hook` though the socle was never committed → every re-run self-blocked (commit on main blocked by the hook just installed). Fresh-repo path already propagated → asymmetry was the bug. Fix: fatal socle commit + identity precheck; verified on identity-less repo → aborts rc1 with ZERO mutation, 57/57 tests green.
- **future application**: any init/bootstrap installing enforcement (hooks, protection, immutability) + committing — activate LAST, gate on the commit, precheck identity/clean-tree up front, make every link propagate (no `||` swallow). TEST partial-failure path (identity-less / commit-blocked repo) → must abort with zero mutation and stay re-runnable.
## LRN-069 — token-authed remote writes under CC perms: inline-env (never `export`), token in the header, keep `git push` on ASK as the real gate
- **pattern**: a secrets-guard `Bash(export *)` in `permissions.deny` auto-denies ANY command whose FIRST token is `export …` — a false positive (`export GIT_CONFIG_VALUE_0="Authorization: token $TOK" …` reads as blocked when only the `export` prefix tripped it, not the git/curl op). Correct model for token-authed remote writes from tool calls: (a) INLINE env assignment `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraHeader GIT_CONFIG_VALUE_0="Authorization: token $TOK" git push …` (no `export` keyword → passes; token rides the http header via git env-config, NEVER in argv nor written to the clone's `.git/config`); (b) keep `Bash(git push *)` on ASK (not deny) — that prompt IS the per-write human gate; don't suppress it, don't allow-list pushes in settings.
@@ -927,8 +929,8 @@ rules:
- **future application**: scripting token-authed git/curl writes under CC perms → inline env (never `export`), token in `Authorization` header (curl `-H`, git `GIT_CONFIG_*` extraHeader), keep `git push` on ASK as the approval. Tool-call denied unexpectedly → read `permissions.deny` for an over-broad prefix rule (`export *`, `env`, `printenv`) catching a false positive BEFORE concluding the op itself is blocked.
## LRN-070 — clean-tree-gated migration + a dirty submodule: diagnose pointer-vs-content, ignore=dirty not blind reset
- **pattern**: an op gated on a clean tree (`git status --porcelain`) is blocked by a submodule showing ` M`. FIRST distinguish: (a) **pointer move** — gitlink (HEAD) ≠ submodule HEAD → resettable via `git submodule update`/`checkout`; (b) **dirty content** — gitlink UNCHANGED, files modified INSIDE the submodule → a local edit. For an intentional local edit, `checkout --`/`submodule update` correctly REFUSE to discard it, and a blind "reset" would DESTROY it. Exclude it non-destructively: `git config submodule.<name>.ignore dirty` (local `.git/config`) → status stops reporting the submodule's dirty content, gate passes, edit preserved. Commit it to `.gitmodules` to share the ignore across clones.
- **context**: claude gitflow self-migration. `skills-external/gstack` showed ` M`; gitlink `070722a` == submodule HEAD `070722a` (NOT a pointer move), 2 tracked-modified files (`bun.lock`+`package.json`) = the [[BLK-008]] Playwright 1.61 bump (Ubuntu 26.04 browser). The planned "reset" (D2) would have discarded the browser fix; `submodule.skills-external/gstack.ignore=dirty` cleared the tree for `migrate_local`, bump intact.
- **pattern**: op gated on clean tree (`git status --porcelain`) blocked by submodule showing ` M`. FIRST distinguish: (a) **pointer move** — gitlink (HEAD) ≠ submodule HEAD → resettable via `git submodule update`/`checkout`; (b) **dirty content** — gitlink UNCHANGED, files modified INSIDE submodule → local edit. For intentional local edit, `checkout --`/`submodule update` correctly REFUSE to discard it, and blind "reset" would DESTROY it. Exclude it non-destructively: `git config submodule.<name>.ignore dirty` (local `.git/config`) → status stops reporting submodule's dirty content, gate passes, edit preserved. Commit to `.gitmodules` to share ignore across clones.
- **context**: claude gitflow self-migration. `skills-external/gstack` showed ` M`; gitlink `070722a` == submodule HEAD `070722a` (NOT a pointer move), 2 tracked-modified files (`bun.lock`+`package.json`) = the [[BLK-008]] Playwright 1.61 bump (Ubuntu 26.04 browser). Planned "reset" (D2) would have discarded browser fix; `submodule.skills-external/gstack.ignore=dirty` cleared tree for `migrate_local`, bump intact.
- **future application**: any clean-tree-gated op (migrate/release/bisect) on a superproject with a submodule carrying intentional local edits → diagnose pointer-vs-content FIRST (compare gitlink to submodule HEAD); for content, `submodule.<name>.ignore=dirty`, never a blind reset. Cross-ref [[BLK-008]] (gstack -dirty by design).
## LRN-071 — fail-loud must cover the helper's OWN commit, not just its inputs — 3rd occurrence of the swallowed-commit pattern
@@ -938,9 +940,9 @@ rules:
- **future application**: any helper whose RETURN VALUE gates a downstream "success" — audit that EVERY fallible internal op propagates its failure, ESPECIALLY the load-bearing commit. `set -uo pipefail` without `-e` does NOT abort mid-function; an unchecked failing command followed by a returning-0 line exits 0 and lies. Check `cmd || other` forms, no-`-e` blocks, every "report success after the op" line. Test the partial-failure path (commit-blocked repo) → must fail loud, empty, non-zero.
## LRN-072 — a stranded-artifact bug can be fixed by NOT creating the artifact (negative diff), not by plumbing its commit
- **pattern**: 3rd member of the post-FINISH-artifact class (memory, docs, GSD ROADMAP) — but UNLIKE the first two (real artifacts ALWAYS produced → couple a commit), the GSD artifact came from a SPECULATIVE, opt-in, rarely-used producer (init-project auto-bootstrapping a multi-session engine at project creation). Reflex fix (reorder + build `gsd-commit.sh` + tests) = machinery to faithfully commit an artifact nobody uses. The right fix was a NEGATIVE diff: delete the producer → orphan never created → bug dissolves, zero new code (BLK-011).
- **the refutation that got there**: framing "ROADMAP redundant with TODO" WRONG (gsd ≫ roadmap = state machine/crash-recovery/cost/parallel/worktree; TODO ≠ gsd ROADMAP = different altitude + consumer). Reading REFUTED both premises, yet the CONCLUSION (remove the step) held for a STRONGER reason: speculatively scaffolding a heavy engine the sole user doesn't use, at creation, is bad per se. Right answer, reason corrected before engraving — change the QUESTION before changing the code.
- **future application**: stranded / duplicated / uncommitted-artifact bug → BEFORE building machinery for the artifact, ask whether the step that PRODUCES it is used / wanted / non-speculative. Speculative or unused (esp. personal/single-user repo) → DELETE the producer; cleanest fix = the absent one. Distinguish speculative-at-creation (REMOVE) from deliberate-on-demand (KEEP). Family: [[BLK-010]], [[BLK-011]], [[BDR-036]].
- **pattern**: 3rd member of post-FINISH-artifact class (memory, docs, GSD ROADMAP) — but UNLIKE first two (real artifacts ALWAYS produced → couple a commit), GSD artifact came from SPECULATIVE, opt-in, rarely-used producer (init-project auto-bootstrapping multi-session engine at project creation). Reflex fix (reorder + build `gsd-commit.sh` + tests) = machinery to faithfully commit artifact nobody uses. The right fix was a NEGATIVE diff: delete the producer → orphan never created → bug dissolves, zero new code (BLK-011).
- **the refutation that got there**: framing "ROADMAP redundant with TODO" WRONG (gsd ≫ roadmap = state machine/crash-recovery/cost/parallel/worktree; TODO ≠ gsd ROADMAP = different altitude + consumer). Reading REFUTED both premises, yet the CONCLUSION (remove the step) held for a STRONGER reason: speculatively scaffolding a heavy engine the sole user doesn't use, at creation, is bad per se. Right answer, reason corrected before engraving — change QUESTION before changing code.
- **future application**: stranded / duplicated / uncommitted-artifact bug → BEFORE building machinery for artifact, ask whether step that PRODUCES it is used / wanted / non-speculative. Speculative or unused (esp. personal/single-user repo) → DELETE producer; cleanest fix = absent one. Distinguish speculative-at-creation (REMOVE) from deliberate-on-demand (KEEP). Family: [[BLK-010]], [[BLK-011]], [[BDR-036]].
## LRN-073 — a skill's worked-example must use FICTIONAL ids, never live registry ids (they prime real-data behavior)
- **pattern**: prune-memory's STEP-2 plan example named real LRN-014 + LRN-016 ("merge these"). A real-data run merged exactly that pair — though they're COMPLEMENTARY (header-ids vs checkbox-CSS), a merge its own rule forbids. Example ids that match live entries, in context at audit time, PRIME the action: you can't tell "judged correctly" from "pattern-matched its own example".
@@ -966,15 +968,15 @@ rules:
## LRN-077 — test fixtures must carry NEUTRAL names (pass for the right reason)
- **Date**: 2026-06-30
- **pattern**: a baseline agent on a worktree named `wt-pre-reconcile` read "pre-reconcile" FROM THE DIR NAME and inferred staleness — reasoning for the WRONG reason (the name), not the right one (verify git). Fixtures + the GREEN test were re-frozen under NEUTRAL names so the engine reaches truth by querying git, never by reading a path hint.
- **meta — same symptom, distinct cause as [[LRN-074]]**: 074 = COMMAND-ASSUMPTION (ugrep parsed `-9..` → false green); 077 = LEAKY FIXTURE (name telegraphs the answer). Different mechanisms, SAME symptom: test passes/fails for the wrong reason. Cross-cutting lesson = verify a test passes for the RIGHT reason, not merely that it passes — whether the false signal comes from an assumed command (074) or a leaky fixture (077).
- **meta — same symptom, distinct cause as [[LRN-074]]**: 074 = COMMAND-ASSUMPTION (ugrep parsed `-9..` → false green); 077 = LEAKY FIXTURE (name telegraphs answer). Different mechanisms, SAME symptom: test passes/fails for wrong reason. Cross-cutting lesson = verify a test passes for the RIGHT reason, not merely that it passes — whether the false signal comes from an assumed command (074) or a leaky fixture (077).
- **future application**: name fixtures/paths neutrally; for any green, ask "did it pass because the subject did the work, or because something leaked the answer?"
- **corroboration 2026-07-02 (T6c)**: 3rd family member — test truth borrowed from TRANSIENT env state. run-reconcile T6c asserted `$MEM/../skills/darwin-skill` = `.claude/skills/` (the [[LRN-042]] parasite dir), not canonical `skills/`; born green because the parasite still existed, red since the same-day cleanup, unnoticed until the 2026-07-02 audit re-ran the suite ([[EVAL-011]]'s "20/20" silently 19/1 for 2 days). Oracles target CANONICAL paths (never derived `X/../Y`); re-run suites after ANY env cleanup tests may have silently depended on; "green at build" ≠ "green now".
## LRN-078 — semver number DERIVES from the change nature; "breaking" = requires a migration
- **Date**: 2026-06-30
- **pattern**: framing a release as "it's 4.0.0 → find the breaking changes to justify it" is backwards; the number FOLLOWS the nature of the changes. The real question = "is there a breaking change?", not "how do I justify the target". Solo / mono-user repo, no public API ⇒ "breaking" = casse mon propre usage / EXIGE une migration de ma part.
- **pattern**: framing release as "it's 4.0.0 → find the breaking changes to justify it" is backwards; number FOLLOWS nature of changes. The real question = "is there a breaking change?", not "how do I justify the target". Solo / mono-user repo, no public API ⇒ "breaking" = casse mon propre usage / EXIGE une migration de ma part.
- **applied (v4.0.0)**: gitflow universal = TRUE breaking workflow change (master→main, mandatory branches, hook, 6-repo migration) → MAJOR on its own. caveman removal = VERIFIED nothing invoked it (grep: only the kept memory format-rule + frozen fixtures, settings/hooks clean) → a clean `### Removed` (capability gone, nothing breaks, no migration), NOT breaking. The MAJOR rests on gitflow alone; don't mislabel a removal as breaking.
- **future application**: pick MAJOR/MINOR/PATCH from the changes; the lineage gives the digits. Verify "does X actually break / require migration?" from the refs (grep), not from the size of the change or the desire for a round number.
- **future application**: pick MAJOR/MINOR/PATCH from changes; lineage gives digits. Verify "does X actually break / require migration?" from the refs (grep), not from the size of the change or the desire for a round number.
## LRN-079 — orchestrator-skill TDD: replay the flow on a throwaway repo, RED = flow minus the new step
- **Date**: 2026-06-30
@@ -983,10 +985,10 @@ rules:
## LRN-080 — measure whether the model already does X before adding an instruction to make it do X
- **Date**: 2026-06-30
- **pattern**: the --help chantier (implement [[BDR-001]] as a global CLAUDE.md instruction "on --help → render help + stop") was KILLED by its behavioral RED. Before writing a line, measured the control (6 reps, `/web-validate` + `/harden`, no instruction): **6/6 already rendered rich help AND stopped without dispatching** — the supposedly-absent behavior was fully present. Residual value = format consistency across 6 divergent shapes → not worth ~5 lines in a compressed CLAUDE.md on a solo repo. A phantom-value addition avoided.
- **why it matters**: [[LRN-075]] (test the UNGUIDED control) paying off one chantier later — measuring the RED before building is what caught it. For UNIVERSAL conventions the model already honors (--help, common flags, standard shapes), a "teach it to do X" instruction buys nothing but tokens; the only thing left to buy is consistency, which must clear its own ROI bar.
- **future application**: before adding any global instruction to ELICIT a behavior, run the behavioral control first — does the model already do it unaided? If yes, the only remaining value is standardization; price it honestly vs the cost (esp. a compressed CLAUDE.md). Often: don't add it.
- **corroboration 2026-06-30**: 3 consecutive "make the model do X" chantiers — --help ([[BDR-001]]), darwin re-baseline ([[BDR-043]]/[[LRN-082]]), auto-skill-dispatch ([[BDR-044]]) — ALL measured won't-build/moot. A backlog of "add instruction to elicit behavior Y" has a high phantom-value rate (universal conventions + aggressive existing mandates like superpowers L1 already elicit Y) → sweep such backlogs measure-first, expect kills.
- **pattern**: --help chantier (implement [[BDR-001]] as global CLAUDE.md instruction "on --help → render help + stop") KILLED by its behavioral RED. Before writing a line, measured the control (6 reps, `/web-validate` + `/harden`, no instruction): **6/6 already rendered rich help AND stopped without dispatching** — the supposedly-absent behavior was fully present. Residual value = format consistency across 6 divergent shapes → not worth ~5 lines in a compressed CLAUDE.md on a solo repo. Phantom-value addition avoided.
- **why it matters**: [[LRN-075]] (test the UNGUIDED control) paying off one chantier later — measuring RED before building caught it. For UNIVERSAL conventions the model already honors (--help, common flags, standard shapes), a "teach it to do X" instruction buys nothing but tokens; the only thing left to buy is consistency, which must clear its own ROI bar.
- **future application**: before adding any global instruction to ELICIT a behavior, run behavioral control first — does model already do it unaided? If yes, only remaining value is standardization; price it honestly vs cost (esp. compressed CLAUDE.md). Often: don't add it.
- **corroboration 2026-06-30**: 3 consecutive "make the model do X" chantiers — --help ([[BDR-001]]), darwin re-baseline ([[BDR-043]]/[[LRN-082]]), auto-skill-dispatch ([[BDR-044]]) — ALL measured won't-build/moot. Backlog of "add instruction to elicit behavior Y" has high phantom-value rate (universal conventions + aggressive existing mandates like superpowers L1 already elicit Y) → sweep such backlogs measure-first, expect kills.
## LRN-081 — Commit trailers: Claude-COMPOSED content only, never on staging of user-authored text
- **Date**: 2026-06-30
@@ -1005,16 +1007,16 @@ rules:
## LRN-083 — Subagents are an INVALID instrument for measuring MAIN-LOOP spontaneous routing
- **Date**: 2026-06-30
- **pattern**: measuring whether the MAIN loop self-invokes a skill on implicit intent: dispatched subagents are non-discriminating — SUBAGENT-STOP tells them to SKIP the L1 routing mandate, delegated-execute framing suppresses meta-routing → they hand-do the task regardless of main-loop prose strength. Result pins to the no-route FLOOR (artifact, not signal). Complement of [[LRN-028]] (there subagents OVER-saw installed skills, invalidating a no-skill baseline; here they UNDER-route, invalidating a routing-measurement) — both = subagent ≠ main-loop condition.
- **pattern**: measuring whether MAIN loop self-invokes a skill on implicit intent: dispatched subagents are non-discriminating — SUBAGENT-STOP tells them to SKIP L1 routing mandate, delegated-execute framing suppresses meta-routing → they hand-do the task regardless of main-loop prose strength. Result pins to the no-route FLOOR (artifact, not signal). Complement of [[LRN-028]] (there subagents OVER-saw installed skills, invalidating a no-skill baseline; here they UNDER-route, invalidating a routing-measurement) — both = subagent ≠ main-loop condition.
- **why it matters**: a 0/N subagent RED reads as "under-triggers → build the chantier" but is the [[LRN-028]] trap — the instrument can't tell strong prose from weak. Concluding from it = pass/fail for the WRONG reason ([[LRN-074]]/[[LRN-077]]).
- **context**: 2026-06-30 auto-skill-dispatch RED. 6 subagents on toy implicit-intent tasks → 0/6 routed → RETIRED as non-discriminating, NOT reported as a number. Reframed; measured in REAL fresh main-loop sessions.
- **future application**: measure main-loop spontaneous routing/discernment in FRESH main-loop sessions (full L0–L4, no SUBAGENT-STOP, real user-turn). Observable instrument = the HUMAN typing the prompts + watching live — cron/schedule-spawned fresh sessions are the right CONDITION but UNOBSERVABLE to the orchestrator (they notify the owner, not the dispatcher), so they can't be the measurement vehicle. Never substitute a subagent for a fresh session in a routing RED. See [[LRN-028]], [[LRN-075]], [[LRN-080]].
## LRN-084 — A protection hook enforces PROD safety, not the full branch-flow — the exemption masked the rule-vs-guard divergence
- **Date**: 2026-07-01
- **pattern**: the gitflow pre-commit hook is a PROTECTION guard (block code on main/develop), NOT a flow enforcer. It exempts `.claude/**` and can only test "on a protected base" — it can NEVER verify "branched FROM develop" (no base knowledge). "Every change via a branch from develop" is only HALF-encoded by the hook; the base half lives upstream in `gitflow_start`. The exemption is scoped to the SIDE-CAR ([[BDR-034]]); it has no branch to follow when memory IS the work → standalone memory fell back to `main`.
- **why it matters**: multi-repo raccord committed 5 `chore(memory)` direct on `main`, NOTHING flagged it — nothing violated, exemption worked as designed. Divergence = guard (declares PROD protection) vs intended rule (all via branch); exemption MASKED it, raccord revealed it by violating the unencoded half. A guard encoding only PART of the intent reads as full enforcement — a false-green.
- **future application**: when a guard exempts a class or checks one predicate, ask what it does NOT encode and whether a human leans on it for MORE than it enforces. Enforce the unencoded half where it actually lives (the aiguillage at skill start, [[BDR-045]]), do not push it into a guard that structurally can't hold it. Verify the guard's real scope against the rule's full scope before trusting "it would have caught it." See [[BDR-034]], [[BDR-045]], [[LRN-034]].
- **pattern**: the gitflow pre-commit hook is a PROTECTION guard (block code on main/develop), NOT a flow enforcer. It exempts `.claude/**` and can only test "on a protected base" — it can NEVER verify "branched FROM develop" (no base knowledge). "Every change via a branch from develop" is only HALF-encoded by hook; base half lives upstream in `gitflow_start`. The exemption is scoped to the SIDE-CAR ([[BDR-034]]); it has no branch to follow when memory IS the work → standalone memory fell back to `main`.
- **why it matters**: multi-repo raccord committed 5 `chore(memory)` direct on `main`, NOTHING flagged it — nothing violated, exemption worked as designed. Divergence = guard (declares PROD protection) vs intended rule (all via branch); exemption MASKED it, raccord revealed it by violating unencoded half. Guard encoding only PART of intent reads as full enforcement — false-green.
- **future application**: when a guard exempts a class or checks one predicate, ask what it does NOT encode and whether a human leans on it for MORE than it enforces. Enforce the unencoded half where it actually lives (the aiguillage at skill start, [[BDR-045]]), do not push it into a guard that structurally can't hold it. Verify guard's real scope against rule's full scope before trusting "it would have caught it." See [[BDR-034]], [[BDR-045]], [[LRN-034]].
---
@@ -1055,9 +1057,9 @@ rules:
## LRN-089 — a pass-through wrapper whose callee reads ambient state silently ignores its args
- **Date**: 2026-07-03
- **pattern**: a CLI/dispatcher that forwards `"$@"` to a function which derives its TARGET from ambient state (HEAD, cwd, env, "current X") rather than from those args → the args are silently dropped. The call SITE looks parameterized (`finish bugfix audit-bugs`) but the callee acts on whatever state it's standing in → wrong-target action, NO error. `gitflow_finish` read `HEAD`, never `$1/$2`; `finish bugfix X` from another branch merged that other branch.
- **context**: audit 2026-07-02, `lib/gitflow.sh:257` `finish) gitflow_finish "$@"` passed args the function never consulted. Surfaced when a finish "for" one branch merged another (LOT3). [[BLK-015]].
- **future application**: any wrapper/dispatcher forwarding args to a callee that resolves its target from ambient state — either (a) make the callee USE the args as the target, or (b) if the ambient-state contract is deliberate, treat passed args as an ASSERTION and refuse loudly when they disagree with the state. Never let forwarded args be silently dropped: silent-drop = the caller believes they steered, the callee ignored them. Sibling of "presence-flag ≠ capability" [[LRN-087]] — both = a visible signal lying about the real behavior.
- **pattern**: CLI/dispatcher forwarding `"$@"` to a function deriving its TARGET from ambient state (HEAD, cwd, env, "current X") rather than from those args → args silently dropped. The call SITE looks parameterized (`finish bugfix audit-bugs`) but the callee acts on whatever state it's standing in → wrong-target action, NO error. `gitflow_finish` read `HEAD`, never `$1/$2`; `finish bugfix X` from another branch merged that other branch.
- **context**: audit 2026-07-02, `lib/gitflow.sh:257` `finish) gitflow_finish "$@"` passed args the function never consulted. Surfaced when finish "for" one branch merged another (LOT3). [[BLK-015]].
- **future application**: any wrapper/dispatcher forwarding args to a callee resolving its target from ambient state — either (a) make callee USE args as target, or (b) if ambient-state contract is deliberate, treat passed args as ASSERTION and refuse loudly when they disagree with state. Never let forwarded args be silently dropped: silent-drop = the caller believes they steered, the callee ignored them. Sibling of "presence-flag ≠ capability" [[LRN-087]] — both = visible signal lying about real behavior.
- **Reference**: `lib/gitflow.sh` gitflow_finish arg-guard, `lib/gitflow-test.sh` T12. [[BLK-015]].
## LRN-090 — external-repo audit: open WIRED subsystems before declarative
@@ -1097,10 +1099,10 @@ rules:
- **cousin**: [[BDR-050]] the pipeline; [[BDR-049]] fresh verifier; conditions [[LRN-083]].
## LRN-096 — A backstop is code: prove it can FAIL (flip-test) before trusting its green
- **pattern**: deterministic guard replacing a forgettable advisory is itself code; UNPROVEN guard = vacuous guard — [[LRN-048]] (a pass must prove it looked) applied to guards. LRN-093 backstop (refuse `\n` in grep/tf patterns) shipped with a regex requiring whitespace before `tf` → silently MISSED `tf` at line start (where the real locks sit). Flip-test (feed the guard a KNOWN offender, assert it bites) caught the hole; without it the guard would have green-lit the very class it was built to kill. So: a flip-test is MANDATORY at guard creation, part of the guard, not optional QA.
- **why it matters**: the whole point of a backstop is that it fires on the bad case; a guard that can't fail proves nothing and is WORSE than the advisory it replaced (false confidence). Advisory→backstop move ([[LRN-047]] [[LRN-091]]) is sound only if the backstop is verified against a real miss.
- **context**: lot 5 `lib/tests/no-vacuous-locks.test.sh` 2026-07-04. Built the guard, flip-test RED'd (regex too weak, missed line-start `tf`), fixed the regex, flip-test green. Guard ships WITH the flip-test inline, self-proves on every run.
- **future application**: building any guard/lint/census/backstop — bundle a flip-test (a synthetic offender the guard must catch) in the same file; a guard whose failure path was never exercised is untrusted. Corroborates [[LRN-047]]/[[LRN-091]] (advisory→deterministic): the *quality bar* on the deterministic replacement.
- **pattern**: deterministic guard replacing forgettable advisory is itself code; UNPROVEN guard = vacuous guard — [[LRN-048]] (a pass must prove it looked) applied to guards. LRN-093 backstop (refuse `\n` in grep/tf patterns) shipped with regex requiring whitespace before `tf` → silently MISSED `tf` at line start (where real locks sit). Flip-test (feed guard a KNOWN offender, assert it bites) caught the hole; without it guard would have green-lit the very class it was built to kill. So: a flip-test is MANDATORY at guard creation, part of the guard, not optional QA.
- **why it matters**: the whole point of a backstop is that it fires on the bad case; a guard that can't fail proves nothing and is WORSE than the advisory it replaced (false confidence). Advisory→backstop move ([[LRN-047]] [[LRN-091]]) sound only if backstop verified against a real miss.
- **context**: lot 5 `lib/tests/no-vacuous-locks.test.sh` 2026-07-04. Built guard, flip-test RED'd (regex too weak, missed line-start `tf`), fixed regex, flip-test green. Guard ships WITH flip-test inline, self-proves on every run.
- **future application**: building any guard/lint/census/backstop — bundle a flip-test (a synthetic offender the guard must catch) in the same file; a guard whose failure path was never exercised is untrusted. Corroborates [[LRN-047]]/[[LRN-091]] (advisory→deterministic): the *quality bar* on deterministic replacement.
- **cousin**: [[LRN-048]] prove it looked; [[LRN-093]] the class this guards; [[LRN-046]] deterministic-oracle discipline.
## LRN-097 — Community blog pattern ≠ official feature: verify against docs before building infra
@@ -1144,8 +1146,8 @@ rules:
- **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, then called AskUserQuestion. The user saw ONLY the question UI — the checklist never reached them ("là on a rien, je dois ouvrir le fichier"). Harness reliably renders only the LAST text of a turn; text before a tool call can be swallowed by the tool UI.
- **why**: conversational deliverable (commands to copy-paste, a report) fails silently if any tool call follows the print — user sees "nothing displayed" while the transcript contains it. Structural fix: the deliverable IS the turn's final text; collect answers BEFORE printing, or let the reply arrive as the next user message.
- **pattern**: /deploy hand-back printed full checklist, then called AskUserQuestion. The user saw ONLY the question UI — the checklist never reached them ("là on a rien, je dois ouvrir le fichier"). Harness reliably renders only LAST text of a turn; text before a tool call can be swallowed by tool UI.
- **why**: conversational deliverable (commands to copy-paste, a report) fails silently if any tool call follows the print — user sees "nothing displayed" while the transcript contains it. Structural fix: deliverable IS the turn's final text; collect answers BEFORE printing, or let reply arrive as next user message.
- **context**: 2026-07-05 /deploy run 2 (bchanot-cv). Skill patched same turn: checklist display-only (no NEXT.sh file at all — user: throwaway once deployed) + hand-back ends the turn, no tool call after.
- **future application**: designing any skill/flow output meant to be read+used from the conversation — put it LAST; never sandwich a deliverable between tool calls; prefer plain-text report requests over blocking question tools after a deliverable.
- **cousin**: [[LRN-100]] same skill lineage; CLAUDE.md communication doctrine (final message carries everything).
@@ -1154,8 +1156,8 @@ rules:
- **pattern**: job3 docs-drift audit dispatched an exploration subagent (Bash + Read/Grep, "audit BODIES — do NOT modify any file") to check graphify skill docs. It ran `graphify .` to check CLI behavior — a real build, not a read — leaving an empty `graphify-out/` dir at repo root. The prompt said "read-only" and "verify via Read/Grep/Bash (read-only)" but never named the specific command class to avoid; the agent treated "run the CLI to see what it does" as within a Bash read-only mandate.
- **why**: "read-only" is a framing about FILES, not an instruction the model maps onto every tool call by default — a subagent with Bash access will happily execute a program to observe its behavior, which is investigative but not read-only if the program writes to disk. The fix only landed after a main-session correction mid-run ("do NOT run graphify... verify by reading the installed source instead"), not from the original prompt.
- **context**: 2026-07-06, job3 audit exploration phase (`.audit/job3-report.md` A1/A2 findings, incident noted in the report header). No tracked file was touched; the stray dir was harmless but wasted a round-trip and could have mutated git-visible state on a less-guarded command.
- **future application**: any subagent dispatch framed as "read-only" / "audit" / "verify" that grants Bash — explicitly ban execution of the subject-under-test's own CLI/build/generator commands, and name the safe alternative (read installed source, grep docs) in the same sentence. Don't rely on the word "read-only" alone to scope tool use.
- **context**: 2026-07-06, job3 audit exploration phase (`.audit/job3-report.md` A1/A2 findings, incident noted in report header). No tracked file was touched; the stray dir was harmless but wasted a round-trip and could have mutated git-visible state on a less-guarded command.
- **future application**: any subagent dispatch framed "read-only" / "audit" / "verify" that grants Bash — explicitly ban execution of subject-under-test's own CLI/build/generator commands, and name safe alternative (read installed source, grep docs) in same sentence. Don't rely on the word "read-only" alone to scope tool use.
- **cousin**: [[LRN-100]] (tool must clean its own scratch) — same class of "prose framing ≠ enforced constraint", different failure mode.
## LRN-103 — BLK-009 was stale: re-probe confirms `paths:` frontmatter works at BOTH levels now
@@ -1207,7 +1209,7 @@ rules:
- **cousin**: [[BDR-058]] (this job's fix), darwin-skill's OVERSCOPED git-commit finding (job8 report — 3rd-party code, not patched, accepted risk under human-checkpoint gating, twin of [[LRN-105]]'s no-execute mandate for OUR read-only audits).
## LRN-110 — magic MCP `component_builder`'s local callback server = unauthenticated prompt-injection channel
- **context**: job8 audit read `dist/utils/callback-server.js:36` (+ `create-ui.js:35-38`) in the installed `@21st-dev/magic` package. `21st_magic_component_builder` opens a plain HTTP server on `127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin check, staying open up to 10 minutes per call. Any POST body to `/data` is injected VERBATIM into the tool result the model consumes — any local process or open browser tab on the machine can win the race against the legitimate browser hand-back.
- **context**: job8 audit read `dist/utils/callback-server.js:36` (+ `create-ui.js:35-38`) in installed `@21st-dev/magic` package. `21st_magic_component_builder` opens a plain HTTP server on `127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin check, staying open up to 10 minutes per call. Any POST body to `/data` injected VERBATIM into tool result the model consumes — any local process or open browser tab on the machine can win the race against legitimate browser hand-back.
- **future application**: this is in the third-party package's code, not our config — don't try to patch a vendored/npx-installed dependency. The only real lever is on OUR side of the boundary: never allowlist a tool with this shape, keep it `ask`-gated so a human sees every invocation (see [[BDR-059]]). Applies to any MCP tool whose implementation opens a listener to receive async results, not just this one — check the listener's auth/origin scoping when auditing MCP server code, the tool's *description* text tells you nothing about it.
- **cousin**: [[BDR-059]] (the settings fix), [[LRN-111]] (why the allowlist stays empty), job8 report §2 surface 1 finding A#0. Magic MCP retired 2026-09-22 ([[BDR-093]]).
@@ -1224,16 +1226,16 @@ rules:
- **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).
- **pattern**: fix one cited instance of banned pattern, leave 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.
- **fix**: every pattern-fix ends with (1) whole-surface grep proving zero residue, (2) 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). Check that would have caught A1/A4/A5/A2 at make-test time instead of review.
- **future application**: any "fix pattern X" task → grep agents/ lib/ hooks/ templates/ skills/, add/extend 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`.
- **pattern**: job7 added gitleaks scan to `_gitflow_emit_pre_commit` (the GENERATOR), but 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 installed hook was stale.
- **fix**: after editing any template-generated artifact, regenerate installed copy (`gitflow.sh install-hook`) AND add 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).
@@ -1278,9 +1280,9 @@ rules:
- **cousin**: [[LRN-119]] (same GSC+CrUX build); SDD skill's own "never HEAD~1" warning (same base-selection bug class).
## LRN-121 — Shell allowlist validation: `grep -Eq` is fragile; use a whole-string POSIX `case`
- **pattern**: guarding a user-supplied label to shell-safe ASCII with `printf '%s' "$v" | grep -Eq '^[A-Za-z0-9._-]+$'` failed 3 adversarial gate passes: (1) command-injection framing (label interpolated into an agent-composed Bash line); (2) parser differential — the guard pre-scanned argv for the literal `--label` while the downstream `argparse` ALSO accepts `--label=v` and abbreviations (`--labe`, `allow_abbrev=True`), those forms reached the parser unchecked; (3) `grep -q` matches PER LINE, a label with an embedded newline (`ok\nrm -rf`) passes on its FIRST line. Fix = replace the whole mechanism, don't patch again: `_label_safe() ( LC_ALL=C; case "$1" in ''|[!A-Za-z0-9]*|*[!A-Za-z0-9._-]*) exit 1;; esac )` — POSIX `case`, whole-string, C-locale subshell. No grep (no per-line), no regex, no second grammar to differ from; a newline is just a non-allowed byte caught by `*[!...]*`; `LC_ALL=C` stops UTF-8 collation widening `[A-Za-z0-9]` to homoglyphs (U+FF11, Kelvin U+212A).
- **pattern**: guarding user-supplied label to shell-safe ASCII with `printf '%s' "$v" | grep -Eq '^[A-Za-z0-9._-]+$'` failed 3 adversarial gate passes: (1) command-injection framing (label interpolated into agent-composed Bash line); (2) parser differential — guard pre-scanned argv for literal `--label` while downstream `argparse` ALSO accepts `--label=v` and abbreviations (`--labe`, `allow_abbrev=True`), those forms reached parser unchecked; (3) `grep -q` matches PER LINE, label with embedded newline (`ok\nrm -rf`) passes on its FIRST line. Fix = replace the whole mechanism, don't patch again: `_label_safe() ( LC_ALL=C; case "$1" in ''|[!A-Za-z0-9]*|*[!A-Za-z0-9._-]*) exit 1;; esac )` — POSIX `case`, whole-string, C-locale subshell. No grep (no per-line), no regex, no second grammar to differ from; a newline is just a non-allowed byte caught by `*[!...]*`; `LC_ALL=C` stops UTF-8 collation widening `[A-Za-z0-9]` to homoglyphs (U+FF11, Kelvin U+212A).
- **why it matters**: three distinct bypasses of the SAME guard = the approach was wrong, not each patch. `grep`'s line-orientation + locale-sensitive ranges, plus argv-prescan-vs-real-parser grammar drift, are the three classic ways an allowlist "passes" a string it shouldn't. Whole-string `case` in C locale closes all three at once. These were defense-in-depth (downstream used `"$2"`/`"$@"`/JSON-key, never `sh -c`/`eval` → not exploitable in the real exec chain) — but the backstop still took a categorical rewrite, and 3 security-gate BLOCKs to get there.
- **future application**: validate shell input WHOLE-STRING (`case` or bash `[[ =~ ]]`), never `grep -q` (per-line). Set `LC_ALL=C` for byte-wise ranges. Argv pre-scan guard must be STRICTER than the downstream parser (reject `=`-joined/abbrev) or validate post-parse against the value the parser settled on. When a fix is bypassed twice → STOP patching, replace the mechanism (re-plan, not whack-a-mole).
- **future application**: validate shell input WHOLE-STRING (`case` or bash `[[ =~ ]]`), never `grep -q` (per-line). Set `LC_ALL=C` for byte-wise ranges. Argv pre-scan guard must be STRICTER than downstream parser (reject `=`-joined/abbrev) or validate post-parse against the value parser settled on. When a fix is bypassed twice → STOP patching, replace the mechanism (re-plan, not whack-a-mole).
- **cousin**: [[LRN-119]] (fail-open engine this hardens), [[BDR-063]] (token store whose labels these guard), [[LRN-045]] (renaming-command leak-guard regexes — same charset-guard family).
---
@@ -1318,9 +1320,9 @@ rules:
- **cousin**: [[BDR-066]] (model routing: reflection/audit big, execution sonnet), [[LRN-113]] (consumer-staleness sweep on a pattern fix).
## LRN-126 — splitting a monolith agent severs every IMPLICIT data path; forward each consumed field through the handoff contract
- **pattern**: wave-4 split (client-handover-writer monolith → reflection parent + sonnet doc-writer child) silently dropped 2 inputs the extracted STEPs consumed. `DEPLOY_HINTS` (detected in parent STEP 2, consumed by child STEP 14) + `--skip-seo` flag (parsed from `$ARGUMENTS`, gated child STEP 13) worked in the monolith by shared scope; after the split they were dead — never added to the PACKAGE. Child rendered a §8 without platform tailoring; `--skip-seo` became a silent no-op. Caught only by the opus whole-branch review, not the census.
- **why**: monolith: `$ARGUMENTS`, detected vars, STEP-N side-outputs share one scope, later STEPs read them free. Split turns each free read into a data path that MUST cross the parent→child contract explicitly; every implicit read is a severed wire unless forwarded.
- **future application**: splitting an agent: enumerate EVERY field the child reads (grep child for `PACKAGE.`, bare var names, `$ARGUMENTS` flags), diff against what the parent SETS before dispatch. Any child-consumed field the parent never populates = severed path = renders a hole or a silent no-op. A census that checks shape (model pin, gate-free) will NOT catch this — needs a data-flow read.
- **pattern**: wave-4 split (client-handover-writer monolith → reflection parent + sonnet doc-writer child) silently dropped 2 inputs extracted STEPs consumed. `DEPLOY_HINTS` (detected in parent STEP 2, consumed by child STEP 14) + `--skip-seo` flag (parsed from `$ARGUMENTS`, gated child STEP 13) worked in the monolith by shared scope; after the split they were dead — never added to the PACKAGE. Child rendered a §8 without platform tailoring; `--skip-seo` became a silent no-op. Caught only by the opus whole-branch review, not the census.
- **why**: monolith: `$ARGUMENTS`, detected vars, STEP-N side-outputs share one scope, later STEPs read them free. Split turns each free read into data path that MUST cross parent→child contract explicitly; every implicit read is a severed wire unless forwarded.
- **future application**: splitting an agent: enumerate EVERY field child reads (grep child for `PACKAGE.`, bare var names, `$ARGUMENTS` flags), diff against what parent SETS before dispatch. Any child-consumed field the parent never populates = severed path = renders a hole or a silent no-op. A census that checks shape (model pin, gate-free) will NOT catch this — needs a data-flow read.
- **cousin**: [[LRN-125]] (route consumer to right tier on a split), [[BDR-066]] (reflection/execution split), [[LRN-113]] (sweep ALL consumers). Distinct: 113/125 = WHICH agent/tier a consumer routes to; this = WHICH fields must cross the contract.
## LRN-127 — SDD implementers must not run destructive git ops on files outside their task scope
@@ -1331,9 +1333,9 @@ rules:
## LRN-128 — a version RESET (backward bump) is editorial reflection, not the forward-only release-executor
- **pattern**: first public release cut as v1.0.0 from an internal 4.x lineage = backward version.txt (4.0.0→1.0.0) + CHANGELOG restructure (new public `[1.0.0]` on top, old 1.0-4.0 lineage under a `## Pre-release (internal history)` banner) + tag swap (delete v4.0.0, tag v1.0.0). The sonnet `release-executor` (release-candidate skill's mechanical prep span) assumes a FORWARD semver bump — its prep = `[Unreleased]`→`[X.Y.Z]` move + version increment. Cannot derive a backward reset, the CHANGELOG restructure, or the existing-`[1.0.0]`-collision handling.
- **pattern**: first public release cut as v1.0.0 from internal 4.x lineage = backward version.txt (4.0.0→1.0.0) + CHANGELOG restructure (new public `[1.0.0]` on top, old 1.0-4.0 lineage under `## Pre-release (internal history)` banner) + tag swap (delete v4.0.0, tag v1.0.0). Sonnet `release-executor` (release-candidate skill's mechanical prep span) assumes FORWARD semver bump — its prep = `[Unreleased]`→`[X.Y.Z]` move + version increment. Cannot derive a backward reset, the CHANGELOG restructure, or the existing-`[1.0.0]`-collision handling.
- **why**: a reset is a JUDGMENT act (what's public vs pre-release, how to frame the launch, what to do with the old lineage) = reflection tier, not the executor's mechanical forward move.
- **future application**: version RESET or any non-standard release → do PREP MANUALLY inline (big model), use `gitflow.sh` only for branch mechanics (start/finish), KEEP the skill's human gates (when-to-release, push). Don't dispatch the forward-only executor for it. [[BDR-067]] [[BDR-066]]
- **future application**: version RESET or any non-standard release → do PREP MANUALLY inline (big model), use `gitflow.sh` only for branch mechanics (start/finish), KEEP skill's human gates (when-to-release, push). Don't dispatch the forward-only executor for it. [[BDR-067]] [[BDR-066]]
## LRN-129 — `git cherry` (patch-id) proves a stale/divergent branch has nothing orphaned before you delete it
@@ -1352,9 +1354,9 @@ rules:
- **Applied**: [[BDR-069]].
## LRN-131 — WebSearch is not verification for a number; require a primary source — 2026-07-17
- **pattern**: a statistic reaches a client only with `<claim> — <source, year, venue|vendor> — measured: <what the source ACTUALLY measured> — <link>`. The `measured:` field is what catches the error.
- **context**: "VSI (Visual Stability Index) — new 2026 Core Web Vital" lived in seo-analyzer as a threshold, stated as fact. It does NOT exist — absent from the CrUX API metric list AND web.dev; 10 SEO blogs cross-cited it into apparent consensus, several falsely claiming CrUX already collected it. And EVERY stat in agents/resources/ was real but grafted onto the wrong subject: Aggarwal 40% = ALL methods (pinned on "add stats"); AccuraCast 58.9% = Person-schema PREVALENCE (pinned on QAPage lift, meaning inverted — FAQPage was 1.8%); LLMrefs 3x = brand-mentions-vs-backlinks (pinned on freshness decay).
- **future**: the failure mode is plausible RECOMBINATION — what a model half-remembering a search produces. The old rule "cross-check via WebSearch" LAUNDERS the blog consensus instead of catching it. An API's metric list (e.g. developer.chrome.com/docs/crux) is decisive: a metric the API can't return is one you can't score. See [[LRN-132]] (same family, subagent summaries).
- **pattern**: statistic reaches a client only with `<claim> — <source, year, venue|vendor> — measured: <what the source ACTUALLY measured> — <link>`. `measured:` field catches the error.
- **context**: "VSI (Visual Stability Index) — new 2026 Core Web Vital" lived in seo-analyzer as threshold, stated as fact. It does NOT exist — absent from the CrUX API metric list AND web.dev; 10 SEO blogs cross-cited it into apparent consensus, several falsely claiming CrUX already collected it. EVERY stat in agents/resources/ was real but grafted onto wrong subject: Aggarwal 40% = ALL methods (pinned on "add stats"); AccuraCast 58.9% = Person-schema PREVALENCE (pinned on QAPage lift, meaning inverted — FAQPage was 1.8%); LLMrefs 3x = brand-mentions-vs-backlinks (pinned on freshness decay).
- **future**: failure mode is plausible RECOMBINATION — what a model half-remembering a search produces. Old rule "cross-check via WebSearch" LAUNDERS blog consensus instead of catching it. An API's metric list (e.g. developer.chrome.com/docs/crux) is decisive: a metric the API can't return is one you can't score. See [[LRN-132]] (same family, subagent summaries).
## LRN-132 — a subagent summary is a claim, not a fact — verify before planning on it — 2026-07-17
- **pattern**: relaying a subagent's characterisation without checking it propagates plausible-but-false. Treat every relayed finding as a claim to verify against a primary source or a live test.
@@ -1461,9 +1463,9 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-150 — Sourced shell lib is not a subprocess: prefix printers, honor inherited errexit
- **Date**: 2026-09-15
- **Pattern**: `source lib.sh` shares the caller's shell. Two bites. (a) bare `ok()`/`warn()`/`info()` in the lib OVERRIDE the caller's same-named funcs. `doctor.sh` counts ERRORS/WARNS inside its own `warn()` → a lib `warn` disconnects the counter and doctor prints "No errors" while warnings scroll. Prefix every lib printer (`_gspw_ok`, `_gspw_warn`, `_gspw_info`). (b) caller's `set -euo pipefail` applies INSIDE the lib's functions: a failing command-substitution assignment (`x="$(. /etc/os-release; [ "$ID" = ubuntu ] && printf ...)"`) aborts the CALLER when the func is called as a bare statement. Reproduced — exit 1 on every non-Ubuntu host, latent in `install-plugins.sh` since [[BDR-029]].
- **Rule**: public func called bare → `return 0` on every path + `|| true` on every capture. Func allowed to return non-zero → call it ONLY as an `if` condition.
- **Future application**: any new `lib/*.sh` sourced by a script that owns printers or sets `-e`. Check BOTH facets before wiring; the printer one is silent (no error, just a lying summary).
- **Pattern**: `source lib.sh` shares caller's shell. Two bites. (a) bare `ok()`/`warn()`/`info()` in lib OVERRIDE caller's same-named funcs. `doctor.sh` counts ERRORS/WARNS inside its own `warn()` → a lib `warn` disconnects the counter and doctor prints "No errors" while warnings scroll. Prefix every lib printer (`_gspw_ok`, `_gspw_warn`, `_gspw_info`). (b) caller's `set -euo pipefail` applies INSIDE lib's functions: failing command-substitution assignment (`x="$(. /etc/os-release; [ "$ID" = ubuntu ] && printf ...)"`) aborts CALLER when func is called as bare statement. Reproduced — exit 1 on every non-Ubuntu host, latent in `install-plugins.sh` since [[BDR-029]].
- **Rule**: public func called bare → `return 0` on every path + `|| true` on every capture. Func allowed to return non-zero → call ONLY as `if` condition.
- **Future application**: any new `lib/*.sh` sourced by script owning printers or setting `-e`. Check BOTH facets before wiring; the printer one is silent (no error, just a lying summary).
- **Reference**: `lib/gstack-playwright.sh`, `doctor.sh:12-15`. Links [[BDR-088]].
---
@@ -1494,9 +1496,9 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-154 — Untracking a generated file then merging deletes it from disk
- **Date**: 2026-09-15
- **Pattern**: `git rm --cached` removes from the index and KEEPS the working file, which is the whole point when untracking a tool-generated artifact. But `gitflow finish` checks out the target branch first, where the file is still tracked, so git restores it; the merge then applies the deletion to a tracked file and removes it from disk. `.gitignore` does not protect it — it only stops a re-add. Net effect: the file survives the commit and dies at the merge, several minutes later, which reads as unrelated.
- **Detection**: the working tree is clean and the file is simply absent. Nothing errors. Only a post-merge `ls` catches it.
- **Future application**: untracking any generated file — know the regeneration command BEFORE merging, and `ls` the path right after `finish`. If nothing regenerates it, keep it tracked.
- **Pattern**: `git rm --cached` removes from index and KEEPS working file, the whole point when untracking a tool-generated artifact. But `gitflow finish` checks out target branch first, where file is still tracked, so git restores it; merge then applies deletion to a tracked file and removes it from disk. `.gitignore` does not protect it — it only stops a re-add. Net effect: file survives commit, dies at merge several minutes later, reads as unrelated.
- **Detection**: working tree clean, file simply absent. Nothing errors. Only post-merge `ls` catches it.
- **Future application**: untracking any generated file — know regeneration command BEFORE merging, `ls` the path right after `finish`. If nothing regenerates it, keep it tracked.
- **graphify specifics**: `graphify install --platform claude` copies the skill and touches nothing else. `graphify claude install` is a different command — it writes the CLAUDE.md section and the `.claude/settings.json` hooks, rewrites both guarded configs, and does NOT copy the skill. Confusing the two wastes a recovery attempt.
- **Reference**: `CLAUDE.md` machine-owned section, commit 80ccdaf. Links [[BDR-090]].
@@ -1514,16 +1516,16 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-157 — Taste is invisible to a gap-only trigger; ask at plan time
- **Date**: 2026-09-16
- **Pattern**: a trigger that fires only on missing outcome / scope / constraints lets every taste choice through — "add a share icon" is complete by those criteria and the icon's side is decided downstream. More budget changes nothing; the fix is a new trigger class (VISIBLE / PUBLIC NAME / SCOPE). Cost geometry: a fresh re-dispatch keeps the working tree and loses the executor's reasoning → the same question costs about one executor run more mid-run than at PLAN. So: sweep once at the plan step, keep the mid-run channel for leftovers. Executor tags the class; orchestrator re-reads it (tag = hint, a mis-tag would offload class 4 onto the human). Relayed questions obey [[LRN-102]]: context inside `AskUserQuestion`, nothing the user needs printed before it.
- **Future application**: any "ask more" request → check WHICH trigger is blind before touching a quota. Any orchestrator with a "decide it yourself" fallback on an executor halt → route by class first.
- **Pattern**: trigger firing only on missing outcome / scope / constraints lets every taste choice through — "add a share icon" is complete by those criteria, icon's side decided downstream. More budget changes nothing; the fix is a new trigger class (VISIBLE / PUBLIC NAME / SCOPE). Cost geometry: fresh re-dispatch keeps working tree, loses executor's reasoning → same question costs about one executor run more mid-run than at PLAN. So: sweep once at plan step, keep mid-run channel for leftovers. Executor tags class; orchestrator re-reads it (tag = hint, mis-tag would offload class 4 onto the human). Relayed questions obey [[LRN-102]]: context inside `AskUserQuestion`, nothing the user needs printed before it.
- **Future application**: any "ask more" request → check WHICH trigger is blind before touching quota. Any orchestrator with "decide it yourself" fallback on executor halt → route by class first.
- **Reference**: [[BDR-091]], `lib/contract-interview.md` STEP 2 + MID-RUN CLARIFICATION.
## LRN-158 — A hardened installer + a symlinked config dir = documented command fails; stage under a throwaway HOME
- **Date**: 2026-09-22
- **Context**: `21st install-skill` (= `21st skills install --global`) is upstream's documented one-liner. Here it dies: `Refusing to access symbolic link /home/…/.claude/skills`. The installer walks every segment of `<HOME>/.claude/skills/<n>/SKILL.md` with an `assertNoSymlinkComponents` guard (anti symlink-escape); this repo's whole model is `~/.claude/skills -> repo/skills`. Two correct designs, mutually exclusive on the same path.
- **Pattern**: don't fight the guard and don't unlink the config dir. Run the installer with `HOME=$(mktemp -d)` so it writes into a pristine real tree, then move the output to the vendored dir the repo controls and symlink from there. Same shape as the impeccable/ctx7 staging (`mktemp -d`, install, `mv` into `skills-external/`), with HOME as the extra lever. Two conditions make it safe: the command must need nothing else from HOME (checked: manifest + content fetch are unauthenticated, hash-verified), and the moved payload must be self-contained.
- **Also**: read the npm tarball, not the vendor's web page. 21st.dev's `/mcp` and `/llms.txt` still document the MCP `init --client` flow with an API key; the package README states the CLI supersedes it. `curl registry.npmjs.org/<pkg>` + untar + read `README.md`/`dist` answered every question (commands, exit codes, where files land) that the site got wrong.
- **Future application**: any vendor installer that writes into `~/.claude`, `~/.config` or `~/.agents` on this machine. Probe first with a fake HOME containing the symlink, before wiring it into `install-plugins.sh` — the failure is instant and unambiguous.
- **Context**: `21st install-skill` (= `21st skills install --global`) is upstream's documented one-liner. Here it dies: `Refusing to access symbolic link /home/…/.claude/skills`. Installer walks every segment of `<HOME>/.claude/skills/<n>/SKILL.md` with `assertNoSymlinkComponents` guard (anti symlink-escape); this repo's whole model is `~/.claude/skills -> repo/skills`. Two correct designs, mutually exclusive on same path.
- **Pattern**: don't fight the guard and don't unlink the config dir. Run installer with `HOME=$(mktemp -d)` so it writes into pristine real tree, then move output to the vendored dir the repo controls and symlink from there. Same shape as impeccable/ctx7 staging (`mktemp -d`, install, `mv` into `skills-external/`), HOME as extra lever. Two conditions make it safe: the command must need nothing else from HOME (checked: manifest + content fetch are unauthenticated, hash-verified), and the moved payload must be self-contained.
- **Also**: read the npm tarball, not the vendor's web page. 21st.dev's `/mcp` and `/llms.txt` still document MCP `init --client` flow with API key; package README states CLI supersedes it. `curl registry.npmjs.org/<pkg>` + untar + read `README.md`/`dist` answered every question (commands, exit codes, where files land) the site got wrong.
- **Future application**: any vendor installer writing into `~/.claude`, `~/.config` or `~/.agents` on this machine. Probe first with fake HOME containing the symlink, before wiring into `install-plugins.sh` — failure is instant and unambiguous.
- **Reference**: [[BDR-093]], `install-plugins.sh` Step 8.7, `update-all.sh` 7.4. Links [[LRN-034]] (run the real thing), [[BLK-014]]-class symlink/self-heal issues.
## LRN-159 — A pin whose payload is fetched at install time rots: pin + fallback, and read the installer's output, not its exit code
@@ -1535,8 +1537,8 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-160 — Prose guardrails are judgment, not boundary: a well-argued brief walks a sub-agent through them
- **Date**: 2026-09-22
- **Context**: 2026-09-21 00:21, old server. Reviewer sub-agent (opus, atlast SDD task 26) briefed by the orchestrator: "Tracing lftp semantics against a scratch tree of your own making, outside the repository, is allowed". It ran `mirror --reverse --delete` against a local `file://` tree; target resolved to a real path; `mirror --delete` = `rm -r` on target dirs absent from source, `--exclude` ignored. 90 s: home, `~/.claude`, `/tmp` outputs, NAS (`uid=1000`), 15 Gitea repos (Gitea ran as bchanot = uid 1000, no Docker bridge needed). Reviewer's next Bash rc 1 with its output file gone, then API "Not logged in" (credentials wiped). Config of the day already had hard_deny "deploy to provider" + soft_deny `rsync --delete`: neither names lftp nor a local trace. 4 days of faunosteo never pushed; Gitea on the same disk.
- **Pattern**: (a) an LLM classifier reads intent; the orchestrator's brief IS the sub-agent's user voice, so a reasoned authorization passes. Only static deny rules (resolve first, inherited by sub-agents, per-segment on `&&`) and OS rights are boundaries. (b) "Trace what it would do" is execution; a scratch target from a variable is one unset var away from `/`. (c) The event deletes its own evidence when the agent's uid owns the logs, the config and the transcripts. (d) A remote backs up only what it holds: push at branch creation and at every commit, from a hook, not from discipline. (e) `git merge` fires post-merge, not post-commit.
- **Context**: 2026-09-21 00:21, old server. Reviewer sub-agent (opus, atlast SDD task 26) briefed by orchestrator: "Tracing lftp semantics against a scratch tree of your own making, outside the repository, is allowed". Ran `mirror --reverse --delete` against local `file://` tree; target resolved to a real path; `mirror --delete` = `rm -r` on target dirs absent from source, `--exclude` ignored. 90 s: home, `~/.claude`, `/tmp` outputs, NAS (`uid=1000`), 15 Gitea repos (Gitea ran as bchanot = uid 1000, no Docker bridge needed). Reviewer's next Bash rc 1 with its output file gone, then API "Not logged in" (credentials wiped). Config of the day already had hard_deny "deploy to provider" + soft_deny `rsync --delete`: neither names lftp nor a local trace. 4 days of faunosteo never pushed; Gitea on the same disk.
- **Pattern**: (a) LLM classifier reads intent; orchestrator's brief IS sub-agent's user voice, so reasoned authorization passes. Only static deny rules (resolve first, inherited by sub-agents, per-segment on `&&`) and OS rights are boundaries. (b) "Trace what it would do" is execution; scratch target from a variable is one unset var away from `/`. (c) Event deletes its own evidence when agent's uid owns logs, config and transcripts. (d) A remote backs up only what it holds: push at branch creation and at every commit, from a hook, not from discipline. (e) `git merge` fires post-merge, not post-commit.
- **Future application**: any new destructive capability → static deny first, prose second, doctrine third. Any orchestrator brief → never "X is allowed outside the repo". Sub-agent tools: report-only agents trace by reading. Probe a guard with the real sub-agent path (auto mode inherited), not the main session.
- **Reference**: [[BDR-095]], `/mnt/cloudpex/RECOVERY/00-incident/`, atlast transcript `26e76a0b…` + stub `agent-a7d9119…`. Links [[BDR-090]], [[BDR-092]], [[LRN-155]], [[LRN-114]].
@@ -1690,3 +1692,11 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-188 — A CHECK oracle tied to code shape breaks on refactor; assert the property
- **Context**: contract criterion 5 used `grep -A6 '^_higgsfield_probe()' | grep -c '</dev/null …' | grep -qx 2`. The gtimeout fallback reshaped the function into a loop: second redirect moved past the 6-line window → GATE 0 UNMET on correct code. Rewritten: extract the body `awk '/^fn\(\)/,/^}/'`, require the redirect on EVERY invocation line, control = strip redirects → differs. Orchestrator edited its own oracle outside a human gate, logged in the contract, surfaced to the user, fresh verifier judged it stricter.
- **Apply**: oracles state a property over the whole unit (function body, section range), never a line window or an exact count of incidental lines; any oracle edit after a gate is logged + surfaced. [[LRN-093]], [[BDR-109]]
## LRN-189 — `| grep -q` under pipefail is a SIGPIPE race, not a portability nit
- **Context**: 15 gitflow-test assertions `git log … | grep -q "Merge …"` false on macOS, merge present. grep -q exits at first match → producer SIGPIPE → rc 141 → pipefail false. Linux hides it by write buffering, race exists there too (doc-shape fail-open). Challenger-verified: GNU grep main() sets done_on_match when stdout is /dev/null, so `grep PAT >/dev/null` keeps the race.
- **Apply**: producer out of the pipeline. Tests: `grep -q PAT < <(cmd)`. Prod: call inside the condition `if grep -q PAT <<<"$(cmd 2>&1)"` (a bare `out=$(cmd)` aborts under `set -e` when the function runs bare). `printf '%s' "$v" | grep -q` safe (builtin, one write). Same class: `| head -1`, awk `{print; exit}`. Not decidable by text census → structural fix, no `grep -q` lint rule. [[BDR-110]]
## LRN-190 — Oracle hygiene: wrapped lines, baselines, no rm -rf via variable
- **Context**: GATE 0 criterion 4 NOT-MET while code correct: executor wrapped `grep -q … \` + `<<<"$(…)"` at 80 cols (my own style rule), single-line regex missed it. Criterion 7 `shellcheck` bare would fail on pre-existing info notes outside Health Stack scope. Criterion 2 CHECK held `rm -rf "$d"` (destructive-tools rule), executor's copy refused by permission system.
- **Apply**: join continuations first (`sed -e ':a' -e 'N' -e '$!ba' -e 's/\\\n[[:space:]]*/ /g'`); lint criteria compare counts against base ref (`git show base:file | shellcheck -`); planted fixtures cleaned with `rm -f file; rmdir dir`. Oracle edits after a red floor logged in CLARIFICATIONS as "oracle maintenance", criterion text never loosened. Extends [[LRN-188]].
+23 -3
View File
@@ -7,12 +7,12 @@ routing lines, complete scope, TTY fix on ctx7 + 21st (option A), deny aliases.
- [x] /ship-feature run: 9 plan tasks, fix wave after the final review, doc sync, registries ([[BDR-109]])
- [ ] parked (final review, rulings in BDR-109): remedy line ignores a pinned lock version (latent while `latest`); Step 8.6 spawns `higgsfield version` up to 3 times; rollback needs `npm uninstall -g @higgsfield/cli` + session removal + hand removal of `skills-external/higgsfield-*`
- [ ] parked (per-task minors, none blocking): "rename" comment vs rm-then-mv; no `--` before the clone URL; ssh URL can prompt; no sweep of a stale `.higgsfield-stage.*`; timeout path itself untested; "pack not installed" when only unlisted skills are synced; doctor version read unbounded
- [ ] user decision: close the remaining npm global-install spellings in settings.json deny (`npm i <pkg> -g`, `npm add -g`, `npm -g i`) — pattern grammar for a mid-string wildcard unverified ([[BLK-025]])
- [x] user decision: close the remaining npm global-install spellings in settings.json deny (`npm i <pkg> -g`, `npm add -g`, `npm -g i`) — pattern grammar for a mid-string wildcard unverified ([[BLK-025]]) — CLOSED 2026-10-06 (reconcile, user go: soft_deny settings.json:472 names `npm add -g` + flag-after-package)
- [ ] user decision: pin a commit for higgsfield-ai/skills and a version for `@higgsfield/cli` (security gate, 2 MEDIUM, accepted as is)
- feature/higgsfield-pack merged into develop 2026-09-30 (df6dbce, user go)
- [x] soft_deny "Global npm installs" entry merged 2026-09-30 (chore/npm-global-soft-deny)
- [ ] user hand edit pending: remove the four `Bash(npm … -g|--global *)` lines from `permissions.deny` (they override the classifier); then the first global install is the live test of the entry
- [ ] (was) user decision pending: npm global installs from `deny` to a prompt tier — `ask` does not prompt under `defaultMode: auto` ([[BDR-090]]); option = one `autoMode.soft_deny` entry (vet the package first), deny lines removed by the user by hand
- [x] (was) user decision pending: npm global installs from `deny` to a prompt tier — `ask` does not prompt under `defaultMode: auto` ([[BDR-090]]); option = one `autoMode.soft_deny` entry (vet the package first), deny lines removed by the user by hand — DONE 2026-09-30 (soft_deny "Global npm installs", 95168c1; reconcile 2026-10-06)
## 2026-09-29 — effort round: every skill carries a level next to its model pin (feature/effort-round)
User table: low fix-a-line/run-a-script · medium day-to-day · high refactor/resisting bug ·
@@ -889,7 +889,7 @@ versioned (durable, referenced by decisions.md e.g. BDR-076). Universal via the
symmetry + /doc clean pass: README/USAGE/ARCHITECTURE.md) — 37c79f0
- [x] merge chore/purge-transient-docs → develop (docs/ transient purge
655e364 + reconcile e75ea79) — reaches main at next release
- [ ] Makefile help text: profile-list help lists 5/10 profiles (:57) —
- [ ] Makefile help text: profile-list help lists 5/10 profiles (:57) — (re-verified OPEN 2026-10-06: 11 profiles, Makefile:62 lists 5)
1-line hotfix. (test glob :31 FIXED — has run-*.sh, reconcile 2026-08-25)
Re-verified OPEN 2026-09-01: lib/profiles/ has 10, Makefile:57 lists 5
(backend, full, seo, web-full, web missing).
@@ -2035,3 +2035,23 @@ dans un runner; capitalize reste main-loop.
- [x] T3 BDR-084 + CHANGELOG + journal.
- [x] T4 make test rc 0 + shellcheck clean (SC2016 silencé, littéral
voulu). Merge NON fait — gate humain.
## macos-portability follow-ups (2026-10-06)
- [ ] effort-pins re-red trigger: update-all.sh applies effort pins only at
~:572, after every vendoring step; an abort upstream drops them from the
gitignored SKILL.md again (effort-routing red). Pin after each vendoring
step or flag in doctor.
- [ ] [deferred] Linux `make test` run before the next release: every
portability replacement is meant to be GNU-identical, unverified here.
## doc-sync 2026-10-06 deferred (global audit before 2.0.0, user: log)
- [ ] P16 LICENSE file + README License section — SPDX pick is the user's (Standard-Readme requires one; clone URL now public Gitea)
- [ ] P43 CHANGELOG Known residual says "Linux make test due before next release" while 2.0.0 is being cut — run it or reword at release resume
- [ ] P17 README restructure to Standard-Readme (Install / Usage / Configuration / License); move inline reference parts to USAGE.md, CONFIGURE.md from settings.json + .env.example
- [ ] P18 README Requirements line (Linux apt/dnf/pacman + macOS brew) once the Linux run is done
- [ ] P22 `make new-skill` scaffold: agent stub lacks `effort:` pin, skill stub lacks entry level, body loads `.claude/agents/$(name).md` instead of `$HOME/.claude/agents/<name>.md` → /hotfix
- [ ] P31 USAGE narrative: Exemple 9 "sans superpowers" (vendored, always on), "Edit tool" bypasses /hotfix, Pattern E + Exemple 9 route bugs to /ship-feature instead of /bugfix|/hotfix
- [ ] P32 USAGE missing sections: design work (21st CLI, design stack, /site-motion), media generation (Higgsfield toggles), gitflow auto-push + global hooks
- [ ] P33 USAGE token figures ("Budget Pro ~11k tokens/5h", per-pattern) have no source in code — verify or drop
- [ ] P34 USAGE + agents/plugin-advisor.md "gstack ON/OFF", "context7 ON" vocabulary — gstack is per-profile, ctx7 is a CLI; move both together
- [ ] P41 templates/settings/settings.json: `permissions.ask` entries (npx, docker rm, make deploy, psql…) inert under defaultMode auto → config fix, not doc
@@ -0,0 +1,64 @@
# CONTRACT — macos-portability
- date: 2026-10-06 | flow: bugfix | branch: bugfix/macos-portability
- status: active
- plan: .claude/tasks/plans/2026-10-06-macos-portability-1030.md (r3, 4 challenge passes)
## REQUEST (verbatim — IMMUTABLE)
> ok, alors fais les bugfix, puis on va faire du /reconcile et du /prune-memory puis le doc-sync
>
> /bugfix make test is red on develop: 24 FAIL lines across several suites (wc -l whitespace compares, design-tool-gate, effort pins on two skills-external, unpushed-guard T4, graphify THRESHOLD_DOWN, skill-routing-census mutants, statusline T11, T3 repo citations). Log at scratchpad/t.log. Goal: green make test.
## CLARIFICATIONS
- Pass A: none — outcome (`make test` rc 0) and scope derivable.
- Diagnosis correction: the first count (24) came from the release-prep executor's partial read; the full log holds 13 red suites / ~45 FAIL lines. The bug names in the request (unpushed-guard T4, THRESHOLD_DOWN, census mutants, T11) are the same `sed -i` / `wc -l` / SIGPIPE classes.
- Q (pass B, scope): tests only, or tests + the prod scripts carrying the same idioms? / A (user, 2026-10-06): "Tests + prod". [gated 2026-10-06]
- Q (pass B, doctrine): POSIX/BSD-portable on native userland, or GNU via Homebrew on PATH? / A (user, 2026-10-06): "POSIX/BSD portable". [gated 2026-10-06]
- [gated 2026-10-06] STEP 3 approval: user answered "go" to plan r3 including the two prod sites the challenge surfaced (lib/doc-shape.sh:70, update-all.sh:607) and the `[deferred]` Linux verification.
- Delegated internals: helper names inside libs, awk vs sed where both are portable, test-id naming (T4b, Alphabet flip), census allowlist file format.
- Machine-state step (orchestrator, not the executor): `bash lib/effort-pins.sh` re-pins the two gitignored SKILL.md (plan §14).
- Executor never runs install-plugins.sh, update-all.sh, link.sh, doctor.sh, `npm`, `git commit`, or any mirror/transfer tool. Tests run through `make test [suite=…]` only.
- GATE 0 runs with `GATES_TIMEOUT=1200` (criterion 1 is the full suite, ~10 min; default 120 s would time out): wrapper script in the session scratchpad sets the variable and calls `gates.sh run`.
- [oracle maintenance 2026-10-06, orchestrator, NOT a human gate] criterion 2 CHECK no longer runs `rm -rf` through a variable (destructive-tools rule): `rm -f file; rmdir dir`. Criterion 7 CHECK compares shellcheck finding counts against develop instead of requiring zero: the two edited test files already carried info-level notes on develop. Criterion texts tightened, not loosened.
- [oracle maintenance 2026-10-06, orchestrator, NOT a human gate] criterion 4 CHECK joins backslash line continuations before matching: the executor wrapped the `grep -q … <<<"$(…)"` sites at 80 columns (style rule), the single-line regex missed them while the code is the planned form. Criterion text unchanged.
- Style: functions ≤ 25 logic lines, ≤ 5 locals, 80 cols, shellcheck clean on every edited file; comments say WHY (the portability reason), one line each.
## ACCEPTANCE CRITERIA
1. The whole hermetic suite is green on this macOS machine (no suite removed from `SUITES`, no new SKIP).
CHECK: n_before=$(grep -c 'SKIP' /private/tmp/claude-501/-Users-b-chanot-Documents-claude/b4349baf-52d5-4b3f-ac33-5b44a2ad990c/scratchpad/t.log); out=$(make test 2>&1); rc=$?; echo "$out" | tail -3; [ "$rc" -eq 0 ] || exit 1; echo "$out" | grep -qE '^(FAIL|RED | FAIL)' && exit 1; n_after=$(echo "$out" | grep -c 'SKIP'); [ "$n_after" -le "$n_before" ] || { echo "SKIP grew $n_before -> $n_after"; exit 1; }; echo SUITE_GREEN
EXPECT: SUITE_GREEN
EVIDENCE: MET exit=0 marker-found :: GREEN ✓ G5 hook-drift: installed .githooks/pre-commit == generator emit-hook ================ 4 GREEN / 0 RED / 1 SKIP (review-guards) =====…
2. `lib/tests/portability-census.test.sh` exists, scans the file list given as args (default: tracked `*.sh` + `hooks/*`), skips comment lines, holds a file:line allowlist with reasons (lib/tests/guard-bash.test.sh fixtures + itself), is green on the tree, and reds (exit 2) on a planted `sed -i 's/a/b/' x` in a mktemp dir.
CHECK: t=lib/tests/portability-census.test.sh; [ -f "$t" ] || exit 1; grep -q 'guard-bash.test.sh' "$t" || exit 1; bash "$t" >/dev/null 2>&1 || exit 1; d=$(mktemp -d); printf '#!/usr/bin/env bash\nsed -i '"'"'s/a/b/'"'"' x\n' > "$d/planted.sh"; bash "$t" "$d/planted.sh" >/dev/null 2>&1; rc=$?; rm -f "$d/planted.sh"; rmdir "$d"; [ "$rc" -eq 2 ] || { echo "planted rc=$rc"; exit 1; }; echo CENSUS_FLIPS
EXPECT: CENSUS_FLIPS
EVIDENCE: MET exit=0 marker-found :: CENSUS_FLIPS
3. No GNU-only idiom survives at the planned prod sites: `sed -i` gone from install-plugins.sh (orphan-comment sed :1207-1208 deleted), `grep -oP` gone from update-all.sh, `realpath -m` gone from lib/gstack-links.sh, bare `timeout 15` gone from lib/design-tool-gate.sh (perl alarm present), `| grep -Eq` gone from lib/doc-shape.sh:70.
CHECK: grep -q 'sed -i' install-plugins.sh && exit 1; grep -q 'grep -oP' update-all.sh && exit 1; grep -q 'realpath -m' lib/gstack-links.sh && exit 1; grep -qE '(^|[^_a-z])timeout 15' lib/design-tool-gate.sh && exit 1; grep -q "alarm" lib/design-tool-gate.sh || exit 1; grep -qE 'git diff HEAD -- "\$p" \| grep' lib/doc-shape.sh && exit 1; grep -q 'N; /^\\n$/d' install-plugins.sh && exit 1; echo PROD_PORTABLE
EXPECT: PROD_PORTABLE
EVIDENCE: MET exit=0 marker-found :: PROD_PORTABLE
4. Class (A) prod sites keep the CLI call INSIDE the condition (errexit-safe): lib/profile.sh enable_skill/disable_skill, install-plugins.sh install_plugin, lib/toggle-external.sh pack_hints match `grep -q… <<<"$(…)"` and no `| grep -q` remains on those lines; lib/tests/profile-set-managed.test.sh has a fake-claude case returning non-zero.
CHECK: joined() { sed -e ':a' -e 'N' -e '$!ba' -e 's/\\\n[[:space:]]*/ /g' "$1"; }; for f in lib/profile.sh lib/toggle-external.sh install-plugins.sh lib/design-tool-gate.sh; do joined "$f" | grep -nE '(claude|21st|"\$CLAUDE_BIN")[^|]*\| *grep -q' && { echo "pipeline left in $f"; exit 1; }; done; joined lib/profile.sh | grep -cE 'grep -q[iE]* .*<<<"\$\(' | grep -qE '^[2-9]' || exit 1; joined lib/toggle-external.sh | grep -qE 'grep -q[iE]* .*<<<"\$\(' || exit 1; joined install-plugins.sh | grep -qE 'grep -q[iE]* .*<<<"\$\(' || exit 1; grep -qE 'exit [1-9]' lib/tests/profile-set-managed.test.sh || exit 1; echo CAPTURE_IN_CONDITION
EXPECT: CAPTURE_IN_CONDITION
EVIDENCE: MET exit=0 marker-found :: CAPTURE_IN_CONDITION
5. gstack-links refusal is portable and tested: T4b (dst with a missing intermediate dir under src) present and green; the helper refuses a `..` component.
CHECK: grep -q 'T4b' lib/tests/gstack-links.test.sh || exit 1; grep -q '\.\.' lib/gstack-links.sh || exit 1; out=$(make test suite=lib/tests/gstack-links.test.sh 2>&1); echo "$out" | grep -q '^FAIL' && exit 1; echo "$out" | grep -q 'PASS=' || exit 1; echo T4B_GREEN
EXPECT: T4B_GREEN
EVIDENCE: MET exit=0 marker-found :: T4B_GREEN
6. doctrine-citers resolver is fixed-string with the Alphabet/Alpha flip case; design-tool-gate.test.sh precondition covers /opt/homebrew/bin/21st; both suites green.
CHECK: grep -q 'Alphabet' lib/tests/doctrine-citers.test.sh || exit 1; grep -q '/opt/homebrew/bin/21st' lib/tests/design-tool-gate.test.sh || exit 1; grep -q '/opt/homebrew/bin' lib/design-tool-gate.sh || exit 1; for s in lib/tests/doctrine-citers.test.sh lib/tests/design-tool-gate.test.sh; do out=$(make test suite=$s 2>&1); echo "$out" | grep -q '^FAIL' && exit 1; done; echo CITERS_GATE_GREEN
EXPECT: CITERS_GATE_GREEN
EVIDENCE: MET exit=0 marker-found :: CITERS_GATE_GREEN
7. Every edited shell file is shellcheck-clean: no finding that was not already on develop (two edited test files carry pre-existing info notes outside the repo Health Stack scope: seo-data.test.sh SC2015 ×27, gstack-playwright.test.sh ×2). [oracle maintenance 2026-10-06, surfaced in the report]
CHECK: files=$(git diff --name-only develop -- '*.sh' 'hooks/*' | grep -E '\.sh$|^hooks/'); [ -n "$files" ] || exit 1; new=$(shellcheck -f gcc $files 2>/dev/null | wc -l | tr -d ' '); old=0; for f in $files; do if git cat-file -e "develop:$f" 2>/dev/null; then c=$(git show "develop:$f" | shellcheck -f gcc - 2>/dev/null | wc -l | tr -d ' '); old=$((old + c)); fi; done; echo "findings develop=$old branch=$new"; [ "$new" -le "$old" ] && echo SHELLCHECK_CLEAN
EXPECT: SHELLCHECK_CLEAN
EVIDENCE: MET exit=0 marker-found :: findings develop=29 branch=29 SHELLCHECK_CLEAN
8. Linux behaviour unchanged: every replacement semantically identical under GNU tools (producer capture, `-i.bak`, `tr -d ' '`, python3 perms, `touch -t`, perl alarm, `sed -n` token). Human judgement; a Linux `make test` run is `[deferred 2026-10-06]` to the user.
9. TODO.md carries the known re-red trigger (update-all.sh applies effort pins only after every vendoring step) and the deferred Linux run.
CHECK: grep -q 'effort-pins' .claude/tasks/TODO.md && grep -qi 'linux' .claude/tasks/TODO.md && echo TODO_NOTED
EXPECT: TODO_NOTED
EVIDENCE: MET exit=0 marker-found :: TODO_NOTED
## FILE SCOPE
Tests: lib/gitflow-test.sh, lib/tests/{run-release-candidate,run-doc-commit}.sh, lib/tests/{floor-guard,profile-census,profile-default,effort-pins,source-scope,fast-libs,doctrine-citers,gstack-links,gstack-playwright,design-tool-gate,profile-set-managed}.test.sh, lib/seo-data/seo-data.test.sh, NEW lib/tests/portability-census.test.sh.
Prod: lib/gstack-links.sh, lib/design-tool-gate.sh, lib/profile.sh, lib/toggle-external.sh, lib/doc-shape.sh, update-all.sh, install-plugins.sh.
Bookkeeping: .claude/tasks/TODO.md.
Untouched on purpose: hooks/rtk-rewrite.sh (sha-pinned, not pipefail), doctor.sh, Makefile, CLAUDE.md, settings.json.
@@ -0,0 +1,149 @@
# Plan — make test green on macOS: portable shell idioms (tests + prod) — r3
Date: 2026-10-06 · Branch: bugfix/macos-portability · Kind: build-plan (bugfix)
r2 after the three-lens challenge (correctness FATAL(6), robustness
CONCERNS(4), simplicity FATAL(2)); every BLOCKER/MAJOR closed by a named
change below, see § Challenge log.
## Bug
`make test` red on develop: 13 suites, ~45 FAIL lines. Suite and libs were
written and validated on Linux (GNU userland); this machine is macOS (BSD
userland, bash 5.3 from Homebrew, `timeout`/`gdate` only via
/opt/homebrew/bin, NO gnu-sed). Per-suite FAIL → cause map:
| Suite | FAIL | Cause |
|---|---|---|
| lib/gitflow-test.sh | 15 "merged into…" | early-exit consumer under pipefail (A) |
| lib/tests/run-release-candidate.sh | CHANGELOG not finalized | `sed -i` no suffix (B) |
| floor-guard, profile-census, profile-default .test.sh | 1+2+1 | `sed -i` no suffix (B) |
| effort-pins (3), source-scope (2), fast-libs H2-H4 (3) | string-compare of `wc -l` | BSD wc pads (C) |
| lib/seo-data/seo-data.test.sh | 3 perms + 1 stdlib | `stat -c` (D), `/bin/grep` (E) |
| fast-libs T7-stale | 1 | `touch -d` (F) |
| gstack-links T4 | 3 | `realpath -m` absent → guard never fires (G, prod) |
| design-tool-gate | 5 | bare `timeout` off the sanitized PATH (H, prod) |
| doctrine-citers T3 | 1 | extractor false positive + regex-interpolated resolver (I) |
| effort-routing | 2 | pins dropped from 2 gitignored SKILL.md on 2026-10-05 (J, machine state) |
Not red but same class, found by the challenge: lib/doc-shape.sh:70 (git
producer `| grep -Eq` under pipefail, fails OPEN: structural doc change
classed MINOR), update-all.sh:607 (`grep -oP`, BSD grep rc 2, `|| true`
silently empties `_plugins` → marketplace plugins never update on macOS).
## Root cause classes and their portable form
- (A) **Early-exit consumer under `set -o pipefail`**: `grep -q`, `head -1`,
awk `{print; exit}` exit before the producer finished → producer gets
SIGPIPE → rc 141 → pipeline false. Reproduced 5/5 on `git log | grep -q`.
NOT fixed by `grep PAT >/dev/null` (GNU grep treats stdout=/dev/null like
`-q`, challenger-verified in grep's main()). Portable form: take the
producer OUT of the pipeline — tests: `grep -q PAT < <(cmd)`; prod:
`out="$(cmd)"` then grep/awk/head the variable (here-string). Only sites
whose producer is an external command under pipefail. `printf '%s' "$v" |
grep -q` is safe (builtin writes once; 50/50 probe) and is left alone.
- (B) `sed -i 'x' f` → tests (temp dirs): `sed -i.bak 'x' f && rm -f f.bak`.
Prod on a user dotfile: unique sibling `t=$(mktemp "$f.XXXXXX")`, `sed 'x'
"$f" >"$t" && cat "$t" >"$f"`, `rm -f "$t"`, explicit failure branch (never
`-i.bak`: it would overwrite and then delete a hand-made ~/.zshrc.bak, and a
failing sed in a non-final `&&` escapes errexit).
- (C) `$(… | wc -l)` → append `| tr -d ' '` (idiom of hooks/unpushed-guard.sh:41).
- (D) `stat -c '%a'` → `python3 -I -c 'import os,stat,sys; print(oct(stat.S_IMODE(os.stat(sys.argv[1]).st_mode))[2:])' F`.
- (E) `/bin/grep` → `/usr/bin/grep` (LRN-074 pin, 6 files already).
- (F) `touch -d '10 days ago' F` → `touch -t 200001010000 F` (POSIX; cache_status only tests `-mtime -N`).
- (G) `realpath -m` → emulate: walk up to the nearest EXISTING ancestor, `cd` + `pwd -P`, append the unresolved remainder.
- (H) bare `timeout 15` → `perl -e 'alarm shift; exec @ARGV' 15 …` (perl ships in /usr/bin on both OS; keeps the 15 s bound everywhere, comment stays true).
- (I) citation name must start with a non-blank; resolver matches FIXED strings (awk `index()` on heading / bold lines), never interpolates the name into `-E`.
## Decided (user, 2026-10-06)
- Scope: tests AND prod sites. Doctrine: POSIX/BSD-portable scripts on the
native userland of both OS. Rejected: Homebrew GNU tools on PATH.
## Fix plan (exact sites)
1. lib/gitflow-test.sh — the 26 assertions whose producer is an external
command (git, bash …) `cmd | grep -q[xF|i] PAT` → `grep -q[xF|i] PAT <
<(cmd)` (A); the 11 `printf '%s' "$v" | grep -q` sites stay. Same form at
lib/tests/gstack-playwright.test.sh:126-127 (negative destructive-command
guard, fails OPEN on SIGPIPE), lib/tests/run-doc-commit.sh:104 (git
status), lib/tests/run-release-candidate.sh:55 (git show).
2. lib/profile.sh:269-274 and lib/design-tool-gate.sh:163-165 — capture
`claude plugin list` into a variable, then awk+grep on it (kills both the
awk `exit` and the grep -q race). lib/profile.sh:285,374,444,
lib/design-tool-gate.sh:170, lib/toggle-external.sh:142,
install-plugins.sh:474 — the call STAYS inside the condition:
`if grep -q[iE] PAT <<<"$(cmd 2>&1)"; then` (A). Never a bare
`out="$(cmd)"`: profile.sh:374/444 (enable_skill/disable_skill),
install-plugins.sh:474 (install_plugin) and toggle-external.sh:142
(pack_hints) run bare under `set -euo pipefail`, a bare capture of a
failing CLI would abort `profile.sh set` / `make plugin` (confirmation
pass, bash 5.3 probe). lib/tests/profile-set-managed.test.sh — add a
fake-claude case returning rc≠0 on `plugin enable` so the regression is
caught.
3. lib/design-tool-gate.sh:125 — `line="$(perl -e 'alarm shift; exec @ARGV' 15 21st whoami 2>/dev/null </dev/null)"`, first line via `${line%%$'\n'*}` (H + A). install-plugins.sh:1143 `TFD_WHO=$(21st whoami … | head -1)` — same capture-then-first-line (A, errexit-safe).
4. lib/design-tool-gate.sh `ensure_21st_on_path` / `ensure_claude_on_path` — add `/opt/homebrew/bin/<bin>` to the candidate list (Apple-Silicon npm global prefix, `npm prefix -g` = /opt/homebrew here). SAME change: lib/tests/design-tool-gate.test.sh:20-21 hermeticity precondition gains `|| [ -e /opt/homebrew/bin/21st ]` so CLI_ABSENT_10 skips loudly instead of reaching a real CLI.
5. lib/doc-shape.sh:70 — `grep -Eq … < <(git diff HEAD -- "$p")` (A).
6. update-all.sh:607 — `grep -oP '(?<=❯ )\S+'` → `sed -n 's/.*❯ \([^[:space:]][^[:space:]]*\).*/\1/p'` (non-empty token, so a line ending in "❯ " never yields `claude plugin update ""`; one token per line, as `claude plugin list` prints one plugin per line).
7. install-plugins.sh:1201,1204 — sibling-temp form (B). Delete :1207-1208
(`{ N; /^\n$/d; }`): dead on GNU (pattern space never `^\n$`), and BSD `N`
at EOF would DROP the marker comment; deletion = GNU behaviour preserved.
8. lib/tests/run-release-candidate.sh:43, floor-guard.test.sh, profile-census.test.sh, profile-default.test.sh — `-i.bak` + rm (B); BSD sed expands `\n` in the replacement, no rewrite.
9. lib/tests/effort-pins.test.sh:41,99,109; source-scope.test.sh:48,49,66; fast-libs.test.sh:46,47,50 — `| tr -d ' '` (C).
10. lib/seo-data/seo-data.test.sh:26,28,480 (D); :139 (E).
11. lib/tests/fast-libs.test.sh:39 (F).
12. lib/gstack-links.sh:64 — `_gstack_links_realpath_m <path>` helper (G):
refuse (rc 1, warn) any path holding a `..` component; walk up with
`[ -d ]` to the nearest existing directory; `CDPATH= cd -P -- "$dir" &&
pwd -P`; append the unresolved remainder. Line 63 unchanged (plain
`realpath` is native on both). lib/tests/gstack-links.test.sh — add T4b:
dst=`$SRC/missing/x` (parent absent) must be refused, nothing created.
13. lib/tests/doctrine-citers.test.sh:19 extractor `["“][^"”[:space:]][^"”]{1,59}["”]`; :27-28 resolver → awk, fixed strings (I): a heading line resolves when the text right after `#+ ` STARTS with the name and the next char is one of ` ` `:` `(` `—` or end of line; a `**name` substring anywhere on a line still resolves (bullet labels like `- **Secrets**`). Flip fixture gains a case: doctrine heading "Alphabet", citation "Alpha" → must stay DANGLING.
14. Machine state (no commit): `bash lib/effort-pins.sh` re-pins the two skills (J).
15. Regression guard `lib/tests/portability-census.test.sh` — DETERMINISTIC
idioms only, over a file list given as args (default: tracked `*.sh` +
`hooks/*`), comment lines skipped, exit 2 on hit:
`sed -i ['"]` (no suffix) · `stat -c` · `realpath -m` · `touch -d` ·
`grep -[A-Za-z]*P` · `(^|[^a-z])/bin/grep`.
Allowlist with reasons, file:line: lib/tests/guard-bash.test.sh:221,225
(deny fixtures must keep the GNU spelling an agent types), and the census
file itself. Flip test: plant `sed -i 's/a/b/' x` in a mktemp dir and pass
that file as the arg → must red. NO `| grep -q` rule (not deterministic
by text; the real fix is structural). Makefile untouched (glob already
picks `lib/tests/*.test.sh`).
16. TODO.md: known re-red trigger — update-all.sh applies pins only at :572
after every vendoring step; an abort upstream drops them again. Follow-up:
pin right after each vendoring step or flag in doctor.
Untouched on purpose: hooks/rtk-rewrite.sh:115 (not under pipefail,
sha256-pinned hook); doctor.sh:405,564 (printf/one-line producers, cannot
SIGPIPE; doctor not in the suite); other `printf | grep -q` sites.
## Acceptance
- `make test` exits 0 on this macOS machine; no SKIP added, no suite removed.
- New: gstack-links T4b green; portability census green on the tree and red
on the planted fixture.
- Linux: NOT verifiable from this machine. Every replacement is chosen to be
semantically identical under GNU tools (producer capture, `-i.bak`, tr,
python3, perl alarm, sed -n). `[deferred 2026-10-06]` a Linux `make test`
run before the next release is the human's call.
## Out of scope
- Why the 2026-10-05 fetch dropped the two pins (not reproduced; TODO entry).
- lib/tests/effort-routing.test.sh reading live gitignored files (it is the drift detector).
- doctor.sh `readlink -f` (separate pass).
## Challenge log (r1 → r2)
- BLOCKER (correctness 1, simplicity 1, robustness 4): census red by
construction → §15 narrowed to deterministic idioms, file-list arg,
allowlist, self-exclusion, no `grep -q` rule.
- MAJOR (correctness 2): `>/dev/null` is not a fix under GNU grep → class (A)
form = producer out of the pipeline.
- MAJOR (correctness 3, robustness 3): upstream awk `exit` / `head -1` → §2, §3 capture first.
- MAJOR (correctness 4, robustness 3): missed same-class prod sites → §5 doc-shape, §6 update-all.
- MAJOR (correctness 5, robustness 1): `-i.bak` on dotfiles → sibling temp, explicit failure.
- MAJOR (correctness 6, robustness 2): gstack-links fallback false → §12 ancestor walk + T4b.
- MINOR accepted: `/usr/bin/grep` (LRN-074), `touch -t`, dead :1208 deleted,
Makefile untouched, perl alarm bound, /opt/homebrew/bin candidates,
fixed-string resolver, TODO re-red trigger.
- MINOR declined: none.
- Confirmation pass (correctness CONCERNS(2), r3): MAJOR errexit on bare
capture → §2 keeps the call inside the condition + rc≠0 fake-claude case;
MAJOR hermeticity → §4 precondition; MINOR 4 unlisted class-(A) test sites
→ §1; resolver semantics + Alphabet/Alpha flip → §13; `..`/CDPATH/-P →
§12; non-empty token → §6.
+15 -9
View File
@@ -9,20 +9,26 @@ Repo layout and structural principles. Command workflows live in
claude-config/
├── CLAUDE.global.md # Global coding preferences — deployed as ~/.claude/CLAUDE.md
├── CLAUDE.md # Project-scope instructions (this repo only)
├── settings.json # Global permissions (deny / ask / allow rules)
├── README.md / USAGE.md / ARCHITECTURE.md / CHANGELOG.md / MIGRATION.md
├── version.txt # Current release version
├── .env.example # Placeholder template for ~/.claude/.env (secrets never committed)
├── settings.json # Global permissions (deny / ask / allow) + autoMode classifier tiers
├── install.sh # Bootstrap: Claude Code CLI + auth + submodules + link + plugins
├── install-plugins.sh # One-shot installer: prerequisites + all plugins
├── link.sh # Symlinks this repo into ~/.claude/
├── install-plugins.sh # One-shot installer: prerequisites + all plugins + default profile
├── link.sh # Symlinks this repo into ~/.claude/, sets git's global core.hooksPath
├── doctor.sh # Setup diagnostic
├── update-all.sh # One-command update for all components
├── Makefile # Unified entry point: make install / doctor / update
├── plugins.lock.json # Version pinning for non-marketplace dependencies
├── hooks/ # Session start, statusline, RTK rewrite + ctx7 + design-toolchain reminders
├── Makefile # Unified entry point: make install / doctor / update / test (make help)
├── plugins.lock.json # Version pinning for non-marketplace dependencies and vendored skills
├── hooks/ # Claude Code hooks: session start, statusline, RTK rewrite, ctx7 + design-toolchain reminders, attention notify, unpushed-work guard
├── githooks/ # Generated git hooks (pre-commit, post-commit, post-merge, reference-transaction), git's global core.hooksPath
├── .githooks/ # This repo's own copy of the same hooks
├── rules/ # Rule files deployed to ~/.claude/rules (path-scoped or always-on)
├── agents/ # Execution units called by skills (never invoked directly)
├── skills/ # Entry points invoked via /skill-name
├── skills-external/ # Vendored skill packs (gstack submodule + installer-fetched design packs)
├── skills-external/ # Vendored skill packs: gstack submodule, design skills, superpowers, agent-skills, MengTo scroll skills, 21st and Higgsfield packs (machine-owned copies gitignored)
├── templates/ # Per-project templates (CLAUDE.md, settings, memory registries, deploy runbook, gitignore)
└── lib/ # Shared shell libs (gitflow, profiles, commit helpers, archetypes, tests)
└── lib/ # Shared libs: gitflow, profiles, vendoring, effort pins, gates, archetypes, tests
```
## Architecture principles
@@ -30,4 +36,4 @@ claude-config/
- `skills/` = entry points you invoke via `/skill-name`
- `agents/` = execution units called by skills (never invoked directly by user)
- `templates/` = symlinked to `~/.claude/templates/` — copy into projects via `/onboard` or manually
- **Graphify** builds a knowledge graph of any codebase (`/graphify query`), producing a navigable wiki in `graphify-out/wiki/`. This map helps Claude understand project structure, find relevant code faster, and reason across files. Essential for large-scope tasks (multi-file features, complex bugs, architectural changes). Small tasks should skip it and read files directly.
- **Graphify** builds a knowledge graph of any codebase (`/graphify query`), producing a navigable wiki in `graphify-out/wiki/`. This map helps Claude understand project structure, find relevant code faster, and reason across files. Essential for large-scope tasks (multi-file features, complex bugs, architectural changes). Small tasks should skip it and read files directly. Proposed only from 200 tracked code files: the session-start banner informs, the user decides; nothing builds a graph without that go.
+27 -1
View File
@@ -2,12 +2,14 @@
All notable changes to claude-config will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/).
Format follows [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [2.0.0] — 2026-10-06
Upgrading from 1.x: see [MIGRATION.md](./MIGRATION.md#upgrading-an-existing-machine-to-200).
### Added
- **Higgsfield pack, off by default**: `make plugin` installs the `@higgsfield/cli` CLI (Step 8.6) and clones the skills of higgsfield-ai/skills into `skills-external/higgsfield-*` through the new `lib/higgsfield-skills.sh`; `make update` refreshes the skills, and the CLI when npm installed it; `make doctor` reports the CLI and its session without ever warning. The pack belongs to no profile: `lib/toggle-external.sh enable higgsfield` links the seven allowlisted media skills, `enable higgsfield-websites` the landing-page aid, and no `profile set` or `make link` re-enables either. `CLAUDE.global.md` routes explicit media-generation asks to it. Hermetic suite `lib/tests/higgsfield.test.sh`.
- **Effort round (BDR-108)**: every skill carries an entry level next to its model pin. `lib/effort-pins.txt` (map) + `lib/effort-pins.sh` (idempotent re-apply after the last vendoring step of `install-plugins.sh` and `update-all.sh`) replace the hardcoded brainstorming/writing-plans loop and extend the pins to the design stack (high, one level per stack since the last loaded wins), superpowers, agent-skills and the 21st pack; `skills-perso` low, `pdf-translate` medium, `site-motion` high; doctrine: the design stack loads paired with the first Read (a lone Skill call applies nothing). Model pins stay tier aliases: the latest version of a tier is also the cheapest or same-priced, so the quality/price trade-off is tier × effort, never version. `lib/effort-audit.py` prints thinking coverage per scope (sub-agent records carry no thinking count on ~90 % of requests: EVAL-037's "executors stay cheap" was a measurement gap, not a finding).
@@ -222,6 +224,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
seeded like a real tree (gstack off, nothing linked).
### Changed
- Default session model `claude-fable-5-1` (settings.json `model`).
- **`full` = everything the other profiles carry** (user rule: full does
what every specialized profile does), minus the 9 removed gstack
skills, the 21st generation/review trio and one named exception
@@ -468,6 +471,26 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
plugin cache or `claude plugin list`.
### Fixed
- **`make test` was red on macOS: the suite and seven scripts assumed a GNU
userland.** Under `pipefail`, `cmd | grep -q` lets grep exit at the first
match and the producer dies of SIGPIPE (rc 141): `lib/gitflow-test.sh`
reported 15 false "merged into" failures and `lib/doc-shape.sh` failed open.
`update-all.sh` read the marketplace plugin list with `grep -oP`, which BSD
grep rejects, so `make update` updated no marketplace plugin on macOS. The
design gate bounded `21st whoami` with `timeout`, absent from the macOS
system PATH, and reported READY BUT UNVERIFIED. `lib/gstack-links.sh` relied
on `realpath -m`, so its refusal to write into the gstack submodule never
fired. Producers are now captured before matching. In-place edits use
`sed -i.bak` in tests and a temp-sibling copy on the user's shell profile. The
other idioms have portable forms: `tr -d ' '` after `wc -l`, python3 for
file modes, `touch -t`, a `realpath -m` emulation that refuses `..`, perl
`alarm` for the 15 s bound and `sed -n` token extraction.
`lib/design-tool-gate.sh` also looks for `claude` and `21st` under
`/opt/homebrew/bin` and no longer trips on an empty array under macOS's bash
3.2. A failing `claude plugin enable` no longer aborts `profile set`. New
hermetic suite `lib/tests/portability-census.test.sh` flags GNU-only idioms
(`sed -i` without suffix, `stat -c`, `realpath -m`, `touch -d`, `grep -P`,
bare `/bin/grep`) in tracked shell files.
- `install-plugins.sh` never offered the ctx7 and 21st logins: both blocks required stdout to be a terminal, and stdout is the `tee` pipe of the install log. They now test stdin alone, as `update-all.sh` already did.
- `lib/effort-pins.sh` residual LOW (security re-gate of BDR-108): INT/TERM trap removes the mktemp sibling and exits 130 (never an EXIT trap, the installer owns one); the post-write re-read message no longer claims CRLF and is reached by a stubbed unit test; the rejected map line is printed through `printf '%q'` so a caller's `echo -e` cannot interpret map content; the fixture suite guards its `mktemp -d` and skips the read-only case visibly under root.
- `update-all.sh` re-fetched the vendored skills at every run but never re-applied the effort pins: brainstorming/writing-plans lost their xhigh until the next `make plugin` (BDR-107 gap, closed by `lib/effort-pins.sh`).
@@ -556,6 +579,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
the plugin while the 7 symlinks are still linked, delete the
`skills/<7>` symlinks or re-run `make plugin` to avoid duplicate skill
descriptions.
- The macOS portability fix has only been run on macOS. Its replacements are
meant to behave identically on GNU/Linux, and a Linux `make test` run is due
before the next release.
## [1.5.0] — 2026-09-13
+36 -7
View File
@@ -1,4 +1,6 @@
# Migration guide — `.claude/` restructure (2026-04-23)
# Migration guides
## `.claude/` restructure (2026-04-23)
The claude-config layout moved task tracking, memory registries, and audit
reports out of scattered roots (`tasks/`, `SEO.md`, `HARDEN.md`, etc.) into
@@ -9,7 +11,7 @@ claude-config skills and were onboarded before this change.
---
## TL;DR — full migration in one block
### TL;DR — full migration in one block
Run from the project root. Inspect the output before committing.
@@ -62,6 +64,8 @@ done
# .claude/memory/learnings.md (LRN-XXX format) then delete LESSONS.legacy.md
# 6. Update .gitignore - see "Gitignore patch" section below
# (`bash ~/.claude/lib/gitflow.sh reconcile` appends only the missing
# template lines and never rewrites project rules.)
# 7. Update CLAUDE.md - see "CLAUDE.md patch" section below
@@ -72,7 +76,7 @@ git check-ignore -v .claude/memory/decisions.md .claude/tasks/TODO.md 2>&1
---
## Gitignore patch
### Gitignore patch
If your project's `.gitignore` contains a bare `.claude/` rule, it will ignore
every memory/tasks/audit file you just created. Replace that line with:
@@ -84,9 +88,13 @@ every memory/tasks/audit file you just created. Replace that line with:
!.claude/memory/
!.claude/audits/
!.claude/settings.json
!.claude/deploy/
# These stay ignored (per-machine state)
.claude/settings.local.json
.claude/agent-memory/
.claude/gstack/
.claude/deploy/PENDING.json
.claude/deploy/NEXT.sh
```
Verify after edit:
@@ -101,7 +109,7 @@ git check-ignore .claude/settings.local.json .claude/agent-memory/
---
## CLAUDE.md patch
### CLAUDE.md patch
If your project's `CLAUDE.md` references `tasks/LESSONS.md` / `tasks/TODO.md`,
update the `## Session start`, `## Workflow`, `## After code changes`, and
@@ -126,7 +134,7 @@ Add a new section referencing the registries (full template in
---
## What gets committed vs ignored
### What gets committed vs ignored
| Path | Committed? | Reason |
|------|-----------|--------|
@@ -134,12 +142,14 @@ Add a new section referencing the registries (full template in
| `.claude/memory/*.md` | ✅ yes | Shared decisions/learnings/blockers |
| `.claude/audits/*.md` | ✅ yes | Snapshot of project state — version-able |
| `.claude/settings.json` | ✅ yes | Shared project config |
| `.claude/deploy/*.md` | ✅ yes | Deploy runbook + incidents |
| `.claude/settings.local.json` | 🚫 no | Per-machine overrides |
| `.claude/agent-memory/` | 🚫 no | Per-session agent state |
| `.claude/deploy/PENDING.json`, `NEXT.sh` | 🚫 no | Per-deploy transient state |
---
## Post-migration sanity check
### Post-migration sanity check
```bash
# 1. No legacy tasks/ dir left
@@ -161,10 +171,29 @@ All four checks should be clean before committing the migration.
---
## If anything goes wrong
### If anything goes wrong
- The migration block only uses `mv`, not `rm` — nothing is deleted.
- Old `LESSONS.md` is preserved as `LESSONS.legacy.md` — review it, copy
meaningful entries into `.claude/memory/learnings.md` (with `LRN-XXX` IDs),
then delete.
- To undo: `git checkout .` before commit.
---
## Upgrading an existing machine to 2.0.0
2.0.0 removes components that 1.x installed. `make plugin` installs their replacements; the leftovers go by hand.
```bash
git pull --recurse-submodules
make plugin # vendors the 7 superpowers skills (Step 8e), installs the Higgsfield and 21st CLIs (8.6, 8.7), re-runs link.sh (10), applies the default profile (11)
claude plugin uninstall superpowers@superpowers-marketplace # once, if the plugin is still cached
make doctor
```
- **Superpowers**: the plugin is gone. Its 7 wired skills are vendored at v6.4.1 and always on. The 8 other skills and the session-start injection are not replaced.
- **Magic MCP**: replaced by the `21st` CLI (`21st login`, no API key). If 1.x registered the `magic` server, remove it from `~/.claude.json` and drop `MAGIC_API_KEY` from `~/.claude/.env`. Nothing reads them any more.
- **Git hooks in every repo**: `link.sh` sets git's global `core.hooksPath` to `~/.claude/githooks`. Every repo on the machine now gets the gitflow pre-commit guard and pushes each commit as it lands. For a foreign clone: `git config gitflow.protect false` and `git config gitflow.autopush false`.
- **Default profile**: a machine with no profile selected now runs `full`. Check with `make profile-current`. Nine gstack skills (`ship`, `land-and-deploy`, `setup-deploy`, `autoplan`, `context-save`, `learn`, `careful`, `guard`, `design-shotgun`) left every profile and are denylisted.
- **Higgsfield**: installed, off by default. Nothing to do until you enable it.
+3 -3
View File
@@ -1,7 +1,7 @@
.PHONY: help install plugin link doctor update new-skill profile profile-list profile-current profile-reset onboard test scan-secrets seo-connect
help: ## Show available commands
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " make %-14s %s\n", $$1, $$2}'
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " make %-16s %s\n", $$1, $$2}'
install: ## First-time setup: install Claude Code + auth + symlinks + plugins
bash install.sh
@@ -41,7 +41,7 @@ test: ## Run deterministic tests hermetically (one: make test suite=lib/tests/x.
*) bash "$$t" || fail=1 ;; \
esac; done; exit $$fail
scan-secrets: ## Gitleaks sweep: this repo's history + ~/.claude (job7 backstop). Extra repos: make scan-secrets repos="path1 path2"
scan-secrets: ## Gitleaks sweep: this repo's history + ~/.claude. Extra repos: make scan-secrets repos="path1 path2"
@command -v gitleaks >/dev/null 2>&1 || { echo "gitleaks not installed — https://github.com/gitleaks/gitleaks"; exit 1; }
@mkdir -p .audit
@fail=0; \
@@ -59,7 +59,7 @@ scan-secrets: ## Gitleaks sweep: this repo's history + ~/.claude (job7 backstop)
profile: ## Run profile.sh (usage: make profile cmd="set design")
@bash lib/profile.sh $(cmd)
profile-list: ## List skill profiles (design, dev, qa, audit, minimal)
profile-list: ## List skill profiles (audit, backend, design, dev, full, max, minimal, qa, seo, web, web-full)
@bash lib/profile.sh list
profile-current: ## Show the active profile (label + match)
+49 -30
View File
@@ -16,18 +16,19 @@ Not a collection of prompts — an operating layer on top of Claude Code:
the cheapest model that can do the job (haiku collects, sonnet executes,
opus judges, the session model only reflects).
- **Hooks and permissions** are deterministic guardrails: gitflow enforced
by a pre-commit hook, every commit pushed by post-commit and post-merge
hooks, `main`/`develop` undeletable by a reference-transaction hook,
by a pre-commit hook in every repo (`make link` points git's global
`core.hooksPath` at `~/.claude/githooks`), every commit pushed by
post-commit and post-merge hooks, `main`/`develop` undeletable by a reference-transaction hook,
deny-first permission rules, secrets kept in `~/.claude/.env` and
never in config files.
- **Templates and memory** seed every project with persistent registries
(decisions, learnings, blockers) — what a session learns, the next
session knows.
(decisions, learnings, blockers, journal, evals) — what a session
learns, the next session knows.
## How it works
```bash
git clone --recurse-submodules https://github.com/bchanot/claude
git clone --recurse-submodules https://git.bchanot.fr/bchanot/claude
cd claude
make install # CLI + auth + symlinks + plugins (pinned in plugins.lock.json)
make doctor # verify everything
@@ -39,7 +40,7 @@ Day to day:
```bash
/onboard # bring an existing repo into the framework
/ship-feature "…" # brainstorm → plan → adversarial challenge → TDD → review → merge
/ship-feature "…" # brainstorm → plan → adversarial challenge → TDD → verify + security gates → review → merge on your go
/feat "…" # same idea, 1-5 files, no ceremony
/close # flush decisions and learnings to memory before quitting
make update # keep CLI, plugins, and submodules current
@@ -51,8 +52,9 @@ make update # keep CLI, plugins, and submodules current
locked, `make doctor` proves it works.
- **Cost-shaped.** Model tiering routes reflection to the big model and
execution to cheap ones — the expensive context does only what it must.
- **Safe by default.** Protected branches, ask-before-run on risky tools,
parameterized secrets: the guardrails are code, not good intentions.
- **Safe by default.** Protected branches, deny rules and auto-mode soft/hard blocks on
risky tools, transfer and mirror tools denied outright, parameterized
secrets: the guardrails are code, not good intentions.
- **It compounds.** Memory registries, audit skills, and doc-sync keep every
project's knowledge growing across sessions instead of evaporating.
@@ -68,7 +70,7 @@ commands, settings, secrets, maintenance.
Doctrine: the session model (Fable) does main-loop reflection ONLY —
brainstorm, plan, contract, audit judgment, gates, loop decisions — enforced
by a blocking gate (`lib/model-gate.md` + `lib/model-check.sh`) at the entry
of the 13 reflection orchestrators. Nothing dispatched inherits silently:
of the 15 reflection skills (the orchestrators plus `/analyze`). Nothing dispatched inherits silently:
typed agents carry a frontmatter pin, built-ins get an explicit `model=` at
every call site.
@@ -90,7 +92,7 @@ The pure-execution skills `/doc`, `/status`, `/commit-change`,
`/release-candidate` **dispatch** their agent (instead of inline-loading it)
so the pin takes effect and the work leaves the big session model; `/hotfix`
was split like `/feat` (reflection inline + gate, `hotfixer` executor) and so
joins the gated group (13th); `/client-handover`'s nested skill-runner
joins the gated group; `/client-handover`'s nested skill-runner
children are dispatched `model:"fable"` (they carry reflection).
## Effort routing (BDR-107, BDR-108)
@@ -100,7 +102,7 @@ Second axis of the same table: how hard each phase thinks. Session default
appliers, medium executors, high judgment, xhigh challengers and gates; none
on haiku, which rejects the parameter). Every user-invoked skill carries an
entry level (`/status` low … `/ship-feature` xhigh); the vendored externals
(design stack, superpowers, agent-skills, 21st) get theirs from
(design stack, superpowers, agent-skills, MengTo scroll skills, 21st) get theirs from
`lib/effort-pins.txt`, re-applied by `lib/effort-pins.sh` after every
vendoring step. Orchestrators shift per phase through the `effort-low` …
`effort-max` skills (`lib/effort-shift.md`, always sent with another tool
@@ -116,6 +118,7 @@ never version. Census `lib/tests/effort-routing.test.sh`; transcript audit
All scripts use their own location to find the repo — run them from anywhere.
The plugins step logs to `install-YYYYMMDD-HHMMSS.log`.
The last step applies the default profile, `full`, when none is selected, and re-applies an existing selection.
**Optional — Context7** (fast doc lookup for React / Next.js / Prisma…): the plugins
step installs the `ctx7` CLI and wires it into Claude Code. The doc-fetch surface is
@@ -136,14 +139,21 @@ ctx7 login # optional: OAuth / API key for higher rate limits
| Component | Type | Description | Docs |
|---|---|---|---|
| **Superpowers skills** | Vendored (7, always on) | brainstorming, writing-plans, subagent-driven development, TDD, code review request, git worktrees, writing-skills — pinned v6.4.1 in plugins.lock.json, no plugin, no session injection | [obra/superpowers](https://github.com/obra/superpowers) |
| **GStack** | Plugin (toggle) | Full-product workflow: UI + design + deploy + browser QA. Skip for backend/CLI projects. | [garrytan/gstack](https://github.com/garrytan/gstack) |
| **GStack** | Git submodule (per profile) | Product workflow skills: plan reviews, design, browser QA, security (`cso`), `health`. Linked per profile; 9 broken or doctrine-breaking skills are denylisted in `lib/gstack-removed.sh`. | [garrytan/gstack](https://github.com/garrytan/gstack) |
| **GSD v2** | External CLI | Multi-session orchestration: crash recovery, cost tracking, parallel workers, context-fresh execution. | [gsd-build/gsd-2](https://github.com/gsd-build/gsd-2) |
| **RTK** | Plugin (always on) | Code rewrite hook. Zero passive cost. | [rtk-ai/rtk](https://github.com/rtk-ai/rtk) |
| **security-guidance** | Plugin (always on) | Security hook. Zero passive cost. | [anthropics/claude-code](https://github.com/anthropics/claude-code) |
| **RTK** | CLI + hook (always on) | Rust Token Killer: the `hooks/rtk-rewrite.sh` PreToolUse hook rewrites Bash commands through `rtk` to cut output tokens. Zero passive cost. | [rtk-ai/rtk](https://github.com/rtk-ai/rtk) |
| **security-guidance** | Plugin (always on) | Security hook. Regex layer and commit/push review on; the Stop-time diff review is off (`ENABLE_STOP_REVIEW=0`). | [anthropics/claude-code](https://github.com/anthropics/claude-code) |
| **ui-ux-pro-max** | Plugin (toggle) | Design system, color/typography choices. Enable for design-heavy projects. | [nextlevelbuilder/ui-ux-pro-max-skill](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill) |
| **Context7** | Plugin (toggle) | Fast-evolving libs doc lookup (Next.js, React, Prisma...). Works anonymously; optional `ctx7 login` raises rate limits. | [context7.com](https://context7.com/) |
| **Context7** | CLI (`ctx7`) | Doc lookup for fast-evolving libs (Next.js, React, Prisma...), used through the `find-docs` skill. Works anonymously; optional `ctx7 login` raises rate limits. | [context7.com](https://context7.com/) |
| **pr-review-toolkit** | Plugin (toggle) | Multi-agent PR review. | [anthropics/claude-code](https://github.com/anthropics/claude-code) |
| **Graphify** | Python CLI | Codebase → knowledge graph → navigable wiki. Helps Claude map and search projects efficiently. | [pypi: graphifyy](https://pypi.org/project/graphifyy/) |
| **21st.dev** | External CLI + skill pack | Component catalog and UI generation (`21st`), browser login, no API key. 7 skills in `skills-external/21st-*`, linked per profile (see the 21st.dev CLI section). | [npm: @21st-dev/cli](https://www.npmjs.com/package/@21st-dev/cli) |
| **Higgsfield** | External CLI + skill pack (off by default) | Image, video, audio and brand media generation, metered credits (see the Higgsfield CLI section). | [higgsfield-ai/skills](https://github.com/higgsfield-ai/skills) |
| **Semgrep** | Python CLI (pinned) | SAST engine behind the security gate (`security-auditor`). | [pypi: semgrep](https://pypi.org/project/semgrep/) |
| **Impeccable** | npm CLI + skill (pinned) | Deterministic anti-slop detector (`npx impeccable detect`, 45 rules) and the `/impeccable` design verbs. | [npm: impeccable](https://www.npmjs.com/package/impeccable) |
| **Design skills** | Vendored | `emil-design-eng`, `frontend-design` (Anthropic example-skills), `design-motion-principles`: UI polish, anti-slop build, motion. | [emilkowalski/skill](https://github.com/emilkowalski/skill) · [kylezantos/design-motion-principles](https://github.com/kylezantos/design-motion-principles) |
| **agent-skills** | Vendored (commit-pinned) | `observability-and-instrumentation`, `deprecation-and-migration`, `ci-cd-and-automation`. | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) |
| **MengTo scroll skills** | Vendored (commit-pinned) | Five scroll-choreography skills: `scroll-world-storytelling`, `build-threejs-scroll-worlds`, `scroll-scrubbed-visual-sequence`, `scroll-scrubbed-word-reveal`, `scroll-progress-timeline`. | [MengTo/Skills](https://github.com/MengTo/Skills) |
Versions are pinned in `plugins.lock.json`. To update: edit the file, then re-run `install-plugins.sh`.
@@ -160,7 +170,7 @@ a different package, ships its own conflicting `graphify` bin) — see
|---|---|
| `/init-project` | Initialize a complete project from scratch (full orchestrator, 12+ steps) |
| `/ship-feature` | Ship a feature end-to-end with validation gates (full orchestrator) |
| `/onboard` | Onboard an existing project — generate CLAUDE.md, settings, .claudeignore |
| `/onboard` | Onboard an existing project: CLAUDE.md, settings, .claudeignore, archetype audits, report and a sequenced TODO backlog |
| `/feat` | Small feature implementation (1-5 files, lightweight) |
| `/bugfix` | Structured bug fix with root cause investigation |
| `/hotfix` | Quick fix for superficial bugs (typos, CSS, config — max 2 files) |
@@ -173,7 +183,7 @@ a different package, ships its own conflicting `graphify` bin) — see
| `/commit-change` | Smart commit grouping from staged/unstaged changes |
| `/gitflow` | Gitflow branch operations — bootstrap main+develop, start a typed branch, directed merge |
| `/release-candidate` | Cut a versioned release — finalize version.txt + CHANGELOG, merge develop→main, tag, push |
| `/deploy` | Run a project's deploy from its committed runbook — instantiate the delta, resume cold |
| `/deploy` | Compose the deploy checklist from a project's committed runbook (delta only); you run it, the skill resumes cold on your report |
| `/graphify` | Codebase knowledge graph — navigation for large-scope tasks |
| `/plugin-check` | Check active plugins vs project needs — recommend enable/disable |
| `/health` | Code quality dashboard (gstack) — setup diagnostic is `make doctor` |
@@ -191,6 +201,8 @@ a different package, ships its own conflicting `graphify` bin) — see
| `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) |
| `/profile` | Activate a skill profile (web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal) (default: full) |
| `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean |
| `/site-motion` | Site-level motion: scroll engine choice, page transitions, pin/scrub sequencing across a page or Astro route (design stack) |
| `/effort-low` … `/effort-max` | Effort shifters the orchestrators send per phase; type `/effort-max` to re-run a stuck turn at maximum |
> This table lists personal skills. Gstack skills (investigate, review, retro,
> office-hours, cso…) and marketplace plugins add many more — run
@@ -221,13 +233,15 @@ cd my-existing-project/
```
/ship-feature "feature description"
# → STEP 0: plugin check
# → STEP 1-2: brainstorm + plan (superpowers)
# → STEP 0: plugin check, project context, contract
# → STEP 1-2: brainstorm + plan (vendored superpowers skills)
# → STEP 2b: adversarial plan-challenge (3 lenses, report-only)
# → STEP 3: validation gate — user approval required
# → STEP 4-7: implement (TDD) → review → capitalize (memory)
# → STEP 8: sync README (doc-sync)
# → STEP 9: finish (merge / PR)
# → STEP 4: implement (TDD)
# → STEP 5: verify + secure (fresh verifier and security-auditor gates)
# → STEP 6-7: review → capitalize (memory)
# → STEP 8: doc sync (public docs, committed before finish)
# → STEP 9: finish, `gitflow finish` into develop on your explicit go
```
For small features (1-5 files), use `/feat` instead — no orchestration overhead.
@@ -241,7 +255,7 @@ Settings follow a hierarchy (highest priority first):
```
managed-settings.json → enterprise (cannot be overridden)
CLI flags → session only
.claude/settings.local → personal machine overrides (gitignored)
.claude/settings.local.json → personal machine overrides (gitignored)
.claude/settings.json → project rules (committed)
~/.claude/settings.json → global user rules (this repo)
```
@@ -326,9 +340,10 @@ npm i -g @21st-dev/cli
`make plugin` does both (Step 8.7 installs the CLI, then offers the login in
an interactive terminal) and installs the skill pack that drives it:
`21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`, plus the two
publishing skills `-registry` and `-design-sync`. The five design skills
follow the active profile: they are on under `full`, the default profile,
and under `design`, `web` and `web-full`. The two publishing skills,
publishing skills `-registry` and `-design-sync`. Of the five design skills, `21st-ui-build` and
`21st-cli-use` follow the active profile: on under `full`, the default profile, and under `design`,
`web` and `web-full`. The other three, `-ui-explore`, `-ui-review` and
`-ai`, are on under `max` only. The two publishing skills,
`-registry` and `-design-sync`, are in no profile and stay parked until
`bash lib/toggle-external.sh enable 21st` turns on all seven.
@@ -413,7 +428,7 @@ bash doctor.sh # full diagnostic (symlinks, plugins, permissions, t
bash update-all.sh # update all components (CLI, plugins, submodules, symlinks)
# Claude Code
/health # gstack code-quality dashboard (doctor.sh -> make doctor)
/health # gstack code-quality dashboard (setup diagnostic: make doctor)
/status # project snapshot (plugins, git, GSD milestone)
/plugin-check "description" # audit plugin config vs project needs
@@ -423,7 +438,9 @@ make plugin # install plugins only
make link # create/update symlinks into ~/.claude/
make doctor # diagnostic
make update # update Claude Code, config, submodules, plugins, and verify
make test # run deterministic tests (lib/tests/*.test.sh + lib/seo-data/*.test.sh + lib/gitflow-test.sh + lib/tests/run-*.sh)
make test [suite=lib/tests/x.test.sh] # hermetic deterministic tests: every suite, or one
make scan-secrets [repos="…"] # gitleaks sweep of this repo's history and ~/.claude, reports in .audit/ (never committed)
make help # list make targets
make onboard # onboard an existing project (run from its dir)
make seo-connect # connect a Google account for /seo FULL (OAuth consent)
make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/max/backend/design/dev/qa/audit/minimal)
@@ -433,7 +450,7 @@ make profile-reset # go to the default profile (full)
make new-skill name=myskill # scaffold agent + skill files
```
`doctor.sh` checks: symlinks, GStack submodule, vendored skills (curl-pinned externals in `plugins.lock.json` + `link.sh`'s `EXTERNAL_SKILLS`, per the active profile), Playwright browser cache, prerequisites (git, Node, Cargo, Python, Claude Code), plugins, permissions, token budget, config consistency.
`doctor.sh` checks: symlinks, GStack submodule, vendored skills (curl-pinned externals in `plugins.lock.json` + `link.sh`'s `EXTERNAL_SKILLS`, per the active profile), Playwright browser cache, prerequisites (git, Node, Cargo, Python, Claude Code), plugins, permissions, token budget, config consistency, git hooks (global core.hooksPath + generated githooks/), scratchpad (TMPDIR quota), Higgsfield CLI and session, seo-data layer.
---
@@ -441,4 +458,6 @@ make new-skill name=myskill # scaffold agent + skill files
[`USAGE.md`](./USAGE.md) — workflows and skill decision tree ·
[`ARCHITECTURE.md`](./ARCHITECTURE.md) — layout and principles ·
[`CHANGELOG.md`](./CHANGELOG.md) — version history.
[`CHANGELOG.md`](./CHANGELOG.md) — version history ·
[`MIGRATION.md`](./MIGRATION.md): upgrade guides ·
[`templates/settings/SETTINGS.md`](templates/settings/SETTINGS.md): permission tiers and guardrails
+22 -18
View File
@@ -83,7 +83,7 @@ Tu veux...
│ → /prune-memory ← curer / compresser les registres .claude/memory/
│
└─ Quelque chose ne marche pas ?
→ /health ← diagnostic complet (symlinks, plugins, permissions, token budget)
→ make doctor ← diagnostic d'installation (symlinks, plugins, permissions, hooks, token budget)
```
### Règle de décision simplifiée
@@ -122,7 +122,7 @@ Tu veux...
| Changer profil skills | `/profile` |
| Audit/polish design (anti-slop) | `/impeccable` |
| Sweep groupé tous axes (nettoyage + sécu + reconcile + doc) | `/tour` |
| Rien ne marche | `/health` |
| Rien ne marche | `make doctor` (terminal) |
---
@@ -147,10 +147,10 @@ Tu veux...
| `/commit-change` | Commits bien structurés | Groupe les changements par unité logique |
| `/gitflow` | Opérations de branches gitflow | Bootstrap main+develop, branche typée, merge dirigé |
| `/release-candidate` | Couper une release versionnée (develop en avance sur main) | Finalise version.txt + CHANGELOG, merge develop→main, tag, push |
| `/deploy` | Déployer via le runbook du projet | Instancie le delta depuis le dernier deploy, reprend à froid |
| `/deploy` | Déployer via le runbook du projet | Instancie le delta depuis le dernier deploy, reprend à froid ; tu exécutes la checklist, Claude ne déploie jamais |
| `/graphify` | Navigation codebase large-scope | Knowledge graph, pour tâches multi-fichiers |
| `/skills-perso` | Lister ses skills personnels | Skills créés dans ~/.claude/skills/ |
| `/health` | Quand quelque chose ne fonctionne pas | Lance doctor.sh |
| `/health` | Tableau de bord qualité du code (gstack) | Le diagnostic d'installation est `make doctor` |
| `/status` | Reprendre après une pause | Snapshot : plugins, git, GSD milestone |
| `/audit-delta` | Audit récurrent du delta depuis le dernier run | Axes : conformité / bugs / dead code / sécurité |
| `/capitalize` | Avant /clear ou /compact | Flush contexte non capitalisé + réconcilie .claude/tasks/TODO.md |
@@ -181,7 +181,7 @@ audits avec fix), xhigh pour l'architecture et l'audit avant validation
(`/ship-feature`, `/onboard`, `/analyze`). Les orchestrateurs décalent
ensuite le niveau par phase (`lib/effort-shift.md`), et `/effort-max` tapé à
la main relance un tour bloqué au maximum. Les skills externes vendorés
(pile design, superpowers, 21st) reçoivent leur niveau de
(pile design, superpowers, agent-skills, skills scroll MengTo, 21st) reçoivent leur niveau de
`lib/effort-pins.txt`. Un skill chargé seul par Claude n'applique pas son
niveau : il doit partir avec un autre appel d'outil dans le même message.
@@ -220,9 +220,9 @@ Hotfix/quick fix → tout OFF (skills superpowers vendorisés, toujours
# → STEP 1 : interview (skip si prompt complet)
# → STEP 4 : ★ GATE — valider l'architecture
# → STEP 7 : ★ GATE — valider le plan d'implémentation
# → STEP 8-10 : implémentation TDD + review
# → STEP 10b-c: capitalize mémoire + sync README (avant finish)
# → STEP 11 : finish (merge / commit initial)
# → STEP 8-10 : implémentation TDD + gates verify/sécurité + review
# → STEP 10b-c: capitalize mémoire + sync docs publiques (avant finish)
# → STEP 11 : finish, `gitflow finish` vers develop sur ton feu vert explicite
# 3. Features suivantes
/ship-feature "description de la feature"
@@ -240,7 +240,7 @@ Hotfix/quick fix → tout OFF (skills superpowers vendorisés, toujours
# Dans un terminal (depuis le dossier projet) :
gsd # démarrer une session
/gsd init # initialise .gsd/ + ROADMAP (une fois, à la demande)
/gsd init # initialise .gsd/ + milestones (une fois, à la demande)
/gsd auto # mode autonome, walk away
# Pour suivre :
@@ -274,9 +274,12 @@ cd mon-projet-existant/
| 1 | Archetype detection (scan ~/.claude/lib/project-archetypes/*.md) | archétype SELECTED + implications auto |
| 1b | Gate monorepo (A/B/C si détecté) | mode choisi |
| 2 | Config baseline (onboarder agent) | CLAUDE.md, settings.json, .claudeignore, .claude/tasks/ + .claude/memory/ + .claude/audits/ |
| 2.5| Lib d'animation (`motion`), proposée, opt-in | dépendance si acceptée |
| 2.6| Gitflow init (main + develop, hooks) | branches + .githooks/ |
| 3 | Interview deep = business minimum (users, deadlines, équipe, légal, perfs) + adaptative par archétype | brief enrichi |
| 3.5| ctx7 doc audit — fast-libs détectées, cache pré-fetché si besoin | .ctx7-cache/ |
| 4 | Graphify (proposé dès 200 fichiers code, l'utilisateur décide) | graphify-out/GRAPH_REPORT.md |
| 4.5| Espace d'audit + contexte archétype | .onboard-audit/archetype-context.md |
| 5 | Analyze read-only (analyzer agent) | .onboard-audit/analyze.md |
| 6 | Audits parallèles selon archétype : | .onboard-audit/*.md (9 fichiers max) |
| | — dette tech (general-purpose, audit read-only) |
@@ -287,6 +290,7 @@ cd mon-projet-existant/
| | — performance (Lighthouse ou static bundle audit) |
| | — accessibilité (axe ou static a11y audit) |
| 7 | Synthèse structurée dans .claude/audits/ | ONBOARD_REPORT, AUDIT_GOOD, AUDIT_ISSUES, AUDIT_PROPOSALS |
| 7b | Challenge adversarial des propositions avant la gate | AUDIT_PROPOSALS.md challengé |
| 8 | Validation gate utilisateur | choix A/B/C/D/E |
| 9 | Backlog .claude/tasks/TODO.md séquencé avec /skill recommandé par tâche | .claude/tasks/TODO.md |
@@ -308,7 +312,7 @@ cat .claude/audits/ONBOARD_REPORT.md
# Multi-session (GSD) : gsd init à la main — voir docs gsd-pi
```
**Archétypes supportés (P1)** : static-html, wordpress, nextjs-app-router, astro-static, react-spa, rest-api-node, rest-api-python, cli-tool, library, dotfiles-meta.
**Archétypes supportés** : astro-static, cli-tool, data-notebook, desktop-electron, docker-compose-infra, dotfiles-meta, drupal, firmware-embedded, game-engine-native, ghost, library, mobile-expo, mobile-flutter, nextjs-app-router, react-spa, rest-api-node, rest-api-python, shopify, static-html, strapi, terraform-infra, web-game, woocommerce, wordpress.
Ajouter un archétype : créer `~/.claude/lib/project-archetypes/<name>.md` (voir `_TEMPLATE.md`).
### Pattern D — Hotfix / bugfix · ~200-800t
@@ -340,7 +344,7 @@ Ajouter un archétype : créer `~/.claude/lib/project-archetypes/<name>.md` (voi
```
# Feature simple, pas d'orchestration lourde
/feat "ajouter un endpoint GET /api/v1/users/:id/stats"
# → planning léger, implémentation directe, tests
# → plan + challenge, exécuteur `feater`, gates fraîches verifier + sécurité
# Pas de brainstorming superpowers, pas de gate de validation
```
@@ -351,7 +355,7 @@ Ajouter un archétype : créer `~/.claude/lib/project-archetypes/<name>.md` (voi
| Scope | Feature complète, multi-fichiers | 1-5 fichiers max |
| Orchestration | Pipeline superpowers complet | Planning léger, direct |
| Gate de validation | Oui | Non |
| Code review auto | Oui (superpowers) | Non |
| Code review auto | Oui (superpowers) | Gates verifier + security-auditor |
| Tokens estimés | ~1500-3000t | ~300-600t |
---
@@ -530,7 +534,7 @@ Convention: snake_case Python, camelCase TypeScript."
**Workflow long avec GSD v2 :**
```
# Après /init-project, on initialise GSD à la demande (plus auto-bootstrappé).
# Le ROADMAP.md généré par `gsd init` contiendra :
# Les roadmaps de milestone (`.gsd/milestones/<ID>/<ID>-ROADMAP.md`) contiendront :
# Milestone 1: Boutique in-app + Stripe
# Milestone 2: PvP + matchmaking
# Milestone 3: Leaderboard + saisons
@@ -538,7 +542,7 @@ Convention: snake_case Python, camelCase TypeScript."
# Dans un terminal :
cd cardforge/
gsd # démarre session GSD
/gsd init # crée .gsd/ + ROADMAP (à la demande — plus auto à l'init)
/gsd init # crée .gsd/ + milestones (à la demande — plus auto à l'init)
/gsd auto # GSD travaille sur Milestone 1 de façon autonome
# → research Stripe API + docs
# → plan décomposé en tâches
@@ -556,7 +560,7 @@ gsd # démarre session GSD
gsd
/gsd quick "Implémenter la boutique in-app avec Stripe"
# ou
/gsd auto # si ROADMAP.md est déjà à jour
/gsd auto # si la roadmap du milestone est à jour
```
---
@@ -876,7 +880,7 @@ PROJECT STATUS
CONFIG
Version : v2.5.0
Plugins ON: context7 (~200t), skills superpowers vendorisés (toujours actifs, 0 t passif)
GSD v2 : installed (2.64.0)
GSD v2 : installed (3.0.0)
PROJECT
CLAUDE.md : found
@@ -951,13 +955,13 @@ Updated: Slice 4 plan — Payment Element instead of CardElement
Continue? (yes)
```
GSD v2 met à jour le plan dans `.gsd/ROADMAP.md` sans perdre le travail déjà fait.
GSD v2 met à jour le plan dans la base GSD (`.gsd/gsd.db`) sans perdre le travail déjà fait.
#### Ce que ce workflow démontre
- **`/status`** est le point d'entrée naturel après une pause — snapshot complet en 1 commande.
- **GSD v2 `step mode`** est préférable à `auto` après une longue pause — permet de vérifier que les décisions sont toujours valides.
- **`.gsd/ROADMAP.md`** est la source de vérité du progress — parsé par `/status` et par GSD lui-même.
- **La base GSD (`.gsd/gsd.db`)** est la source de vérité du progress; `/status` la lit via `gsd headless query`.
- **`/gsd discuss`** permet de modifier l'architecture en cours de route sans recommencer depuis zéro.
---
+29 -6
View File
@@ -471,7 +471,8 @@ echo ""
install_plugin() {
local name="$1"
local source="$2"
if claude plugin list 2>/dev/null | grep -qi "$name"; then
# CLI call inside the condition: no pipe (SIGPIPE on macOS), no errexit abort
if grep -qi "$name" <<<"$(claude plugin list 2>/dev/null)"; then
ok "$name (already installed)"
return
fi
@@ -1140,7 +1141,8 @@ apply_effort_pins "$REPO" || warn "effort pins: map lines rejected — fix lib/e
# the session. Mirrors the ctx7 auth block (Step 6), stdin-only test included.
if command -v 21st &>/dev/null; then
# `whoami` is a local token read (no network): "Logged in as <user> (saved …)."
TFD_WHO="$(21st whoami 2>/dev/null | head -1)"
TFD_WHO="$(21st whoami 2>/dev/null)" || true
TFD_WHO="${TFD_WHO%%$'\n'*}" # first line, no head(1) in a pipeline
if [[ "$TFD_WHO" == "Logged in as "* ]]; then
ok "21st: ${TFD_WHO%.}"
elif [ -t 0 ]; then
@@ -1196,16 +1198,37 @@ fi
# Remove obsolete effort config — effort is now set in settings.json
# ("effortLevel"), which supersedes both the old CLAUDE_EFFORT env var and the
# `claude --effort max` alias (the alias would even override settings.json).
# sed_profile <expr> — in-place sed on $SHELL_PROFILE. BSD in-place sed needs
# a suffix, and -i.bak would clobber a hand-made .bak, so write a unique
# sibling temp and copy it back. rc 1 leaves the profile as it was.
sed_profile() {
local tmp
tmp="$(mktemp "$SHELL_PROFILE.XXXXXX")" || return 1
# the profile is truncated by the copy-back: past that point $tmp is the
# only complete copy, so a failed write keeps it
sed "$1" "$SHELL_PROFILE" >"$tmp" || { rm -f "$tmp"; return 1; }
if ! cat "$tmp" >"$SHELL_PROFILE"; then
warn "profile write failed — full copy kept at $tmp" >&2
return 1
fi
rm -f "$tmp"
}
EFFORT_CLEANED=0
if grep -qF 'export CLAUDE_EFFORT=max' "$SHELL_PROFILE" 2>/dev/null; then
sed -i '/export CLAUDE_EFFORT=max/d' "$SHELL_PROFILE"; EFFORT_CLEANED=1
if sed_profile '/export CLAUDE_EFFORT=max/d'; then
EFFORT_CLEANED=1
else
warn "could not remove CLAUDE_EFFORT from $SHELL_PROFILE"
fi
fi
if grep -qF "alias claude='claude --effort max'" "$SHELL_PROFILE" 2>/dev/null; then
sed -i "\#alias claude='claude --effort max'#d" "$SHELL_PROFILE"; EFFORT_CLEANED=1
if sed_profile "\#alias claude='claude --effort max'#d"; then
EFFORT_CLEANED=1
else
warn "could not remove the claude effort alias from $SHELL_PROFILE"
fi
fi
if [ "$EFFORT_CLEANED" -eq 1 ]; then
# Remove orphaned comment lines left before the deleted entries
sed -i '/^# Claude Code — added by install-plugins.sh$/{ N; /^\n$/d; }' "$SHELL_PROFILE"
info "Removed obsolete effort alias/env from $SHELL_PROFILE (effort set in settings.json)"
fi
+21 -7
View File
@@ -70,6 +70,7 @@ ensure_claude_on_path() {
for cand in \
"$HOME/.claude/local/claude" \
"$HOME/.local/bin/claude" \
/opt/homebrew/bin/claude \
/usr/local/bin/claude; do
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
done
@@ -94,6 +95,7 @@ ensure_21st_on_path() {
local cand
for cand in \
"$HOME/.local/bin/21st" \
/opt/homebrew/bin/21st \
/usr/local/bin/21st; do
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
done
@@ -113,7 +115,8 @@ ensure_21st_on_path
# out; the FIRST LINE of stdout does. A token env, when already exported by
# the user's shell profile, wins without a CLI call (never requested here:
# tool calls don't share a shell, and a secret doesn't belong in a comment
# or the transcript). `timeout 15` bounds a hung CLI; stdin is closed so a
# or the transcript). A perl `alarm` of 15 s bounds a hung CLI (GNU
# `timeout` is absent from the macOS system PATH); stdin is closed so a
# CLI that reads stdin can't eat the gate's own `read` loop; stderr never
# enters the match (stdout only). Echoes: in | out | unknown:<diagnostic>.
twentyfirst_auth_state() {
@@ -122,11 +125,13 @@ twentyfirst_auth_state() {
return
fi
local line rc
if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)"; then
if line="$(perl -e 'alarm shift; exec @ARGV' 15 21st whoami \
2>/dev/null </dev/null)"; then
rc=0
else
rc=$?
fi
line="${line%%$'\n'*}" # first line, without head(1) in a pipeline
if [ "$rc" -eq 0 ]; then
case "$line" in
"Logged in as "*) echo in; return ;;
@@ -160,14 +165,22 @@ tool_active() {
;;
plugin)
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then echo unknown; return; fi
if "$CLAUDE_BIN" plugin list 2>/dev/null \
| awk -v p="^[[:space:]]*❯ ${name}@" '$0 ~ p {f=1; next} f && /Status:/ {print; exit}' \
| grep -q "✔ enabled"
# capture first, then match: an early-exit awk/grep -q in a pipe
# SIGPIPEs the producer (rc 141 under pipefail on macOS)
local plist
plist="$("$CLAUDE_BIN" plugin list 2>/dev/null)" || true
if grep -q "✔ enabled" < <(awk -v p="^[[:space:]]*❯ ${name}@" \
'$0 ~ p {f=1; next} f && /Status:/ {print; exit}' <<<"$plist")
then echo active; else echo inactive; fi
;;
mcp)
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then echo unknown; return; fi
if "$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${name}"; then echo active; else echo inactive; fi
if grep -q "^${name}" \
<<<"$("$CLAUDE_BIN" mcp list 2>/dev/null)"; then
echo active
else
echo inactive
fi
;;
cli)
command -v "$name" >/dev/null 2>&1 || { echo inactive; return; }
@@ -222,7 +235,8 @@ print_unverified() {
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
fi
local entry name diag
for entry in "${unverified_cli[@]}"; do
# bash 3.2 (macOS /bin/bash) errors on an empty array under set -u
for entry in ${unverified_cli[@]+"${unverified_cli[@]}"}; do
name="${entry%% (*}"
diag="${entry#*\(}"; diag="${diag%\)}"
echo " $name could not answer: $diag —" \
+2 -1
View File
@@ -67,7 +67,8 @@ _path_exceeds_reason() {
printf 'new/untracked doc (a creation, not a MINOR drift-patch): %s\n' "$p"
return
fi
if git diff HEAD -- "$p" | grep -Eq '^\+#{1,6}[ \t]'; then
# producer out of the pipe: grep -q would SIGPIPE git (fails open on macOS)
if grep -Eq '^\+#{1,6}[ \t]' < <(git diff HEAD -- "$p"); then
printf 'adds a section heading (structural change, not a factual tweak): %s\n' "$p"
return
fi
+26 -26
View File
@@ -48,7 +48,7 @@ chk "socle: re-ignore PENDING" 'grep -qxF ".claude/deploy/PENDING.json" .gitigno
chk "hook installed" '[ -x .githooks/pre-commit ] && [ "$(git config core.hooksPath)" = .githooks ]'
chk "tree CLEAN after init" '[ -z "$(git status --porcelain)" ]'
chk "hook TRACKED in commit" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
chk "socle IN root commit" 'git show HEAD:.gitignore | grep -qxF ".claude/deploy/PENDING.json"'
chk "socle IN root commit" 'grep -qxF ".claude/deploy/PENDING.json" < <(git show HEAD:.gitignore)'
echo "T2b — init existing (master→main rename + adoption via chore/gitflow-adopt merge)"
newrepo existing
@@ -59,10 +59,10 @@ hookon
gitflow_init >/dev/null 2>&1
chk "master→main renamed" 'git rev-parse --verify -q refs/heads/main >/dev/null && ! git rev-parse --verify -q refs/heads/master >/dev/null'
chk "develop created" 'git rev-parse --verify -q refs/heads/develop >/dev/null'
chk "adoption commit" 'git log main --oneline | grep -q "adopt gitflow"'
chk "adoption commit" 'grep -q "adopt gitflow" < <(git log main --oneline)'
chk "existing tree CLEAN" '[ -z "$(git status --porcelain)" ]'
chk "existing hook tracked" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
chk "kept project rule" 'git show HEAD:.gitignore | grep -qxF "node_modules/"'
chk "kept project rule" 'grep -qxF "node_modules/" < <(git show HEAD:.gitignore)'
echo "T2c — init existing under a LIVE pre-commit (global hooks simulated): socle lands via merge"
newrepo live; git symbolic-ref HEAD refs/heads/master
@@ -73,9 +73,9 @@ git config core.hooksPath "$WORK/globalhooks" # stands in for git's GLOBAL
# shellcheck disable=SC2034
live_rc=0; GITFLOW_NO_PUSH=1 gitflow_init >/dev/null 2>&1 || live_rc=$?
chk "T2c init succeeds under the live hook (rc 0)" "[ $live_rc -eq 0 ]"
chk "T2c socle reached main via a merge commit" 'git log main --oneline -1 | grep -q "Merge chore/gitflow-adopt"'
chk "T2c .gitignore socle on main" 'git show main:.gitignore | grep -qxF ".claude/deploy/PENDING.json"'
chk "T2c hooks tracked on main" 'git ls-tree -r main --name-only | grep -q "^.githooks/pre-commit$"'
chk "T2c socle reached main via a merge commit" 'grep -q "Merge chore/gitflow-adopt" < <(git log main --oneline -1)'
chk "T2c .gitignore socle on main" 'grep -qxF ".claude/deploy/PENDING.json" < <(git show main:.gitignore)'
chk "T2c hooks tracked on main" 'grep -q "^.githooks/pre-commit$" < <(git ls-tree -r main --name-only)'
chk "T2c adoption branch deleted" '! git rev-parse --verify -q refs/heads/chore/gitflow-adopt >/dev/null'
chk "T2c develop created from main" '[ "$(git rev-parse develop)" = "$(git rev-parse main)" ]'
chk "T2c repo hook active afterwards" '[ "$(git config core.hooksPath)" = .githooks ]'
@@ -107,7 +107,7 @@ newrepo finfeat; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start feature f1 >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
main_before="$(git rev-parse main)"
gitflow_finish >/dev/null 2>&1
chk "merged into develop" 'git log develop --oneline | grep -q "Merge feature/f1 into develop"'
chk "merged into develop" 'grep -q "Merge feature/f1 into develop" < <(git log develop --oneline)'
chk "main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
chk "branch deleted" '! git rev-parse --verify -q refs/heads/feature/f1 >/dev/null'
@@ -117,7 +117,7 @@ gitflow_start chore c1 >/dev/null 2>&1
mkdir -p .claude/memory; echo m>.claude/memory/x.md; git add -A; git commit -q -m "chore(memory)"
main_before="$(git rev-parse main)"
gitflow_finish >/dev/null 2>&1
chk "chore merged into develop" 'git log develop --oneline | grep -q "Merge chore/c1 into develop"'
chk "chore merged into develop" 'grep -q "Merge chore/c1 into develop" < <(git log develop --oneline)'
chk "chore main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
chk "chore branch deleted" '! git rev-parse --verify -q refs/heads/chore/c1 >/dev/null'
@@ -125,8 +125,8 @@ echo "T7 — finish hotfix → main + develop fan-out"
newrepo finhot; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start hotfix h1 >/dev/null 2>&1; echo p>patch.txt; git add patch.txt; git commit -q -m patch
gitflow_finish >/dev/null 2>&1
chk "hotfix in main" 'git log main --oneline | grep -q "Merge hotfix/h1 into main"'
chk "hotfix in develop" 'git log develop --oneline | grep -q "Merge hotfix/h1 into develop"'
chk "hotfix in main" 'grep -q "Merge hotfix/h1 into main" < <(git log main --oneline)'
chk "hotfix in develop" 'grep -q "Merge hotfix/h1 into develop" < <(git log develop --oneline)'
chk "hotfix branch gone" '! git rev-parse --verify -q refs/heads/hotfix/h1 >/dev/null'
echo "T8 — finish hotfix also lands in OPEN release"
@@ -134,7 +134,7 @@ newrepo finhotrel; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start release 1.0 >/dev/null 2>&1; echo r>rel.txt; git add rel.txt; git commit -q -m relwork
gitflow_start hotfix h2 >/dev/null 2>&1; echo p>p2.txt; git add p2.txt; git commit -q -m patch2
gitflow_finish >/dev/null 2>&1
chk "hotfix in open release" 'git log release/1.0 --oneline | grep -q "Merge hotfix/h2 into release/1.0"'
chk "hotfix in open release" 'grep -q "Merge hotfix/h2 into release/1.0" < <(git log release/1.0 --oneline)'
echo "T9 — reconcile is additive + idempotent + preserves project rules"
newrepo recon; echo a>a; git add a; git commit -q -m a
@@ -181,11 +181,11 @@ mism_out="$(gitflow_finish bugfix other 2>&1)"; mism_rc=$?
chk "arg-mismatch → nonzero rc" "[ $mism_rc -ne 0 ]"
chk "arg-mismatch → HEAD untouched" '[ "$(git symbolic-ref --short HEAD)" = feature/standon ]'
chk "arg-mismatch → branch kept" 'git rev-parse --verify -q refs/heads/feature/standon >/dev/null'
chk "arg-mismatch → develop NOT merged" '! git log develop --oneline | grep -q "Merge feature/standon into develop"'
chk "arg-mismatch → develop NOT merged" '! grep -q "Merge feature/standon into develop" < <(git log develop --oneline)'
chk "arg-mismatch → message names both" 'printf "%s" "$mism_out" | grep -q "current branch" && printf "%s" "$mism_out" | grep -q "bugfix/other"'
# match: naming the current branch explicitly finishes exactly like the no-arg path
gitflow_finish feature standon >/dev/null 2>&1
chk "arg-match → merged into develop" 'git log develop --oneline | grep -q "Merge feature/standon into develop"'
chk "arg-match → merged into develop" 'grep -q "Merge feature/standon into develop" < <(git log develop --oneline)'
chk "arg-match → branch deleted" '! git rev-parse --verify -q refs/heads/feature/standon >/dev/null'
echo "T13 — finish release fan-out (main+develop+delete), 2 open releases + bugfix→develop-only"
@@ -193,8 +193,8 @@ newrepo finrel; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start release 9.9.9 >/dev/null 2>&1; echo v>VERSION; git add VERSION; git commit -q -m "bump 9.9.9"
finish_rc=0; gitflow_finish >/dev/null 2>&1 || finish_rc=$?
chk "T13a finish rc 0" "[ $finish_rc -eq 0 ]"
chk "T13a main has release commit" 'git log main --oneline | grep -q "bump 9.9.9"'
chk "T13a develop has release commit" 'git log develop --oneline | grep -q "bump 9.9.9"'
chk "T13a main has release commit" 'grep -q "bump 9.9.9" < <(git log main --oneline)'
chk "T13a develop has release commit" 'grep -q "bump 9.9.9" < <(git log develop --oneline)'
chk "T13a release branch deleted" '! git rev-parse --verify -q refs/heads/release/9.9.9 >/dev/null'
newrepo finrel2; echo a>a; hookon; gitflow_init >/dev/null 2>&1
@@ -202,14 +202,14 @@ gitflow_start release 1.0 >/dev/null 2>&1; echo r1>r1; git add r1; git commit -q
gitflow_start release 2.0 >/dev/null 2>&1; echo r2>r2; git add r2; git commit -q -m rel2
gitflow_start hotfix hboth >/dev/null 2>&1; echo p>p; git add p; git commit -q -m hotfixboth
gitflow_finish >/dev/null 2>&1
chk "T13b hotfix in release/1.0" 'git log release/1.0 --oneline | grep -q "Merge hotfix/hboth into release/1.0"'
chk "T13b hotfix in release/2.0" 'git log release/2.0 --oneline | grep -q "Merge hotfix/hboth into release/2.0"'
chk "T13b hotfix in release/1.0" 'grep -q "Merge hotfix/hboth into release/1.0" < <(git log release/1.0 --oneline)'
chk "T13b hotfix in release/2.0" 'grep -q "Merge hotfix/hboth into release/2.0" < <(git log release/2.0 --oneline)'
newrepo finbugfix; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start bugfix bx >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m bugfixwork
main_before="$(git rev-parse main)"
gitflow_finish >/dev/null 2>&1
chk "T13c develop has bugfix commit" 'git log develop --oneline | grep -q "Merge bugfix/bx into develop"'
chk "T13c develop has bugfix commit" 'grep -q "Merge bugfix/bx into develop" < <(git log develop --oneline)'
chk "T13c main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
chk "T13c bugfix branch deleted" '! git rev-parse --verify -q refs/heads/bugfix/bx >/dev/null'
@@ -259,7 +259,7 @@ git add secret.txt
gl_out="$(git commit -q -m "add secret" 2>&1)"; gl_rc=$?
chk "T16a fake secret on feature branch → blocked" "[ $gl_rc -ne 0 ]"
chk "T16a message mentions gitleaks" 'printf "%s" "$gl_out" | grep -qi gitleaks'
chk "T16a nothing committed" '! git log --oneline 2>/dev/null | grep -q "add secret"'
chk "T16a nothing committed" '! grep -q "add secret" < <(git log --oneline 2>/dev/null)'
git restore --staged secret.txt 2>/dev/null || true; rm -f secret.txt
# T16b — a clean commit is unaffected
@@ -295,19 +295,19 @@ gitflow_finish >/dev/null 2>&1
# <sha>:path` proves BDR-065's "git history = the archive" recovery.
# shellcheck disable=SC2034 # pf_add_sha is used in the deferred chk eval string
pf_add_sha="$(git log develop --full-history --format=%H -- docs/superpowers/specs/s.md | tail -1)"
chk "T17a merged into develop" 'git log develop --oneline | grep -q "Merge feature/pf into develop"'
chk "T17a merged into develop" 'grep -q "Merge feature/pf into develop" < <(git log develop --oneline)'
chk "T17a develop TIP has no transient" '[ -z "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
chk "T17a purge commit on record" 'git log develop --oneline | grep -q "purge transient planning artifacts"'
chk "T17a purge commit on record" 'grep -q "purge transient planning artifacts" < <(git log develop --oneline)'
chk "T17a artifact recoverable from history" '[ "$(git show "$pf_add_sha":docs/superpowers/specs/s.md 2>/dev/null)" = spec ]'
chk "T17a non-transient code survives" 'git ls-tree -r develop --name-only | grep -qx feat.txt'
chk "T17a non-transient code survives" 'grep -qx feat.txt < <(git ls-tree -r develop --name-only)'
chk "T17a feature branch deleted" '! git rev-parse --verify -q refs/heads/feature/pf >/dev/null'
# T17b — no artifacts → purge is a silent no-op, no spurious commit
newrepo purgenone; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start feature pn >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
gitflow_finish >/dev/null 2>&1
chk "T17b merged into develop" 'git log develop --oneline | grep -q "Merge feature/pn into develop"'
chk "T17b no purge commit created" '! git log develop --oneline | grep -q "purge transient"'
chk "T17b merged into develop" 'grep -q "Merge feature/pn into develop" < <(git log develop --oneline)'
chk "T17b no purge commit created" '! grep -q "purge transient" < <(git log develop --oneline)'
# T17c — opt-out (GITFLOW_PURGE_TRANSIENT=0) keeps the artifacts on develop
newrepo purgeoff; echo a>a; hookon; gitflow_init >/dev/null 2>&1
@@ -330,7 +330,7 @@ newrepo pushsrc; echo a>a; hookon; gitflow_init >/dev/null 2>&1
bare="$WORK/pushsrc.git"; git init -q --bare "$bare"; git remote add origin "$bare"
git push -q origin main develop 2>/dev/null
gitflow_start feature ap >/dev/null 2>&1
chk "T18a start pushed the branch" 'git ls-remote --heads origin feature/ap | grep -q feature/ap'
chk "T18a start pushed the branch" 'grep -q feature/ap < <(git ls-remote --heads origin feature/ap)'
echo w>w; git add w; git commit -q -m w 2>/dev/null
chk "T18b commit pushed by post-commit" '[ "$(git rev-parse HEAD)" = "$(git -C "$bare" rev-parse feature/ap)" ]'
echo w2>>w; git add w; GITFLOW_NO_PUSH=1 git commit -q -m w2 2>/dev/null
@@ -381,7 +381,7 @@ chk "T20b pre-commit rewritten == emitted" 'diff -q <(_gitflow_emit_pre_commit)
chk "T20c post-commit restored" '[ -x .githooks/post-commit ]'
chk "T20d second run is silent" '[ -z "$(gitflow_reconcile_hooks 2>/dev/null)" ]'
mkdir -p sub; cd sub || exit 1; echo "# stale" >> ../.githooks/post-merge
chk "T20e works from a subdirectory" 'gitflow_reconcile_hooks 2>/dev/null | grep -q post-merge'
chk "T20e works from a subdirectory" 'grep -q post-merge < <(gitflow_reconcile_hooks 2>/dev/null)'
cd .. || exit 1
newrepo plain; echo a>a; git add a; git commit -q -m a
chk "T20f non-gitflow repo → silent, no .githooks created" '[ -z "$(gitflow_reconcile_hooks 2>/dev/null)" ] && [ ! -d .githooks ]'
+20 -1
View File
@@ -49,6 +49,25 @@ if ! declare -F info >/dev/null 2>&1; then
info() { echo -e "${BLUE}→${NC} $1"; }
fi
# _gstack_links_realpath_m <path> — prints <path> resolved through its
# nearest existing ancestor (BSD realpath has no -m). rc 1 + warning when
# the path holds a `..` component: it cannot be resolved lexically.
_gstack_links_realpath_m() {
local path="${1%/}" rest="" dir base
case "/$path/" in
*/../*) warn "refusing path with '..': $1" >&2; return 1 ;;
esac
dir="$path"
while [ -n "$dir" ] && [ ! -d "$dir" ]; do
base="${dir##*/}"
rest="/$base$rest"
case "$dir" in */*) dir="${dir%/*}" ;; *) dir=. ;; esac
done
[ -n "$dir" ] || dir=/
dir="$(CDPATH='' cd -P -- "$dir" && pwd -P)" || return 1
printf '%s%s\n' "${dir%/}" "$rest"
}
# _gstack_links_guard_dst <src> <dst> — removes a stale <dst> symlink
# (gstack ./setup plants `skills/gstack -> skills-external/gstack` when
# the dir is absent), refuses ever writing INTO <src> (dst resolving
@@ -61,7 +80,7 @@ _gstack_links_guard_dst() {
rm -f "$dst"
fi
real_src="$(realpath "$src")"
real_dst="$(realpath -m "$dst")"
real_dst="$(_gstack_links_realpath_m "$dst")" || return 1
case "$real_dst" in
"$real_src"/*|"$real_src")
warn "refusing to write into the gstack submodule: $dst" >&2
+14 -8
View File
@@ -265,13 +265,15 @@ skill_status() {
# `claude plugin list` is the source of truth — settings.json may be
# ahead of or behind reality if the user toggled outside this tool.
if command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
# Match the plugin block by name then check Status line
if "$CLAUDE_BIN" plugin list 2>/dev/null \
| awk -v p="$skill" '
# Match the plugin block by name then check Status line. List is
# captured first: an early-exit awk/grep -q in a pipe SIGPIPEs the
# producer (rc 141 under pipefail on macOS).
local plist
plist="$("$CLAUDE_BIN" plugin list 2>/dev/null)" || true
if grep -q "✔ enabled" < <(awk -v p="$skill" '
/^[[:space:]]*❯ '"$skill"'@/ { found=1; next }
found && /Status:/ { print; exit }
' \
| grep -q "✔ enabled"; then
' <<<"$plist"); then
echo "enabled"
else
echo "disabled"
@@ -282,7 +284,7 @@ skill_status() {
;;
mcp)
if command -v "$CLAUDE_BIN" >/dev/null 2>&1 && \
"$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${skill}"; then
grep -q "^${skill}" <<<"$("$CLAUDE_BIN" mcp list 2>/dev/null)"; then
echo "enabled"
else
echo "disabled"
@@ -371,7 +373,10 @@ enable_skill() {
if [ "$(skill_status "$skill" "$type")" = "enabled" ]; then
: # already on
elif command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
if "$CLAUDE_BIN" plugin enable "${skill}@${marketplace}" 2>&1 | grep -qiE "enabled|already"; then
# CLI call stays inside the condition: a failing CLI must not abort
if grep -qiE "enabled|already" \
<<<"$("$CLAUDE_BIN" plugin enable \
"${skill}@${marketplace}" 2>&1)"; then
ok "enabled plugin: ${skill}@${marketplace}"
else
warn "could not enable plugin: ${skill}@${marketplace}"
@@ -441,7 +446,8 @@ disable_skill() {
if [ "$(skill_status "$skill" "$type")" = "disabled" ]; then
: # already off
elif command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
if "$CLAUDE_BIN" plugin disable "$key" 2>&1 | grep -qiE "disabled|already"; then
if grep -qiE "disabled|already" \
<<<"$("$CLAUDE_BIN" plugin disable "$key" 2>&1)"; then
ok "disabled plugin: $key"
else
warn "could not disable plugin: $key"
+8 -4
View File
@@ -23,9 +23,13 @@ has "list shows client-a" "$LIST" '"client-a"'
has "list shows client-b" "$LIST" '"client-b"'
has "list shows a property" "$LIST" 'sc-domain:a.com'
hasnt "list redacts refresh tokens" "$LIST" 'RT_AAA'
PERM="$(stat -c '%a' "$STORE")"
# stat -c is GNU only; python3 gives the same octal mode on BSD and GNU
octal_perm() {
python3 -I -c 'import os,stat,sys; print(oct(stat.S_IMODE(os.stat(sys.argv[1]).st_mode))[2:])' "$1"
}
PERM="$(octal_perm "$STORE")"
[ "$PERM" = "600" ] && ok "store file is 0600" || no "store file 0600" "got $PERM"
DPERM="$(stat -c '%a' "$(dirname "$STORE")")"
DPERM="$(octal_perm "$(dirname "$STORE")")"
[ "$DPERM" = "700" ] && ok "store dir is 0700" || no "store dir 0700" "got $DPERM"
rm -rf "$TMP"
@@ -136,7 +140,7 @@ import safe_fetch as sf
try: sf.safe_fetch("file:///etc/passwd"); print("OK")
except sf.UnsafeTarget: print("REFUSED")')"
has "non-http scheme refused" "$SCHEME" 'REFUSED'
IMP="$(/bin/grep -E "^(import|from) " "$SD/safe_fetch.py" | /bin/grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse")"
IMP="$(/usr/bin/grep -E "^(import|from) " "$SD/safe_fetch.py" | /usr/bin/grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse")"
[ "$IMP" = "0" ] && ok "safe_fetch is stdlib-only" || no "safe_fetch is stdlib-only" "$IMP non-stdlib imports"
hasnt "no requests dependency" "$(cat "$SD/safe_fetch.py")" 'import requests'
@@ -477,7 +481,7 @@ has "clear reports ok" "$CL" '"status": "ok"'
has "clear reports count" "$CL" '"cleared": 1'
L7="$(python3 "$SD/tokenstore.py" list --file "$S6")"
has "clear empties store" "$L7" '"accounts": []'
PERM6="$(stat -c '%a' "$S6")"
PERM6="$(octal_perm "$S6")"
[ "$PERM6" = "600" ] && ok "store stays 0600 after clear" || no "store 0600 after clear" "got $PERM6"
# via the real fetch.sh dispatch layer
python3 "$SD/tokenstore.py" set --file "$S6" --label back --refresh-token RT_BACK \
+1 -1
View File
@@ -18,7 +18,7 @@ bad() { echo "FAIL $1 — $2"; FAIL=$((FAIL + 1)); }
# own probes must never resolve a REAL 21st — else CLI_ABSENT_10 (and every
# other case) would silently exercise this machine's CLI instead of the stub.
if PATH=/usr/bin:/bin command -v 21st >/dev/null 2>&1 \
|| [ -e /usr/local/bin/21st ]; then
|| [ -e /usr/local/bin/21st ] || [ -e /opt/homebrew/bin/21st ]; then
echo "FAIL precondition: system-wide 21st present," \
"CLI_ABSENT case not hermetic"
FAIL=$((FAIL + 1))
+19 -4
View File
@@ -16,16 +16,27 @@ check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
# _citers_extract <file>… → "file:line:name" per cited section (quoted) or label (§)
_citers_extract() {
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^"“]{0,12}["“][^"”]{2,60}["”]' "$@" 2>/dev/null \
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^"“]{0,12}["“][^"”[:space:]][^"”]{1,59}["”]' "$@" 2>/dev/null \
| sed -E 's/^([^:]+:[0-9]+):.*["“]([^"”]+)["”]$/\1:\2/'
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^§]{0,60}§ ?[A-Z][A-Za-z][A-Za-z -]{1,40}' "$@" 2>/dev/null \
| sed -E 's/^([^:]+:[0-9]+):.*§ ?([A-Za-z][A-Za-z -]+)$/\1:\2/; s/[[:space:]]+$//'
}
# _citers_resolve <doctrine> <name> → rc 0 when a heading or a bold label starts with <name>
# _citers_resolve <doctrine> <name> → rc 0 when a heading starts with <name>
# (then end, space, `:`, `(` or `—`) or a bold label `**<name>` appears.
# Fixed-string awk: the name is never read as a regex (BSD grep -E rejects
# some, and "Alpha" must not resolve against "## Alphabet").
_citers_resolve() {
/usr/bin/grep -qE "^#+ ${2}( |$|:|\(|—)" "$1" && return 0
/usr/bin/grep -qF -- "**${2}" "$1"
awk -v n="$2" '
/^#+ / {
t = $0; sub(/^#+ /, "", t)
if (substr(t, 1, length(n)) == n) {
r = substr(t, length(n) + 1)
if (r == "" || r ~ /^[ :(]/ || index(r, "—") == 1) found = 1
}
}
index($0, "**" n) { found = 1 }
END { exit !found }' "$1"
}
# citers_check <doctrine> <file>… → prints DANGLING lines; rc = their count (capped 99)
@@ -45,6 +56,10 @@ FIX="$(mktemp -d)"; trap 'rm -rf "$FIX"' EXIT
printf '## Alpha\n\n**Always English, always caveman**: rule.\n\n## Memory registries (`x`)\n' > "$FIX/doctrine.md"
printf 'ok: see CLAUDE.md "Alpha" and CLAUDE.md "Memory registries" (Always English, always caveman)\n' > "$FIX/good.md"
printf 'bad: (see CLAUDE.md "Memory registries" § Language) and CLAUDE.md "Beta"\n' > "$FIX/bad.md"
printf '## Alphabet\n' > "$FIX/prefix.md"
printf 'prefix: see CLAUDE.md "Alpha"\n' > "$FIX/alpha.md"
citers_check "$FIX/prefix.md" "$FIX/alpha.md" >/dev/null
check T2d-name-prefix-of-heading-stays-dangling "$?" 1
citers_check "$FIX/doctrine.md" "$FIX/good.md" >/dev/null; check T1-resolving-citations-pass "$?" 0
out=$(citers_check "$FIX/doctrine.md" "$FIX/bad.md"); rc=$?
check T2-dangling-section-and-label-caught "$rc" 2
+3 -3
View File
@@ -38,7 +38,7 @@ check T6b-no-name-still-frontmatter "$(fm_effort "$EXT/noname/SKILL.md")" "low"
snap="$(cat "$EXT"/*/SKILL.md)"
bash "$LIB" "$REPO" >/dev/null 2>&1
check T7-idempotent "$(cat "$EXT"/*/SKILL.md)" "$snap"
check T7b-no-tmp-left "$(find "$EXT" -name '*.tmp' | wc -l)" 0
check T7b-no-tmp-left "$(find "$EXT" -name '*.tmp' | wc -l | tr -d ' ')" 0
# rejections: nothing written, rc 1
for bad in 'alpha turbo' '../evil high' 'alpha high extra'; do
@@ -96,7 +96,7 @@ else
printf 'ro high\n' > "$WORK/h14/lib/effort-pins.txt"
chmod 555 "$d14"; out="$(bash "$LIB" "$WORK/h14" 2>&1)"; rc=$?; chmod 755 "$d14"
check T14-write-failure-no-temp \
"$rc|$(printf '%s' "$out" | grep -c 'ERR ')|$(find "$d14" -name 'SKILL.md.*' | wc -l)" "1|1|0"
"$rc|$(printf '%s' "$out" | grep -c 'ERR ')|$(find "$d14" -name 'SKILL.md.*' | wc -l | tr -d ' ')" "1|1|0"
fi
# T15: SIGINT during the awk write removes the temp sibling, exit 130
@@ -106,7 +106,7 @@ bash -c 'source "$1"; awk() { kill -INT $$; sleep 2; }
_effort_pin_write "$2" sig high' _ "$LIB" "$d15/SKILL.md" >/dev/null 2>&1
rc=$?
check T15-sigint-removes-temp \
"$rc|$(find "$d15" -name 'SKILL.md.*' | wc -l)" "130|0"
"$rc|$(find "$d15" -name 'SKILL.md.*' | wc -l | tr -d ' ')" "130|0"
# T15b: previous INT trap restored on a normal return, no EXIT trap set
mkrepo h15b tr; d15b="$WORK/h15b/skills-external/tr"
+4 -4
View File
@@ -36,17 +36,17 @@ check T4-none "$(bash "$L" detect "$tmp/cpp" >/dev/null 2>&1; echo $?)" 1
check T5-missing "$(bash "$L" cache-status "$tmp/js" || true)" missing
mkdir -p "$tmp/js/.ctx7-cache"; touch "$tmp/js/.ctx7-cache/react-core.md"
check T6-fresh "$(bash "$L" cache-status "$tmp/js")" fresh
touch -d '10 days ago' "$tmp/js/.ctx7-cache/react-core.md"
touch -t 200001010000 "$tmp/js/.ctx7-cache/react-core.md"
check T7-stale "$(bash "$L" cache-status "$tmp/js" || true)" stale
# --- hook: fires once per session, silent on stable projects ---
hook() { printf '{"prompt":"add a hook","session_id":"%s","cwd":"%s"}' \
"$1" "$2" | TMPDIR="$tmp" bash "$H"; }
check H1-fires "$(hook s1 "$tmp/js" | grep -c 'Fast-moving')" 1
check H2-once "$(hook s1 "$tmp/js" | wc -l)" 0
check H3-cpp-quiet "$(hook s2 "$tmp/cpp" | wc -l)" 0
check H2-once "$(hook s1 "$tmp/js" | wc -l | tr -d ' ')" 0
check H3-cpp-quiet "$(hook s2 "$tmp/cpp" | wc -l | tr -d ' ')" 0
check H4-notif-quiet \
"$(printf '{"prompt":"<task-notification>x","session_id":"s3","cwd":"%s"}' \
"$tmp/js" | TMPDIR="$tmp" bash "$H" | wc -l)" 0
"$tmp/js" | TMPDIR="$tmp" bash "$H" | wc -l | tr -d ' ')" 0
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+2 -1
View File
@@ -101,7 +101,8 @@ check_kind STUB "$rc" 2 "$out" 'FLOOR STUB'
# ── THRESHOLD_DOWN ────────────────────────────────────────────────────────
d=$(mk_repo threshold); base=$(git -C "$d" rev-parse HEAD)
sed -i 's/lines: 80/lines: 60/' "$d/vitest.config.ts"
# BSD sed -i needs a suffix argument
sed -i.bak 's/lines: 80/lines: 60/' "$d/vitest.config.ts" && rm -f "$d/vitest.config.ts.bak"
out=$(cd "$d" && bash "$LIB" "$base" 2>&1); rc=$?
check_kind THRESHOLD_DOWN "$rc" 2 "$out" 'FLOOR THRESHOLD_DOWN'
+8
View File
@@ -98,4 +98,12 @@ check T4-nothing-created "$([ -e "$DST4" ] && echo present || echo absent)" \
check T4-warns "$(printf '%s' "$out4" | grep -qi 'refusing' \
&& echo yes || echo no)" yes
# ── T4b: dst under src with a missing parent — refused, nothing created ──
DST4B="$SRC/missing/x"
link_gstack_helpers "$SRC" "$DST4B" >/dev/null 2>&1
rc4b=$?
check T4b-rc "$rc4b" 1
check T4b-nothing-created \
"$([ -e "$SRC/missing" ] && echo present || echo absent)" absent
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+3 -2
View File
@@ -123,8 +123,9 @@ check T7-update-conflict-nondestructive "$t7_state" "1:Y:Y"
# ── T8 — no destructive command anywhere in the lib source ───────────────
d8=OK
sed 's/#.*//' "$L" | grep -qE 'git [^|;]*(checkout|reset|clean|stash)' && d8=BAD
sed 's/#.*//' "$L" | grep -qwE '(rm|rmdir|unlink|truncate|mv)' && d8=BAD
# producer out of the pipe: grep -q SIGPIPEs it under pipefail on BSD
grep -qE 'git [^|;]*(checkout|reset|clean|stash)' < <(sed 's/#.*//' "$L") && d8=BAD
grep -qwE '(rm|rmdir|unlink|truncate|mv)' < <(sed 's/#.*//' "$L") && d8=BAD
check T8-no-destructive-command "$d8" OK
# ── T9-T14 — browsers-report, fixture cache + playwright-core installs ───
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# lib/tests/portability-census.test.sh — regression guard for GNU-only shell
# idioms that break on macOS (BSD userland). Deterministic idioms only:
# sed -i with no suffix, stat -c, realpath -m, touch -d, grep -P,
# a bare /bin/grep (LRN-074: pin /usr/bin/grep).
# Not covered on purpose: `cmd | grep -q` under pipefail (not decidable by
# text; fixed structurally by taking the producer out of the pipe).
# Usage: portability-census.test.sh [file…]
# no args: flip-test, then scan tracked *.sh + hooks/*
# args : scan only those files; exit 2 on any hit
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
RE="sed -i ['\"]|stat -c|realpath -m|touch -d|grep -[A-Za-z]*P([^A-Za-z]|$)"
RE="$RE|(^|[^a-z])/bin/grep"
# file:line (or file:*) exempt from the scan, each with its reason.
ALLOW=(
"lib/tests/guard-bash.test.sh:221" # deny fixture: GNU spelling
"lib/tests/guard-bash.test.sh:225" # deny fixture: GNU spelling
"lib/tests/portability-census.test.sh:*" # spells the idioms
)
_allowed() {
local entry
for entry in "${ALLOW[@]}"; do
entry="${entry%% #*}"
[ "$entry" = "$1:$2" ] || [ "$entry" = "$1:*" ] && return 0
done
return 1
}
# scan <file>… → prints file:line:text per hit, rc 2 when any
scan() {
local f rel line lno text hits=0
for f in "$@"; do
rel="${f#"$ROOT"/}"
while IFS= read -r line; do
lno="${line%%:*}"; text="${line#*:}"
case "$text" in [[:space:]]*"#"*|"#"*) continue ;; esac
_allowed "$rel" "$lno" && continue
printf 'GNU-ONLY: %s:%s:%s\n' "$rel" "$lno" "$text"; hits=$((hits+1))
done < <(grep -nE -- "$RE" "$f" 2>/dev/null)
done
[ "$hits" -eq 0 ] || return 2
}
if [ "$#" -gt 0 ]; then scan "$@"; exit $?; fi
# flip-test: a planted GNU idiom must be caught before the real census runs
PLANT="$(mktemp -d)" || exit 1; trap 'rm -rf "$PLANT"' EXIT
printf '#!/usr/bin/env bash\nsed -i '"'"'s/a/b/'"'"' x\n' > "$PLANT/planted.sh"
scan "$PLANT/planted.sh" >/dev/null; flip=$?
if [ "$flip" -ne 2 ]; then echo "FAIL flip: plant not caught"; exit 1; fi
mapfile -t FILES < <(cd "$ROOT" && git ls-files '*.sh' 'hooks/*' \
| sed "s#^#$ROOT/#")
scan "${FILES[@]}"; rc=$?
[ "$rc" -eq 0 ] && echo "PASS portability census (${#FILES[@]} files)"
exit "$rc"
+2 -1
View File
@@ -178,7 +178,8 @@ run_mutant T3-mutant-removed "$M1" 'REMOVED_LISTED:qa:ship' \
# Mutant 2: the superset profile drops a name full carries.
M2="$WORK/mutant-superset"; cp -r "$BASE" "$M2"
sed -i '/^beta$/d' "$M2/max.profile"
# BSD sed -i needs a suffix argument
sed -i.bak '/^beta$/d' "$M2/max.profile" && rm -f "$M2/max.profile.bak"
run_mutant T4-mutant-superset "$M2" 'SUPERSET_GAP:beta' \
FIXTURE_SUPERSET_DETECTED
+2 -1
View File
@@ -127,7 +127,8 @@ printf ' none \r' > "$FX/.active-profile"
out="$(statusline)"
check_has T10-full "$out" "profile: full"
sed -i 's/^DEFAULT_PROFILE="full"/DEFAULT_PROFILE="otherish"/' "$FX/lib/profile.sh"
# BSD sed -i needs a suffix argument
sed -i.bak 's/^DEFAULT_PROFILE="full"/DEFAULT_PROFILE="otherish"/' "$FX/lib/profile.sh" && rm -f "$FX/lib/profile.sh.bak"
rm -f "$FX/.active-profile"
out="$(statusline)"
check_has T11-otherish "$out" "profile: otherish"
+21
View File
@@ -80,4 +80,25 @@ check T16b-obs-back "$([ -e "$FX/skills/observability-and-instrumentation" ] &&
check T16c-obs-park-gone "$([ -e "$FX/skills-disabled/observability-and-instrumentation" ] && echo p || echo n)" n
check T17-no-mcp-ever "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
# --- a failing `claude plugin enable` must warn, never abort `set` ---
# Guards the capture-inside-the-condition form: a bare out="$(claude …)"
# would trip errexit and skip every entry after the plugin.
mkdir -p "$FX/failbin"
cat > "$FX/failbin/claude" <<'EOF'
#!/usr/bin/env bash
[ "$1 $2" = "plugin enable" ] && exit 1
exit 0
EOF
chmod +x "$FX/failbin/claude"
cat > "$FX/lib/profiles/pluginish.profile" <<'EOF'
fake-plug plugin@fake-market
gs-a
EOF
rm -f "$FX/skills/gs-a"
PATH="$FX/failbin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" set pluginish \
>/dev/null 2>&1
check T18-failing-plugin-enable-keeps-going \
"$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+1 -1
View File
@@ -101,7 +101,7 @@ printf ' err: %s\n' "$(printf '%s' "$ERR" | grep -i decisions | head -1)"
if [ "$RC" -eq 4 ]; then ok "mixed → exit 4"; else ko "expected 4, got $RC"; fi
if [ "$(git -C "$R" rev-parse HEAD)" = "$BEFORE" ]; then ok "NOTHING committed (README not half-committed)"; else ko "a commit slipped through"; fi
if printf '%s' "$ERR" | grep -q '.claude/memory/decisions.md'; then ok "stderr names the offender"; else ko "offender not named"; fi
if git -C "$R" status --porcelain | grep -q ' M README.md'; then ok "README left dirty (not embarked)"; else ko "README state wrong"; fi
if grep -q ' M README.md' < <(git -C "$R" status --porcelain); then ok "README left dirty (not embarked)"; else ko "README state wrong"; fi
rm -rf "$R"
echo "T2 — dynamic pathspec: clean passed path filtered, no abort"
+3 -2
View File
@@ -40,7 +40,8 @@ echo
( cd "$WORK" || exit 1
bash "$GITFLOW" start release 4.0.0 >/dev/null # base develop → release/4.0.0 (lib L49/L71)
printf '4.0.0\n' > version.txt # prep: version bump
sed -i 's/## \[Unreleased\]/## [Unreleased]\n\n## [4.0.0] — 2026-06-30/' CHANGELOG.md
# BSD sed -i needs a suffix argument: -i.bak then drop the backup
sed -i.bak 's/## \[Unreleased\]/## [Unreleased]\n\n## [4.0.0] — 2026-06-30/' CHANGELOG.md && rm -f CHANGELOG.md.bak
git commit -qam "chore(release): 4.0.0 — version.txt + CHANGELOG"
bash "$GITFLOW" finish >/dev/null # fan-out main+develop+delete (lib L108-111)
# TAG = the gap. Lives in the SKILL (lib untouched). RED skips it, GREEN does it.
@@ -52,7 +53,7 @@ echo "=== assertions (RC_TAG=$RC_TAG) ==="
if [ "$(git -C "$WORK" show main:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "fan-out: main carries the release (version.txt 4.0.0)"; else no "fan-out: main version.txt != 4.0.0"; fi
if [ "$(git -C "$WORK" show develop:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "merge-back: develop carries 4.0.0"; else no "merge-back failed"; fi
if git -C "$WORK" show-ref --verify -q refs/heads/release/4.0.0; then no "release/4.0.0 NOT deleted"; else ok "release/4.0.0 branch deleted"; fi
if git -C "$WORK" show main:CHANGELOG.md | $GREP -q '## \[4.0.0\]'; then ok "CHANGELOG [4.0.0] on main"; else no "CHANGELOG not finalized"; fi
if $GREP -q '## \[4.0.0\]' < <(git -C "$WORK" show main:CHANGELOG.md); then ok "CHANGELOG [4.0.0] on main"; else no "CHANGELOG not finalized"; fi
if git -C "$WORK" rev-parse -q --verify refs/tags/v4.0.0 >/dev/null; then
if [ "$(git -C "$WORK" rev-list -n1 v4.0.0)" = "$(git -C "$WORK" rev-parse main)" ]; then ok "tag v4.0.0 on main's release-merge commit"; else no "tag v4.0.0 exists but not on main HEAD"; fi
else
+3 -3
View File
@@ -45,8 +45,8 @@ case "$G" in *"*"*) check C2-grep-has-no-glob "has-glob" ok ;;
*) check C2-grep-has-no-glob ok ok ;; esac
# --- findargs: one token per line, 3 tokens per dir ---
N="$(cd "$TMP/plain" && bash "$S" findargs | wc -l)"
D="$(cd "$TMP/plain" && bash "$S" list | wc -l)"
N="$(cd "$TMP/plain" && bash "$S" findargs | wc -l | tr -d ' ')"
D="$(cd "$TMP/plain" && bash "$S" list | wc -l | tr -d ' ')"
check D1-findargs-3-tokens-per-dir "$N" "$((D * 3))"
check D2-findargs-first-token "$(cd "$TMP/plain" && bash "$S" findargs | head -1)" '!'
@@ -63,7 +63,7 @@ check E1-excludes-dist "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/dist/'
check E2-keeps-src "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/src/')" 1
check E3-excludes-nodem "$(find . "${FEXCL[@]}" -name '*.png' | grep -c 'node_modules')" 0
# public/ survives: the audit's own resource checks live there
check E4-keeps-public "$(find . "${FEXCL[@]}" -name 'favicon.ico' | wc -l)" 1
check E4-keeps-public "$(find . "${FEXCL[@]}" -name 'favicon.ico' | wc -l | tr -d ' ')" 1
cd / || exit 1
# --- usage ---
+1 -1
View File
@@ -139,7 +139,7 @@ pack_hints() {
21st)
if ! command -v 21st >/dev/null 2>&1; then
warn "the \`21st\` CLI is not on PATH — install it: npm i -g @21st-dev/cli"
elif ! 21st whoami 2>/dev/null | grep -q '^Logged in as '; then
elif ! grep -q '^Logged in as ' <<<"$(21st whoami 2>/dev/null)"; then
warn "not signed in to 21st — component retrieval and 21st AI need: 21st login"
fi
;;
+5 -1
View File
@@ -112,6 +112,10 @@ arbitrary code execution (`Bash(*)`, wildcarded interpreters such as
`permissions.allow` says. `awk` and `echo` pass through a static rule; `node`
cannot.
### Package installs
Global npm installs run their install scripts with your rights on the whole machine. `npm install -g`, `npm i -g` and their `--global` spellings are in `permissions.deny`. An `autoMode.soft_deny` entry catches every other spelling (a flag after the package name, `npm add -g`) and holds the install until you name the package in the current turn, after Claude states its publisher, age, download volume, install scripts and known advisories. A second `soft_deny` entry covers `npx`, `pnpm dlx` and `yarn dlx` of a package absent from the manifest and lockfile. Project-local scripts and declared packages pass through `autoMode.allow`.
### Scope of intent
A `soft_deny` clears on the user's instruction, and this config scopes that to
@@ -124,7 +128,7 @@ no separate setting for this.
- `Read(**/.env)` only blocks the Read tool. `Bash(cat .env)` bypasses it unless separately denied.
→ Use `.claudeignore` for hard file exclusion regardless of tool.
- `disableBypassPermissionsMode: "disable"` prevents switching to bypass mode mid-session.
- Prefer `ask` over `allow` for anything touching external systems.
- Prefer `autoMode.soft_deny` over `allow` for anything touching external systems.
- `deny` in `~/.claude/settings.json` cannot be overridden by project-level `allow` — deny always wins.
- Under `defaultMode: auto`, `ask` does not raise a prompt (see above). A destructive
command belongs in `deny` or in `autoMode.soft_deny`, not in `ask`.
+2 -1
View File
@@ -603,8 +603,9 @@ fi
echo ""
echo "── Updating marketplace plugins..."
if command -v claude &>/dev/null; then
# sed -n: BSD grep has no PCRE mode (rc 2 emptied the list)
_plugins=$(claude plugin list 2>/dev/null \
| grep -oP '(?<=❯ )\S+' || true)
| sed -n 's/.*❯ \([^[:space:]][^[:space:]]*\).*/\1/p' || true)
if [ -n "$_plugins" ]; then
while IFS= read -r _p; do
_name="${_p%%@*}"