From 0efdff0d558000aea7c1a0cb0bb647f027e132c3 Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 15:10:20 +0200 Subject: [PATCH 1/7] fix(portability): make test green on macOS, GNU-only idioms replaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite and seven libs assumed a GNU userland: `cmd | grep -q` under pipefail (grep exits at the first match, the producer takes SIGPIPE, rc 141 → 15 false "merged into" FAILs in gitflow-test), `sed -i` with no suffix, `wc -l` padding compared as a string, `stat -c`, `touch -d`, `realpath -m` (gstack-links refusal never fired), bare `timeout` off the sanitized PATH (design gate READY BUT UNVERIFIED), `grep -oP` (update-all emptied the plugin list). Thirteen suites were red on this machine. Portable forms on the native userland of both OS: producer captured out of the pipeline, `sed -i.bak` in tests and a temp-sibling `sed_profile` on the user dotfile, `tr -d ' '`, python3 perms, `touch -t`, a `realpath -m` emulation that refuses `..`, perl `alarm` for the 15 s bound, `sed -n` token extraction. Regression tests: gstack-links T4b, doctrine-citers Alphabet/Alpha flip, profile-set-managed T18 (failing CLI no longer aborts `set`), new portability-census suite over the tracked shell files. Linux run deferred (see TODO). --- install-plugins.sh | 35 +++++++++++++--- lib/design-tool-gate.sh | 28 +++++++++---- lib/doc-shape.sh | 3 +- lib/gitflow-test.sh | 52 +++++++++++------------ lib/gstack-links.sh | 21 +++++++++- lib/profile.sh | 22 ++++++---- lib/seo-data/seo-data.test.sh | 12 ++++-- lib/tests/design-tool-gate.test.sh | 2 +- lib/tests/doctrine-citers.test.sh | 23 ++++++++-- lib/tests/effort-pins.test.sh | 6 +-- lib/tests/fast-libs.test.sh | 8 ++-- lib/tests/floor-guard.test.sh | 3 +- lib/tests/gstack-links.test.sh | 8 ++++ lib/tests/gstack-playwright.test.sh | 5 ++- lib/tests/portability-census.test.sh | 60 +++++++++++++++++++++++++++ lib/tests/profile-census.test.sh | 3 +- lib/tests/profile-default.test.sh | 3 +- lib/tests/profile-set-managed.test.sh | 21 ++++++++++ lib/tests/run-doc-commit.sh | 2 +- lib/tests/run-release-candidate.sh | 5 ++- lib/tests/source-scope.test.sh | 6 +-- lib/toggle-external.sh | 2 +- update-all.sh | 3 +- 23 files changed, 255 insertions(+), 78 deletions(-) create mode 100644 lib/tests/portability-census.test.sh diff --git a/install-plugins.sh b/install-plugins.sh index 38c5e7f..c590355 100644 --- a/install-plugins.sh +++ b/install-plugins.sh @@ -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 (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 — 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 diff --git a/lib/design-tool-gate.sh b/lib/design-tool-gate.sh index 4a920a0..52404ae 100755 --- a/lib/design-tool-gate.sh +++ b/lib/design-tool-gate.sh @@ -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:. 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 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 —" \ diff --git a/lib/doc-shape.sh b/lib/doc-shape.sh index 4bfb9a3..52e09c4 100755 --- a/lib/doc-shape.sh +++ b/lib/doc-shape.sh @@ -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 diff --git a/lib/gitflow-test.sh b/lib/gitflow-test.sh index 0b95df5..bdefd10 100644 --- a/lib/gitflow-test.sh +++ b/lib/gitflow-test.sh @@ -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 # :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 ]' diff --git a/lib/gstack-links.sh b/lib/gstack-links.sh index b3b0c6c..39719e5 100644 --- a/lib/gstack-links.sh +++ b/lib/gstack-links.sh @@ -49,6 +49,25 @@ if ! declare -F info >/dev/null 2>&1; then info() { echo -e "${BLUE}→${NC} $1"; } fi +# _gstack_links_realpath_m — prints 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 — removes a stale symlink # (gstack ./setup plants `skills/gstack -> skills-external/gstack` when # the dir is absent), refuses ever writing INTO (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 diff --git a/lib/profile.sh b/lib/profile.sh index 8dbb0d5..9c1f911 100755 --- a/lib/profile.sh +++ b/lib/profile.sh @@ -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" diff --git a/lib/seo-data/seo-data.test.sh b/lib/seo-data/seo-data.test.sh index d13d260..bc24cbf 100644 --- a/lib/seo-data/seo-data.test.sh +++ b/lib/seo-data/seo-data.test.sh @@ -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 \ diff --git a/lib/tests/design-tool-gate.test.sh b/lib/tests/design-tool-gate.test.sh index d6f8a46..83a92b0 100755 --- a/lib/tests/design-tool-gate.test.sh +++ b/lib/tests/design-tool-gate.test.sh @@ -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)) diff --git a/lib/tests/doctrine-citers.test.sh b/lib/tests/doctrine-citers.test.sh index a21eb3c..7281874 100644 --- a/lib/tests/doctrine-citers.test.sh +++ b/lib/tests/doctrine-citers.test.sh @@ -16,16 +16,27 @@ check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1)); # _citers_extract … → "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 → rc 0 when a heading or a bold label starts with +# _citers_resolve → rc 0 when a heading starts with +# (then end, space, `:`, `(` or `—`) or a bold label `**` 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 … → 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 diff --git a/lib/tests/effort-pins.test.sh b/lib/tests/effort-pins.test.sh index 1e66381..329136c 100755 --- a/lib/tests/effort-pins.test.sh +++ b/lib/tests/effort-pins.test.sh @@ -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" diff --git a/lib/tests/fast-libs.test.sh b/lib/tests/fast-libs.test.sh index 9db21dc..4678a86 100644 --- a/lib/tests/fast-libs.test.sh +++ b/lib/tests/fast-libs.test.sh @@ -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":"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 ] diff --git a/lib/tests/floor-guard.test.sh b/lib/tests/floor-guard.test.sh index ccba234..b0b0323 100644 --- a/lib/tests/floor-guard.test.sh +++ b/lib/tests/floor-guard.test.sh @@ -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' diff --git a/lib/tests/gstack-links.test.sh b/lib/tests/gstack-links.test.sh index 803496a..1783cbe 100755 --- a/lib/tests/gstack-links.test.sh +++ b/lib/tests/gstack-links.test.sh @@ -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 ] diff --git a/lib/tests/gstack-playwright.test.sh b/lib/tests/gstack-playwright.test.sh index 1baa684..5cce44d 100644 --- a/lib/tests/gstack-playwright.test.sh +++ b/lib/tests/gstack-playwright.test.sh @@ -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 ─── diff --git a/lib/tests/portability-census.test.sh b/lib/tests/portability-census.test.sh new file mode 100644 index 0000000..cc3dc90 --- /dev/null +++ b/lib/tests/portability-census.test.sh @@ -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 … → 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" diff --git a/lib/tests/profile-census.test.sh b/lib/tests/profile-census.test.sh index 3aadcc1..8d150d0 100755 --- a/lib/tests/profile-census.test.sh +++ b/lib/tests/profile-census.test.sh @@ -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 diff --git a/lib/tests/profile-default.test.sh b/lib/tests/profile-default.test.sh index 56987b7..b80acd8 100644 --- a/lib/tests/profile-default.test.sh +++ b/lib/tests/profile-default.test.sh @@ -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" diff --git a/lib/tests/profile-set-managed.test.sh b/lib/tests/profile-set-managed.test.sh index e38434a..7d4fdab 100644 --- a/lib/tests/profile-set-managed.test.sh +++ b/lib/tests/profile-set-managed.test.sh @@ -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 ] diff --git a/lib/tests/run-doc-commit.sh b/lib/tests/run-doc-commit.sh index 5d16d22..e5cd8ed 100755 --- a/lib/tests/run-doc-commit.sh +++ b/lib/tests/run-doc-commit.sh @@ -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" diff --git a/lib/tests/run-release-candidate.sh b/lib/tests/run-release-candidate.sh index 3bfe4ae..5e39f49 100644 --- a/lib/tests/run-release-candidate.sh +++ b/lib/tests/run-release-candidate.sh @@ -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 diff --git a/lib/tests/source-scope.test.sh b/lib/tests/source-scope.test.sh index b674b6c..02946c2 100644 --- a/lib/tests/source-scope.test.sh +++ b/lib/tests/source-scope.test.sh @@ -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 --- diff --git a/lib/toggle-external.sh b/lib/toggle-external.sh index 9e9657b..5b54f9f 100755 --- a/lib/toggle-external.sh +++ b/lib/toggle-external.sh @@ -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 ;; diff --git a/update-all.sh b/update-all.sh index 87fb47d..f83d907 100644 --- a/update-all.sh +++ b/update-all.sh @@ -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%%@*}" From e5b6cc5ef4ec3edb701c1d81b91e9a6bb8bf97e5 Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 15:19:22 +0200 Subject: [PATCH 2/7] docs(changelog): macOS portability fix + deferred Linux run under [Unreleased] --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8ffed..0fcc8e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -466,6 +466,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`). @@ -554,6 +574,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 From 276fa67c803486a0c40d73b15f8147cc7949343b Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 15:19:53 +0200 Subject: [PATCH 3/7] =?UTF-8?q?chore(memory):=20BDR-110=20+=20BLK-026/027?= =?UTF-8?q?=20+=20LRN-189/190=20+=20journal=20=E2=80=94=20macOS=20portabil?= =?UTF-8?q?ity=20bugfix,=20contract=20+=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/blockers.md | 14 ++ .claude/memory/decisions.md | 8 + .claude/memory/journal.md | 2 + .claude/memory/learnings.md | 10 ++ .claude/tasks/TODO.md | 8 + .../2026-10-06-macos-portability-1105.md | 64 ++++++++ .../2026-10-06-macos-portability-1030.md | 149 ++++++++++++++++++ 7 files changed, 255 insertions(+) create mode 100644 .claude/tasks/contracts/2026-10-06-macos-portability-1105.md create mode 100644 .claude/tasks/plans/2026-10-06-macos-portability-1030.md diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index 957f63d..9bbacc5 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -45,6 +45,8 @@ rules: | 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) | --- @@ -282,3 +284,15 @@ 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 -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). diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index f9ede33..86b2dc9 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -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 | --- @@ -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]]. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index eeffc8f..c76ebf6 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -562,3 +562,5 @@ 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. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 2ce5ad9..7d76820 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -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 | --- @@ -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` 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]]. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 1f61de5..e0271f6 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -2035,3 +2035,11 @@ 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. diff --git a/.claude/tasks/contracts/2026-10-06-macos-portability-1105.md b/.claude/tasks/contracts/2026-10-06-macos-portability-1105.md new file mode 100644 index 0000000..e5f84dc --- /dev/null +++ b/.claude/tasks/contracts/2026-10-06-macos-portability-1105.md @@ -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. diff --git a/.claude/tasks/plans/2026-10-06-macos-portability-1030.md b/.claude/tasks/plans/2026-10-06-macos-portability-1030.md new file mode 100644 index 0000000..721559b --- /dev/null +++ b/.claude/tasks/plans/2026-10-06-macos-portability-1030.md @@ -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 ` 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 ` 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. From 6bc7c12bf3babdfce33688c843e5e01eb6af2388 Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 15:24:59 +0200 Subject: [PATCH 4/7] =?UTF-8?q?chore(memory):=20reconcile=202026-10-06=20?= =?UTF-8?q?=E2=80=94=20npm=20deny=20items=20closed,=20Makefile=20help=20re?= =?UTF-8?q?-verified=20open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/journal.md | 1 + .claude/tasks/TODO.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index c76ebf6..cc7fe44 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -564,3 +564,4 @@ rules: ## 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. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index e0271f6..0c93f65 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -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 -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 -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). From a84aaaabbd5a6bb37126ac25d2ef57c46fce60ba Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 15:40:26 +0200 Subject: [PATCH 5/7] =?UTF-8?q?chore(memory):=20prune=202026-10-06=20?= =?UTF-8?q?=E2=80=94=20BLK-028=20merge,=20bounded=20caveman=20pass=20on=20?= =?UTF-8?q?37=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/blockers.md | 13 ++- .claude/memory/decisions.md | 4 +- .claude/memory/evals.md | 12 +-- .claude/memory/journal.md | 1 + .claude/memory/learnings.md | 168 ++++++++++++++++++------------------ 5 files changed, 104 insertions(+), 94 deletions(-) diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index 9bbacc5..cfe9f7f 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -38,8 +38,8 @@ 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 | @@ -47,6 +47,7 @@ rules: | 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 | --- @@ -296,3 +297,11 @@ rules: - **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/` 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]]. diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 86b2dc9..e1cbe79 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -982,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 diff --git a/.claude/memory/evals.md b/.claude/memory/evals.md index 7a81849..9ece412 100644 --- a/.claude/memory/evals.md +++ b/.claude/memory/evals.md @@ -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]]. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index cc7fe44..5034935 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -565,3 +565,4 @@ rules: ## 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. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 7d76820..7ec9cb9 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -681,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-` 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-` 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". --- @@ -711,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]]. --- @@ -720,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]]. @@ -760,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 @@ -840,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 '^## -'`, 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 ]` @@ -855,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 @@ -878,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]]. @@ -914,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. @@ -929,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..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..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..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 @@ -940,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". @@ -968,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 @@ -985,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 @@ -1007,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]]. --- @@ -1057,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 @@ -1099,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 @@ -1146,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). @@ -1156,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 @@ -1209,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]]). @@ -1226,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). @@ -1280,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). --- @@ -1320,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 @@ -1333,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 @@ -1354,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 ` — — measured: — `. 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 ` — — measured: — `. `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. @@ -1463,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]]. --- @@ -1496,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]]. @@ -1516,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 `/.claude/skills//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/` + 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 `/.claude/skills//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/` + 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 @@ -1537,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]]. From c6fb2e42191a5247ea87fb74e95788d8de07bd95 Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 16:00:41 +0200 Subject: [PATCH 6/7] =?UTF-8?q?docs:=20global=20sync=20before=202.0.0=20?= =?UTF-8?q?=E2=80=94=20components,=20slash=20table,=20profiles,=20GSD=203.?= =?UTF-8?q?0.0,=20migration=20guide,=20package-install=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 24 +++++++---- CHANGELOG.md | 5 ++- MIGRATION.md | 43 +++++++++++++++--- Makefile | 6 +-- README.md | 79 +++++++++++++++++++++------------- USAGE.md | 40 +++++++++-------- templates/settings/SETTINGS.md | 6 ++- 7 files changed, 134 insertions(+), 69 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 434a70c..a063658 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fcc8e1..590dff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ 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] +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). @@ -220,6 +222,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 diff --git a/MIGRATION.md b/MIGRATION.md index c63ba88..1c4f8ce 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -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. diff --git a/Makefile b/Makefile index fd551bc..2141a32 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 578f702..c49dfa9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/USAGE.md b/USAGE.md index 86087d3..b6ae6ad 100644 --- a/USAGE.md +++ b/USAGE.md @@ -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/.md` (voir `_TEMPLATE.md`). ### Pattern D — Hotfix / bugfix · ~200-800t @@ -340,7 +344,7 @@ Ajouter un archétype : créer `~/.claude/lib/project-archetypes/.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/.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//-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. --- diff --git a/templates/settings/SETTINGS.md b/templates/settings/SETTINGS.md index 6cedb68..dadeaf8 100644 --- a/templates/settings/SETTINGS.md +++ b/templates/settings/SETTINGS.md @@ -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`. From b3f3ae2441fd7dd149ae6556d7c24ff0f5f19865 Mon Sep 17 00:00:00 2001 From: bchanot Date: Tue, 6 Oct 2026 16:00:54 +0200 Subject: [PATCH 7/7] chore(memory): doc-sync deferred items + journal 2026-10-06 --- .claude/memory/journal.md | 1 + .claude/tasks/TODO.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 5034935..ccaecb7 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -566,3 +566,4 @@ rules: - /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). diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 0c93f65..115fc7f 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -2043,3 +2043,15 @@ dans un runner; capitalize reste main-loop. 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/.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