diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index 6d9739e..8f6e83b 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -27,8 +27,10 @@ rules: | BLK-005 | 2026-05-21 | gstack submodule rename (checkpoint→context-save) breaks profile entries | resolved | | BLK-006 | 2026-05-21 | `profile.sh current` false-negative via `~/.claude` symlink (`cd` not `cd -P`) | resolved | | BLK-007 | 2026-06-02 | 6 gstack source skills (ios-*, spec) unlinked post-bump — invisible to profiles + `gstack on` | resolved | +| BLK-008 | 2026-06-23 | gstack ./setup on Ubuntu 26.04: Playwright chromium unsupported → gstack browser (/browse, /qa, screenshots) silently dead | resolved (211c7d4) | +| BLK-009 | 2026-06-25 | user-level path-scoped rules (`paths:` frontmatter in `~/.claude/rules/`) never inject — broken in CC 2.1.190 (#21858) | upstream, open | | BLK-010 | 2026-06-27 | init-project: scaffold (STEP 5) + bootstrap README (5b) have no deterministic commit owner; worktree `add -b` on unborn HEAD | resolved (uncommitted) | -| BLK-011 | 2026-06-27 | init-project STEP 13 GSD post-FINISH creates ROADMAP.md → stranded doc (3rd post-FINISH artifact) | open | +| BLK-011 | 2026-06-27 | init-project STEP 13 GSD post-FINISH creates ROADMAP.md → stranded doc (3rd post-FINISH artifact) | resolved (STEP 12 removed) | | BLK-012 | 2026-06-29 | gitflow_init half-applied: socle-commit failure swallowed → hook activated on partial run → re-run self-blocks | resolved | --- @@ -139,8 +141,9 @@ rules: - **Date**: 2026-06-27 - **Friction**: init-project STEP 13 (GSD v2 init) runs post-FINISH (STEP 11). `gsd init` creates `.gsd/` + `ROADMAP.md` (a public doc). Created AFTER FINISH integrates → ROADMAP never in the merge/PR. Same PR-stranding class as the doc-sync twin, 3rd post-FINISH artifact. - **Real cause**: artifact-producing step ordered after FINISH (= BDR-034 class). `gsd init` is a CLI mechanism distinct from doc-syncer; ROADMAP is sync-only for doc-syncer (never created by it, BDR-022 rules), so the doc-sync coupled chantier does not touch it. -- **Solution**: open — separate thread. Candidate: reorder GSD before FINISH, or commit ROADMAP after `gsd init`. Out of scope for doc-sync coupled (different mechanism). -- **Status**: open +- **Solution**: open — separate thread. Candidate: reorder GSD before FINISH, or commit ROADMAP after `gsd init`. Out of scope for doc-sync coupled (different mechanism). [historical candidates — NOT the route taken] +- **Resolution**: RESOLVED 2026-06-29 — by REMOVAL, not by committing the orphan. init-project STEP 12 (speculative gsd auto-bootstrap) DELETED → ROADMAP/.gsd never created post-FINISH → orphan dissolves, no commit helper built. TRUE reason: auto-bootstrapping a heavy multi-session ENGINE the sole user doesn't use, AT project-creation, is bad on its own terms. NOT the initial framing "ROADMAP redundant with TODO" — that was wrong and would have aged badly: gsd ≫ roadmap (state machine / crash-recovery / cost / parallel / worktree), and TODO ≠ gsd ROADMAP (different altitude + consumer). Reasoning trace: BOTH initial premises (gsd=only-roadmap; TODO-redundant) REFUTED on read, yet conclusion A (remove STEP 12) held for the STRONGER reason — right answer, reason corrected before engraving. Deliberate gsd use KEPT (onboarder PHASE 6 `/onboard add gsd`, plugin-advisor reco, status-reporter `.gsd/` read, USAGE `gsd init`). Removed STEP 12 + header 12→11-step + 10c note + 4 USAGE refs; coherence sweep = zero dangling refs. [[LRN-072]] +- **Status**: resolved (init-project STEP 12 removed — `skills/init-project/SKILL.md`; branch bugfix/blk-011-gsd-roadmap). Title says "STEP 13" — stale (was STEP 12 at removal per BDR-036 renumber); left per append-only. - **Reference**: discovered in doc-sync-coupled analysis (2026-06-27). Sibling [[BLK-010]] + twin [[BDR-034]]. ## BLK-012 — gitflow_init non-transactional: socle-commit failure swallowed → hook activated on partial run → re-run self-blocks diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index da0a3e4..c9edde2 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -42,14 +42,28 @@ rules: | BDR-018 | 2026-06-02 | `profile gstack on/off` verb — toggle gstack keeping active-profile label | accepted | | BDR-019 | 2026-06-09 | Remove `disable-model-invocation` repo-wide — align skills with CLAUDE.md routing | accepted | | BDR-020 | 2026-06-11 | `/audit-delta`: per-axis SHA markers + always-on fix gate + unreachable-first-run = full report-only | accepted | +| BDR-021 | 2026-06-27 | CLAUDE.md restructure: contradiction purge, project-specific sections labeled, critical sections never compressed | accepted | | BDR-022 | 2026-06-18 | doc-syncer scoped to public docs; `.claude/` + `CLAUDE.md` read-only context, never targets; conventions + clean mode | accepted | | BDR-023 | 2026-06-19 | Merge /close into /capitalize — 2 modes + TODO reconcile; /close alias | accepted | +| BDR-024 | 2026-06-27 | `profile show --plain` = claude-free parse contract for the design gate | accepted | +| BDR-025 | 2026-06-27 | Design gate profile-based; remedy `/profile design`; magic required-but-manual; unknown → fail-visible; claude via PATH-repair | accepted | +| BDR-026 | 2026-06-27 | Secret source-of-truth outside the repo (`~/.claude/.env`) reached via a `repo/.env` symlink | accepted | +| BDR-027 | 2026-06-27 | Minimal npm-via-nvm bootstrap over a centralized prereq lib | accepted | +| BDR-028 | 2026-06-27 | Hand-curated config install-immutable (auto-revert guard) + de-vendor installer-managed skills | accepted | +| BDR-029 | 2026-06-27 | Installer auto-fixes gstack browser on an OS newer than its pinned Playwright supports | accepted | +| BDR-030 | 2026-06-27 | gstack skills activated ON-DEMAND per profile, not pre-installed; OFF by default stays | accepted | +| BDR-031 | 2026-06-27 | global CLAUDE.md lightening = COMPRESSION, not path-scope / externalization | accepted | +| BDR-032 | 2026-06-27 | skill `/validate` → `/web-validate` (rename user surface, keep internals) | accepted | +| BDR-033 | 2026-06-27 | design-gate §4: anim-lib suggestion — suggest-only, non-blocking, stateless 1-line | accepted | | BDR-034 | 2026-06-26 | Coupled-capitalize invariant v1 — memory commit auto per dev flow (Frame 2) | accepted | | BDR-035 | 2026-06-26 | Analyze-before-plan invariant v1 — read-before bookend of coupled-capitalize | accepted | | BDR-036 | 2026-06-27 | Doc-sync coupled invariant — commit docs doc-syncer patches (twin of BDR-034, BUILT not reordered) | accepted | | BDR-037 | 2026-06-27 | v2 capitalize Stop-hook rejected → wire /capitalize+/close to the include | accepted | | BDR-038 | 2026-06-27 | deploy skill: per-project learning runbook, two-moment cold-resume | accepted | | BDR-039 | 2026-06-29 | Gitea branch protection = Option-1 owner-pushable, not require-PR | accepted | +| BDR-040 | 2026-06-29 | doc-syncer MINOR-shape oracle: deterministic floor under LLM's MINOR call | accepted | +| BDR-041 | 2026-06-30 | /reconcile = deterministic declared-vs-real engine + thin gated skill (reconciler, not lister) | accepted | +| BDR-042 | 2026-06-30 | /release-candidate = thin orchestrator over gitflow release; the tag lives in the skill, not the lib | accepted | --- @@ -613,3 +627,31 @@ rules: - **why**: gitflow integrates by **local directed merges** — `gitflow finish` runs `git merge --no-ff` on the owner's machine then pushes the merge commit. require-PR would REJECT those pushes: every feature/bugfix/release merge would need a manual PR, and the **hotfix fan-out** (hotfix → main + develop + each open `release/*`) becomes 3+ manual PRs per hotfix. For a solo-owner Gitea, required reviews add zero review value, only friction. Owner-pushable keeps the protection's real teeth (no force-push, no deletion, no non-owner push) without breaking the local-merge workflow. Protection is a BACKSTOP — the per-repo pre-commit hook + the "finish only on an explicit human signal" rule are the primary controls. - **alternatives**: require-PR + required reviews (rejected — breaks `gitflow finish`'s local merges; the 3-way hotfix fan-out becomes manual PRs; no review value for a solo owner, pure friction); no protection (rejected — leaves force-push + branch deletion + accidental non-owner push open; it is the deterministic backstop the advisory rules can't guarantee); protect `main` only (rejected — `develop` is equally a protected base in the model, needs the same force-push/deletion guard). - **reference**: `lib/gitflow-migrate.sh` `_protect()` (POST `/repos/{o}/{r}/branch_protections`, owner whitelist); applied to all 6 repos 2026-06-29 (journal). Hook backstop in `lib/gitflow.sh` (pre-commit); CLAUDE.md "Version control — gitflow (universal)". Pairs with [[LRN-069]] (the `git push` ASK gate at the tool-call layer). + +## BDR-040 — doc-syncer MINOR-shape oracle: deterministic floor under LLM's MINOR call +- **date**: 2026-06-29 +- **status**: accepted +- **Problem**: doc-syncer AUTO MODE classifies drift NONE/MINOR/SIGNIFICANT by LLM judgment, no deterministic backstop. SIGNIFICANT mislabeled MINOR → silent auto-patch + auto-commit, skips the SIGNIFICANT gate (RISK-1). Follow-up [[BDR-036]] flagged. +- **Decision**: `lib/doc-shape.sh` re-checks SHAPE of each MINOR patch BEFORE the silent auto-commit. Envelope (per path, `git diff HEAD`): adds ATX heading | added > DOC_SHAPE_MAX_ADDED (def 20) | removed > MAX_REMOVED (def 20) | new/untracked file | non-doc → EXCEEDS. Aggregate: ANY path exceeds → whole set escalates to the EXISTING SIGNIFICANT gate (STEP A4 `Apply? yes/no/select`; no=revert all, select=keep subset). Thresholds env-overridable. +- **Oracle NOT a blocking gate (B rejected)**: [[BDR-036]] graved MINOR-non-gated as CONSCIOUS (visible surface replaces gate; blocking gate = friction disproportionate). Oracle does NOT gate genuine MINOR (auto-commit untouched, zero friction) — only re-routes shape-suspect patches. Tightens the DEFINITION of MINOR deterministically ([[LRN-046]] oracle > judge), adds no gate. Option B (human gate on every MINOR) REJECTED — contradicts [[BDR-036]], rejects the premise the reading refuted. +- **ENGRAVED LIMIT — do not over-read the guarantee**: oracle catches STRUCTURAL/size significance, NOT semantic. A 3-line edit that CHANGES MEANING, no heading, small → still reads MINOR (rc 0) and auto-commits. Deterministic FLOOR under LLM judgment = REDUCTION of RISK-1's gross cases, NOT elimination. LLM owns the semantic call above the floor; the visible surface ([[BDR-036]]) stays the content backstop. +- **Scope tranché**: ① oracle + ② [[LRN-071]] masked-commit fix built. ③ branch-guard (doc-commit refusing main/develop) DEFERRED — duplicates the protected-base predicate a 3rd time (lib + gitflow hook + here); migrated repos have the hook → ③ guards a state that shouldn't exist. Reconsider only for repos outside `gitflow init`. +- **Build**: TDD RED→GREEN. run-doc-shape.sh 19/19 (incl. threshold boundary + env-override) + behavioral Scenario D. Wired doc-syncer STEP A4 + doc-commit.md ACKNOWLEDGMENTS coherence. shellcheck clean. + +## BDR-041 — /reconcile = deterministic declared-vs-real engine + thin gated skill (reconciler, not lister) +- **Date**: 2026-06-30 +- **Status**: accepted +- **Decision**: `/reconcile` answers "what work is REALLY open?" by confronting DECLARATIVE sources (TODO `[x]`/`[ ]`/`[~]`, registry statuses, `## Index`) against REAL state (git/fs, registry BODY). Split: `lib/reconcile.sh` (deterministic engine — body enumeration, `reconcile_oracle_*`, BLK last-block-wins, lexical deferral sweep, contradiction candidates, pure `reconcile_verdict` kernel) + `skills/reconcile/SKILL.md` (thin orchestration + A/B/C write-back gate). Founding principle = RECURSIVE COHERENCE: never use a declarative source as oracle (Index/checkbox/status/path-name) — enumerate from BODY `## ID` headings, decide done/stale from git/fs. TESTED: run-reconcile.sh T1 reds if engine reads `## Index` (shim → 51≠72, canary LRN-020 dropped). Registries READ-ONLY (curation = /prune-memory). +- **Why**: RED proved a capable agent reconciles when GUIDED ("use git + justify") but MIRRORS the TODO when not (false positives + missed contradiction), and even guided hits the compound-status trap (BLK-008). Value = determinism + cheapness + gate, NOT teaching ([[LRN-031]], [[LRN-075]]). Mechanical 80% → script; judgment 20% → thin skill (writing-skills: automate mechanical, document judgment). +- **Alternatives rejected**: monolithic teaching/discipline skill (agents reconcile when guided → no teaching value); `grep '[ ]'` lister (reproduces the lie); trust Index (drift, [[LRN-055]]); blocking write-back gate (friction — A/B/C surface chosen). +- **Honest limits (graven)**: deferral detection LEXICAL (marked-only; unmarked "à reprendre quand X" missed); contradictions = CANDIDATES (token overlap), surfaced not asserted; cross-repo "not verifiable here"; cross-ref verdicts ("[~] done because chantier X below complete") surfaced, not auto-resolved. +- **Reference**: `lib/reconcile.sh`, `lib/tests/run-reconcile.sh` (20/20) + `lib/tests/fixtures/` (neutral-named, [[LRN-077]]), `skills/reconcile/SKILL.md`, CLAUDE.md "Skill routing". Born of the 2026-06-29 manual inventory (its known-good oracle). Built via superpowers:writing-skills. See [[LRN-075]], [[LRN-076]], [[EVAL-011]]. + +## BDR-042 — /release-candidate = thin orchestrator over gitflow release; the tag lives in the skill +- **Date**: 2026-06-30 +- **Status**: accepted +- **Decision**: `/release-candidate ` cuts a release by ORCHESTRATING the existing `lib/gitflow.sh` mechanic, NOT rewriting it. Flow: preconditions (clean tree, identity, develop ahead of main) → `gitflow start release` → prep (version.txt + CHANGELOG, breaking documented) → run-tests gate → HUMAN "WHEN to release" gate → `gitflow finish` (fan-out main+develop+delete, lib L108-111) → **`git tag -a vX.Y.Z main`** → push gated. The `git tag` lives in the SKILL, lib UNTOUCHED — the tag is release-specific (version + message + human call), the lib's fan-out is generic. Scheme `vX.Y.Z`, CONTINUES the version.txt/CHANGELOG lineage (never restart at v1.0.0 — desyncs from a CHANGELOG already at 3.x). +- **Why**: gitflow release was wired (start base=develop L49, finish fan-out main+develop L108-111) but had NO tag step (grep-confirmed: zero `git tag` in gitflow.sh). The tag is the only gap; an orchestrator supplies it without touching the tested mechanic. +- **Consequence (accepted)**: a release cut by calling `gitflow finish` directly, bypassing the skill, fans out but is NOT tagged → `/release-candidate` is the CANONICAL sole release path. Acceptable for a solo repo; revisit (tag in lib) only if direct-lib releases become a need. +- **Alternatives rejected**: tag inside `gitflow_finish` (atomic but modifies the tested generic mechanic for a release-specific concern — lib=mechanic/skill=judgment); restart tags at v1.0.0 (desyncs tag↔CHANGELOG lineage). +- **Reference**: `skills/release-candidate/SKILL.md`, `lib/tests/run-release-candidate.sh` (RED no-tag → GREEN 5/5), CLAUDE.md routing. Built via writing-skills TDD. Consumes the gitflow model [[BDR-039]]. See [[LRN-078]], [[LRN-079]], [[EVAL-012]]. diff --git a/.claude/memory/evals.md b/.claude/memory/evals.md index 0d55720..d5d50d6 100644 --- a/.claude/memory/evals.md +++ b/.claude/memory/evals.md @@ -30,6 +30,9 @@ rules: | EVAL-007 | 2026-06-26 | Coupled-capitalize machinery — TDD 13 + e2e, surgical scope proven | keep | | EVAL-008 | 2026-06-27 | Doc-sync coupled machinery — 28/28 real-exec, swap-sweep caught prior debt | keep | | EVAL-009 | 2026-06-27 | deploy skill subagent-driven build: multi-stage review + pressure-test net-positive | keep | +| EVAL-010 | 2026-06-29 | prune-memory hardening: RED-7 deterministic fix + RED-8 accept + 34-row index backfill | keep | +| EVAL-011 | 2026-06-30 | /reconcile build: RED contaminated→corrected (unguided control), GREEN behavioral confirmed, dogfooded on itself | keep | +| EVAL-012 | 2026-06-30 | /release-candidate build: RED (gitflow fans out, no tag) → GREEN 5/5 (tag), throwaway-repo flow replay | keep | --- @@ -113,3 +116,23 @@ rules: - **method**: per-task review (sonnet; opus on the keystone) + writing-skills pressure-test (fresh agent on a `PENDING.json`+moved-HEAD fixture) + final whole-branch review (opus). - **anomalies**: (1) the PLAN's code carried 3 latent bugs — missing `git add` for new files, SC2086 unquoted `$viol`, comment-before-shebang SC1128 — all caught by the implementer's TDD+shellcheck gate → plan-code is a DRAFT, the test gate is load-bearing. (2) the final whole-branch review caught 2 Important seam-bugs INVISIBLE to per-task reviews: target-repo `.claude/`-ignored silent no-op ([[LRN-066]]) + `NEXT.sh`-absence non-regeneration → holistic review earns its keep. (3) pressure-test confirmed the cold-resume discipline holds under temptation (the agent excluded the moved-HEAD `0034`). (4) a reviewer subagent bugged out once (user killed it) → re-dispatched clean (transient, not a finding). - **action**: keep. Multi-stage adversarial review + a behavioral pressure-test caught classes of bug single-pass review misses — worth the cost on a keystone skill. + +## EVAL-010 — prune-memory hardening (RED-7 fix + RED-8 accept + 34-row index backfill) +- **Date**: 2026-06-29 +- **method**: read-first cartography (sub-agent, confirmed) → RED-7 closed by a DETERMINISTIC test ([[LRN-046]]) + STEP-2 example fictionalized → RED-8 re-reviewed, consciously accepted ([[LRN-047]]) → 34 missing Index rows composed + inserted in ID-sorted slots → STEP-4 verify zero MISSING/ORPHAN; deterministic suite all-green, shellcheck clean. +- **anomalies**: (1) RED-7 test FALSE-GREEN caught in real time — ugrep parsed `-9..` as an option → empty → green; fixed via /usr/bin/grep ([[LRN-074]]). The RED was WATCHED, not assumed. (2) RED-7 premise verified: LRN-014/016 ARE complementary → the old example modeled a WRONG merge, not just primed it. (3) backfill: 4/5 title-derived Applies-to (the awk-missed entries) missed a real future-app nuance on re-read → corrected before insert (without the 5-check, 4 Index rows would have diverged from source, engraved forever). (4) almost wrote a colliding EVAL-009 (deploy) — read the file first → EVAL-010. (5) pre-existing LRN-021 Index row out of ID-order → moved. +- **action**: keep. RED-7 GREEN (deterministic), RED-8 documented-accept, drift 34→0. [[LRN-073]] + [[LRN-074]] engraved — 2 pattern-families this session (fail-silent [[LRN-066]]/[[LRN-071]] + command-assumption [[LRN-074]]). + +## EVAL-011 — /reconcile build (TDD): contaminated RED corrected, behavioral GREEN, dogfood on itself +- **Date**: 2026-06-30 +- **output**: `lib/reconcile.sh` (engine) + `lib/tests/run-reconcile.sh` (20/20) + `lib/tests/fixtures/` (neutral) + `skills/reconcile/SKILL.md` (thin) + CLAUDE.md routing. +- **method**: 2-arm RED — GUIDED baselines (A/B, "use git + justify") SUCCEEDED = contaminated; UNGUIDED tempting baselines (a4872/a0f68) MIRRORED the TODO = real failure ([[LRN-075]]). RED-B = deterministic Index-ignore with TEETH (shim engine to read Index → reds). GREEN behavioral (a8404): same terrain + skill → verified via engine, stale reported done w/ SHAs, applied A/B/C gate, surfaced cross-ref as candidate. Dogfood: ran on the live repo, found its OWN chantier, marked S3 PARTIAL (routage absent per path oracle), not done. +- **anomalies**: (1) first baseline LEADING → corrected with an unguided control ([[LRN-075]]). (2) fixture-name false-signal — a0f68 read "pre-reconcile" from the dir name → re-froze fixtures neutral ([[LRN-077]]). (3) harness caught a real bug mid-build: BLK-004 status bleed from BLK-005's header ([[LRN-076]]). (4) META: marked `[x] routage DONE` BEFORE the CLAUDE.md edit succeeded (errored — Read-first) → created the exact declared-vs-real gap `/reconcile` traps, caught by the next verify. The tool's build produced the gap it detects. +- **action**: keep. RED watched red before green ([[LRN-074]] discipline), bug caught + fixed, behavioral loop closed. + +## EVAL-012 — /release-candidate build (TDD): RED no-tag → GREEN 5/5, throwaway-repo replay +- **Date**: 2026-06-30 +- **output**: `skills/release-candidate/SKILL.md` (thin orchestrator, 45 l) + `lib/tests/run-release-candidate.sh` (flow replay) + CLAUDE.md routing + CHANGELOG [Unreleased] entries (/reconcile + /release-candidate). +- **method**: read-first cartography (gitflow release wired: start L49 base=develop, finish L108-111 fan-out; grep-confirmed NO `git tag` → the gap). TDD on a throwaway repo: RED (`RC_TAG=0`) = start→prep→finish → 4 GREEN (fan-out / merge-back / branch-deleted / CHANGELOG) + 1 RED (tag v4.0.0 absent — gitflow never tags); GREEN (`RC_TAG=1`) = + `git tag -a` → 5/5, tag on main's merge commit. shellcheck clean (caught + fixed an SC2164 mid-build). +- **anomalies**: (1) versioning reasoning corrected by the user — number derives from change nature, not justification ([[LRN-078]]); caveman verified `Removed` not breaking from refs, not memory. (2) tag-in-skill consequence (direct-lib release wouldn't tag) made explicit + accepted, not left implicit. (3) layers kept distinct — this built+tested the skill; cutting the real v4.0.0 is a separate later act. +- **action**: keep. RED red for the right reason (gap = tag), GREEN closes it, teeth proven. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index e699f91..1f1dd93 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -223,3 +223,41 @@ rules: - Migrated ALL 6 repos to gitflow one-by-one (faunosteo, config, bchanot-cv, zenquality, game, claude): master→main, develop, Option-1 owner-pushable protection, master deleted — each delete behind a user eyeball + GO, ZERO loss, no force/`--no-verify`, settings intact. game = already-on-main variant (no master); zenquality keeps `cleanup/post-smtp-fix` (out-of-convention, conscious); bchanot-cv adopted a pre-existing clone (surfaced, not assumed). - claude SELF-APPLIED (ultimate dogfood): its own committed lib migrated it. Chantier landed C1 `feat(gitflow)` 167ea96 + C2 `chore(memory)` 1254643 + socle 620071b; hook now governs claude. gstack submodule dirty (BLK-008 Playwright bump) excluded via `submodule.ignore=dirty` (LRN-070), not reset. - Permission insight: `Bash(export *)` deny false-positives inline-env; `git push` ASK = the real remote-write gate (LRN-069). BLK-010 CLOSED (verified `gitflow_init` root commit closes all 3 components — index+body, append-only). + +## 2026-06-29 (cont.) — MINOR-gate strengthening (doc-syncer) +- Read-first cartography REFUTED the literal premise: "strengthen MINOR gate" = 3 distinct problems; the literal reading (blocking gate on MINOR, option B) contradicts engraved [[BDR-036]]. Same trap as gitflow — premise refuted by the real, not assumed. +- Scope tranché ①+②, ② first, never B, ③ deferred. Built test-first (Iron Law RED→GREEN, RED shown before each GREEN). +- ② masked-commit fix ([[LRN-071]]) — 3rd occurrence of the swallowed-commit pattern ([[LRN-066]], [[LRN-068]]/[[BLK-012]]). `doc-commit.sh` exit 5 fail-loud. RED T8 proved the masking (rc 0 + stale hash + false "committed"), GREEN 32/32. +- ① MINOR-shape oracle ([[BDR-040]], `lib/doc-shape.sh`) — 19/19 + behavioral Scenario D. Engraved limit: structural floor, NOT semantic (reduction of RISK-1's gross cases, not elimination). +- Branch `feature/minor-gate-strengthening`; committed code + memory; FINISHED → develop (`0f0bd7f`) on explicit signal. Held the merge until the explicit go — the "avis-en-question" wasn't it. + +## 2026-06-29 (cont. 2) — BLK-011 resolved by REMOVAL (init-project GSD bootstrap) +- User challenge reframed the chantier: don't plumb a commit for the stranded ROADMAP — ask if gsd belongs at init AT ALL. Read REFUTED both my option-premises (gsd ≫ roadmap; TODO ≠ gsd ROADMAP) but conclusion A (remove STEP 12) held for a STRONGER reason: speculative auto-bootstrap of an unused multi-session engine at creation is bad per se. Best fix = NEGATIVE diff ([[LRN-072]]). +- Removed init-project STEP 12 (+ header 12→11-step, 10c note, 4 USAGE coherence fixes). Coherence sweep = zero dangling STEP-12 refs (the "test" for a removal). Deliberate gsd use KEPT (onboarder PHASE 6, plugin-advisor, status-reporter). [[BLK-011]] → resolved. +- Branch `bugfix/blk-011-gsd-roadmap`; FINISHED → develop (`ce4391a`) on explicit signal; pushed develop to origin (6 commits, SSH). + +## 2026-06-29 (cont. 3) — prune-memory hardening (RED-7/8 + index backfill) +- Read-first cartography (confirmed my own measurements). RED-7 (example-priming): the STEP-2 example named live LRN-014+016 and modeled merging them — verified COMPLEMENTARY, a merge the skill forbids. Fix = fictionalize example to 9xx + DETERMINISTIC test ([[LRN-046]], not flaky behavioral). [[LRN-073]]. +- RED-7 test caught its OWN false-green in real time: ugrep parsed `-9..` as an option → empty → green; fixed via /usr/bin/grep. 4th command-assumption miss this session → [[LRN-074]] (2nd engraved pattern-family, alongside fail-silent [[LRN-066]]/[[LRN-071]]). +- RED-8 (added-negation): consciously ACCEPTED as documented limit ([[LRN-047]] — FP-prone guard worse than honest limit on a destructive skill). +- Index backfill: 34 missing rows (decisions 11, learnings 21, blockers 2) composed + ID-sorted insert; drift 34→0, STEP-4 verify OK. Re-read the 5 awk-missed Applies-to → 4 corrected a nuance the title dropped. Moved pre-existing out-of-order LRN-021. [[EVAL-010]]. +- Branch `bugfix/prune-memory-hardening`; no finish yet (awaiting signal). LAST of 3 chantiers. + +## 2026-06-29 (cont. 4) — TODO reconcile + /reconcile skill queued +- Session question "open-work queue really empty?" answered by READING sources (TODO, BLK, BDR/LRN deferred) vs REAL git state, not conversation memory. TODO lied 7 lines: FINISH+PUSH prune-memory already done (merge `73e12be`, develop==origin), 3× `[ ] Commit` (tree clean → shipped), `.gitmodules` follow-up (a) done (`be1dcef`), doc-sync twin done ([[BDR-036]]), v2 Stop-hook marked "deferred" but REJECTED ([[BDR-037]]). +- Contradiction caught: chantier `--help` (STEP 0.5 per SKILL.md) contradicts [[BDR-001]] accepted (helper via session-start hook; per-SKILL.md copy REJECTED) → `--help` BLOCKED pending BDR-001 resolution (supersede or re-route). +- Our OWN manual inventory had an error: line 26 cleanup-machine declared "auto-cleaned next make plugin" but fs shows darwin-skill still present → demoted "done"→"still deferred" after fs check. Proof-by-example the queue needs a RECONCILER (declared-vs-real), not a `[ ]`-grepper. +- Reconciled TODO (5 ticked + 1 requalify + 1 split, annotated `reconcile 2026-06-29` w/ evidence) + queued `/reconcile` skill chantier (4-cat output, inter-registry contradiction detection, GATED TODO edit). Sequencing: /reconcile FIRST (oracle = today's inventory, perishable) → resolve BDR-001 → --help. + +## 2026-06-30 — /reconcile skill shipped (declared-vs-real reconciler) +- Built `/reconcile` via superpowers:writing-skills (TDD): engine `lib/reconcile.sh` + harness 20/20 + thin gated skill. Recursive coherence (never trust a declarative source, incl. Index) made a TESTED guarantee — T1 reds on an Index-reader shim. [[BDR-041]]. +- RED 2-arm: guided baselines succeed (contaminated) / unguided mirror the TODO (real failure) → value = determinism+gate, not teaching ([[LRN-075]]). GREEN behavioral confirmed; dogfooded on its own chantier (S3 marked partial honestly). [[EVAL-011]]. +- Learnings: unguided-control RED ([[LRN-075]]); last-block-wins status + BLK-004 bleed bug ([[LRN-076]]); neutral fixture names = same symptom/distinct cause as [[LRN-074]] ([[LRN-077]]). +- Ship: feature/reconcile-skill → develop (gitflow finish). Push to origin gated (ASK). + +## 2026-06-30 (cont.) — /release-candidate skill built (gitflow release orchestrator) +- Built `/release-candidate` via writing-skills TDD: thin orchestrator over gitflow release + the version tag the lib lacks (grep-confirmed no `git tag` in gitflow.sh). RED (gitflow fans out, no tag) → GREEN 5/5 on a throwaway repo. [[BDR-042]], [[EVAL-012]]. +- Decisions: tag in the skill not the lib (release-specific vs generic mechanic); canonical sole release path (direct-lib release wouldn't tag, accepted); vX.Y.Z continues the lineage. +- Learnings: semver derives from change nature, caveman = Removed not breaking ([[LRN-078]]); orchestrator-skill TDD = throwaway-repo flow replay ([[LRN-079]]). +- CHANGELOG [Unreleased]: added /reconcile + /release-candidate under ### Added (so the eventual v4.0.0 captures them — /reconcile shipped without its entry, rectified here). +- Ship: feature/release-candidate-skill → develop (gitflow finish). Push gated (ASK). Real v4.0.0 cut = separate later act (layer 2). diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 3fa8a75..5136b27 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -28,7 +28,6 @@ rules: | LRN-006 | 2026-05-03 | `caveman-shrink` (and any MCP middleware proxy) non-functional without upstream wrapper | any MCP middleware/proxy package — never `claude mcp add` it bare | | LRN-007 | 2026-05-06 | `toggle-external.sh enable` missed source-only state (3rd lifecycle case) | toggle scripts for tools with separate install + symlink steps | | LRN-008 | 2026-05-06 | Biggest skill-quality wins from edge-case tables, not workflow rewrites | any skill <85 — first check for FAILURE PATHS / EDGE CASES / ERROR HANDLING section | -| LRN-021 | 2026-05-20 | Refactor commands→skills must sweep `~/.claude/commands/` for orphan wrappers | any refactor moving `agents/foo.md` → `skills/foo/SKILL.md`; onboard/init-project audits | | LRN-009 | 2026-05-06 | Dry-run scoring noise wrongly triggers reverts on already-strong skills | darwin-skill ratchet on skills >91 — relax or use real subagent eval | | LRN-010 | 2026-05-06 | `~/.claude/skills,agents` symlink to Documents/claude — git from `~/.claude` fails | any optimization or batch edit on personal skills/agents | | LRN-011 | 2026-05-07 | Single subagent emits N independently-gated scores → labeled extraction + axis-aware loop + per-axis escalation | any audit pipeline shipping multiple gated metrics from one subagent | @@ -40,15 +39,37 @@ rules: | LRN-017 | 2026-05-12 | Thin-dispatcher SKILL.md round-1 win = fallback + frontmatter triggers (+15 to +30) | any `/darwin-skill` round-1 on a dispatcher SKILL.md | | LRN-018 | 2026-05-12 | Darwin eval subagents drift on total math — recompute in main thread | any subagent-driven SKILL.md rescore | | LRN-019 | 2026-05-15 | Deployable-project doc split: README dev-quickstart + DEPLOY 14-section prod-VPS topology | any onboard/doc-syncer/scaffold producing docs for a deployable project | +| LRN-020 | 2026-05-18 | profile-sentinel-collision: literal labels in cmd output must not match profile filenames | a CLI reporting a real named-identifier OR a "nothing applied" state — keep sentinels outside the namespace (string-eq consumers break) | +| LRN-021 | 2026-05-20 | Refactor commands→skills must sweep `~/.claude/commands/` for orphan wrappers | any refactor moving `agents/foo.md` → `skills/foo/SKILL.md`; onboard/init-project audits | +| LRN-022 | 2026-05-21 | audit `lib/profiles/*.profile` against the gstack skill list after every submodule bump | any gstack submodule bump / external-skill-source move; a "missing:" warning = upstream rename/deletion (link.sh can't fix) | +| LRN-023 | 2026-05-21 | scripts invoked via symlink must resolve `$REPO` with `cd -P` (physical), not logical `cd` | any script invoked via a symlink that derives its repo root from `$BASH_SOURCE` (cd -P/realpath; Python .resolve()) | | LRN-024 | 2026-06-02 | New sibling command sharing logic → extract helper + refactor existing caller, never copy-paste; assert pre/post state equality | adding a subcommand/branch reusing logic inline in a peer command | | LRN-025 | 2026-06-02 | `.gitignore` gstack allowlist must cover ALL toggleable skills (incl. parked) — else enabling one = untracked git noise | any toggle that moves local-symlink skills into a tracked dir; post-submodule-bump reconcile | | LRN-026 | 2026-06-09 | `disable-model-invocation: false` = ENABLED not blocking; only `true` blocks (model + orchestrator); binary, no per-caller | Claude Code skill frontmatter; deciding self-route/chain vs human-only entry point | | LRN-027 | 2026-06-11 | Agents improvise audit boundaries from file dates when no machine state — periodic skills need machine-readable state file, never inference | any recurring/periodic skill needing "since last run" semantics | +| LRN-028 | 2026-06-11 | "no-skill" subagent baselines invalid when the skill is installed globally | any A/B skill eval / TDD RED baseline / darwin with-vs-without — control must REMOVE the capability, not omit mention | +| LRN-029 | 2026-06-11 | an edit adding an exception to a blanket rule will contradict it — counterbalanced blind judges catch it | skill/doc/spec edits adding a branch/exception; scoring any self-modified artifact (counterbalanced blind judges) | | LRN-030 | 2026-06-18 | Opus 4.8 under-delegates subagents/memory/custom-tools by default — counter via explicit CLAUDE.md fan-out rule | any Opus 4.8 session; tuning delegation; inline-vs-subagent decision | | LRN-031 | 2026-06-19 | Skill value = gate + anti-noise + determinism, not re-coding what a capable agent does free | building/reviewing any skill; writing-skills TDD fixture design | +| LRN-032 | 2026-06-19 | a rule has a domain; applying it outside = category error — check artifact type first | invoking a limit/convention/style rule — confirm it governs THIS artifact class | +| LRN-033 | 2026-06-19 | multibyte separator breaks `printf %-Ns` byte-width padding — pad via `${#}` char-count | aligning any column with non-ASCII (·, —, box-drawing, accents) | +| LRN-034 | 2026-06-21 | narrated state ≠ ground truth; the missed alarm was internal contradiction — verify vs git | anyone asserts "X is done" — verify (git/file/grep) before building on it | +| LRN-035 | 2026-06-21 | honest dedup: name-mention ≠ definition-instance; a dosage rule can make "dedup" a no-op | any "X repeated N times → factor it" — audit what each occurrence IS | +| LRN-036 | 2026-06-21 | `command -v ` in a shelled-out script depends on PATH carrying the cli's bin, not the alias | any script shelling out a CLI from a hook/subshell | +| LRN-037 | 2026-06-21 | verify the load-bearing scenario on the REAL subject in REAL context, not a stub/logic argument | any "fixed/works" claim on a critical path — produce the real run output | +| LRN-038 | 2026-06-23 | Playwright host-platform override for distros newer than its hardcoded support list | any pinned tool with an OS allowlist breaking on a fresh OS upgrade | +| LRN-039 | 2026-06-23 | installers drift hand-curated config → snapshot+trap-restore guard; anchor gitignore for pollution | audit a fresh install with `git status` right after `make install` | +| LRN-040 | 2026-06-23 | OS newer than a pinned tool = TWO layers (version build + security policy) | "tool X broke after an OS upgrade" — check both build-support and OS hardening | +| LRN-041 | 2026-06-23 | a check reading a symlink an earlier install step makes → false negative if that step's precondition unmet | any "X not found in FILE" where FILE is a symlink/derived path | +| LRN-042 | 2026-06-23 | `npx skills add` / gstack `./setup` resolve install target RELATIVE TO CWD — repo CWD = wrong dir | before any `npx add` / ` init` that materializes a dotfile dir, set CWD | +| LRN-043 | 2026-06-25 | CLAUDE.md skill-routing: cut name-obvious lines, keep only non-derivable signal + dense catch-all | compressing any routing/dispatch table whose entries the model sees elsewhere | +| LRN-044 | 2026-06-25 | Edit/Write refuse to write THROUGH a symlink — pass the resolved real path | before editing any `~/.claude/...` config file — resolve it first | +| LRN-045 | 2026-06-25 | renaming a command: audit exact-name leak-guard / forbidden-token regexes | when renaming, grep the BARE old token inside regex/test/gate files | | LRN-046 | 2026-06-25 | Destructive skill: deterministic oracle (byte-identical / count census) > semantic judge | any destructive/irreversible skill; behavioral-oracle TDD | | LRN-047 | 2026-06-25 | A noisy safety guard (13/13 FP) = a guard you learn to ignore = risk → refine, don't tolerate | any guard/alert/lint that can false-positive | | LRN-048 | 2026-06-25 | A "0/OK/pass" must prove it LOOKED (counted both sides), else verify hard-wired to pass | any verify/test/lint reporting success | +| LRN-049 | 2026-06-25 | non-destructive repeated nudge: stateless-minimal surface > state marker (conditional on stakes) | any repeated advisory in a stateless surface — bound noise before reaching for a marker | +| LRN-050 | 2026-06-25 | on a symlinked/live file, show-before-write is the ONLY control gate | before editing any file — check if it is live, treat pre-write diff as an approval gate | | LRN-051 | 2026-06-26 | `git commit -- pathspec` strict on no-match → filter scoped commits to changed paths | any scoped-commit automation | | LRN-052 | 2026-06-26 | Hash-anchoring: 2 cases it does NOT apply (pre-code founding, squash-merge) | capitalizing founding/arch decisions; squash repos | | LRN-053 | 2026-06-26 | Read-before teeth = verifiable disposition in the artifact, not the act of reading | any read-before / check-before wiring | @@ -69,6 +90,15 @@ rules: | LRN-068 | 2026-06-29 | enforcement-bootstrap must be transactional: activate the guard LAST + gate it on the bootstrap commit succeeding; precheck identity | any init that installs a hook/protection AND commits | | LRN-069 | 2026-06-29 | token-authed remote writes under CC perms: inline-env (never `export`), token in header not argv, keep `git push` on ASK as the gate | scripting git/curl writes to a private remote from tool calls | | LRN-070 | 2026-06-29 | clean-tree-gated migration blocked by a dirty submodule → diagnose pointer-vs-content; for a local edit use `submodule..ignore=dirty`, never blind reset | migrating/releasing a superproject whose submodule carries intentional local edits | +| LRN-071 | 2026-06-29 | fail-loud must cover the helper's OWN commit, not just its inputs — 3rd occurrence of the swallowed-commit pattern (a failed op masked by a later returning-0 statement) | any helper whose return value gates a downstream "success" — audit every fallible internal op propagates, esp. the commit | +| LRN-072 | 2026-06-29 | a stranded-artifact bug can be fixed by NOT creating the artifact (negative diff), not by plumbing its commit — if the producing step is speculative/unused, delete it | a stranded/duplicated/uncommitted-artifact bug — before building machinery, ask if the PRODUCING step is wanted; speculative-at-creation → remove, deliberate-on-demand → keep | +| LRN-073 | 2026-06-29 | a skill's worked-example must use FICTIONAL ids, never live registry ids (they prime real-data behavior) | any skill/agent with a worked example over the SAME data it operates on — use reserved/fictional ids; test deterministically that no live id appears | +| LRN-074 | 2026-06-29 | system `grep`/`awk` may be ugrep/mawk: don't assume flag-parsing, use `/usr/bin/grep`, watch the RED go red (4th command-assumption miss this session) | any shell test/guard riding on grep/awk/sed semantics — pin `/usr/bin/`, run the assertion, confirm it reds on the defect before trusting green | +| LRN-075 | 2026-06-30 | skill-vs-no-skill RED must test the UNGUIDED control: a "use git + justify" baseline makes a capable agent succeed (contaminated RED); real failure shows only on the tempting prompt | building any skill whose value is determinism/gate over a capable agent — strip guidance AND tempt the failure; control succeeds → don't author (or rescope) | +| LRN-076 | 2026-06-30 | append-only registry status mutates in place (UPDATE/FINAL blocks): CURRENT status = LAST status line, not Index, not first; range scan inclusive of next header bleeds a sibling's status word | parsing any in-place-mutated status; take last line, bound entry exclusive of next header | +| LRN-077 | 2026-06-30 | test fixtures must carry NEUTRAL names — a name that telegraphs the answer lets the subject pass by reading the name, not doing the work | designing any test fixture/path; same symptom as [[LRN-074]] (passes for WRONG reason), distinct cause (leaky fixture vs assumed command) | +| LRN-078 | 2026-06-30 | semver number DERIVES from the change nature, not "justify a target"; solo-repo "breaking" = requires a migration of own usage; a removal nothing invokes = Removed not breaking | choosing a release version; classifying MAJOR/MINOR/PATCH; deciding if a removal is breaking | +| LRN-079 | 2026-06-30 | orchestrator-skill TDD = replay the prescribed flow on a throwaway repo (gitflow-test style): RED runs the flow minus the new step → the outcome assertion reds on the gap | testing a skill that orchestrates an existing mechanic + one new step | --- @@ -794,3 +824,52 @@ rules: - **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. - **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 +- **pattern**: a surgical-commit helper guarded LOUD on its INPUTS (scope) but SILENT on its OWN `git commit`. `doc-commit.sh`: `set -uo pipefail` (no `-e`) + unguarded `git commit` → on rejection (pre-commit hook on a protected branch / signing / etc.) execution CONTINUES: `printf "committed"` lies, `git rev-parse --short HEAD` emits the PREVIOUS HEAD hash, function exits 0. Orchestrator reads rc 0 + non-empty hash → believes success; docs silently uncommitted, tree dirty (RISK-2). +- **RECURRENT (3×) — audit systematically, not an isolated bug**: same fail-silent-where-it-must-fail-loud class in the surgical-commit family — [[LRN-066]] (`deploy-commit.sh`: porcelain hides a git-ignored path → silent no-op; fix = loud rc 5) + [[LRN-068]]/[[BLK-012]] (`gitflow_init`: socle-commit failure swallowed by `||` then `git branch` returned 0 → init continued past the dead commit) + this. The common mechanism: a fallible op (esp. a commit) whose failure isn't propagated, MASKED by a later returning-0 statement. The motif RETURNS; treat it as a known smell. +- **fix**: guard the commit — `if ! git commit …; then LOUD + return 5; fi`. rc 5 = "tried, git refused" (distinct from rc 3 = "could not start"). Empty stdout (no stale hash), loud stderr. Proven by T8: RED showed the masking (rc 0 + stale hash + false "committed"), GREEN rc 5 + empty + REJECTED, 32/32. +- **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). The reflex fix (reorder + build `gsd-commit.sh` + tests) would have added 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**: the framing "ROADMAP redundant with TODO" was 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**: a stranded / duplicated / uncommitted-artifact bug → BEFORE building machinery to handle the artifact, ask whether the step that PRODUCES it is actually used / wanted / non-speculative. Speculative or unused (esp. a personal/single-user repo) → DELETE the producer; the cleanest fix is the 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". +- **fix**: fictionalize example ids (9xx — can't match a live registry) + make the example model a CORRECT action. Lock it DETERMINISTICALLY ([[LRN-046]]): assert the example carries only fictional ids — not a flaky behavioral "did priming fire" test (RED-7). +- **future application**: any skill/agent whose instructions contain a worked example over the SAME data it operates on (registries/files/records) → use reserved/fictional identifiers; test deterministically that no live id appears in the example block. + +## LRN-074 — system `grep`/`awk` may be ugrep/mawk: don't assume flag-parsing, use `/usr/bin/grep`, watch the RED go red +- **pattern**: a RED-7 test used `grep -vE '-9[0-9][0-9]$'`; the system grep is UGREP → parsed the leading `-9..` as an OPTION → errored → empty → FALSE GREEN (a RED that never goes red). Caught only because the output was READ, not assumed. 4th time this session an assumed command behavior was false on execution (after `set -o pipefail` + `grep -q` SIGPIPE, …). The skill's own verify already hard-codes `/usr/bin/grep` (line 189) for this exact reason — re-learned. +- **fix**: `/usr/bin/grep` (GNU) where GNU semantics matter; avoid leading-dash regex args (or use `-e`/`--`); never trust the system tool is GNU/POSIX (mawk≠gawk, ugrep≠grep). +- **future application**: any shell test/guard whose correctness rides on grep/awk/sed semantics → pin `/usr/bin/` AND run the assertion, confirming it goes red on the defect before trusting green. Execute, don't assume command behavior. RECURRENT motif — audit any "assumed tool behavior" the way the fail-silent family ([[LRN-066]]/[[LRN-071]]) is audited. + +## LRN-075 — skill-vs-no-skill RED must test the UNGUIDED control, not a leading one +- **Date**: 2026-06-30 +- **pattern**: building `/reconcile`, the first baseline ("repo git — use git + justify each item") made a capable agent reconcile correctly → CONTAMINATED RED (writing-skills: control doesn't exhibit the failure → nothing to fix). Real failure surfaced only on the UNGUIDED tempting prompt ("is the queue empty?", todo-pointed, no git hint): agent MIRRORED the TODO — stale `[ ]` reported open (false positives), decisions.md never opened, contradiction missed. Variance: one rep FLAIRED staleness but wrote a disclaimer ("à vérifier") instead of verifying — a disclaimer is not a verification. +- **why**: skill value was never "teach an agent to reconcile" (it can, guided) — it is determinism + cheapness + gate ([[LRN-031]]), so the answer never depends on phrasing or whether the agent felt like checking git. Confirmed engine-heavy / skill-thin design. +- **future application**: any skill-vs-no-skill / TDD RED — the control must REMOVE guidance AND tempt the failure ([[LRN-028]] sibling). Unguided control still succeeds → don't author (or rescope to determinism/gate). The skill also helps the agent who SUSPECTS but doesn't verify, not only the ignorant one. + +## LRN-076 — append-only registry status mutates in place: LAST block wins +- **Date**: 2026-06-30 +- **pattern**: a BLK entry evolves via `UPDATE`/`FINAL` blocks (BLK-008: `resolved` → middle `REVERTED, UPSTREAM/open` → `FINAL — RESOLVED`). A guided baseline read the MIDDLE block → reported BLK-008 upstream/open, wrong. Current status = the LAST status-bearing line in the body, never the Index, never the first. Bonus bug the harness caught mid-build: a `sed` range inclusive of the NEXT entry's header bled a sibling's status word (BLK-005 header "...upstream rename" polluted BLK-004) → drop `^## BLK-` header lines before extracting. +- **future application**: parsing any in-place-mutated status (blockers, revision blocks) — take the LAST status line, bound the entry EXCLUSIVE of the next header. Don't trust Index/first-line. + +## 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 = a COMMAND-ASSUMPTION (ugrep parsed `-9..` → false green); 077 = a LEAKY FIXTURE (name telegraphs the answer). Different mechanisms, SAME symptom: the 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). +- **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?" + +## 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. Semver runs the other way: 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. +- **applied (v4.0.0)**: gitflow universal = a 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, then 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. + +## LRN-079 — orchestrator-skill TDD: replay the flow on a throwaway repo, RED = flow minus the new step +- **Date**: 2026-06-30 +- **pattern**: a thin orchestrator skill (composes an existing tested mechanic + ONE new step) is not unit-testable as a function, but its FLOW is testable by replay on a throwaway repo (gitflow-test style). RED = run the prescribed sequence WITHOUT the new step (the existing mechanic alone) and assert the desired outcome → it reds on exactly the gap. GREEN = add the step. For `/release-candidate`: `gitflow start release`→prep→`finish` (no tag) → assert `vX.Y.Z` on main → REDS (gitflow fans out but never tags); add `git tag` → 5/5. Teeth: the single toggled line (`RC_TAG`) flips red↔green so GREEN can't pass by accident. +- **future application**: for any orchestrator over a lib mechanic, test the END-TO-END flow on a disposable repo; isolate the NEW step so the RED reds precisely on it (don't re-test the lib's generic part — it has its own tests). diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index ae39d41..e9cba12 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -25,8 +25,9 @@ Root causes trouvées (logs install-20260623-181416.log) : cycle minimal↔full OK ; git propre (symlinks gstack gitignorés) ; profil full restauré - [~] Cleanup machine courante : $REPO/.claude/skills/darwin-skill + .agents/skills VIDE restent (rm bloqué par garde permission .claude/) → auto-nettoyés au prochain `make plugin` + [reconcile 2026-06-29 : TOUJOURS présents (fs-vérifié, darwin-skill 116K daté 23/06) — `make plugin` pas rejoué depuis. Reste différé, déclencheur = prochain install.] - [x] Capitalize — LRN-042 (Bug B CWD-relatif) + BDR-030 (gstack on-demand par profil) + journal 2026-06-23 -- [ ] Commit (via /commit-change) +- [x] Commit (via /commit-change) — DONE (reconcile 2026-06-29 : working tree clean, travaux shippés) ## profile.sh — verbe `gstack on|off` - [x] Extraire helper `enable_all_gstack()` (boucle de cmd_reset) — anti-duplication @@ -80,7 +81,7 @@ Root causes trouvées (logs install-20260623-181416.log) : - [x] Ajouter 2 niveaux d'audit (LOCAL code-only / FULL live+externe) - [x] Adapter scoring, legal, GEO aux deux niveaux - [x] Renumeroter proprement (0-14) + corriger toutes les refs internes -- [ ] Commit +- [x] Commit — DONE (reconcile 2026-06-29 : working tree clean, agent seo-analyzer live) ## /onboard — cso archetype-aware Problème : prompt cso fallback est non-adaptatif — cherche XSS/SQLi/CORS même sur firmware. @@ -131,6 +132,7 @@ Subtasks : - [x] Tester : shellcheck OK ; matrix React/Vue/RN/backend/with-motion/no-package/pnpm tous corrects ## Helper `--help` / `help` sur tous les skills (option C) +> ⚠️ BLOQUÉ (reconcile 2026-06-29) : contredit BDR-001 (accepted) qui a REJETÉ "copier le helper dans chaque SKILL.md" (maintenance entropy) au profit d'un hook session-start. Or ce chantier planifie STEP 0.5 par SKILL.md. Le TODO note lui-même "aucun skill ne gère --help aujourd'hui" → la voie hook de BDR-001 n'a jamais produit de --help fonctionnel. TRANCHER d'abord : BDR-001 périmé → marquer superseded, OU repasser par le hook. Ne pas lancer avant résolution. Problème : aucun skill ne gère `--help` aujourd'hui. `argument-hint` affiche juste la syntaxe en autocomplétion, pas de description/exemples. L'utilisateur doit lire le SKILL.md ou deviner. Objectif : `/ --help` (ou `/ help`) affiche un bloc standardisé (description, args, exemples, cross-refs) et exit SANS dispatcher l'agent ni modifier quoi que ce soit. @@ -210,7 +212,7 @@ obligatoire avant axe suivant. Construit via superpowers:writing-skills (TDD). - [x] Vérif finale : skill découvrable (~/.claude/skills/audit-delta via symlink skills/), frontmatter valide, worktrees de test nettoyés - [x] Capitalize : BDR-020 + LRN-027 + journal 2026-06-11 -- [ ] Commit (via /commit-change quand prêt) +- [x] Commit (via /commit-change quand prêt) — DONE (reconcile 2026-06-29 : working tree clean, skill audit-delta live) ## 2026-06-11 — darwin eval: 4 confirmed bugs fix (branch auto-optimize/*-bugfixes) @@ -245,8 +247,8 @@ doc-sync twin chantier deferred. Safety in the pathspec, never `git add -A`. - [x] Task 4 — ship-feature reorder (capitalize before FINISH) — e8eff7e - [x] Task 5 — init-project founding-decisions capitalize (F5) — df60df6 - [x] Task 6 — behavioral verify + shellcheck + CHANGELOG + BDR/LRN — this commit -- [ ] v2 (deferred) — Stop hook (non-blocking, BDR-033 style) reusing the detector -- [~] twin chantier — doc-sync → own plan (2026-06-27). NOTE: "reorder before FINISH" REFUTED — doc-syncer commits nothing, needs reorder + NEW doc-commit mechanism. +- [x] v2 — REJETÉ (pas différé) — BDR-037 (reconcile 2026-06-29) : aucun event CC ne supporte un nag de fin-de-session (Stop = par-tour, SessionEnd = debug-log only). Vrai manque = câblage, corrigé en wirant /capitalize+/close à l'include. Aucun code à écrire. +- [x] twin chantier — doc-sync DONE (reconcile 2026-06-29) : chantier propre livré ci-dessous (2026-06-27, BDR-036). La note REFUTED était juste — doc-commit BÂTI, pas reorder-seul. ## 2026-06-27 — doc-sync coupled (twin of coupled-capitalize) Plan: [.claude/tasks/2026-06-27-doc-sync-coupled.md](2026-06-27-doc-sync-coupled.md) @@ -262,8 +264,8 @@ reorder + CREATE doc-commit.sh/.md (mirror memory-commit, 4 deltas). Surface-don - [x] Task 6b — wire doc-commit into feat/bugfix/hotfix DOC SYNC — 1b01b95. commit-change exempt (no DOC SYNC); hotfix wired (include no-ops on empty). - [x] Task 7 — close: `run-doc-behavioral.md` + shellcheck clean + 28/28 + CHANGELOG + BDR-036 / LRN-058-060 / EVAL-008. surface-replaces-gate + partial-init + scope-expansion engraved honestly. - [x] RESOLVED 2026-06-29 — [[BLK-010]] closed by `gitflow_init` root commit (init-project STEP 5f): scaffold/README get a deterministic commit owner + HEAD born before the worktree step. Verified (mechanism + STEP 5f wiring + T2 test); blockers.md index+body updated. -- [ ] flagged separate — [[BLK-011]] GSD ROADMAP.md post-FINISH (now STEP 12 after Task 5 renumber; BLK-011 record itself left at STEP 13 — append-only) -- [ ] flagged separate — strengthen doc-sync MINOR gate (own doc-syncer chantier) +- [x] RESOLVED 2026-06-29 — [[BLK-011]] closed by REMOVAL: init-project STEP 12 (speculative gsd auto-bootstrap) deleted → orphan never created. Negative diff, not commit-plumbing ([[LRN-072]]). See chantier below. +- [x] DONE 2026-06-29 — doc-sync MINOR gate strengthened: ① shape-oracle [[BDR-040]] + ② masked-commit fix [[LRN-071]] (③ branch-guard deferred). See chantier below. ## 2026-06-29 — gitflow universal model + 6-repo migration (DONE) Goal: universal gitflow across all `bchanot/*` Gitea repos. Lib built across prior sessions; migrated + hardened + dogfooded this session. @@ -275,4 +277,91 @@ Goal: universal gitflow across all `bchanot/*` Gitea repos. Lib built across pri - [x] Deleted merged branches: `feat/deploy-skill` (local+remote) + `cleanup/caveman-always-on` (remote) - [x] Dogfood PROVEN: hook whitelists `.claude/**` on main + Option-1 lets owner push (commit `1620e5b`) - [x] Capitalize: BDR-039 (Option-1 protection), LRN-068/069/070, BLK-010 closed + BLK-012, journal 2026-06-29 — committed + pushed on main -- [ ] follow-up (optional) — `submodule.gstack.ignore=dirty` into committed `.gitmodules` (share across clones); zenquality `cleanup/post-smtp-fix` rename `/` or finish+delete +- [x] follow-up (a) — `submodule.gstack.ignore=dirty` committé dans `.gitmodules` — DONE (reconcile 2026-06-29 : commit `be1dcef` sur main, mergé via hotfix/gstack-ignore-gitmodules) +- [ ] follow-up (b) — zenquality `cleanup/post-smtp-fix` rename `/` ou finish+delete (AUTRE repo, optionnel) + +## 2026-06-29 — MINOR-gate strengthening (doc-syncer) [branch feature/minor-gate-strengthening] +Read-first cartography refuted the literal premise: "strengthen MINOR gate" = 3 problems; +the literal one (blocking gate on MINOR) contradicts engraved [[BDR-036]]. Scope: ①+②, not B, +③ deferred. Built test-first (Iron Law). +- [x] ② fix masked commit failure — `doc-commit.sh` exit 5 fail-loud ([[LRN-071]], 3rd occurrence of the swallowed-commit pattern). RED T8 proved masking, GREEN 32/32 + taxonomy (sh header/funcdoc + `doc-commit.md` rc-5 row) +- [x] ① MINOR-shape oracle — `lib/doc-shape.sh` ([[BDR-040]]) + `run-doc-shape.sh` 19/19 (boundary + env-override). Wired doc-syncer STEP A4 (escalate whole set → existing SIGNIFICANT gate; no=revert all, select=keep subset) + `doc-commit.md` ACKNOWLEDGMENTS coherence + behavioral Scenario C/D +- [x] shellcheck clean (doc-commit.sh, doc-shape.sh, both test harnesses); coherence ref-sweep clean +- [x] Capitalize — BDR-040 + LRN-071 + CHANGELOG (Added/Fixed) + journal 2026-06-29 (cont.) +- [x] FINISH — merged feature/minor-gate-strengthening → develop (`0f0bd7f`) on explicit signal +- [~] ③ branch-guard in doc-commit DEFERRED — duplicates protected-base predicate 3rd time (lib + hook + here); all migrated repos have the hook. Reconsider only for repos outside `gitflow init` + +## 2026-06-29 — BLK-011 GSD ROADMAP post-FINISH [branch bugfix/blk-011-gsd-roadmap] +User reframed: don't plumb a commit for the stranded ROADMAP — ask if gsd belongs at init at all. +Read refuted both option-premises (gsd ≫ roadmap; TODO ≠ gsd ROADMAP) but conclusion A held for a +stronger reason: speculative auto-bootstrap of an unused engine at creation is bad per se ([[LRN-072]]). +- [x] Resolve by REMOVAL — deleted init-project STEP 12 (negative diff −26/+13), not a commit helper +- [x] Ref-coherence sweep ("test" for a removal) — header 12→11-step, 10c note, 4 USAGE refs; zero dangling STEP-12 refs repo-wide +- [x] Scope guardrails — deliberate gsd use KEPT (onboarder PHASE 6, plugin-advisor, status-reporter) +- [x] Capitalize — [[BLK-011]] resolved (true reason + premise trace) + [[LRN-072]] + CHANGELOG Removed + journal 2026-06-29 (cont. 2) +- [x] FINISH — merged bugfix/blk-011-gsd-roadmap → develop (`ce4391a`); develop pushed to origin (6 commits, SSH) + +## 2026-06-29 — prune-memory hardening (RED-7/8 + index backfill) [branch bugfix/prune-memory-hardening] +LAST of 3 chantiers. Read-first cartography confirmed RED-7/8 + measured 34-row index drift. +- [x] RED-7 (example-priming) — fictionalized STEP-2 example to 9xx ids (live ids primed a wrong merge of complementary LRN-014/016); DETERMINISTIC test (run-deterministic.sh) per [[LRN-046]]. Caught its own ugrep false-green → /usr/bin/grep ([[LRN-074]]). [[LRN-073]] +- [x] RED-8 (added-negation inversion) — consciously ACCEPTED as documented limit in BACKLOG ([[LRN-047]]); no fragile guard built +- [x] Index backfill — 34 missing rows (decisions 11, learnings 21, blockers 2) composed + ID-sorted insert; drift 34→0, STEP-4 verify OK; moved pre-existing out-of-order LRN-021 +- [x] Capitalize — [[LRN-073]] + [[LRN-074]] + [[EVAL-010]] + journal 2026-06-29 (cont. 3) +- [x] FINISH — merged bugfix/prune-memory-hardening → develop — DONE (reconcile 2026-06-29 : merge `73e12be`) +- [x] PUSH — develop → origin — DONE (reconcile 2026-06-29 : develop == origin/develop, 0 commit en avance) + +## 2026-06-29 — skill /reconcile (RÉCONCILIATEUR file-ouverte ↔ réel) [SHIPPED 2026-06-30 — develop aede7af, pushed] +Genèse : l'inventaire manuel du 2026-06-29 a prouvé que le TODO mentait (5 cases fait-mais-non-coché ++ 1 "auto-nettoyé" qui ne l'était pas + 1 rejeté marqué "deferred"). Cet inventaire EST la spec du +skill ET son cas de test de référence (résultat manuel connu-bon à reproduire). + +PRINCIPE NON NÉGOCIABLE — ce n'est PAS un grep des `[ ]` du TODO (ça reproduirait le mensonge : dirait +"ouvert" sur du fait-mais-non-coché). C'est un RÉCONCILIATEUR : confronte les sources DÉCLARATIVES à +l'état RÉEL et signale les ÉCARTS. Un lister-de-todos ne vaut rien (grep le fait) ; un "le TODO prétend +X, le réel est Y" vaut beaucoup. +- Sources DÉCLARATIVES : TODO.md ; BDR deferred/follow-ups/caveats ; BLK status=open|upstream ; + LRN caveats "revisit if / re-run if". +- État RÉEL (oracles) : git (branche mergée/absente, commit existe, origin sync, working tree clean) ; + fichier live = committé (skill/agent présent + linké = shippé) ; statut registre. + +SORTIE = les 4 catégories de l'inventaire 2026-06-29 : + 1. actionnable maintenant (non bloqué) + 2. bloqué (condition externe — upstream) + 3. différé (déclencheur conditionnel) + 4. écart TODO↔réalité (fait-mais-non-coché / rejeté-marqué-deferred / "auto-fait" non vérifié) + + CONTRADICTIONS inter-registres (ex. BDR-001 accepted "pas de helper par SKILL.md" vs chantier + --help qui copie dans chaque SKILL.md). + + réconciliation TODO **GATED** : montre les écarts, DEMANDE avant cocher/requalifier + (modifie un fichier tracké → jamais silencieux). + +Subtasks (à détailler au lancement) : +- [x] Spec : table oracle-par-source (commit existe / branche absente / tree clean / skill linké / + statut registre) — chaque "déclaré" a son test réel — DONE (lib/reconcile.sh : reconcile_oracle_*) +- [x] Décider build : superpowers:writing-skills (TDD, RED = fixture TODO menteur reproduisant les 7 écarts) — DONE (RED a4872 miroir + RED-B Index-ignore à dents + GREEN comportemental a8404) +- [x] `skills/reconcile/SKILL.md` — DONE (skill mince : orchestration + gate A/B/C + limites honnêtes) +- [x] routage CLAUDE.md (triggers : "reconcile", "file vraiment vide ?", + "qu'est-ce qui reste ouvert", "inventaire chantiers") — DONE (CLAUDE.md "Skill routing" + link.sh) +- [x] Détecteur de contradictions inter-registres (BDR accepted vs chantier qui le contredit) — DONE (reconcile_contradiction_candidates ; surface, n'asserte pas) +- [x] Gate de réconciliation (diff TODO proposé, A/B/C confirm avant edit) — DONE (SKILL.md "The gate" ; registres read-only) +- [x] Test final = reproduire l'inventaire 2026-06-29 (cat. 1-4 + contradiction BDR-001) comme oracle — DONE (run-reconcile.sh 20/20, fixtures neutres, RED prouvé rouge avant le vert) +- SHIPPED 2026-06-30 : feat `82e6322` + mémoire `6b512be` → merge `aede7af` (feature/reconcile-skill supprimée) → poussé origin/develop. main intact. BDR-041 + LRN-075/076/077 + EVAL-011 capitalisés. + +## [QUEUED] skill /release-candidate — orchestrateur gitflow release (lib vérifiée, le tag est le gap) +Pertinent maintenant : develop ahead de main, prochaine étape gitflow = release. +VÉRIFIÉ dans lib/gitflow.sh (2026-06-30) — release CÂBLÉE, pas que hotfix : +- start base=develop (`gitflow_base_for` L49) ; `gitflow start release ` positionne sur la branche (L71). +- finish fan-out (`gitflow_finish` L108-111) : merge main + merge-back develop + delete (locale, `_gitflow_delete` L96). +- GAP CONFIRMÉ (grep clean) : AUCUN `git tag` ni bump version dans tout gitflow.sh → la mécanique merge mais ne tague PAS. + +Design (à la conception) : ORCHESTRATEUR au-dessus du gitflow existant — NE PAS réécrire la mécanique. +- `gitflow start release ` (depuis develop) → positionne. +- prep sur la branche release : bump VERSION + CHANGELOG (Keep-a-Changelog) + RC fixes. +- `gitflow finish` (merge main + merge-back develop + delete — déjà câblé). +- FOURNIR le tag manquant : `git tag` sur main au merge-commit (le seul morceau absent de la lib). +- push gaté (ASK, [[LRN-069]]) : main + develop + tag. + +Subtasks (à détailler au lancement) : +- [ ] Décider : tag dans le skill VS étendre `gitflow finish` avec un arg tag optionnel (orchestrateur préféré — ne pas réécrire la mécanique) +- [ ] `skills/release-candidate/SKILL.md` — orchestration start→prep→finish→tag→push(gaté) + gate humain "WHEN to release" +- [ ] routage CLAUDE.md +- [ ] test (worktree jetable : prouver fan-out main+develop + tag présent sur main + branche supprimée) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6b706..db22ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] - +## [4.0.0] — 2026-06-30 ### Added +- **Gitflow universal model** — `/gitflow` + `lib/gitflow.sh`: branch model (`main` / `develop` / `feature` / `bugfix` / `release` / `hotfix`) with directed `--no-ff` merges + hotfix fan-out (main + develop + open `release/*`); `start` / `finish` / `init` verbs. `lib/gitflow-migrate.sh` onboards an existing repo (`master`→`main`, seed `develop`, install the pre-commit hook, set Gitea Option-1 owner-pushable protection on `main`+`develop`), applied to all 6 repos. Wired into `/init-project` (STEP 5f `gitflow init` owns the scaffold root commit) + `/onboard` (STEP 2.6). See **BREAKING** under Changed +- `/deploy` — per-project deploy runbook in `.claude/deploy/` (`PROCEDURE.md` / `INCIDENTS.md` ledger / `STATE.json` oracle / `PENDING.json` cold-resume bridge / `NEXT.sh`); two-moment spine (instantiate checklist → out-of-band deploy → MARK success or LEARN from failure, patching the runbook in place); surgical `lib/deploy-commit.sh`; plus `/setup-deploy` +- Analyze-before-plan invariant — dev flows (`feat` / `bugfix` / `hotfix`, `ship-feature`) READ related memory before planning (ship-feature also reads related code) and must NAME each surfaced ID in the plan; shared `lib/analyze-before-plan.md` (read-before bookend of coupled-capitalize) +- Animation-library auto-detection/install — `motion` (`motion-v` for Vue 3 / Nuxt) auto-installed in `/init-project` (STEP 5e), opt-in in `/onboard` (STEP 2.5) on eligible stacks; `plugin-advisor` detects + reports only; logic in `lib/animation-lib-check.sh` +- Design-toolchain gate — `lib/design-tool-gate.sh` + `lib/design-gate.md` + a `design-toolchain-reminder` hook enforce the full design toolchain on UI work (profile-based), with a suggest-only non-blocking anim-lib note when a motion signal hits an eligible stack +- `lib/toggle-external.sh` — enable/disable non-marketplace tools; gstack now OFF by default (opt-in, activated on-demand per profile), Magic MCP (21st-dev) installed disabled by default +- Secrets single source-of-truth — real secret in `~/.claude/.env` reached via a repo `.env` symlink + `.env.example` placeholder; `MAGIC_API_KEY` resolved from it +- `/reconcile` — declared-vs-real reconciler: confronts TODO checkboxes + registry statuses (never the `## Index`) against real git/fs and surfaces the gaps in four categories + contradiction candidates, with a gated TODO write-back. Engine `lib/reconcile.sh` (body enumeration, git/fs oracles, last-block-wins status); thin skill +- `/release-candidate` — orchestrator over the gitflow release mechanic that adds the version tag the lib doesn't: finalize `version.txt` + CHANGELOG, fan-out `develop`→`main` + back, tag `vX.Y.Z`, push (gated). Lib stays the generic mechanic; the skill owns the tag - Coupled-capitalize: dev flows (feat / hotfix / bugfix / commit-change, ship-feature, init-project) auto-commit their memory in the same breath, via shared `lib/capitalize-commit.md` + `lib/memory-commit.sh` (surgical — `.claude/memory` + `.claude/tasks` only, never `git add -A`) - Coupled doc-sync: dev flows (feat / bugfix / hotfix, ship-feature, init-project) auto-commit the public docs `doc-syncer` patches, via shared `lib/doc-commit.md` + `lib/doc-commit.sh` (surgical — only the patched files, never `git add -A`, never `.claude/` / `CLAUDE.md`; refuses an out-of-scope path loudly with exit 4). `doc-syncer` surfaces `PATCHED_FILES` (one path per line) as the handoff +- `lib/doc-shape.sh` — deterministic MINOR-shape oracle for `doc-syncer` AUTO MODE: re-checks each LLM-classified MINOR patch (added-heading / size / new-file / non-doc envelope, thresholds env-overridable) and escalates a shape-suspect patch to the existing SIGNIFICANT gate instead of silently auto-committing it. A structural floor under the LLM's classification, not a blocking gate (genuine MINOR still auto-commits, zero friction); catches structural/size significance, not semantic - `/audit-delta` — recurring multi-axis audit (norms / bugs / dead code / security) scoped to changes since last run, with per-axis SHA markers - `/capitalize` — flush uncapitalized context to the memory registries before `/clear` or `/compact` - `/prune-memory` — curate and compress the `.claude/memory/` registries - `/close` — end-of-session memory ritual (decisions / learnings / blockers) - `/pdf-translate` — translate a PDF to another language, output as HTML (via Vision) - `/harden` — web hardening audit (SSL/TLS, HSTS, CSP, headers) -- `/validate` — W3C HTML/CSS validity + WCAG accessibility audit +- `/web-validate` — W3C HTML/CSS validity + WCAG accessibility audit - `/client-handover` — final ship + branded client deliverable (Markdown / HTML / PDF) - `/profile` — partition skills by usage profile (design / dev / qa / audit / minimal / full) - `frontend-design` and `design-motion-principles` skills (external marketplaces) @@ -25,22 +35,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). - `.claude/{tasks,memory,audits}/` governance layout + 5 memory registries (decisions, learnings, blockers, journal, evals) ### Changed +- **BREAKING (gitflow):** never commit code directly on `main` / `develop` — branch first (`gitflow start `) and integrate via `gitflow finish`. A generated per-repo pre-commit hook BLOCKS direct code commits on `main` / `develop` (exempts `.claude/**`, merges, the root commit). Existing repos must run `lib/gitflow-migrate.sh`. This workflow rupture is what takes the project from 3.x to 4.0.0 +- `settings.json`: `git push` / `git tag` moved to the **ask** permission tier — a tool-call backstop for the "finish / release only on an explicit human signal" rule +- `install.sh` / `install-plugins.sh` made self-sufficient — nvm-installs Node/npm when missing, installs the `jq` prerequisite, runs `npx skills add` from `$HOME`; auto-reverts hand-curated config (`CLAUDE.md` + `settings.json` + `.claude/settings.json`) after install via an EXIT trap (install-immutable guard); auto-fixes the gstack browser on an OS newer than the pinned Playwright supports (Ubuntu 26.04) +- graphify upgraded to 0.8.x (skill pinned 0.8.45) — Gemini backend, monorepo support, CLI export, encoding fixes; CLAUDE.md + the pre-tool hook now prefer `graphify query` over `GRAPH_REPORT.md` +- `/seo` + `/geo`: CMS-plugin-first + shared-file edit discipline; Bing / IndexNow submission now mandatory - `/ship-feature`: capitalize + memory commit moved before FINISH (was after) — fixes memory committed after a push/PR and stranded outside it - `/init-project`: new STEP 10b captures founding architecture decisions as BDRs before FINISH - `/ship-feature` + `/init-project`: DOC SYNC moved before FINISH (was after) — fixes public docs patched then left uncommitted and stranded outside the push/PR (ship-feature STEP 9→8, init-project STEP 12→10c; GSD 13→12) -- `/validate` renamed to `/web-validate` — clearer scoped name (W3C + WCAG); routing, skill profiles, cross-references, and the client-deliverable leak-guard updated (the guard still matches legacy `/validate` so older client docs stay covered) - `/seo` split into parallel `seo` + `geo` agents with shared resources - `/onboard` rewritten: archetype-aware pipeline (orchestrator + config-only agent), security audit archetype-aware - `doc-syncer`: stack-aware audit + deploy-doc gating; later scoped to public docs only, `.claude/` read-only; sync-only ROADMAP handling — planned→shipped reconciliation from code/git, never from `.claude/`; numeric incoherence → HUMAN question - `CLAUDE.md`: major refactor (contradiction purge, restructure), subagent-delegation rule, design-toolchain mandate, memory-registry governance - Memory registries: enforced English + caveman format -- Default model / effort settings updated +- `settings.json`: `permissions.defaultMode` → `auto` (classifier-gated autonomy; `disableAutoMode` dropped) + `remoteControlAtStartup` + `skipAutoPermissionPrompt` + `effortLevel: xhigh`; model pin removed ### Removed +- `/init-project`: STEP 12 (speculative GSD v2 auto-bootstrap at project creation) removed — it ran `gsd init` AFTER FINISH, creating `ROADMAP.md` + `.gsd/` stranded outside the merge/PR (BLK-011), to bootstrap a multi-session engine that is opt-in and rarely used. Resolved by removal, not by plumbing a commit: GSD stays initializable on-demand (`/onboard add gsd`, or `gsd init` in a terminal), `/status` still reads `.gsd/`, and plugin-advisor still recommends it for multi-session work. init-project is now an 11-step pipeline - `disable-model-invocation` frontmatter removed repo-wide (aligns skills with CLAUDE.md routing) - Caveman plugin always-on integration purged — plugin disabled + uninstalled; SessionStart/UserPromptSubmit hooks, standalone hook files, `install-plugins.sh` STEP 5.5, `update-all.sh` refresh step, `plugins.lock.json` entry, `doctor.sh` checks, and docs removed. On a subscription plan its ~75% output-token compression has no cost benefit, and the always-on hooks added friction on validation gates + client deliverables. The unrelated memory-registry terse-format convention is kept. +- Installer-managed skills de-vendored — `frontend-design` un-tracked + npx-skills artifacts gitignored (re-synced from the plugin cache each run); obsolete `claude --effort max` shell alias removed (`settings.json` `effortLevel` is the source of truth) ### Fixed +- `lib/doc-commit.sh` no longer masks a rejected `git commit` as success: a pre-commit hook / protected branch / signing failure now fails loud with exit 5 and empty stdout (was: false "committed" + the previous HEAD's hash + exit 0, leaving docs silently uncommitted on a dirty tree) - Numerous skill/agent fixes across darwin optimization rounds (geo-analyzer, onboard, init-project, analyzer, plugin-check, prune-memory, …) ## [3.4.0] — 2026-04-15 diff --git a/CLAUDE.md b/CLAUDE.md index 5910a16..e31d310 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -262,8 +262,10 @@ only the non-obvious cases: gstack fallbacks, disambiguation, cryptic names. - Bug / error / 500 → investigate (bugfix if gstack off) - feat / hotfix / bugfix distinguished by file count → see descriptions - Ship / deploy / PR → ship (ship-feature if gstack off) +- Cut a release / tag a version (develop ahead of main) → release-candidate - Docs post-ship → document-release (doc if gstack off); stale-doc audit → doc - Audit of changes since last run → audit-delta +- Open-work inventory / "queue empty?" / stale TODO vs real git → reconcile - Design / UI (build, system, audit, polish) → see "Design work" below - Architecture review → plan-eng-review - Before /clear or /compact → capitalize; end-of-session ritual → close diff --git a/USAGE.md b/USAGE.md index a01b74d..fa1a20f 100644 --- a/USAGE.md +++ b/USAGE.md @@ -195,7 +195,6 @@ Hotfix/quick fix → tout OFF sauf superpowers # → STEP 8-10 : implémentation TDD + review # → STEP 10b-c: capitalize mémoire + sync README (avant finish) # → STEP 11 : finish (merge / commit initial) -# → STEP 12 : propose GSD v2 si multi-session détecté # 3. Features suivantes /ship-feature "description de la feature" @@ -204,16 +203,16 @@ Hotfix/quick fix → tout OFF sauf superpowers ### Pattern B — Projet long (multi-session, plusieurs jours) · ~1500-2500t/session CC ``` -# Même départ que Pattern A, mais au STEP 12 : -# → Répondre "yes" à "Initialize GSD v2?" -# → ROADMAP.md est créé avec les milestones +# Même départ que Pattern A. GSD v2 n'est PAS bootstrappé à la création +# (init-project ne propose plus gsd) — on l'initialise À LA DEMANDE, dans un +# terminal, quand on décide de passer en multi-session. # À chaque reprise de session (dans Claude Code) : /status # snapshot : plugins + git + milestone GSD en cours -# Ensuite dans un terminal (depuis le dossier projet) : +# Dans un terminal (depuis le dossier projet) : gsd # démarrer une session -/gsd init # si pas encore fait +/gsd init # initialise .gsd/ + ROADMAP (une fois, à la demande) /gsd auto # mode autonome, walk away # Pour suivre : @@ -400,9 +399,9 @@ Verify : npx expo export --platform web --output-dir /tmp/expo-check --clear **Si le projet devient long (plusieurs features sur semaines) :** ``` -# STEP 12 propose GSD v2 : répondre "yes" -# Puis dans terminal : +# Pour passer en multi-session, initialise GSD à la demande dans un terminal : gsd +/gsd init /gsd auto # → GSD gère deck-building, sharing backend, etc. milestone par milestone ``` @@ -501,8 +500,8 @@ Convention: snake_case Python, camelCase TypeScript." **Workflow long avec GSD v2 :** ``` -# Après /init-project (STEP 12 → "yes") -# Le ROADMAP.md généré contient : +# Après /init-project, on initialise GSD à la demande (plus auto-bootstrappé). +# Le ROADMAP.md généré par `gsd init` contiendra : # Milestone 1: Boutique in-app + Stripe # Milestone 2: PvP + matchmaking # Milestone 3: Leaderboard + saisons @@ -510,6 +509,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 auto # GSD travaille sur Milestone 1 de façon autonome # → research Stripe API + docs # → plan décomposé en tâches diff --git a/agents/doc-syncer.md b/agents/doc-syncer.md index b443d64..69c5bbe 100644 --- a/agents/doc-syncer.md +++ b/agents/doc-syncer.md @@ -792,9 +792,26 @@ Categorize: - **NONE** → exit completely silent. No output (no `PATCHED_FILES` → the doc-commit step sees an empty list and no-ops). -- **MINOR** → patch silently. One-line confirmation per file: - `doc-sync: patched ()` -- **SIGNIFICANT** → surface to user before patching: +- **MINOR** → patch, then VERIFY SHAPE with the deterministic oracle BEFORE the + silent auto-commit. The LLM made the MINOR call; the oracle re-checks that the + patch's SHAPE actually holds, catching a SIGNIFICANT mislabeled MINOR (RISK-1): + ``` + bash "$HOME/.claude/lib/doc-shape.sh" check # all paths, ONE call + ``` + - **exit 0** (within the MINOR envelope) → genuine MINOR: keep the silent patch. + One-line confirmation per file: `doc-sync: patched ()`. + Proceed to `PATCHED_FILES` + the doc-commit step. + - **exit 1** (shape EXCEEDS — oracle stderr names the offender(s) and why) → the + deterministic oracle OVERRULES the LLM's MINOR call (LRN-046). Do NOT auto-commit. + ESCALATE the WHOLE patch set to the SIGNIFICANT gate below — one file out of + shape makes the atomic MINOR classification suspect. Surface every patched file + + the oracle's reason, then the gate: on `no` → revert ALL + (`git checkout -- `); on `select` → keep the chosen files, + revert the rest. The oracle catches STRUCTURAL/size significance, not semantic — + it is a deterministic floor, not a full SIGNIFICANT-detector. + - **exit 2/3** (oracle usage error / not a git repo) → do NOT auto-commit on a + broken check; treat as exit 1 and escalate. +- **SIGNIFICANT** (or a MINOR the oracle escalated) → surface to user before patching: ``` DOC SYNC — drift detected after this session: diff --git a/lib/doc-commit.md b/lib/doc-commit.md index 6aca8da..44103f1 100644 --- a/lib/doc-commit.md +++ b/lib/doc-commit.md @@ -56,6 +56,7 @@ Pass each line as a SEPARATE argument (see DO step 3). | 0 | empty | helper no-op (nothing pending) | `DOC SYNC: docs already current — nothing to commit`. doc-sync found no drift, or patched nothing. | | 3 | empty | unsafe git state (detached / merge / rebase) | docs stay in the working tree for a manual commit; surface the helper's stderr. Do NOT retry blindly — the tree is mid-operation. | | 4 | empty | **SCOPE VIOLATION — upstream anomaly** | doc-syncer surfaced a `.claude/**` or `CLAUDE.md` path in `PATCHED_FILES`, which it must NEVER patch (BDR-022). STOP. Signal: `⚠️ doc-commit REFUSED — doc-syncer listed a forbidden path (); this violates BDR-022 upstream. Investigate why doc-syncer touched/listed it before re-running.` Do NOT swallow it, do NOT hand-commit the rest — the refusal IS the alarm. | + | 5 | empty | **COMMIT REJECTED — nothing committed** | `git commit` exited non-zero — a pre-commit hook blocked it (e.g. a doc commit on a protected `main`/`develop`), a signing failure, or similar. The docs are STILL in the working tree, uncommitted, on a dirty tree. STOP — do NOT proceed to FINISH as if docs landed (that re-creates the stranding bug this whole snippet fixes). Signal: `⚠️ doc-commit FAILED — the doc commit was rejected (); docs remain uncommitted. Investigate (hook / branch / signing) before retrying.` No hash is emitted; do NOT retry blindly. | | 2 | empty | usage error (no message / bad invocation) | internal bug in this include — fix the call, don't paper over it. | `` is the DOC commit (the one that adds the patched docs). Docs carry NO @@ -88,8 +89,12 @@ on the branch = consumption by the merge, automatic. snippet commits it without a blocking gate, BY CHOICE. NOT the memory case: memory CONTENT was always gated, so its auto-commit only embarked approved entries. Here the VISIBLE surface (rc 0 row, agent-composed summary) REPLACES the gate as the review surface — name - files + summarize, and the PR diff re-shows it. Strengthening the MINOR gate itself = - separate doc-syncer chantier. + files + summarize, and the PR diff re-shows it. UPSTREAM of this snippet, doc-syncer now + runs a deterministic shape oracle (`lib/doc-shape.sh`) on each MINOR patch: a patch whose + SHAPE belies "minor" (adds a heading, is large, is a new file) is escalated to the + SIGNIFICANT gate BEFORE it reaches here — so what this snippet auto-commits is shape-verified + MINOR. The oracle is a STRUCTURAL floor, not a semantic SIGNIFICANT-detector (a small + meaning-changing edit still reads MINOR); the visible surface stays the content backstop. - **Partial init-project fix.** This commits the docs doc-sync patched. It does NOT commit the scaffold or the STEP 5b bootstrap README (no deterministic owner — [[BLK-010]]); ramassing them would re-create the over-reach we ban. ship-feature ends fully fixed; init-project's diff --git a/lib/doc-commit.sh b/lib/doc-commit.sh index 61772a9..536f8b6 100755 --- a/lib/doc-commit.sh +++ b/lib/doc-commit.sh @@ -16,7 +16,8 @@ # doc-commit.sh pending ... # exit 0 if any passed file has changes, 1 if clean # doc-commit.sh commit "" ... # surgical commit # -# Exit codes (commit): 0 ok/no-op · 2 usage · 3 unsafe git state · 4 scope violation. +# Exit codes (commit): 0 ok/no-op · 2 usage · 3 unsafe git state · 4 scope violation · +# 5 commit rejected (git commit exited non-zero — hook / protected branch / signing). # Output contract: diagnostics → stderr; on a real commit the short hash of the doc # commit is the ONLY thing on stdout (empty on no-op/abort), so callers can capture # it: doc_hash=$(doc-commit.sh commit "msg" README.md USAGE.md). @@ -78,7 +79,8 @@ docs_pending() { } # Surgical commit of the passed doc paths only. Returns 0 (ok/no-op), 3 (unsafe), -# 4 (scope violation). On a real commit, prints the doc-commit short hash to stdout. +# 4 (scope violation), 5 (commit rejected by git). On a real commit, prints the +# doc-commit short hash to stdout. commit_docs() { local msg="${1:?commit message required}" shift @@ -118,7 +120,22 @@ commit_docs() { echo "doc-commit: only ignored/no-op changes — no-op" >&2 return 0 fi - git commit -q -m "$msg" -- "${changed[@]}" + # FAIL-LOUD on the commit itself. With `set -uo pipefail` (no -e), a rejected + # commit (pre-commit hook on a protected branch, signing failure, …) would NOT + # abort: the printf below would falsely claim "committed" and rev-parse would + # emit the PREVIOUS HEAD's hash with exit 0 — a silent masked failure. The + # script is fail-closed+loud on scope (exit 4); it must be the same on its own + # commit. Reject → loud, NO hash on stdout, exit 5 (distinct from rc 3 "could + # not start": rc 5 = "tried, git refused"). + if ! git commit -q -m "$msg" -- "${changed[@]}"; then + { + echo "doc-commit: COMMIT REJECTED — git commit exited non-zero" \ + "(pre-commit hook? protected branch? signing?)." + echo "doc-commit: NOTHING committed, working tree left as-is," \ + "NO hash emitted — investigate before retry." + } >&2 + return 5 + fi printf 'doc-commit: committed %d file(s): %s\n' "${#changed[@]}" "${changed[*]}" >&2 git rev-parse --short HEAD } diff --git a/lib/doc-shape.sh b/lib/doc-shape.sh new file mode 100755 index 0000000..4bfb9a3 --- /dev/null +++ b/lib/doc-shape.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# doc-shape.sh — deterministic check that a doc patch has MINOR *shape*. +# +# Companion to doc-commit.sh. doc-syncer AUTO MODE classifies drift as NONE / +# MINOR / SIGNIFICANT by LLM judgment, with no deterministic backstop — so a +# SIGNIFICANT change mislabeled MINOR would auto-commit silently (RISK-1). This +# oracle re-checks the SHAPE of each MINOR patch BEFORE the auto-commit: if a +# patch's shape belies "minor" (adds a section heading, is large, or is a new +# file), it EXCEEDS the MINOR envelope and doc-syncer escalates it to the +# existing SIGNIFICANT gate instead of committing it silently. +# +# SCOPE OF THE GUARANTEE (honest, do not over-read it): this catches STRUCTURAL +# and size significance, NOT semantic significance. A 3-line edit that changes +# meaning but adds no heading and stays small still reads as MINOR-shape. The +# oracle is a deterministic FLOOR under the LLM's judgment (LRN-046) — a +# reduction of RISK-1's gross cases, not an elimination. The LLM still owns the +# semantic call above this floor. +# +# Verdict is AGGREGATE: ANY passed path that exceeds → overall exit 1, every +# offender named on stderr. The LLM classified the SET atomically MINOR; if one +# file's shape disagrees, the whole set is suspect → the whole set escalates. +# +# Envelope (per path, working tree vs HEAD), all deterministic: +# - adds a Markdown ATX heading (^+#{1,6} ) → exceeds (new section) +# - added lines > DOC_SHAPE_MAX_ADDED (def 20) → exceeds (too big for a tweak) +# - removed lines > DOC_SHAPE_MAX_REMOVED (def 20) → exceeds +# - new / untracked file → exceeds (a creation, not a drift-patch) +# - not a recognized public-doc file → exceeds (escalate the anomaly) +# A clean tracked path (no diff) is vacuously within the envelope. +# Known gap: Setext headings (=== / --- underlines) are not detected; ATX is the +# norm in this codebase's docs. +# +# Usage: doc-shape.sh check ... +# Exit: 0 within MINOR envelope · 1 exceeds (reasons→stderr) · 2 usage · 3 not-a-git-repo +# Output: reasons → stderr; stdout stays empty (the exit code carries the verdict). +# +# Sourceable: doc_shape_ok for the doc-syncer flow. + +set -uo pipefail + +DOC_SHAPE_MAX_ADDED="${DOC_SHAPE_MAX_ADDED:-20}" +DOC_SHAPE_MAX_REMOVED="${DOC_SHAPE_MAX_REMOVED:-20}" + +_in_git_repo() { git rev-parse --git-dir >/dev/null 2>&1; } + +# True (0) when the path is a recognized public-doc file (doc-syncer's universe, +# BDR-016): the markdown family, anything under docs/, or a bare standard name. +_is_doc() { + case "$(basename -- "$1")" in + *.md | *.mdx | *.markdown | *.rst) return 0 ;; + README | INSTALL | CONFIGURE | USAGE | DEPLOY | CONTRIBUTING | \ + CHANGELOG | SECURITY | ARCHITECTURE | LICENSE | AUTHORS | NOTICE) return 0 ;; + esac + case "$1" in + docs/* | */docs/*) return 0 ;; + esac + return 1 +} + +# Echo the reason a single path EXCEEDS the MINOR envelope, or nothing if it is +# within. Pure read — never mutates the tree. +_path_exceeds_reason() { + local p="$1" + _is_doc "$p" || { printf 'not a recognized public-doc file: %s\n' "$p"; return; } + [ -e "$p" ] || { printf 'path not found: %s\n' "$p"; return; } + if ! git ls-files --error-unmatch -- "$p" >/dev/null 2>&1; then + 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 + printf 'adds a section heading (structural change, not a factual tweak): %s\n' "$p" + return + fi + local stat added=0 removed=0 + stat="$(git diff HEAD --numstat -- "$p")" + [ -n "$stat" ] && read -r added removed _ <<<"$stat" + case "$added$removed" in *[!0-9]*) printf 'binary or unreadable diff: %s\n' "$p"; return ;; esac + if [ "$added" -gt "$DOC_SHAPE_MAX_ADDED" ]; then + printf 'added %s lines > %s envelope: %s\n' "$added" "$DOC_SHAPE_MAX_ADDED" "$p" + return + fi + if [ "$removed" -gt "$DOC_SHAPE_MAX_REMOVED" ]; then + printf 'removed %s lines > %s envelope: %s\n' "$removed" "$DOC_SHAPE_MAX_REMOVED" "$p" + return + fi +} + +# 0 if EVERY passed path is within the MINOR envelope, 1 if ANY exceeds (each +# offender's reason printed to stderr). Empty list → 0 (vacuously minor). +doc_shape_ok() { + _in_git_repo || { + echo "doc-shape: not a git repo — cannot judge shape" >&2 + return 3 + } + local p reason any=0 + for p in "$@"; do + reason="$(_path_exceeds_reason "$p")" + if [ -n "$reason" ]; then + echo "doc-shape: EXCEEDS MINOR envelope — $reason" >&2 + any=1 + fi + done + return "$any" +} + +main() { + local cmd="${1:-}" + case "$cmd" in + check) + shift + [ "$#" -ge 1 ] || { + echo "usage: doc-shape.sh check ..." >&2 + return 2 + } + doc_shape_ok "$@" + ;; + *) + echo "usage: doc-shape.sh check ..." >&2 + return 2 + ;; + esac +} + +# Run main only when executed, not when sourced. +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + main "$@" +fi diff --git a/lib/reconcile.sh b/lib/reconcile.sh new file mode 100644 index 0000000..a809ce4 --- /dev/null +++ b/lib/reconcile.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# reconcile.sh — deterministic engine for /reconcile. +# +# Confronts DECLARATIVE sources (TODO checkboxes, registry statuses) against REAL +# state (git, fs, registry BODY). It is the engine behind the /reconcile skill; +# the skill orchestrates + gates, this file holds the mechanical truth-probes. +# +# FOUNDING PRINCIPLE — recursive coherence (LRN-055): a reconciler must NEVER trust +# a declarative source as an oracle. So this engine NEVER reads the `## Index` table +# nor believes a `[x]`/`[ ]` checkbox; it enumerates registry entries from the BODY +# `## ID —` headings and decides "done/stale" from git/fs only. It practices what it +# preaches — the recursive-coherence test (run-reconcile.sh T1) reds if this is broken. +# +# HONEST LIMITS (graven, do not over-read): +# - Deferral detection is LEXICAL (reconcile_deferrals): it catches deferrals MARKED +# by a keyword, misses ones phrased without one ("à reprendre quand X"). Deterministic +# on the detectable; the skill SURFACES the ambiguous for human review, never asserts. +# - Contradiction detection surfaces CANDIDATES (token overlap), never asserts a verdict. +# +# Layers: +# reconcile_enumerate_ids — body-only ID enumeration (recursive-coherence core) +# reconcile_oracle_* — live git/fs truth probes +# reconcile_blk_current_status — registry status, LAST block wins (compound/UPDATE/FINAL) +# reconcile_blk_open — blockers whose CURRENT status is not resolved +# reconcile_verdict — pure kernel: declared checkbox × real fact → verdict +# reconcile_deferrals — lexical deferral sweep (honest limit) +# reconcile_contradiction_candidates — accepted-BDR ⇄ open-chantier token overlap (surface) +set -uo pipefail + +RECONCILE_GREP="${RECONCILE_GREP:-/usr/bin/grep}" # LRN-074: pin grep, never assume GNU flags +# markers that LEXICALLY signal a deferral/follow-up (honest limit: marked-only) +RECONCILE_DEFER_RE='[Dd]efer|[Ff]ollow-?up|[Oo]ut.of.scope|OUT-OF-SCOPE|[Rr]econsider|[Rr]evisit|2e passage|won.t.do|optional\)|one-line ticket' + +# --- recursive coherence: enumerate IDs from the BODY, never the ## Index --- +# $1 registry file, $2 prefix (BDR|LRN|BLK|EVAL). Emits one id per line, unique. +reconcile_enumerate_ids() { + "$RECONCILE_GREP" -oE "^## ${2}-[0-9]+" "$1" | "$RECONCILE_GREP" -oE "${2}-[0-9]+" | sort -u +} + +# --- live truth probes (query the REAL repo/fs; this is the "verify, don't believe" core) --- +reconcile_oracle_tree_clean() { # rc 0 = working tree clean + [ -z "$(git -C "${1:-.}" status --porcelain 2>/dev/null)" ] +} +reconcile_oracle_merge_done() { # $1 repo, $2 branch fragment → rc 0 if a merge commit exists + [ -n "$(git -C "${1:-.}" log --oneline --grep "Merge .*$2" -1 2>/dev/null)" ] +} +reconcile_oracle_pushed() { # $1 repo, $2 branch → rc 0 if nothing unpushed vs origin + [ -z "$(git -C "${1:-.}" rev-list "origin/$2..$2" 2>/dev/null)" ] +} +reconcile_oracle_sha_exists() { # $1 repo, $2 sha → rc 0 if the commit object exists + git -C "${1:-.}" cat-file -e "${2}^{commit}" 2>/dev/null +} +reconcile_oracle_msg_committed() { # $1 repo, $2 grep → rc 0 if a commit message matches + [ -n "$(git -C "${1:-.}" log --oneline --grep "$2" -1 2>/dev/null)" ] +} +reconcile_oracle_path_present() { [ -e "${1:?}" ]; } # $1 path → rc 0 if it still exists on disk + +# --- registry status: LAST status-bearing line wins (the BLK-008 trap A fell into) --- +# $1 blockers file, $2 id. Echoes the current status line (compound/UPDATE/FINAL aware). +reconcile_blk_current_status() { + # drop ALL `## BLK-` header lines first: the range is inclusive of the NEXT entry's + # header, and a sibling header may carry a status word (e.g. BLK-005 "...upstream rename") + # → cross-entry bleed. The entry's own header carries no status, so dropping it is safe. + sed -n "/^## ${2} /,/^## BLK-/p" "$1" \ + | "$RECONCILE_GREP" -v '^## BLK-' \ + | "$RECONCILE_GREP" -iE 'status|RESOLVED|REVERTED|upstream|resolved|[^a-z]open' \ + | tail -1 +} +# blockers whose CURRENT status is not resolved → emits "idstatus" +reconcile_blk_open() { + local id st + for id in $(reconcile_enumerate_ids "$1" BLK); do + st=$(reconcile_blk_current_status "$1" "$id") + case "$st" in + *RESOLVED*|*resolved*) : ;; + *) printf '%s\t%s\n' "$id" "$st" ;; + esac + done +} + +# --- pure reconciliation kernel: declared checkbox × real fact → verdict (no git, fully testable) --- +# $1 checkbox char (x| |~), $2 real_done (true|false). +reconcile_verdict() { + case "$1:$2" in + " :true") echo "STALE:open-but-done" ;; + "x:false") echo "STALE:done-but-open" ;; + "~:true") echo "STALE:partial-but-done" ;; + *) echo "CONSISTENT" ;; + esac +} + +# --- lexical deferral sweep (HONEST LIMIT: marked-only) → "srclinetext" --- +reconcile_deferrals() { + [ -f "$1" ] && "$RECONCILE_GREP" -nE "$RECONCILE_DEFER_RE" "$1" 2>/dev/null | sed 's/^/TODO\t/' + [ -f "$2" ] && "$RECONCILE_GREP" -nE "$RECONCILE_DEFER_RE" "$2" 2>/dev/null | sed 's/^/BDR\t/' + return 0 +} + +# --- contradiction CANDIDATES (surface, never assert): CLI-flag token shared by a BDR + the TODO --- +# $1 decisions file, $2 todo file. CLI-flag-like tokens are distinctive enough to flag for review. +reconcile_contradiction_candidates() { + local tok + for tok in $("$RECONCILE_GREP" -oE '\-\-[a-z][a-z-]+' "$1" 2>/dev/null | sort -u); do + "$RECONCILE_GREP" -qF -- "$tok" "$2" 2>/dev/null \ + && printf 'CANDIDATE\t%s\tflag "%s" in a BDR title and an open TODO chantier — review for contradiction\n' "$tok" "$tok" + done + return 0 +} diff --git a/lib/tests/fixtures/real-state.snapshot b/lib/tests/fixtures/real-state.snapshot new file mode 100644 index 0000000..5b61d2a --- /dev/null +++ b/lib/tests/fixtures/real-state.snapshot @@ -0,0 +1,12 @@ +# Frozen oracle answers as of the reconcile point (bdfa9bc). Each value is an +# independently-checkable git/fs truth, hand-recorded — NOT generated by reconcile.sh. +# Consumed by the deterministic kernel test (T4). Live oracles are proven separately (T6). +merge_done:bugfix/prune-memory-hardening=true +pushed:develop=true +tree_clean=true +commit_msg:gitmodules=true +path:.claude/skills/darwin-skill=present +blk_current:BLK-008=resolved +blk_current:BLK-009=open +blk_current:BLK-001=open +blk_current:BLK-003=open diff --git a/lib/tests/fixtures/registry-index-drift.md b/lib/tests/fixtures/registry-index-drift.md new file mode 100644 index 0000000..60b911f --- /dev/null +++ b/lib/tests/fixtures/registry-index-drift.md @@ -0,0 +1,809 @@ +--- +type: learnings_registry +entry_prefix: LRN +schema: + id: LRN-XXX + date: YYYY-MM-DD + pattern: string (what was observed, abstracted) + context: string (where/when it happened - concrete) + future_application: string (when to recall this) +rules: + - Capture learnings that apply beyond current task. + - Abstract from incident — pattern reusable, not one-shot fact. + - Link to source (commit, file, PR) when possible. + - Replaces previous LESSONS.md format. Old file empty — no content to migrate. +--- + +# Learnings registry (LRN) + +## Index + +| ID | Date | Pattern | Applies to | +|----|------|---------|------------| +| LRN-001 | 2026-04-22 | `rtk` shape-compression breaks pipes | any pipeline chaining `rtk curl/cat/read` into `jq`, `python -c`, `awk` | +| LRN-002 | 2026-04-23 | Moving report-file paths requires grepping bash READS, not just WRITES | any refactor that moves a generated file used by a dispatcher | +| LRN-003 | 2026-04-27 | Claude Code `disable*` settings use sentinel string `"disable"`, not boolean | any change to `permissions.defaultMode` or related blocker keys | +| LRN-004 | 2026-04-27 | `framer-motion` rebranded `motion` Nov 2024 — different packages per framework | any new project recommending animation lib; auditing legacy imports | +| LRN-005 | 2026-05-03 | `claude plugin install` does NOT enable — separate `claude plugin enable` required | every plugin installer targeting ALWAYS-ON status | +| LRN-006 | 2026-05-03 | `caveman-shrink` (and any MCP middleware proxy) non-functional without upstream wrapper | any MCP middleware/proxy package — never `claude mcp add` it bare | +| LRN-007 | 2026-05-06 | `toggle-external.sh enable` missed source-only state (3rd lifecycle case) | toggle scripts for tools with separate install + symlink steps | +| LRN-008 | 2026-05-06 | Biggest skill-quality wins from edge-case tables, not workflow rewrites | any skill <85 — first check for FAILURE PATHS / EDGE CASES / ERROR HANDLING section | +| LRN-021 | 2026-05-20 | Refactor commands→skills must sweep `~/.claude/commands/` for orphan wrappers | any refactor moving `agents/foo.md` → `skills/foo/SKILL.md`; onboard/init-project audits | +| LRN-009 | 2026-05-06 | Dry-run scoring noise wrongly triggers reverts on already-strong skills | darwin-skill ratchet on skills >91 — relax or use real subagent eval | +| LRN-010 | 2026-05-06 | `~/.claude/skills,agents` symlink to Documents/claude — git from `~/.claude` fails | any optimization or batch edit on personal skills/agents | +| LRN-011 | 2026-05-07 | Single subagent emits N independently-gated scores → labeled extraction + axis-aware loop + per-axis escalation | any audit pipeline shipping multiple gated metrics from one subagent | +| LRN-012 | 2026-05-07 | Bash heredoc + stdin pipe collision = silent empty output | any shell pipeline piping data into `python3 - <<'PY' ... PY` (or any heredoc'd interpreter) | +| LRN-013 | 2026-05-07 | marked CLI 16.x ignore stdin, dump own cli.js source | any shell MD→HTML via npx marked — use `-i FILE` not stdin | +| LRN-014 | 2026-05-11 | Pandoc base gfm strips header id attrs — need `gfm+gfm_auto_identifiers` | any MD→HTML/PDF with cross-references (`[§4](#nap)`) via pandoc | +| LRN-015 | 2026-05-11 | BrightLocal Free Tools retired 2026 — Moz Local Citation Checker is free replacement | client SEO/NAP docs — re-validate tool URLs + free-tier status annually | +| LRN-016 | 2026-05-11 | Pandoc GFM checkbox markup breaks adjacent-sibling CSS — target `li > input` directly | styling task-list checkboxes in pandoc-rendered HTML/PDF | +| LRN-017 | 2026-05-12 | Thin-dispatcher SKILL.md round-1 win = fallback + frontmatter triggers (+15 to +30) | any `/darwin-skill` round-1 on a dispatcher SKILL.md | +| LRN-018 | 2026-05-12 | Darwin eval subagents drift on total math — recompute in main thread | any subagent-driven SKILL.md rescore | +| LRN-019 | 2026-05-15 | Deployable-project doc split: README dev-quickstart + DEPLOY 14-section prod-VPS topology | any onboard/doc-syncer/scaffold producing docs for a deployable project | +| LRN-024 | 2026-06-02 | New sibling command sharing logic → extract helper + refactor existing caller, never copy-paste; assert pre/post state equality | adding a subcommand/branch reusing logic inline in a peer command | +| LRN-025 | 2026-06-02 | `.gitignore` gstack allowlist must cover ALL toggleable skills (incl. parked) — else enabling one = untracked git noise | any toggle that moves local-symlink skills into a tracked dir; post-submodule-bump reconcile | +| LRN-026 | 2026-06-09 | `disable-model-invocation: false` = ENABLED not blocking; only `true` blocks (model + orchestrator); binary, no per-caller | Claude Code skill frontmatter; deciding self-route/chain vs human-only entry point | +| LRN-027 | 2026-06-11 | Agents improvise audit boundaries from file dates when no machine state — periodic skills need machine-readable state file, never inference | any recurring/periodic skill needing "since last run" semantics | +| LRN-030 | 2026-06-18 | Opus 4.8 under-delegates subagents/memory/custom-tools by default — counter via explicit CLAUDE.md fan-out rule | any Opus 4.8 session; tuning delegation; inline-vs-subagent decision | +| LRN-031 | 2026-06-19 | Skill value = gate + anti-noise + determinism, not re-coding what a capable agent does free | building/reviewing any skill; writing-skills TDD fixture design | +| LRN-046 | 2026-06-25 | Destructive skill: deterministic oracle (byte-identical / count census) > semantic judge | any destructive/irreversible skill; behavioral-oracle TDD | +| LRN-047 | 2026-06-25 | A noisy safety guard (13/13 FP) = a guard you learn to ignore = risk → refine, don't tolerate | any guard/alert/lint that can false-positive | +| LRN-048 | 2026-06-25 | A "0/OK/pass" must prove it LOOKED (counted both sides), else verify hard-wired to pass | any verify/test/lint reporting success | +| LRN-051 | 2026-06-26 | `git commit -- pathspec` strict on no-match → filter scoped commits to changed paths | any scoped-commit automation | +| LRN-052 | 2026-06-26 | Hash-anchoring: 2 cases it does NOT apply (pre-code founding, squash-merge) | capitalizing founding/arch decisions; squash repos | +| LRN-053 | 2026-06-26 | Read-before teeth = verifiable disposition in the artifact, not the act of reading | any read-before / check-before wiring | +| LRN-054 | 2026-06-26 | No deterministic oracle for "already in context" → never add a presence-skip branch | skip-if-seen optimizations over conversation state | +| LRN-055 | 2026-06-26 | Body `## ID —` headings = drift-immune index; the `## Index` table is not | choosing a substrate to index/select over | +| LRN-056 | 2026-06-26 | `grep PAT dir/*.md` on absent dir ERRORS (exit 2), not no-op → guard `[ -d ]` | any glob-fed scan that must no-op on nothing | +| LRN-057 | 2026-06-26 | Match consumption mechanism to consumer (mechanical / external-cognitive / inline) | wiring any produce→consume invariant | +| LRN-058 | 2026-06-27 | Same bug-class ≠ same fix — verify the twin shares the fix's PRECONDITION before replicating | porting a fix to a "same bug" twin | +| LRN-059 | 2026-06-27 | Step-number SWAP flips meanings (sweep refs) ≠ letter-suffix insertion (shifts nothing) | any pipeline renumber | +| LRN-060 | 2026-06-27 | Fail-closed guard proven by what it REFUSES (loudly); pass dynamic lists as argv not separator-string | automated scoped-commit / destructive guards | +| LRN-061 | 2026-06-27 | Runtime net for an unwired skill → check the wiring first (deterministic gap = fix structurally; non-det aléa = net OK, cf BDR-033) | "build a hook/watcher to catch when X isn't done" | +| LRN-062 | 2026-06-27 | deploy first-run detection = file-existence, never `git describe` | any "first run vs incremental" tool — detect by explicit on-disk marker | +| LRN-063 | 2026-06-27 | delta-since-marker = `git diff --name-only X HEAD` (two endpoints), never rev-list/three-dot | any delta-since-checkpoint over git — explicit two endpoints for tree diff | +| LRN-064 | 2026-06-27 | surgical-commit helper family partitions `.claude/`; new subtree needs own allowlist sibling | adding a committable `.claude/X` subtree | +| LRN-065 | 2026-06-27 | cross-session cold-resume skill = disk-bridge read-first (audit-delta convention) | any "do work → user acts out-of-band → resume later" skill | +| LRN-066 | 2026-06-27 | surgical-commit must fail LOUD on git-ignored target paths (else silent no-op) | any helper relying on `git status --porcelain` to detect changes | +| LRN-067 | 2026-06-28 | pipeline that LOOKS 2-level can terminate at SAME level; human-mediated step (interactive menu) masks the double-action until automated | replacing an interactive/human step with a deterministic one over a delegated sub-skill | +| LRN-068 | 2026-06-29 | enforcement-bootstrap must be transactional: activate the guard LAST + gate it on the bootstrap commit succeeding; precheck identity | any init that installs a hook/protection AND commits | +| LRN-069 | 2026-06-29 | token-authed remote writes under CC perms: inline-env (never `export`), token in header not argv, keep `git push` on ASK as the gate | scripting git/curl writes to a private remote from tool calls | +| LRN-070 | 2026-06-29 | clean-tree-gated migration blocked by a dirty submodule → diagnose pointer-vs-content; for a local edit use `submodule..ignore=dirty`, never blind reset | migrating/releasing a superproject whose submodule carries intentional local edits | +| LRN-071 | 2026-06-29 | fail-loud must cover the helper's OWN commit, not just its inputs — 3rd occurrence of the swallowed-commit pattern (a failed op masked by a later returning-0 statement) | any helper whose return value gates a downstream "success" — audit every fallible internal op propagates, esp. the commit | +| LRN-072 | 2026-06-29 | a stranded-artifact bug can be fixed by NOT creating the artifact (negative diff), not by plumbing its commit — if the producing step is speculative/unused, delete it | a stranded/duplicated/uncommitted-artifact bug — before building machinery, ask if the PRODUCING step is wanted; speculative-at-creation → remove, deliberate-on-demand → keep | + +--- + +## LRN-001 — `rtk` shape-compression silently breaks downstream parsers + +- **Date**: 2026-04-22 +- **Pattern**: when tracking tool (`rtk`) intercepts stdout and returns schematized/compressed representation instead of raw payload, every downstream parser breaks silently — user (or LLM) never sees `rtk`'s output, only parser error. +- **Context**: `rtk curl` replaces raw JSON output with tokenized version, regardless of TTY vs pipe. Claude Code hooks auto-rewrite `curl` → `rtk curl`, so behavior impossible to anticipate without knowing hook. +- **Future application**: for any tool auto-rewriting standard commands, explicitly verify pipe behavior. Documented workaround: `exclude_commands=["curl"]` in `~/.config/rtk/config.toml`, or `rtk proxy`. See `BLK-001`. + +## LRN-002 — Moving report-file paths requires grepping bash READS, not just WRITES + +- **Date**: 2026-04-23 +- **Pattern**: when moving write path of generated file (report, artifact, cache), must also grep places that READ that file — not only those that write it. Dispatchers (orchestrator skills dispatching to agent then parsing result) typically contain bash commands like `test -s X.md`, `grep ... X.md`, `wc -l X.md` — refs invisible if only grep for "write" or "output path". +- **Context**: `.claude/audits/` refactor (commit `5c5e82c`). First pass: updated write paths across 5 skills (seo/geo/harden/validate/code-clean) and 3 agents. User asked for verify-gate. They re-grepped, found 10+ bare bash refs (e.g. `test -s HARDEN.md`, `grep -oE ... VALIDATE.md`) missed — dispatchers broken (looking at project root while agent writing to `.claude/audits/`). Fixed in commit `5c5e82c` (bundled with same commit). +- **Future application**: + - Before declaring file-path migration "complete", grep **basename** (`grep -rn "HARDEN\.md"`) plus full path — catch bare bash usages. + - If file used in pipelines (`test`, `grep`, `wc`, `cat`, `head`), search for those verbs explicitly. + - **Verify-gates save work**: one extra round forced exhaustive re-grepping. Without it, two dispatchers shipped broken. + +## LRN-003 — Claude Code `disable*` settings use sentinel string `"disable"`, not boolean + +- **Date**: 2026-04-27 +- **Pattern**: Claude Code blocker-style settings (`disableAutoMode`, `disableBypassPermissionsMode`) use literal string `"disable"` as sentinel. Key absent = feature available; value `"disable"` turns blocker on. Any other value (including `false`, `true`, `null`) has no effect — doc explicitly states this. +- **Context**: switching `permissions.defaultMode` to `"auto"` while `disableAutoMode: "disable"` still present would have failed at startup ("auto mode unavailable"). Naming `disable: "disable"` reads ambiguously — easy to assume boolean toggle and leave key in place. +- **Future application**: + - Before changing `defaultMode`, audit matching `disable*` key in same `permissions` block. If present with value `"disable"`, remove it. + - Same logic for `bypassPermissions` mode and `disableBypassPermissionsMode`. + - Don't trust doc's naming — read value semantics. Sentinel strings beat booleans here because harness can distinguish "unset" from "explicitly off" (admin policy). +- **Reference**: commit `1421578`, doc `https://code.claude.com/docs/en/settings`. + +## LRN-004 — `framer-motion` rebranded `motion` (Nov 2024) — different packages per framework + +- **Date**: 2026-04-27 +- **Pattern**: `framer-motion` renamed `motion` November 2024. Rename not cosmetic: bundles React (`motion/react`), Svelte, vanilla-JS support under single npm package, while Vue gets own parallel package `motion-v`. Legacy package `framer-motion` still installs and works but in maintenance mode — recommending it in new framework default locks projects into legacy import paths day one. Detection of "is animation already covered" must include both names plus broader anim ecosystem (`gsap`, `lottie-react`, `react-spring`, `popmotion`, `@formkit/auto-animate`) to avoid double-installs. +- **Context**: building animation-lib auto-install in `/init-project` and `/onboard`. Initial user phrasing "framer-motion" (old name remembered). Picking package name without verifying rename would have shipped legacy imports in every new scaffold. +- **Future application**: + - For React / Next.js / Remix / Astro+React / Svelte: `motion` (`import { motion } from 'motion/react'`). + - For Vue 3 / Nuxt: `motion-v` (separate package, separate API). + - For React Native: do NOT recommend `motion` — use `react-native-reanimated` (motion targets DOM). + - When auditing existing projects, check both `framer-motion` and `motion` keys in `package.json` deps; treat either as "animation already covered". + - Before adopting any "industry default" lib in framework, verify canonical package name current — naming churn (rebrand, scope change `@org/lib`, fork) common in JS land. +- **Reference**: helper `lib/animation-lib-check.sh`, BDR-005. + +## LRN-005 — `claude plugin install` does NOT enable — `claude plugin enable` separate step + +- **Date**: 2026-05-03 +- **Pattern**: Claude Code CLI splits "available" from "active" for marketplace plugins. `claude plugin install --scope user name@source` only copies plugin into `~/.claude/plugins/cache////`. Does NOT write `name@source: true` into user's `settings.json:enabledPlugins` map. Without explicit `claude plugin enable name@source`, plugin sits dormant — installed but unloaded. Symmetric with `claude plugin disable`, which keeps cache and only removes enabledPlugins entry. +- **Context**: discovered auditing why `security-guidance` and `superpowers` were ✘ disabled in `claude plugin list` despite project's `install-plugins.sh` summary banner declaring them "ALWAYS ON". Root cause: `install_plugin()` only ran `claude plugin install`, never `enable`. Bug stayed invisible because hardcoded `printf "│ ✅ ON : security-guidance rtk superpowers │"` in `session-start.sh` printed same names regardless of actual state — lying banner agreed with lying install. +- **Future application**: + - For any plugin meant ALWAYS ON, follow `claude plugin install` with `claude plugin enable name@source` (idempotent — no-op if already enabled). + - Detect "actually enabled" via `enabledPlugins[name@source] === true` in `settings.json`, NOT presence of cache dir. Pattern implemented in `lib/detect-plugins.sh:plugin_enabled()` (filesystem grep, no subprocess). + - Any banner / status display claiming plugin on must read state, never hardcode names. Hardcoded labels turn single bug into two co-conspiring bugs masking each other. +- **Reference**: commit `2ec7935`, `lib/detect-plugins.sh:plugin_enabled`, `install-plugins.sh:enable_plugin()`. + +## LRN-006 — `caveman-shrink` (and any MCP middleware proxy) needs upstream wrapper to function + +- **Date**: 2026-05-03 +- **Pattern**: some MCP packages are middleware proxies, not standalone servers. They wrap upstream MCP server and transform its responses (e.g. `caveman-shrink` compresses prose fields). Running them bare via `claude mcp add proxy-name -- npx -y proxy-pkg` registers server that errors immediately with "missing upstream command" — every health check fails, and Claude Code reports MCP broken until human intervenes. CLI `claude mcp add` doesn't validate that configured command launches working stdio MCP, so bad registration silently lands. +- **Context**: when adding caveman, upstream installer auto-registers `claude mcp add caveman-shrink -- npx -y caveman-shrink` and prints "registered. wrap an upstream by editing the mcpServers entry". Following that flow leaves user with permanently failing MCP entry until they realize they must edit `~/.claude.json` manually. +- **Future application**: + - For any MCP that is proxy/middleware (read package docs for "upstream", "wraps", "proxy"), register under DERIVED name `-` with upstream baked into args. Example for caveman-shrink wrapping filesystem server: + ``` + claude mcp add caveman-shrink-fs --scope user -- \ + npx -y caveman-shrink npx -y @modelcontextprotocol/server-filesystem /path + ``` + - Detection of "is this MCP correctly set up?" must look for the derived name (`caveman-shrink-*`), not the bare proxy name. Bare-name registration is treated as broken. + - Default install scripts should NOT auto-register middleware MCPs — print the snippet for the user to choose an upstream. See `install-plugins.sh` STEP 5.5. +- **Reference**: commit `9b20b84`, `lib/detect-plugins.sh:detect_caveman_shrink`, `install-plugins.sh` STEP 5.5 MCP block. + +## LRN-007 — `toggle-external.sh enable` missed source-only state + +- **Date**: 2026-05-06 +- **Pattern**: `lib/toggle-external.sh enable ` for npx/external skills (`darwin-skill`, `find-skills`, `emil-design-eng`) handled 2 states only: symlink in `skills-disabled/` → move to `skills/`, or symlink in `skills/` → already enabled. Missed 3rd: source dir at `~/.agents/skills/` but no symlink. First-run after `make plugin` lands here until `bash link.sh` runs. `enable` errored `not installed — run: make plugin` — misleading, plugin already installed. +- **Context**: user ran `./lib/toggle-external.sh enable darwin-skill` after fresh install. `~/.agents/skills/darwin-skill/` populated by `install-plugins.sh` STEP 8.5 npx call, but `link.sh` (separate step) not run, so `skills/darwin-skill` symlink never created. Fix `lib/toggle-external.sh:161-179` — add `elif [ -d "$src" ]` branch creating symlink direct when source dir present. Error message now show resolved source path. +- **Future application**: + - Any toggle script for tools with separate install + symlink steps must check 3 states: disabled-dir, enabled-dir, source-only. Source-only branch create symlink in place, not fail. + - Error messages name path checked, not abstract tool name — caller verify install vs symlink state without rereading script. + - Symmetric pairs (`enable`/`disable`) both handle same lifecycle states; missing state in one half = silent dead end. +- **Reference**: `lib/toggle-external.sh:161-179`, `link.sh:69-83`, `install-plugins.sh:598-633` STEP 8.5. + +## LRN-008 — biggest skill-quality wins come from edge-case tables, not workflow rewrites + +- **Date**: 2026-05-06 +- **Pattern**: darwin-skill round 1 across 18 personal skills. Top 4 gains (analyze +18.5, skills-perso +11.9, refactor +11.0, hotfix +9.0) all from same shape: add 1-page failure-mode table (file-not-found, malformed input, partial state, denied user input) with concrete action per row. Skills already had clean happy-path workflows; D3 (edge cases) was systemic gap. +- **Context**: most personal skills delegate to single agent file. Workflow steps already explicit. Missing: explicit "what when X unexpected" rows. Adding 5-12 row table with `| situation | action |` shape moved D3 from 3-7 → 9-10 and total +5 to +18. +- **Future application**: + - Skill scoring <85: first inspect agent file for EDGE CASES / FAILURE PATHS / ERROR HANDLING section. Absence = strong predictor of D3 weakness. + - Template: rows for `target not found`, `input malformed`, `tool/API timeout`, `user denies action`, `partial output`, `permission denied`. Map each → fallback / retry / ask-user / fail-fast. + - Costs ~15-50 lines, unlocks +5 to +15 score. +- **Reference**: `.claude/audits/DARWIN-SKILL-OPTIMIZATION.md`, commits `649351b`, `eb34627`, `1768d04`, `ef87074`, `a3f28d5`. + +## LRN-009 — dry-run scoring noise wrongly triggers reverts on already-strong skills + +- **Date**: 2026-05-06 +- **Pattern**: darwin-skill ratchet rule = revert if new < old. Dry_run scoring (subagent reads SKILL.md, mentally simulates, scores 8 dims) has ±1pt noise per dim per re-eval. Skill at 91-94 has small headroom, so single noisy -1 on D2 flips total from +1 to -1 (false revert). code-clean + doc both reverted with objectively useful content (empty-approval branch, README/DEPLOY templates) — revert was dry_run noise artifact, not real regression. +- **Context**: ratchet preserves only commits with strict total > old. For dry_run near ceiling, too strict. Real subagent eval would have lower noise floor since output quality differences observable. +- **Future application**: + - Skills baseline >91: skip optimization (diminishing returns), OR use real subagent eval not dry_run, OR relax ratchet to "new ≥ old - 1" with manual diff review. + - Edits to high-scoring skills must be minimal (1-3 lines, surgical) so D2 (workflow clarity) not perturbed by added bulk. + - When reverting content-rich change, log content elsewhere (`~/.claude/notes/`) so work not lost — second smaller patch can reintroduce idea. +- **Reference**: `.claude/audits/DARWIN-SKILL-OPTIMIZATION.md`, commits `63e08f9`→`822d437` revert (code-clean), `c7b8522`→`765d1c1` revert (doc). + +## LRN-010 — ~/.claude/skills + ~/.claude/agents symlink to /home/bchanot-ubuntu/Documents/claude + +- **Date**: 2026-05-06 +- **Pattern**: editing `~/.claude/skills//SKILL.md` or `~/.claude/agents/.md` modifies file at `/home/bchanot-ubuntu/Documents/claude/{skills,agents}/`. `~/.claude` is empty config dir with symlinks; actual git repo + working tree is in Documents/claude. `git add` from `~/.claude` fails with `pathspec is beyond a symbolic link`. Must operate git from Documents/claude. +- **Context**: darwin-skill run created branch in `~/.claude` first (separate git repo, mostly empty). Real branch with skill changes had to be created in Documents/claude. Two repos, two branches. +- **Future application**: + - Any optimization or batch edit on personal skills/agents operates from `/home/bchanot-ubuntu/Documents/claude` for git to track changes. + - `readlink ~/.claude/skills` + `readlink ~/.claude/agents` first if unsure. Both point to Documents/claude/{skills,agents}. + - Don't waste branch in `~/.claude` — nothing to track for skill content. +- **Reference**: `.claude/audits/DARWIN-SKILL-OPTIMIZATION.md`, branch `auto-optimize/skills-20260506-1730` in Documents/claude. + +## LRN-011 — Single subagent emits N independently-gated scores: pattern + +- **Date**: 2026-05-07 +- **Pattern**: when one subagent produces 2+ scores that each must clear independent thresholds (e.g. `/seo` subagent → SEO classique + GEO scores in same `SEO.md`), orchestrator must: + 1. Extract each score via labeled grep (`extract_score_labeled f "Score SEO" + "Score GEO"`) — never fall back to "first /20 found" (collapses scores or fakes duplicate). + 2. Loop continuation: `while (any axis < threshold) AND iter ≤ MAX`. Single-axis condition exits early while other axis still below. + 3. Re-dispatch prompt labels each axis with current score + PASS/FAIL state, plus axis-specific fix list. Generic "improve the audit" wastes iterations on already-passing axis. + 4. Escalation prompt names affected axes explicitly. User chooses per-axis (continue / stop / override per axis). + 5. Override transparency file lists axes separately (e.g. `SEO classique: NOT overridden, GEO (IA): overridden`). + 6. Backward compat: `allow_fallback` flag — fall back to generic single-score parse for primary axis (legacy compat) but NOT for secondary axis (UNKNOWN forces re-dispatch with explicit format demand). +- **Context**: client-handover pipeline gates SEO + GEO independently (BDR-010). Both scores live in same `.claude/audits/SEO.md`, written by one /seo subagent in one dispatch. Naive "extract first /20" collapsed both into SEO classique value — gate fired on SEO only. Pattern above generalizes to any future audit shipping multiple gated metrics from one subagent (e.g. /harden could split TLS + headers + redirects). +- **Future application**: + - Any audit subagent emitting multiple scores → use labeled extractor pattern + axis-aware loop + per-axis escalation. Never collapse to single score for gate. + - When designing new audits with multiple metrics, mandate labeled score format in skill SKILL.md (e.g. `Score : X.X / 20`). Avoids retrofit later. + - When 2+ scores share one subagent, prompt template lists both PASS/FAIL state + axis-specific fix categories. Otherwise subagent wastes iterations on passing axis. +- **Reference**: `agents/client-handover-writer.md` (`extract_score_labeled` STEP 3, axis-aware loop STEP 4, escalation STEP 4, threshold strictness STEP 8 SEO.md branch). BDR-010. + +## LRN-012 — Bash heredoc + stdin pipe collision = silent empty output + +- **Date**: 2026-05-07 +- **Pattern**: when running an inline-heredoc'd interpreter — `python3 - <<'PY' ... PY`, `bash <<'SH' ... SH`, `node -e <<'JS' ... JS` etc. — the heredoc IS the interpreter's stdin. Any data piped from upstream is **silently discarded**. Symptom: `sys.stdin.read()` (or equivalent) returns the heredoc body itself (often empty after the script consumes it via the read), and the produced output is empty. Exit code is `0`, no error message — silent failure. Diagnose via `bash -x` trace: you see the python ran, but no upstream data ever reached it. + - Anti-pattern (broken): `printf '%s' "$DATA" | python3 - <<'PY' \n template = sys.stdin.read() \n ... \n PY` + - Fix 1 (env var): `DATA="$DATA" python3 - <<'PY' \n import os; template = os.environ['DATA'] \n PY` + - Fix 2 (file path arg): `python3 - "$FILE_PATH" <<'PY' \n import sys; template = open(sys.argv[1]).read() \n PY` — note `"$FILE_PATH"` AFTER `-` becomes `sys.argv[1]`. + - Fix 3 (write tempfile, read inside): `echo "$DATA" > /tmp/x; FILE=/tmp/x python3 - <<'PY' \n template = open(os.environ['FILE']).read() \n PY`. +- **Context**: `skills/client-handover/scripts/handover-to-pdf.sh` v1 piped HTML template through a `substitute()` function that ran `python3 - <<'PY'` and read `sys.stdin`. Pipe dropped silently, `.html` output 0 bytes. Caught by post-write `wc -l`; root cause found via `bash -x`. Fixed by passing template path through `HQ_TEMPLATE_PATH` env var, python opens the file directly (`render_template()` in current script). +- **Future application**: + - Never combine an inline heredoc with an upstream pipe targeting the same interpreter. Pick one input channel: heredoc OR pipe, not both. + - When in doubt: pass data via env vars (small payloads), file paths (large payloads), or argv. Reserve stdin for cases where the interpreter has NO heredoc. + - Add post-write size check (`test -s "$FILE"` or `wc -l`) for any generated artifact in a shell pipeline — surfaces silent-failure modes immediately. + - When debugging "script ran but file empty", run `bash -x script.sh` and look for the `+ python3 -` line — if you see no upstream data being consumed, you have the heredoc-pipe collision. +- **Reference**: `skills/client-handover/scripts/handover-to-pdf.sh` `render_template()` (env-var-based, current); BDR-011 caveat list; commit `e06b52a` (final fix shipped with the renderer). +--- + +## LRN-013 — marked CLI 16.x ignore stdin, dump own cli.js source + +- **Date**: 2026-05-07 +- **Context**: `/client-handover` PDF rendering. `handover-to-pdf.sh` fallback chain pandoc → python-markdown → npx marked. On host with only npx, pipeline ran `npx --yes marked < "$src"` and produced 2-page PDF where body = marked package's `cli.js` source (`#!/usr/bin/env node`, `Marked CLI`, copyright, `import { main } from './main.js'`). Real MD content (30 KB) entirely lost. +- **Pattern**: marked 16.x CLI regression — stdin path broken, ignores piped input, prints its own binary source. Only `-i FILE` flag works. Verified: `echo "test" | npx marked` → marked source. `npx marked -i FILE` → correct HTML. +- **Why**: do not assume marked CLI accepts stdin like awk/jq/sed. Check actual conversion output before shipping any MD→HTML renderer. +- **How to apply**: any shell md→html using marked CLI must call `npx --yes marked --gfm -i "$src"`. Keep pandoc + python-markdown ahead in fallback chain — more stable. Smoke-test: render small MD, grep output for known content; fail loudly if mismatch. +- **Reference**: `skills/client-handover/scripts/handover-to-pdf.sh` line ~140 (npx fallback fixed). Commit fixing bug. + +--- + +## LRN-014 — Pandoc base gfm strips header id attrs — need gfm+gfm_auto_identifiers + +- **Date**: 2026-05-11 +- **Pattern**: `pandoc --from=gfm --to=html5` does NOT auto-generate `id` attributes on header elements. Internal anchor links like `[§4 NAP](#nap)` become dead refs in rendered HTML/PDF. Symptom: rendered doc has `

NAP

` (no `id`), browser/PDF anchor resolves nowhere, user clicks link and goes nowhere. Enable id auto-gen by switching to `--from=gfm+gfm_auto_identifiers` — pandoc then emits `

NAP

` (kebab-case slug from header text). +- **Context**: `skills/client-handover/scripts/handover-to-pdf.sh` MD→HTML cascade. 6-chapter handover doc added internal cross-references between chapters (§5 todo references back to §4 NAP table for values). Default `--from=gfm` produced HTML with no header ids — internal links dead. Discovered after rendering test handover, clicking link in PDF, going to top of doc instead of NAP section. +- **Future application**: + - Any pandoc MD→HTML pipeline with `[text](#anchor)` cross-references → enable `gfm_auto_identifiers` extension explicitly. + - Smoke-test internal anchors before shipping any renderer: render → `grep -E 'id="[^"]+"' out.html` → confirm headers have ids. + - Slug rules: pandoc lowercases + replaces non-alpha with `-`, e.g. `## §4 NAP table` → `id="ss-4-nap-table"`. If you control header text, keep slugs predictable. +- **Reference**: `skills/client-handover/scripts/handover-to-pdf.sh` line 121 (`--from=gfm+gfm_auto_identifiers`). Commit `b15b275`. + +--- + +## LRN-015 — BrightLocal Free Tools retired 2026, Moz Local Citation Checker is free replacement + +- **Date**: 2026-05-11 +- **Pattern**: SEO/NAP tool landscape churns yearly. BrightLocal Free Tools page (`brightlocal.com/free-local-tools/`) retired in 2026 — service now paid-only. Moz Local Citation Checker (`moz.com/local`, "Check My Listing" / "Get Free Audit") is current free replacement: 60s NAP-consistency audit across 50+ directories (Google Business, Apple Maps, Yelp, Pages Jaunes, Bing Places), no credit card required. +- **Context**: client-handover NAP checklist (FR + EN versions) recommended brightlocal.com free tools — link dead, page redirects to paid tier. Caught during handover-doc render. Swapped both language versions to Moz Local with explicit "no credit card" note + path through homepage (button labels can change, URL `moz.com/local` is stable). +- **Future application**: + - Any client-facing doc recommending "free SEO/NAP tools" → verify URLs alive + tool still free annually. SEO vendors churn free tiers regularly. + - Prefer linking to vendor homepage + naming the button ("click Check My Listing") over deep links to specific tool URLs. Vendor URLs deprecate; homepages persist. + - Maintain a short list of "verified-recent" free tools in the handover skill rather than rediscovering on each render. +- **Reference**: `skills/client-handover/checklists/seo-geo-manual.md` (FR section line ~218, EN section line ~429). Commit `abd2612`. + +--- + +## LRN-016 — Pandoc GFM checkbox markup breaks adjacent-sibling CSS — target `li > input` directly + +- **Date**: 2026-05-11 +- **Pattern**: pandoc GFM emits task-list checkboxes as `
  • text…
  • ` with **no wrapper class** and **no list-item class**. Adjacent-sibling CSS rule `li input[type="checkbox"] + *` absolutely-positions the first element sibling AFTER the input — typically ``, ``, ``, or `` inside the bullet text. Effect: that inline element gets yanked out of flow, overlaps adjacent content in rendered PDF. Symptom: PDF has links/code-spans visibly overlapping subsequent text. +- **Context**: `skills/client-handover/resources/branding/zenquality.css` task-list styling. Initial rule tried to render custom checkbox box via `+ *` selector targeting the first sibling after ``. Worked when bullet was plain text (no inline elements), broke when bullet contained `` or `` — those got absolutely-positioned. Caught in rendered LIVRAISON.pdf — checkbox icons OK but link/code text overlapped neighbors. +- **Future application**: + - For pandoc GFM checkbox styling, target `li > input[type="checkbox"]` directly. Style native `` via `appearance: none` + custom box rendering (background, border, size) on the input itself. + - Avoid `+ *` and other sibling-selector tricks on bare-input markup — pandoc gives no wrapper to anchor to, siblings vary per bullet content. + - Render checklist with realistic content (``, ``, ``) before signing off — bare text bullets won't surface the bug. + - Symptom signature: rendered PDF has overlapping inline elements ONLY in task lists — points to a sibling-selector rule firing on inline content. +- **Reference**: `skills/client-handover/resources/branding/zenquality.css` `li > input[type="checkbox"]` rule + `li.task-list-item::before` (lines 372–410). Commit `465fe9e`. + +--- + +## LRN-017 — Thin-dispatcher SKILL.md round-1 win = fallback + frontmatter triggers (+15 to +30) + +- **Date**: 2026-05-12 +- **Pattern**: thin-dispatcher SKILL.md (delegates to `agents/.md`, body 15-30 lines, no inline workflow) scores low on darwin rubric (45-70) because dims D2/D3/D4/D5 punish empty body. Round-1 universal fix: + 1. Add fallback clause — `If $HOME/.claude/agents/.md unreachable, emit " agent missing." and STOP. Never improvise — silent behavior change is unsafe.` + 2. Add triggers to frontmatter `description` — explicit `Triggers: "", "", "".` + 3. For destructive skills (refactor, commit-change): add safety rationale + pre-flight check stub. + Δ +13 to +31 observed: status 45.3→76.2 (+30.9), refactor 48.4→74.3 (+25.9), plugin-check 59.2→76.8 (+17.6), commit-change 69.6→83.5 (+13.9). 150% byte cap tight — trim aggressively. +- **Context**: `/darwin-skill` run 2026-05-12, branch `auto-optimize/20260512-1319` merged to master, 5 commits. skills-perso (66.4→80.1, +13.7) NOT a dispatcher — different patch (Known-limits subsection on the heuristic). +- **Future application**: + - Any darwin round-1 on a dispatcher SKILL.md → skip diagnosis, apply this template directly. Saves one eval cycle. + - After round 1, gains flatten near 75-80 → pivot to next-lowest skill, do not grind rounds 2-3 on same target. + - For thin originals (<500B), 150% cap is the binding constraint — pre-trim drafts before committing. +- **Reference**: `.claude/audits/DARWIN-SKILL-2026-05-12.md`. Commits `512df48`..`134561d`. results.tsv at `~/.agents/skills/darwin-skill/results.tsv`. + +--- + +## LRN-018 — Darwin eval subagents drift on total math — recompute in main thread + +- **Date**: 2026-05-12 +- **Pattern**: analyzer subagents asked to score SKILL.md and compute weighted total drift on the formula. Two recurring errors: (a) divide `Σ(dim×weight)` by `100` instead of `10` (off by factor 10 — produces 6.17 instead of 61.7, then sometimes the subagent silently re-multiplies); (b) use D8 weight 7 instead of the spec value 25 (status: spec says D8 weight = 25, easy to confuse with D4 weight = 7). Per-dim judgments themselves stable across runs; computed totals unreliable. +- **Context**: 5 round-1 evals during darwin 2026-05-12. Refactor subagent computed 743÷10 correctly in scratch but wrote `617/100 = 61.7` — actual correct total 74.3. Subsequent prompts explicitly stating "D8 weight is 25" cleared the second error. +- **Future application**: + - Prompt subagent for dim scores only, not weighted total. Main thread computes `Σ(dim_i × weight_i) / 10` deterministically. + - If subagent must compute, include weight table in prompt AND show example computation for one row. + - When comparing baseline vs round-N, use main-thread recomputed totals on BOTH sides, not the two subagents' self-reported numbers. + - Score recalibration between baseline subagent and round-1 subagent is real (independent re-anchoring) — first-round Δ tends to overstate improvement. Direction reliable, magnitude noisy. +- **Reference**: see "Methodology notes" section of `.claude/audits/DARWIN-SKILL-2026-05-12.md`. + +--- + +## LRN-019 — Deployable-project doc split: README dev, DEPLOY prod-VPS 14 sections + +- **Date**: 2026-05-15 +- **Pattern**: deployable project → split docs by audience, not by topic. README = dev + features audience (one-line pitch, Features, Stack, Quick start (dev), Verifying a change, Build & deploy summary, Documentation cross-links, License). DEPLOY.md = ops/SRE audience, prod-only, 14 sections mirroring real VPS-deploy shape (topology table, env vars, VPS provisioning, two-layer firewall = cloud security group + UFW, Docker tuning = log caps + `live-restore`, first-time setup, routine deploys, persistence/volumes, backups + cron + retention, TLS = Caddy/nginx + ACME, observability = logs + healthchecks, hardening = SSH keys-only + fail2ban + unattended-upgrades, rollback, runbook). Dev quick-start NEVER in DEPLOY.md — mixed dev/prod = drift source. Trivial deploy (no Docker, no compose, no fly.toml, no k8s, no scripts/deploy.*) → fold into README, skip DEPLOY.md. +- **Context**: applied 2026-05-15 in `agents/doc-syncer.md` STEP 5/6 rewrite. Generalizes README-vs-DEPLOY ownership drift seen across multi-maintainer repos (devs read one doc, ops read another, both edit independently, conflicts pile up). 14-section template comes from real Scaleway DEV1-S walkthrough — shape works on any provider (Scaleway, Hetzner, OVH, DO, Vultr, plain bare-metal). +- **Future application**: + - Any `/onboard` / `/doc` / `/init-project` producing docs for a deployable project → apply the split directly. Don't ask user "where should dev setup go" — README, always. + - Existing repo has DEPLOY.md with "Local development" / "Dev setup" section → flag as drift, propose moving content to README, removing section from DEPLOY in same patch round. + - Existing repo has README.md mixing prod topology details (firewall, TLS, backups) → flag as drift, propose moving to DEPLOY.md. + - 14-section template = ceiling not floor. Drop sections that don't apply (no DB → drop "Managed DB" section, no domain → drop TLS section). Don't pad to hit 14. + - Audience test before merging a doc section: "would a junior dev clone-and-run with this?" → README. "Would an on-call SRE provisioning a new VPS use this?" → DEPLOY. If both → split it. +- **Reference**: commit `7ee9b42`, `agents/doc-syncer.md` STEP 5 (README template lines 223–335), STEP 6 (DEPLOY.md 14-section template lines 338–541). Linked to [[doc-syncer-readme-auto-deploy-prod]] (BDR-016). + +--- + +## LRN-021 — Refactor migrating commands→skills must sweep `~/.claude/commands/` for orphan wrappers + +- **Date**: 2026-05-20 +- **Pattern**: when refactor moves orchestrator from `.claude/agents/foo.md` into `~/.claude/skills/foo/SKILL.md`, any pre-existing wrapper at `~/.claude/commands/foo.md` that references the old agent path becomes orphan. Wrapper still resolves `/foo` (slash commands take precedence over skills in dispatch), executes broken `Load and follow: .claude/agents/foo.md` instructions, fails silently or hits "file not found" mid-orchestration. Untracked files in `~/.claude/commands/` survive every refactor commit invisibly — git status in project repo never shows them. +- **Context**: 2026-05-20, `/ship-feature` hit BLK-004. Wrapper from before refactor `21960e0` ("changed orchestrators into skills") referenced 6 agent files; 5 deleted by refactor. Wrapper untracked → never flagged for cleanup. Detected only when user invoked `/ship-feature` and read the broken `Load and follow strictly:` list. +- **Future application**: + - Any commit moving orchestrator from `agents/foo.md` → `skills/foo/SKILL.md` → `grep -rln "agents/foo.md" ~/.claude/commands/` and delete stale wrappers in same commit. + - `/onboard` + `/init-project` must check `~/.claude/commands/` for wrappers referencing paths that no longer exist; print warning. + - When auditing skills (darwin-skill, /skills-perso, /profile), also list `~/.claude/commands/*.md` and cross-check each `Load and follow:` line resolves. + - Skills with `disable-model-invocation: true` rely on slash-dispatch — when wrapper exists, wrapper wins. Removing wrapper exposes skill directly; replacing skill behavior requires updating BOTH wrapper and SKILL.md. +- **How to detect early**: post-refactor script — `for f in ~/.claude/commands/*.md; do grep -Eo '\.claude/agents/[a-z-]+\.md' "$f" | while read p; do test -f "$HOME/$p" || echo "ORPHAN $f → missing $p"; done; done`. +- **Reference**: BLK-004, commits `0241e1d` + `21960e0`. + +--- + +## LRN-020 — profile-sentinel-collision: literal labels in cmd output must not match profile filenames + +- **Date**: 2026-05-18 +- **Context**: Adding `lib/profiles/full.profile` exposed an aliasing bug in `lib/profile.sh:421`. `cmd_current` returned literal "full (all gstack skills enabled — no profile set)" when no profile was applied — a sentinel meaning "no profile active, full gstack on". With a real profile now named `full`, output became ambiguous: same word, opposite meanings (sentinel = no profile vs. profile name = canonical full set). Renamed sentinel to "none". +- **Pattern**: when a CLI returns named identifiers from a known namespace (profiles, channels, modes), any sentinel/placeholder value MUST be outside that namespace. Reserve sentinel strings like `none`, `unset`, `default`, `` — never reuse a real identifier as "absence of identifier". +- **Where applicable**: + - Any `cmd_current` / `cmd_status` / `cmd_active` that reports either a real entity OR a "nothing applied" state. + - Profile/preset systems with named profiles. + - Selector outputs in shell scripts where downstream code does `[ "$x" = "" ]`. +- **How to detect early**: + - Before adding a new entity name to a namespace, grep the codebase for hardcoded literals matching the candidate name (`grep -rn '"full"\|"none"\|"default"' lib/`). + - Audit `case` statements + `echo` lines in CLI commands for namespace-reserved labels. +- **Cost when missed**: shell-script consumers parsing the output break silently — `[ "$prof" = "full" ]` matches both meanings. User reads ambiguous status. No type system to catch it. +- **Reference**: `lib/profile.sh:421` sentinel rename in same commit as new `full.profile`. Linked to [[profile-full-superset]] (BDR-017). + +--- + +## LRN-022 — Audit `lib/profiles/*.profile` against gstack skill list after every submodule bump + +- **Date**: 2026-05-21 +- **Context**: 2026-05-21, `/hotfix` on BLK-005. Gstack upstream renamed `checkpoint` skill to `context-save` (shadow conflict with Claude Code native `/checkpoint` rewind alias). Five local `lib/profiles/*.profile` files referenced the dead name. Warning `⚠ missing: checkpoint — try: bash link.sh` looked actionable but link.sh cannot resurrect an upstream-deleted skill — suggested next step dead end. Misdiagnosis cost user confused round-trip before `/hotfix` traced the rename. +- **Pattern**: profiles couple to external naming registry (`skills-external/gstack/*/`). When upstream renames or removes a skill, profiles silently break: `bash lib/profile.sh set ` warns but does not fail; user has no signal at submodule-bump time. Same shape as any pinned-name reference into a vendored dep (config referring to npm subpath, k8s manifest referring to image tag, etc.). +- **Where applicable**: + - Any `git submodule update` or `git pull` inside `skills-external/gstack/` — diff skill list before/after. + - `make plugin`, `bash install-plugins.sh` — any time external skill source moves. + - When `bash lib/profile.sh apply|set ` warns `missing: `, treat warning as ground truth: skill is genuinely absent from `skills-external/gstack/` AND `skills-disabled/`. `link.sh` cannot fix it. +- **How to detect early**: + ```bash + # After any gstack submodule bump: + diff <(ls skills-external/gstack/ | grep -v '^\.' | sort) \ + <(awk '$2 != "personal" && $2 != "external" && $2 !~ /^(plugin|mcp|cli)/ && /^[a-z]/ {print $1}' lib/profiles/*.profile | sort -u) \ + | grep '^>' # entries in profiles but not in gstack = stale references + ``` + Run as part of post-submodule-bump audit. Pair with `bash lib/profile.sh set ` smoke test — any `⚠ missing:` line = stale entry. +- **Cost when missed**: every profile listing dead name emits misleading warning on `set`. User chases `link.sh` (suggested by `enable_skill` at `lib/profile.sh:191`) which silently no-ops. "try: bash link.sh" message hardcodes a fix that only applies to a different failure mode (skill exists upstream but not symlinked yet) — should differentiate. Follow-up: make missing-skill warning say "missing upstream: not in skills-external/gstack/" when applicable. +- **Reference**: BLK-005, commit `69c5ded`. Linked to [[ship-feature-orphan-wrapper]] (LRN-021) — same shape: post-refactor stale references survive because no automated sweep catches them. + +--- + +## LRN-023 — Scripts invoked via symlink must resolve `$REPO` with `cd -P` (physical path), not default `cd` (logical) + +- **Date**: 2026-05-21 +- **Context**: 2026-05-21, BLK-006. `lib/profile.sh:43` used `REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"`. Default `cd` preserves the logical (symlink-following) pathname, so when invoked via `bash "$HOME/.claude/lib/profile.sh"` — a symlinked entry point wired by `link.sh` — `$REPO` resolved to `/home/bchanot-ubuntu/.claude` instead of the real repo `/home/bchanot-ubuntu/Documents/claude`. `$SKILLS_DIR` happened to keep working because `~/.claude/skills` was itself a symlink to the repo, but `$DISABLED_DIR` was a real sibling directory at `~/.claude/skills-disabled` — separate from the repo's actual `skills-disabled/`. `cmd_current` scanned the wrong dir and reported `none` even when 14 gstack skills were genuinely disabled in the repo. +- **Pattern**: any script that + 1. computes paths relative to `$BASH_SOURCE[0]` AND + 2. is meant to be invoked via a symlink at the install location (e.g. `~/.claude/lib/foo.sh -> /lib/foo.sh`) AND + 3. references sibling directories that are NOT also symlinked into the install location + + MUST resolve the script's home via `cd -P` (or `realpath` / `readlink -f`), never default `cd`. Default `cd` returns the logical path the user typed (or the symlinked entry point) — anything you build off that path will follow symlinks for some siblings and fall back to real directories for others, depending on whether each sibling has a symlink in the install location. +- **Where applicable**: + - Any `lib/`, `bin/`, `scripts/` directory in a repo that gets symlinked into `~/.claude/`, `~/.config/`, `/usr/local/`, etc. via an install script. + - Specifically in this repo: `lib/profile.sh`, plus any other script that derives `$REPO`/`$ROOT` from `$BASH_SOURCE`. Audit `grep -rn 'cd "$(dirname "${BASH_SOURCE' lib/ hooks/ agents/`. + - Same pattern in Python (`Path(__file__).resolve().parent.parent` is the safe equivalent — `.resolve()` is the analog of `cd -P`; bare `Path(__file__).parent.parent` is the bug). +- **How to detect early**: + - When writing or reviewing a `REPO=` / `ROOT=` line in a shell script: check whether the script is reachable via a symlink. If yes, `-P` is mandatory. + - Smoke test: from a directory OUTSIDE the repo, invoke the script via both `bash //script.sh` and `bash //script.sh`. Any path the script computes should be identical between the two runs. + - Lint via: `grep -n 'cd "$(dirname "${BASH_SOURCE'