chore(memory): prune-memory 2026-09-24 — index backfill (66 rows), 15 headings normalised, 4 statuses flagged, 6 merges (LRN-163..168), 23 entries compressed
D: every body entry now has an Index row; BDR-074..085, LRN-136, EVAL-026/027 were filed under ### and invisible to the engine and to /reconcile. A: BDR-011/015/031/038 index statuses reflect their supersession; LRN-010 dated path update. B: LRN-147+148, 106+113, 105+107, 142+144, 116+117, 131+132 merged into LRN-163..168, sources kept verbatim and marked superseded. C: tier-1 caveman pass on 23 entries under the negation guard (-5% words: most sentences carry a negation and stay verbatim). Fidelity census: file-level token counts never drop; the per-entry flags on BDR-073 and EVAL-025 are attribution artifacts of the ### fix (bodies byte-identical).
This commit is contained in:
+142
-91
@@ -122,23 +122,53 @@ rules:
|
||||
| LRN-100 | 2026-07-05 | tool gated on clean tree must clean its OWN scratch (else self-DoS next run); contract-changing auto-fix needs structural BREAKING flag in the reviewed artifact | any recurring tool w/ cleanliness precondition; any auto-fix touching an API contract |
|
||||
| LRN-101 | 2026-07-05 | nginx `add_header` inheritance trap: ANY add_header in a location block drops ALL inherited server-level headers on those responses — audit headers on LIVE responses (`curl -I`), never by reading the config; declared infra can be stale (prod ≠ repo stack) | any nginx project audit (zenquality, faunosteo…); any security-header claim |
|
||||
| LRN-102 | 2026-07-05 | deliverable text placed BEFORE a tool call may never render — only the turn's FINAL text is guaranteed displayed; a checklist printed above AskUserQuestion was invisible to the user | any flow whose deliverable is conversational text (checklist, commands, report): end the turn with it, blocking questions come before, never after |
|
||||
| LRN-105 | 2026-07-06 | explorer subagent ran a build tool (`graphify .`) mid read-only audit despite prose instructions to only Read/Grep/Bash-read — the runtime observed a config-protection sentinel deny message and self-corrected only after an explicit main-session correction, not from the original prompt | dispatching any "read-only audit" subagent whose toolset includes Bash: state "do not execute build/generator/mutating commands" explicitly, don't rely on "read-only" framing alone to constrain tool CHOICE |
|
||||
| LRN-106 | 2026-07-06 | job3-B1 froze a fixture + repointed run-reconcile.sh's T2 off the live registry, declared "unblocked", 20/20 green — job4 (next audit, same file, same day) found T3+T5 in the SAME FILE still read the live registry, same fragility, untouched | fixing one instance of a "reads live state it shouldn't" finding: grep the WHOLE file (not just the cited line) for the same pattern before declaring the class closed |
|
||||
| LRN-103 | — | BLK-009 was stale: re-probe confirms `paths:` frontmatter works at BOTH levels now | before acting on ANY open upstream/tool blocker cited to justify a fix, a caveat, or a design const… |
|
||||
| LRN-104 | — | a hook's output message is part of its test contract; no runner = regression invisible | change any hook/script output consumed by a test → run its test same commit. `make test` now the de… |
|
||||
| LRN-105 | 2026-07-06 | explorer subagent ran a build tool (`graphify .`) mid read-only audit despite prose instructions to only Read/Grep/Bash-read — the runtime observed a config-protection sentinel deny message and self-corrected only after an explicit main-session correction, not from the original prompt | superseded by LRN-165 |
|
||||
| LRN-106 | 2026-07-06 | job3-B1 froze a fixture + repointed run-reconcile.sh's T2 off the live registry, declared "unblocked", 20/20 green — job4 (next audit, same file, same day) found T3+T5 in the SAME FILE still read the live registry, same fragility, untouched | superseded by LRN-164 |
|
||||
| LRN-107 | — | read-only subagent mandates must ban copying secret VALUES, not just mutations | superseded by LRN-165 |
|
||||
| LRN-108 | — | `claude mcp add --env KEY=value` writes the VALUE literally; use `${VAR}` unless you mean to | adding ANY MCP server with a secret via `claude mcp add --env`, single-quote the value using `${VAR… |
|
||||
| LRN-109 | 2026-07-07 | job8: `skills` CLI (vercel-labs/skills) fetches only `skillPath` (often just SKILL.md), not sibling refs/scripts/templates the skill text references — darwin-skill install gap, not drift/tamper | installing/auditing any skill via the `skills` CLI whose SKILL.md references relative paths — verify those paths exist post-install, don't trust `skillFolderHash` alone |
|
||||
| LRN-110 | 2026-07-07 | job8: `21st_magic_component_builder` (magic MCP) opens unauth'd 127.0.0.1 callback server, CORS `*`, no token check, 10min window — any local POST lands verbatim in the tool result the model consumes = local prompt-injection channel | any MCP tool that opens a local callback/listener server to receive async results — check auth + origin scoping on the listener, not just the outbound call |
|
||||
| LRN-111 | 2026-07-07 | job8: empty permissions.allow for a risky MCP tool is a VALID posture (not a gap) when transcript census shows zero real invocations — pre-authorizing unused surface buys nothing, ask-gate costs nothing | deciding whether to allowlist any tool/command — check real usage before assuming "no entry = todo" |
|
||||
| LRN-112 | 2026-07-08 | job9: CC nested subagent dispatch SUPPORTED since v2.1.172 (cap 5 levels, `Agent` must be in subagent `tools:`) — "flattens to 1 level" is the pre-2.1.172 regime; live env v2.1.203. Contradicts the operating premise of the whole job1-9 series | a subagent-dispatches-subagent design is VERSION-CONTINGENT, not "broken" — check CC version before flagging; fix = raise floor or re-architect to bundle→L1 |
|
||||
| LRN-113 | 2026-07-08 | partial-pattern-fix = recurring defect of the job1-9 series: fix the cited instance, leave the twins (trailer A1, YAML A4, attribution A5, hook A2). An adversarial review catches twins later; nothing catches them at commit time | any fix of a banned pattern: grep the ENTIRE surface + add a make-test guard (run-review-guards.sh) that REDs if one occurrence subsists |
|
||||
| LRN-113 | 2026-07-08 | partial-pattern-fix = recurring defect of the job1-9 series: fix the cited instance, leave the twins (trailer A1, YAML A4, attribution A5, hook A2). An adversarial review catches twins later; nothing catches them at commit time | superseded by LRN-164 |
|
||||
| LRN-114 | 2026-07-08 | editing a hook GENERATOR (_gitflow_emit_pre_commit) does NOT update the INSTALLED hook (.githooks/pre-commit) — silent drift; T10 diffs the allow/block verdict not content, T16 emits fresh in a throwaway repo → job7 gitleaks backstop inert on the repo 8 days | after editing a template-generated artifact: reinstall (install-hook) + a gate that diffs installed==emit |
|
||||
| LRN-115 | 2026-07-08 | analyzer Edit/Write grants (seo/geo/validator) are NOT dead: needed to write the REPORT (VALIDATE/SEO/GEO.md); the "never edit" rule targets CODE, instruction-level (same as the patron) — verified false-positive | do NOT re-flag as a tool-grant defect; a report-only agent keeps Write for its own report |
|
||||
| LRN-116 | 2026-07-08 | memory backfill release→develop: a BLK marked "resolved" can have its RESOLUTION (code) missing from develop — BLK-016 resolved on release but rtk fix e58037c never back-merged → bug LIVE on develop | before backfilling a resolved blocker: verify the fix CODE is on the target branch, not just the registry entry |
|
||||
| LRN-117 | 2026-07-08 | a release/develop fork silently orphans FUNCTIONAL code on develop, not just memory — RC soak fixes (find-skills, make-update TTY, rtk version-guard) lived only on release for the fork's duration; the review's memory back-merge caught only ~half | at release-finish/reconcile: list develop..release commits touching non-registry code (excl. merges/version) for back-merge review — a registry-gap check alone misses code |
|
||||
| LRN-131 | 2026-07-17 | WebSearch is NOT verification for a number — SEO blogs cross-cite into fake consensus; require primary source + `measured:` field | any stat headed for a client report; verifying a metric/claim exists |
|
||||
| LRN-132 | 2026-07-17 | a subagent summary is a CLAIM, not a fact — 7 disproven in one session (incl. 3 I reproduced writing the fixes) | before planning on any relayed finding; verify vs primary source / live test first |
|
||||
| LRN-116 | 2026-07-08 | memory backfill release→develop: a BLK marked "resolved" can have its RESOLUTION (code) missing from develop — BLK-016 resolved on release but rtk fix e58037c never back-merged → bug LIVE on develop | superseded by LRN-167 |
|
||||
| LRN-117 | 2026-07-08 | a release/develop fork silently orphans FUNCTIONAL code on develop, not just memory — RC soak fixes (find-skills, make-update TTY, rtk version-guard) lived only on release for the fork's duration; the review's memory back-merge caught only ~half | superseded by LRN-167 |
|
||||
| LRN-118 | — | Gitflow-conformity audit: "commits-code" vs "applies-but-defers-commit" is the line that sorts real findings… | any fleet/skill conformity audit — (1) triage by "autonomous commit/push reached?", not "file writt… |
|
||||
| LRN-119 | — | Fail-open engine contract for optional external data (real-if-connected, else graceful) | any "use real data if credentials present, else degrade" seam — put the contract in the shell entry… |
|
||||
| LRN-120 | — | SDD final-review base = `git merge-base`, NOT the ledger's recorded BASE | for ANY whole-branch/final review, derive base from `git merge-base <target> HEAD`, never a stored/… |
|
||||
| LRN-121 | — | Shell allowlist validation: `grep -Eq` is fragile; use a whole-string POSIX `case` | validate shell input WHOLE-STRING (`case` or bash `[[ =~ ]]`), never `grep -q` (per-line). Set `LC_… |
|
||||
| LRN-122 | — | git mv + recreate source path in same commit = rename detection dead | ANY rename-and-replace-in-place (config forks, template splits, versioned API files). Old path must… |
|
||||
| LRN-123 | — | "resolves inside repo" symlink check green-lights stale link once old path re-occupied | symlink/path health checks → assert exact expected target whenever the old target path can be re-oc… |
|
||||
| LRN-124 | — | derived scan artifacts don't belong in git; a tooling hint saying "safe to commit" manufactures the leak | derived security artifacts (scan reports, triage JSONs, audit findings) stay local/ignored; only th… |
|
||||
| LRN-125 | — | don't make an agent dual-use across model tiers; route the audit consumer to a big-model agent, not the sonne… | before making an agent dual-use, check both consumers are on the SAME tier. Audit/reflection consum… |
|
||||
| LRN-126 | — | splitting a monolith agent severs every IMPLICIT data path; forward each consumed field through the handoff c… | when splitting an agent, enumerate EVERY field the child reads (grep child for its input vocabulary… |
|
||||
| LRN-127 | — | SDD implementers must not run destructive git ops on files outside their task scope | dispatch briefs for SDD implementers / fix-subagents MUST bar destructive git ops outside the named… |
|
||||
| LRN-128 | — | a version RESET (backward bump) is editorial reflection, not the forward-only release-executor | version RESET or any non-standard release → do PREP MANUALLY inline (big model), use `gitflow.sh` o… |
|
||||
| LRN-129 | — | `git cherry` (patch-id) proves a stale/divergent branch has nothing orphaned before you delete it | before abandoning/deleting a divergent branch, `git cherry -v <mainline> <branch>` then content-ver… |
|
||||
| LRN-130 | 2026-07-16 | Claude Code deny glob = absolute, no exemption mechanism — 2026-07-16 | — |
|
||||
| LRN-131 | 2026-07-17 | WebSearch is NOT verification for a number — SEO blogs cross-cite into fake consensus; require primary source + `measured:` field | superseded by LRN-168 |
|
||||
| LRN-132 | 2026-07-17 | a subagent summary is a CLAIM, not a fact — 7 disproven in one session (incl. 3 I reproduced writing the fixes) | superseded by LRN-168 |
|
||||
| LRN-133 | 2026-07-17 | an omission must stay LEGIBLE, never silent — tool that can't measure says so in its output | designing any audit/measure output; deciding what a cap/refusal/N-A emits |
|
||||
| LRN-134 | 2026-07-17 | resolve-then-pin in stdlib http.client beats monkeypatching getaddrinfo — dual-stack, thread-safe, no requests; classify the OS-resolved IP not the URL text | closing SSRF/DNS-rebinding on any Python HTTP egress |
|
||||
| LRN-135 | 2026-07-17 | a prefix-only scan for a dangerous construct is bypassable by padding — scan the WHOLE document | refusing any hostile construct (DTD/directive/marker) before parse |
|
||||
| LRN-136 | 2026-07-17 | config-protection live state follows checked-out branch's symlinked settings.json (2026-07-17) | — |
|
||||
| LRN-137 | — | mode-based re-tiering beats file splits for mixed-tier agents | before splitting any agent across model tiers, try MODE + `model=` first; create a new agent file o… |
|
||||
| LRN-138 | 2026-07-22 | gitignore ≠ delete for run-time artifacts read from disk (2026-07-22) | "don't merge transient X" → ask: does the run read X from disk? does X travel via git (worktree, fo… |
|
||||
| LRN-139 | 2026-07-30 | model-trait compensations invert across generations; state WHEN-guidance, not direction (2026-07-30) | at every model-generation bump, grep config for trait-compensating language ("counters model tenden… |
|
||||
| LRN-140 | 2026-08-02 | de-prescription findings: dedup evaporates, self-verify is default, recall survives (2026-08-02) | — |
|
||||
| LRN-141 | 2026-08-24 | adopting an external skill: take the invariants, refuse the machinery (2026-08-24) | — |
|
||||
| LRN-142 | 2026-08-24 | structure locks are fixed-string: reflowing a doctrine paragraph reds them (2026-08-24) | superseded by LRN-166 |
|
||||
| LRN-143 | 2026-08-26 | `cmd \| head \|\| fallback` — pipeline rc is head's (0), fallback dead; bounded output → drop head, else pipefail | any probe/fallback bash in skills before trusting `\|\|` |
|
||||
| LRN-144 | — | census locks grep EXACT single-line phrases; prose rewrap breaks them | superseded by LRN-166 |
|
||||
| LRN-145 | — | hooks reach the terminal only via terminalSequence JSON field | — |
|
||||
| LRN-146 | — | Notification event alone misses end-of-turn; Stop is the missing event | — |
|
||||
| LRN-147 | — | VS Code restores terminals BEFORE ext activation → toast dies every restart | superseded by LRN-163 |
|
||||
| LRN-148 | — | terminal instrumentation is per-terminal + unpredictable; pre-flight test before attaching | superseded by LRN-163 |
|
||||
| LRN-149 | — | Stop hook payload carries background_tasks; use it to skip premature signals | — |
|
||||
| LRN-150 | 2026-09-15 | Sourced lib shares caller shell: bare `ok/warn/info` override its printers, and its `set -e` applies inside | any new lib/*.sh |
|
||||
| LRN-151 | 2026-09-15 | Playwright cache truth = union over `.links`, dir name maps `_`→`-`, revisionOverrides exist | shared versioned binary caches |
|
||||
| LRN-152 | 2026-09-15 | git `protocol.file=user` blocks submodule fixtures; `-c` misses the code under test, `GIT_CONFIG_*` env does not | tests building git fixtures |
|
||||
@@ -148,6 +178,16 @@ rules:
|
||||
| LRN-156 | 2026-09-16 | autoMode.allow = exception tier; static interpreter allow suspended under auto → conditions live in prose | conditional permissions |
|
||||
| LRN-157 | 2026-09-16 | gap-only trigger blind to taste → add a trigger class, not budget; ask at plan, mid-run for leftovers | any "ask more" request |
|
||||
| LRN-158 | 2026-09-22 | Installer refusing symlinked paths vs a symlinked config dir → stage under a throwaway HOME, move the result | any vendor installer writing into ~/.claude or ~/.config |
|
||||
| LRN-159 | 2026-09-22 | A pin whose payload is fetched at install time rots: pin + fallback, and read the installer's output, not its… | any `install-plugins.sh` step whose pinned tool downloads something at install time. Probe both HOM… |
|
||||
| LRN-160 | 2026-09-22 | Prose guardrails are judgment, not boundary: a well-argued brief walks a sub-agent through them | any new destructive capability → static deny first, prose second, doctrine third. Any orchestrator… |
|
||||
| LRN-161 | 2026-09-24 | `git branch -d` guards against the UPSTREAM once one is set: auto-push turns it into a no-op guard | any change to upstream/push config → re-read every `-d`, `--ff-only`, `@{u}`-relative guard. New de… |
|
||||
| LRN-162 | 2026-09-24 | graphify measured: free AST map, paid semantic pass, 2-3k tokens per query, noise from `.claude/` | measure a "context saver" before adopting it — build time, artifact size, tokens per use, noise sou… |
|
||||
| LRN-163 | 2026-09-24 | VS Code terminal instrumentation is per-terminal and unpredictable: pre-flight the pty before attaching | notify-attention over Remote-SSH; any client-side terminal-parsing ext |
|
||||
| LRN-164 | 2026-09-24 | one fixed occurrence ≠ pattern closed: grep the whole surface, add a guard with teeth | any "fix pattern X" task; reads-live-state, banned tokens, stale pins |
|
||||
| LRN-165 | 2026-09-24 | a read-only sub-agent mandate constrains files, not tools: name the banned commands, ban copying secret values | every sub-agent brief framed read-only / audit / verify with Bash or config access |
|
||||
| LRN-166 | 2026-09-24 | structure and census locks are fixed single-line strings: a prose rewrap reds them with zero doctrine lost | editing any doctrine, skill or agent file under lib/tests locks |
|
||||
| LRN-167 | 2026-09-24 | a release/develop fork strands CODE on develop: a "resolved" blocker or a parallel-merged feature can miss its fix | any long-lived fork (release/*, long feature); back-merging a resolved blocker |
|
||||
| LRN-168 | 2026-09-24 | a relayed claim is not a fact: WebSearch consensus and sub-agent summaries both need a primary source or a live test | any number, feature or finding relayed by search or by a sub-agent before it shapes a plan or a client deliverable |
|
||||
|
||||
---
|
||||
|
||||
@@ -261,6 +301,7 @@ rules:
|
||||
- `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.
|
||||
- **Update 2026-09-24**: path now `/home/bchanot/Documents/claude` (home renamed after the 2026-09-21 wipe; symlink layout unchanged).
|
||||
|
||||
## LRN-011 — Single subagent emits N independently-gated scores: pattern
|
||||
|
||||
@@ -626,13 +667,12 @@ rules:
|
||||
---
|
||||
|
||||
## LRN-038 — Playwright host-platform override for distros newer than its hardcoded support list
|
||||
|
||||
- **Date**: 2026-06-23
|
||||
- **Context**: fresh Ubuntu 26.04. gstack `./setup` aborted: "Playwright does not support chromium on ubuntu26.04-x64". Playwright 1.58.2's registry hardcodes `ubuntu20.04/22.04/24.04` only; a newer release → no matching build → hard error. gstack is a pinned submodule (must not edit).
|
||||
- **Pattern**: `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntuXX.04-<arch>` forces a fallback build. MUST include arch (`x64`/`arm64`) — bare `ubuntu24.04` fails ("does not support … ubuntu24.04"). Set it from the WRAPPER: `export` before the submodule's setup (install-time download) AND persist to the shell profile (runtime launch) — both paths call `getHostPlatform`. No submodule edit. Gate on real OS version (`sort -V` compare) so supported distros are untouched. Test with the LOCAL `./node_modules/.bin/playwright` — `bunx playwright` pulls the LATEST playwright (different browser revision than the local import), which masks the result.
|
||||
- **Future application**: any pinned tool that hardcodes an OS allowlist breaks on a fresh OS upgrade. Look for a host-platform override env before bumping/forking the dep. Prove the fallback binary actually runs (`ldd` = no missing libs + a real headless render), not just that the download resolves.
|
||||
- **Pattern**: `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntuXX.04-<arch>` forces a fallback build. MUST include arch (`x64`/`arm64`) — bare `ubuntu24.04` fails ("does not support … ubuntu24.04"). Set from the WRAPPER: `export` before the submodule's setup (install-time download) AND persist to the shell profile (runtime launch) — both paths call `getHostPlatform`. No submodule edit. Gate on real OS version (`sort -V`) → supported distros untouched. Test with the LOCAL `./node_modules/.bin/playwright` — `bunx playwright` pulls the LATEST playwright (different browser revision than the local import), masks the result.
|
||||
- **Future application**: pinned tool hardcoding an OS allowlist breaks on a fresh OS upgrade. Look for a host-platform override env before bumping/forking the dep. Prove the fallback binary actually runs (`ldd` = no missing libs + a real headless render), not just that the download resolves.
|
||||
- **Reference**: `install-plugins.sh` `playwright_platform_override()`, commit 211c7d4. Linked to [[BLK-008]].
|
||||
- **2026-06-23 CORRECTION (override REVERTED, commit b9c3937)**: the override is NOT a usable fix on Ubuntu 26.04. It makes `playwright install` switch to the ubuntu24.04 fallback build, which downloads to 100% then HANGS at extraction (chrome binary never materializes; real machine + sandbox). Turned a 0.5s fast-fail into an install-blocking hang. The isolated proof (`ldd` + headless render) PASSED but used an already-extracted sibling build (rev 1228) — it masked the install-path hang in the real flow (rev 1208). **Sharpened lesson**: proving the binary launches in isolation is NOT proving the install path works — run the ACTUAL install command end-to-end (it must COMPLETE, not just "download resolves" nor "a binary launches"). The override technique stays valid in general, but the EXTRACTION/COMPLETE step is part of "does it work".
|
||||
- **2026-06-23 CORRECTION (override REVERTED, commit b9c3937)**: the override is NOT a usable fix on Ubuntu 26.04. It makes `playwright install` switch to the ubuntu24.04 fallback build, which downloads to 100% then HANGS at extraction (chrome binary never materializes; real machine + sandbox). 0.5s fast-fail → install-blocking hang. Isolated proof (`ldd` + headless render) PASSED on an already-extracted sibling build (rev 1228) — masked the install-path hang in the real flow (rev 1208). **Sharpened lesson**: proving the binary launches in isolation is NOT proving the install path works — run the ACTUAL install command end-to-end (it must COMPLETE, not just "download resolves" nor "a binary launches"). Override technique stays valid in general; the EXTRACTION/COMPLETE step is part of "does it work".
|
||||
|
||||
---
|
||||
|
||||
@@ -647,11 +687,10 @@ rules:
|
||||
---
|
||||
|
||||
## LRN-040 — OS newer than a pinned tool supports = TWO distinct layers (version build + security policy)
|
||||
|
||||
- **Date**: 2026-06-23
|
||||
- **Context**: gstack browser on fresh Ubuntu 26.04. Layer 1 = Playwright 1.58.2 ships no browser build for 26.04 → install errors (the host-platform override "fixes" the error but its fallback build HANGS at extraction — dead end, [[BLK-008]]). Layer 2 = even with Playwright 1.61 (native 26.04 build that launches fine in isolation), the real browse path aborts "No usable sandbox" because Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
|
||||
- **Pattern**: (a) bump the tool PAST the OS-support threshold — don't force the OS to look older (overrides/fallbacks are fragile; prove the install COMPLETES, not just that a binary launches). For a pinned submodule dep: `bun add X@latest` in the submodule, automatable in the installer, idempotent by grepping the dep's support list for the running OS tag before bumping. (b) SEPARATELY handle OS security hardening: Chromium needs `--no-sandbox` where `sysctl kernel.apparmor_restrict_unprivileged_userns=1`; gstack exposes `GSTACK_CHROMIUM_NO_SANDBOX=1` (#1562). Gate persistence on the sysctl, not an OS-version guess.
|
||||
- **Future application**: "tool X broke after an OS upgrade" → check BOTH (1) does X ship a build / support entry for the new OS (bump if not), and (2) does the new OS's hardening (userns/AppArmor/SELinux) block X at runtime (needs an opt-out flag). Fix one without the other and it still fails. Verify the FULL runtime path (drive a real page) — here the isolated `chromium.launch()` PASSED while the real `browse` path failed on the sandbox.
|
||||
- **Pattern**: (a) bump the tool PAST the OS-support threshold — don't force the OS to look older (overrides/fallbacks are fragile; prove the install COMPLETES, not just that a binary launches). Pinned submodule dep: `bun add X@latest` in the submodule, automatable in the installer, idempotent via grep of the dep's support list for the running OS tag before bumping. (b) SEPARATELY handle OS security hardening: Chromium needs `--no-sandbox` where `sysctl kernel.apparmor_restrict_unprivileged_userns=1`; gstack exposes `GSTACK_CHROMIUM_NO_SANDBOX=1` (#1562). Gate persistence on the sysctl, not an OS-version guess.
|
||||
- **Future application**: "tool X broke after an OS upgrade" → check BOTH (1) does X ship a build / support entry for the new OS (bump if not), and (2) does the new OS's hardening (userns/AppArmor/SELinux) block X at runtime (needs an opt-out flag). Fix one without the other → still fails. Verify the FULL runtime path (drive a real page) — isolated `chromium.launch()` PASSED while the real `browse` path failed on the sandbox.
|
||||
- **Reference**: `install-plugins.sh`, `.bashrc` `GSTACK_CHROMIUM_NO_SANDBOX=1`, gstack `browse/src/browser-manager.ts` `shouldEnableChromiumSandbox()`, commit 3b8ffb1. Linked to [[BDR-029]], [[BLK-008]], [[LRN-038]].
|
||||
|
||||
---
|
||||
@@ -777,11 +816,10 @@ rules:
|
||||
- **Reference**: `lib/analyze-before-plan.md` (THE INVARIANT). Conditions [[LRN-046]], [[LRN-034]], [[BDR-033]]. See [[BDR-035]].
|
||||
|
||||
## LRN-055 — Body `## ID —` headings are a drift-immune index; the maintained `## Index` table is not
|
||||
|
||||
- **Date**: 2026-06-26
|
||||
- **Pattern**: When a registry keeps both per-entry `## ID — title` headings AND a hand-maintained `## Index` table, the Index DRIFTS (entries land in the body, the manual update lapses) while headings cannot (an entry IS its heading — 100% coverage by construction). Measured: decisions 11/34 (32%), learnings 21/52 (40%), blockers 2/9 (22%) missing from the Index — scattered in large blocks (e.g. decisions BDR-024–033 unindexed while the newer BDR-034 is), not an old/new split. The manual Index-update step is simply unreliable. Key any selector/scan off `grep '^## <PREFIX>-'`, never the convenience Index. Backfill (prune-memory passe D) = human-TOC hygiene, NOT a selector dependency.
|
||||
- **Context**: analyze-before-plan ([[BDR-035]]) two-pass. First instinct "reuse the Index capitalize maintains"; measuring the drift killed it — the convenient artifact was the unreliable one, the guaranteed one (headings) sat free.
|
||||
- **Future application**: choosing a substrate to index/select over — prefer what the STRUCTURE guarantees over what a step PROMISES to maintain. Verify maintained-artifact completeness before depending on it.
|
||||
- **Pattern**: When a registry keeps both per-entry `## ID — title` headings AND a hand-maintained `## Index` table, the Index DRIFTS (entries land in the body, the manual update lapses) while headings cannot (an entry IS its heading — 100% coverage by construction). Measured: decisions 11/34 (32%), learnings 21/52 (40%), blockers 2/9 (22%) missing from the Index — scattered in large blocks (e.g. decisions BDR-024–033 unindexed while the newer BDR-034 is), not an old/new split. Manual Index-update step unreliable. Key any selector/scan off `grep '^## <PREFIX>-'`, never the convenience Index. Backfill (prune-memory passe D) = human-TOC hygiene, NOT a selector dependency.
|
||||
- **Context**: analyze-before-plan ([[BDR-035]]) two-pass. First instinct "reuse the Index capitalize maintains"; measuring the drift killed it — convenient artifact unreliable, guaranteed one (headings) free.
|
||||
- **Future application**: choosing a substrate to index/select over: prefer what the STRUCTURE guarantees over what a step PROMISES to maintain. Verify maintained-artifact completeness before depending on it.
|
||||
- **Reference**: `lib/analyze-before-plan.md` (PASS 1). `skills/prune-memory` passe D. See [[BDR-035]].
|
||||
|
||||
## LRN-056 — `grep PAT dir/*.md` on an absent dir ERRORS (exit 2), it does not no-op → guard with `[ -d ]`
|
||||
@@ -793,11 +831,10 @@ rules:
|
||||
- **Reference**: `lib/analyze-before-plan.md` (PASS 1 guard). Sibling to [[LRN-051]] (exec-test tool behavior, never assume). See [[BDR-035]].
|
||||
|
||||
## LRN-057 — Match the consumption mechanism to the consumer (mechanical / external-cognitive / inline-cognitive)
|
||||
|
||||
- **Date**: 2026-06-26
|
||||
- **Pattern**: When a produced artifact must be CONSUMED downstream, the mechanism depends on the consumer: (a) MECHANICAL (git merge integrating a branch) — production on the shared substrate = consumption, automatic ([[BDR-034]]'s "commit before FINISH"); (b) EXTERNAL-COGNITIVE (an unmodifiable skill like `superpowers:brainstorming`) — "produced before" ≠ "consumed"; INJECT the artifact into the consumer's INPUT at the invocation boundary (orchestrator = adapter) + a RECONCILIATION gate that EXPOSES the disposition for review (not auto-detect); (c) INLINE-COGNITIVE (same agent reads then plans) — reader=planner, same context → natural consumption, just force the trace ([[LRN-053]]). Don't import (b)'s machinery where (c) suffices, nor assume (a)'s automatism when the consumer is cognitive.
|
||||
- **Context**: analyze-before-plan ([[BDR-035]]). ship-feature brainstorm = external-cognitive → STEP 0d injection + STEP 3 expose-for-review gate; feat/bugfix = inline-cognitive → natural + trace, no injection. The asymmetry vs [[BDR-034]] (mechanical merge) was the chantier's hardest point.
|
||||
- **Future application**: wiring ANY produce→consume invariant — classify the consumer first (mechanical / external-cognitive / inline-cognitive), pick the lightest sufficient mechanism. Stops reflexively importing orchestrator-grade injection+gate where an inline trace would do.
|
||||
- **Context**: analyze-before-plan ([[BDR-035]]). ship-feature brainstorm = external-cognitive → STEP 0d injection + STEP 3 expose-for-review gate; feat/bugfix = inline-cognitive → natural + trace, no injection. Asymmetry vs [[BDR-034]] (mechanical merge) = the chantier's hardest point.
|
||||
- **Future application**: wiring ANY produce→consume invariant: classify the consumer first (mechanical / external-cognitive / inline-cognitive), pick the lightest sufficient mechanism. Stops reflexive import of orchestrator-grade injection+gate where an inline trace would do.
|
||||
- **Reference**: `skills/ship-feature/SKILL.md` STEP 0d/1/2/3, `agents/bugfixer.md`+`feater.md`. Contrast [[BDR-034]] (mechanical). See [[BDR-035]], [[LRN-053]].
|
||||
|
||||
## LRN-058 — Same bug-class ≠ same fix: verify the twin shares the fix's PRECONDITION before replicating
|
||||
@@ -825,10 +862,9 @@ rules:
|
||||
- **Reference**: [[BDR-036]], [[LRN-051]] (changed-paths filter), [[LRN-046]].
|
||||
|
||||
## LRN-061 — Runtime net proposed for an unwired skill → check the wiring first
|
||||
|
||||
- **Date**: 2026-06-27
|
||||
- **Pattern**: Tempted to build a runtime guard/hook/monitor that watches for a bad OUTCOME (memory written but uncommitted)? First ask if the outcome is a MISSING WIRING, not a behavioral lapse. A per-turn Stop-hook was proposed to catch "dirty memory" — but the cause was `/capitalize`+`/close` not calling the commit include (they predate it). Fix for an unwired skill = WIRE it (deterministic, zero-noise, at source); a monitor over a wiring hole pays RECURRING cost to detect a ONE-TIME omission, and a frequent ignored nag is itself a risk ([[LRN-047]]). **NOT "runtime nets are bad"** — the split is by DETERMINISM: a MISSING WIRING is deterministic → repair structurally; a genuinely NON-DETERMINISTIC aléa → a runtime net IS the right tool. Good counter-example: [[BDR-033]] anim-lib nudge — "will the user want motion?" is unknowable statically → a stateless 1-line suggestion is correct. Same determinism test as [[LRN-046]]/[[LRN-049]], applied to the build-or-not question.
|
||||
- **Context**: deferred "v2 capitalize hook" ([[BDR-037]]). Read-phase killed it before code: git proved skills predate the include (oubli), memory already committed by hand 35×, orphans self-heal via `commit_memory`. The hook would've been disabled within an hour (frequent ignored nag).
|
||||
- **Pattern**: Tempted to build a runtime guard/hook/monitor that watches for a bad OUTCOME (memory written but uncommitted)? First ask if the outcome is a MISSING WIRING, not a behavioral lapse. A per-turn Stop-hook was proposed to catch "dirty memory" — but the cause was `/capitalize`+`/close` not calling the commit include (they predate it). Fix for an unwired skill = WIRE it (deterministic, zero-noise, at source); a monitor over a wiring hole pays RECURRING cost for a ONE-TIME omission; a frequent ignored nag is itself a risk ([[LRN-047]]). **NOT "runtime nets are bad"** — the split is by DETERMINISM: a MISSING WIRING is deterministic → repair structurally; a genuinely NON-DETERMINISTIC aléa → a runtime net IS the right tool. Good counter-example: [[BDR-033]] anim-lib nudge — "will the user want motion?" is unknowable statically → a stateless 1-line suggestion is correct. Same determinism test as [[LRN-046]]/[[LRN-049]], applied to the build-or-not question.
|
||||
- **Context**: deferred "v2 capitalize hook" ([[BDR-037]]). Read-phase killed it before code: git proved skills predate the include (oubli), memory committed by hand 35×, orphans self-heal via `commit_memory`. Hook would've been disabled within an hour (frequent ignored nag).
|
||||
- **Future application**: any "build a hook/watcher/lint to catch when X isn't done" — first grep whether X is even WIRED at its source. Deterministic/structural gap (missing include/call) → fix structurally; reserve runtime nets for non-deterministic lapses, never to complete a rollout. Classify by determinism BEFORE building.
|
||||
- **Reference**: [[BDR-037]], [[BDR-034]] (rollout this completes), [[BDR-033]] (the GOOD net — contrast). Conditions [[LRN-047]], [[LRN-049]], [[LRN-054]].
|
||||
|
||||
@@ -856,9 +892,9 @@ rules:
|
||||
- **future application**: any helper relying on `git status --porcelain` to detect changes — add a `git check-ignore` guard; a path that must persist but is ignored has to fail loud, not no-op.
|
||||
|
||||
## LRN-067 — a pipeline that looks 2-level can finish at the SAME level; a human-mediated step masks the collision until automated
|
||||
- **pattern**: an orchestrator delegating to a sub-skill can LOOK two-level (sub assembles parts, orchestrator integrates) yet the sub's TERMINAL node operates at the SAME level as the orchestrator's own finish → double-integration. `subagent-driven-development` assembles tasks on ONE branch (no per-task sub-branches — true) BUT its last flowchart node IS `finishing-a-development-branch` = feature→base merge, the SAME act as the orchestrator's FINISH. init-project (STEP 8 SDD + STEP 11 finish) AND ship-feature (STEP 4 SDD + STEP 9 finish) BOTH invoked finish TWICE. Latent, not visibly broken: SDD's terminal finish is INTERACTIVE (menu → human picks "keep as-is"), so the human SILENTLY de-duplicated. Collision SURFACES the moment the orchestrator's finish becomes DETERMINISTIC (gitflow finish) → real double-merge. Fix = scope the sub-skill by instruction to stop before its terminal step (NO fork — the finish is a flowchart node the controller follows, not a script; verified by reading SDD's scripts). Pressure-test: RED agent chained the finish ("literal next node in the flowchart"); GREEN with the scope instruction stopped + returned.
|
||||
- **context**: gitflow chantier, wiring orchestrators onto `gitflow finish`. Mapping (premise #6) caught it by READING the real (SDD `SKILL.md` + `scripts/`) BEFORE coding — the seam-bug class `deploy` hit, caught earlier this time. Two human-gate backstops survive a missed instruction: SDD's interactive menu + the `gitflow finish` human gate ([[LRN-054]] — no oracle; deterministic layer carries the dangerous case).
|
||||
- **future application**: before replacing an interactive/human-mediated step with a deterministic one, check whether a delegated sub-skill's TERMINAL step operates at the same level — the human gate may have been silently de-duplicating a double-action. Read the sub-skill's real flow (nodes + scripts), don't assume "distinct levels".
|
||||
- **pattern**: an orchestrator delegating to a sub-skill can LOOK two-level (sub assembles, orchestrator integrates) yet the sub's TERMINAL node operates at the SAME level as the orchestrator's finish → double-integration. `subagent-driven-development` assembles tasks on ONE branch (no per-task sub-branches — true) BUT its last flowchart node IS `finishing-a-development-branch` = feature→base merge, the SAME act as the orchestrator's FINISH. init-project (STEP 8 SDD + STEP 11 finish) AND ship-feature (STEP 4 SDD + STEP 9 finish) BOTH invoked finish TWICE. Latent, not visibly broken: SDD's terminal finish is INTERACTIVE (menu → human picks "keep as-is"), so the human SILENTLY de-duplicated. Collision SURFACES when the orchestrator's finish becomes DETERMINISTIC (gitflow finish) → real double-merge. Fix = scope the sub-skill by instruction to stop before its terminal step (NO fork — the finish is a flowchart node the controller follows, not a script; verified by reading SDD's scripts). Pressure-test: RED agent chained the finish ("literal next node in the flowchart"); GREEN with the scope instruction stopped + returned.
|
||||
- **context**: gitflow chantier, wiring orchestrators onto `gitflow finish`. Mapping (premise #6) caught it by READING the real (SDD `SKILL.md` + `scripts/`) BEFORE coding — seam-bug class `deploy` hit, caught earlier this time. Two human-gate backstops survive a missed instruction: SDD's interactive menu + the `gitflow finish` human gate ([[LRN-054]] — no oracle; deterministic layer carries the dangerous case).
|
||||
- **future application**: before replacing an interactive/human-mediated step with a deterministic one, check whether a delegated sub-skill's TERMINAL step operates at the same level — the human gate may have silently de-duplicated a double-action. Read the sub-skill's real flow (nodes + scripts), don't assume "distinct levels".
|
||||
|
||||
## LRN-068 — enforcement-bootstrap must be transactional: activate the guard LAST and gate it on the bootstrap commit succeeding
|
||||
- **pattern**: a routine that BOTH installs an enforcement guard (pre-commit hook, branch protection, lock) AND makes a bootstrap commit must be transactional, else a partial run strands it. Two teeth: (a) precheck preconditions (git identity, clean tree) and fail LOUD before ANY mutation; (b) the guard-activation step must NOT run if the guarded bootstrap commit failed — order activation LAST and gate it on commit success. A `cmd_a || cmd_b` form SWALLOWS cmd_b's failure when a later stmt returns 0 → the failure never propagates; use explicit `if ! …; then … || return 1; fi`.
|
||||
@@ -882,9 +918,9 @@ rules:
|
||||
- **future application**: any helper whose RETURN VALUE gates a downstream "success" — audit that EVERY fallible internal op propagates its failure, ESPECIALLY the load-bearing commit. `set -uo pipefail` without `-e` does NOT abort mid-function; an unchecked failing command followed by a returning-0 line exits 0 and lies. Check `cmd || other` forms, no-`-e` blocks, every "report success after the op" line. Test the partial-failure path (commit-blocked repo) → must fail loud, empty, non-zero.
|
||||
|
||||
## LRN-072 — a stranded-artifact bug can be fixed by NOT creating the artifact (negative diff), not by plumbing its commit
|
||||
- **pattern**: 3rd member of the post-FINISH-artifact class (memory, docs, GSD ROADMAP) — but UNLIKE the first two (real artifacts ALWAYS produced → couple a commit), the GSD artifact came from a SPECULATIVE, opt-in, rarely-used producer (init-project auto-bootstrapping a multi-session engine at project creation). 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]].
|
||||
- **pattern**: 3rd member of the post-FINISH-artifact class (memory, docs, GSD ROADMAP) — but UNLIKE the first two (real artifacts ALWAYS produced → couple a commit), the GSD artifact came from a SPECULATIVE, opt-in, rarely-used producer (init-project auto-bootstrapping a multi-session engine at project creation). Reflex fix (reorder + build `gsd-commit.sh` + tests) = machinery to faithfully commit an artifact nobody uses. The right fix was a NEGATIVE diff: delete the producer → orphan never created → bug dissolves, zero new code (BLK-011).
|
||||
- **the refutation that got there**: framing "ROADMAP redundant with TODO" WRONG (gsd ≫ roadmap = state machine/crash-recovery/cost/parallel/worktree; TODO ≠ gsd ROADMAP = different altitude + consumer). Reading REFUTED both premises, yet the CONCLUSION (remove the step) held for a STRONGER reason: speculatively scaffolding a heavy engine the sole user doesn't use, at creation, is bad per se. Right answer, reason corrected before engraving — change the QUESTION before changing the code.
|
||||
- **future application**: stranded / duplicated / uncommitted-artifact bug → BEFORE building machinery for the artifact, ask whether the step that PRODUCES it is used / wanted / non-speculative. Speculative or unused (esp. personal/single-user repo) → DELETE the producer; cleanest fix = the absent one. Distinguish speculative-at-creation (REMOVE) from deliberate-on-demand (KEEP). Family: [[BLK-010]], [[BLK-011]], [[BDR-036]].
|
||||
|
||||
## 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".
|
||||
@@ -910,15 +946,15 @@ rules:
|
||||
## LRN-077 — test fixtures must carry NEUTRAL names (pass for the right reason)
|
||||
- **Date**: 2026-06-30
|
||||
- **pattern**: a baseline agent on a worktree named `wt-pre-reconcile` read "pre-reconcile" FROM THE DIR NAME and inferred staleness — reasoning for the WRONG reason (the name), not the right one (verify git). Fixtures + the GREEN test were re-frozen under NEUTRAL names so the engine reaches truth by querying git, never by reading a path hint.
|
||||
- **meta — same symptom, distinct cause as [[LRN-074]]**: 074 = 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).
|
||||
- **meta — same symptom, distinct cause as [[LRN-074]]**: 074 = COMMAND-ASSUMPTION (ugrep parsed `-9..` → false green); 077 = LEAKY FIXTURE (name telegraphs the answer). Different mechanisms, SAME symptom: test passes/fails for the wrong reason. Cross-cutting lesson = verify a test passes for the RIGHT reason, not merely that it passes — whether the false signal comes from an assumed command (074) or a leaky fixture (077).
|
||||
- **future application**: name fixtures/paths neutrally; for any green, ask "did it pass because the subject did the work, or because something leaked the answer?"
|
||||
- **corroboration 2026-07-02 (T6c)**: 3rd family member — test truth borrowed from TRANSIENT env state. run-reconcile T6c asserted `$MEM/../skills/darwin-skill` = `.claude/skills/` (the [[LRN-042]] parasite dir), not canonical `skills/`; born green because the parasite still existed, red since the same-day cleanup, unnoticed until the 2026-07-02 audit re-ran the suite ([[EVAL-011]]'s "20/20" silently 19/1 for 2 days). Oracles target CANONICAL paths (never derived `X/../Y`); re-run suites after ANY env cleanup tests may have silently depended on; "green at build" ≠ "green now".
|
||||
|
||||
## LRN-078 — semver number DERIVES from the change nature; "breaking" = requires a migration
|
||||
- **Date**: 2026-06-30
|
||||
- **pattern**: framing a release as "it's 4.0.0 → find the breaking changes to justify it" is backwards. 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.
|
||||
- **pattern**: framing a release as "it's 4.0.0 → find the breaking changes to justify it" is backwards; the number FOLLOWS the nature of the changes. The real question = "is there a breaking change?", not "how do I justify the target". Solo / mono-user repo, no public API ⇒ "breaking" = casse mon propre usage / EXIGE une migration de ma part.
|
||||
- **applied (v4.0.0)**: gitflow universal = TRUE breaking workflow change (master→main, mandatory branches, hook, 6-repo migration) → MAJOR on its own. caveman removal = VERIFIED nothing invoked it (grep: only the kept memory format-rule + frozen fixtures, settings/hooks clean) → a clean `### Removed` (capability gone, nothing breaks, no migration), NOT breaking. The MAJOR rests on gitflow alone; don't mislabel a removal as breaking.
|
||||
- **future application**: pick MAJOR/MINOR/PATCH from the changes; the lineage gives the digits. Verify "does X actually break / require migration?" from the refs (grep), not from the size of the change or the desire for a round number.
|
||||
|
||||
## LRN-079 — orchestrator-skill TDD: replay the flow on a throwaway repo, RED = flow minus the new step
|
||||
- **Date**: 2026-06-30
|
||||
@@ -949,16 +985,15 @@ rules:
|
||||
|
||||
## LRN-083 — Subagents are an INVALID instrument for measuring MAIN-LOOP spontaneous routing
|
||||
- **Date**: 2026-06-30
|
||||
- **pattern**: to measure whether the MAIN loop self-invokes a skill on implicit intent, dispatched subagents are non-discriminating — SUBAGENT-STOP tells them to SKIP the L1 routing mandate, and a delegated-execute framing suppresses meta-routing → they hand-do the task regardless of how strong/weak the main-loop prose is. Result pins to the no-route FLOOR (artifact, not signal). Complement of [[LRN-028]] (there subagents OVER-saw installed skills, invalidating a no-skill baseline; here they UNDER-route, invalidating a routing-measurement) — both = subagent ≠ main-loop condition.
|
||||
- **why it matters**: a 0/N subagent RED reads as "under-triggers → build the chantier" but is the [[LRN-028]] trap — the instrument can't tell strong prose from weak. Concluding from it = a pass/fail for the WRONG reason ([[LRN-074]]/[[LRN-077]]).
|
||||
- **context**: 2026-06-30 auto-skill-dispatch RED. 6 subagents on toy implicit-intent tasks → 0/6 routed → RETIRED as non-discriminating, NOT reported as a number. Reframed; measured instead in REAL fresh main-loop sessions.
|
||||
- **pattern**: measuring whether the MAIN loop self-invokes a skill on implicit intent: dispatched subagents are non-discriminating — SUBAGENT-STOP tells them to SKIP the L1 routing mandate, delegated-execute framing suppresses meta-routing → they hand-do the task regardless of main-loop prose strength. Result pins to the no-route FLOOR (artifact, not signal). Complement of [[LRN-028]] (there subagents OVER-saw installed skills, invalidating a no-skill baseline; here they UNDER-route, invalidating a routing-measurement) — both = subagent ≠ main-loop condition.
|
||||
- **why it matters**: a 0/N subagent RED reads as "under-triggers → build the chantier" but is the [[LRN-028]] trap — the instrument can't tell strong prose from weak. Concluding from it = pass/fail for the WRONG reason ([[LRN-074]]/[[LRN-077]]).
|
||||
- **context**: 2026-06-30 auto-skill-dispatch RED. 6 subagents on toy implicit-intent tasks → 0/6 routed → RETIRED as non-discriminating, NOT reported as a number. Reframed; measured in REAL fresh main-loop sessions.
|
||||
- **future application**: measure main-loop spontaneous routing/discernment in FRESH main-loop sessions (full L0–L4, no SUBAGENT-STOP, real user-turn). Observable instrument = the HUMAN typing the prompts + watching live — cron/schedule-spawned fresh sessions are the right CONDITION but UNOBSERVABLE to the orchestrator (they notify the owner, not the dispatcher), so they can't be the measurement vehicle. Never substitute a subagent for a fresh session in a routing RED. See [[LRN-028]], [[LRN-075]], [[LRN-080]].
|
||||
|
||||
## LRN-084 — A protection hook enforces PROD safety, not the full branch-flow — the exemption masked the rule-vs-guard divergence
|
||||
|
||||
- **Date**: 2026-07-01
|
||||
- **pattern**: the gitflow pre-commit hook is a PROTECTION guard (block code on main/develop), NOT a flow enforcer. It exempts `.claude/**` and can only test "on a protected base" — it can NEVER verify "branched FROM develop" (no base knowledge). So "every change via a branch from develop" is only HALF-encoded by the hook; the base half lives solely upstream in `gitflow_start`. The exemption is scoped to the SIDE-CAR ([[BDR-034]]); it has no branch to follow when memory IS the work → standalone memory fell back to `main`.
|
||||
- **why it matters**: a multi-repo raccord committed 5 `chore(memory)` direct on `main` and NOTHING flagged it — nothing was violated, the exemption worked as designed. The divergence was guard (declares PROD protection) vs intended rule (all via branch); the exemption MASKED it, the raccord revealed it by violating the unencoded half. A guard encoding only PART of the intent reads as full enforcement — a false-green.
|
||||
- **pattern**: the gitflow pre-commit hook is a PROTECTION guard (block code on main/develop), NOT a flow enforcer. It exempts `.claude/**` and can only test "on a protected base" — it can NEVER verify "branched FROM develop" (no base knowledge). "Every change via a branch from develop" is only HALF-encoded by the hook; the base half lives upstream in `gitflow_start`. The exemption is scoped to the SIDE-CAR ([[BDR-034]]); it has no branch to follow when memory IS the work → standalone memory fell back to `main`.
|
||||
- **why it matters**: multi-repo raccord committed 5 `chore(memory)` direct on `main`, NOTHING flagged it — nothing violated, exemption worked as designed. Divergence = guard (declares PROD protection) vs intended rule (all via branch); exemption MASKED it, raccord revealed it by violating the unencoded half. A guard encoding only PART of the intent reads as full enforcement — a false-green.
|
||||
- **future application**: when a guard exempts a class or checks one predicate, ask what it does NOT encode and whether a human leans on it for MORE than it enforces. Enforce the unencoded half where it actually lives (the aiguillage at skill start, [[BDR-045]]), do not push it into a guard that structurally can't hold it. Verify the guard's real scope against the rule's full scope before trusting "it would have caught it." See [[BDR-034]], [[BDR-045]], [[LRN-034]].
|
||||
|
||||
---
|
||||
@@ -1036,16 +1071,16 @@ rules:
|
||||
- **cousin**: [[LRN-047]] noisy gate = ignored; [[LRN-077]] non-deterministic gate; conditions [[BDR-048]].
|
||||
|
||||
## LRN-095 — Orthogonal gates don't contaminate: a conformity check must pass correct-but-insecure code
|
||||
- **pattern**: when a pipeline has distinct gates (request-conformity, security), each judges ONLY its dimension. A conformity verifier must return CONFORME on code that is correct-but-insecure — the vuln is the SECURITY gate's job, not a conformity gap. Proven live: a `get_item` feature satisfying its contract but carrying a `%`-interpolation SQLi → verifier CONFORME, security-auditor BLOCK(1). Fusing the two into one "quality" gate makes each worse: the conformity check starts hunting vulns (scope creep, misses conformity), the security check starts judging feature-completeness (dilutes).
|
||||
- **context**: lot 4 verify-secure-loop dogfood 2026-07-03. The orthogonality is WHY the order invariant matters (re-verify request before re-scan security) — two independent axes re-checked independently.
|
||||
- **pattern**: pipeline with distinct gates (request-conformity, security): each judges ONLY its dimension. A conformity verifier must return CONFORME on code that is correct-but-insecure — the vuln is the SECURITY gate's job, not a conformity gap. Proven live: `get_item` feature satisfying its contract with a `%`-interpolation SQLi → verifier CONFORME, security-auditor BLOCK(1). Fusing both into one "quality" gate makes each worse: conformity check hunts vulns (scope creep, misses conformity), security check judges feature-completeness (dilutes).
|
||||
- **context**: lot 4 verify-secure-loop dogfood 2026-07-03. Orthogonality is WHY the order invariant matters (re-verify request before re-scan security): two independent axes re-checked independently.
|
||||
- **future application**: any multi-dimension gate (review lenses, verify+audit, correctness+perf) — keep each gate single-axis and let a finding on axis B pass axis A's gate; compose verdicts in the orchestrator, don't merge the judges.
|
||||
- **cousin**: [[BDR-050]] the pipeline; [[BDR-049]] fresh verifier; conditions [[LRN-083]].
|
||||
|
||||
## LRN-096 — A backstop is code: prove it can FAIL (flip-test) before trusting its green
|
||||
- **pattern**: a deterministic guard built to replace a forgettable advisory is itself code, and an UNPROVEN guard is a vacuous guard — [[LRN-048]] (a pass must prove it looked) applied to guards themselves. The LRN-093 backstop (refuse `\n` in grep/tf patterns) shipped with a regex requiring whitespace before `tf` → it silently MISSED `tf` at line start (exactly where the real locks sit). A flip-test (feed the guard a KNOWN offender, assert it bites) caught the hole; without it the guard would have green-lit the very class it was built to kill. So: a flip-test is MANDATORY at guard creation, part of the guard, not optional QA.
|
||||
- **why it matters**: the whole point of a backstop is that it fires on the bad case; a guard that can't fail proves nothing and is WORSE than the advisory it replaced (false confidence). The advisory→backstop move ([[LRN-047]] [[LRN-091]], own doctrine) is only sound if the backstop is itself verified against a real miss.
|
||||
- **context**: lot 5 `lib/tests/no-vacuous-locks.test.sh` 2026-07-04. Built the guard, its flip-test RED'd (regex too weak, missed line-start `tf`), fixed the regex, flip-test green. The guard now ships WITH the flip-test inline so it self-proves on every run.
|
||||
- **future application**: building any guard/lint/census/backstop — bundle a flip-test (a synthetic offender the guard must catch) in the same file; a guard whose failure path was never exercised is untrusted. Corroborates [[LRN-047]]/[[LRN-091]] (advisory→deterministic) — this is the *quality bar* on the deterministic replacement.
|
||||
- **pattern**: deterministic guard replacing a forgettable advisory is itself code; UNPROVEN guard = vacuous guard — [[LRN-048]] (a pass must prove it looked) applied to guards. LRN-093 backstop (refuse `\n` in grep/tf patterns) shipped with a regex requiring whitespace before `tf` → silently MISSED `tf` at line start (where the real locks sit). Flip-test (feed the guard a KNOWN offender, assert it bites) caught the hole; without it the guard would have green-lit the very class it was built to kill. So: a flip-test is MANDATORY at guard creation, part of the guard, not optional QA.
|
||||
- **why it matters**: the whole point of a backstop is that it fires on the bad case; a guard that can't fail proves nothing and is WORSE than the advisory it replaced (false confidence). Advisory→backstop move ([[LRN-047]] [[LRN-091]]) is sound only if the backstop is verified against a real miss.
|
||||
- **context**: lot 5 `lib/tests/no-vacuous-locks.test.sh` 2026-07-04. Built the guard, flip-test RED'd (regex too weak, missed line-start `tf`), fixed the regex, flip-test green. Guard ships WITH the flip-test inline, self-proves on every run.
|
||||
- **future application**: building any guard/lint/census/backstop — bundle a flip-test (a synthetic offender the guard must catch) in the same file; a guard whose failure path was never exercised is untrusted. Corroborates [[LRN-047]]/[[LRN-091]] (advisory→deterministic): the *quality bar* on the deterministic replacement.
|
||||
- **cousin**: [[LRN-048]] prove it looked; [[LRN-093]] the class this guards; [[LRN-046]] deterministic-oracle discipline.
|
||||
|
||||
## LRN-097 — Community blog pattern ≠ official feature: verify against docs before building infra
|
||||
@@ -1089,9 +1124,8 @@ rules:
|
||||
- **backmerge**: from release/1.0.0 (74d3804) — 2026-07-08 review remediation A3.
|
||||
|
||||
## LRN-102 — Deliverable text before a tool call may never render: the turn's FINAL text is the only guaranteed display
|
||||
|
||||
- **pattern**: /deploy hand-back printed the full checklist in the assistant message, then called AskUserQuestion. The user saw ONLY the question UI — the checklist never reached them ("là on a rien, je dois ouvrir le fichier"). The harness renders reliably only the LAST text of a turn; text between/before tool calls can be swallowed by the tool UI.
|
||||
- **why**: a skill whose deliverable is conversational (commands to copy-paste, a report) fails silently if any tool call follows the print — the user experiences "nothing displayed" while the transcript technically contains it. Structural fix: the deliverable IS the turn's final text; collect answers BEFORE printing, or let the reply arrive as the next user message.
|
||||
- **pattern**: /deploy hand-back printed the full checklist, then called AskUserQuestion. The user saw ONLY the question UI — the checklist never reached them ("là on a rien, je dois ouvrir le fichier"). Harness reliably renders only the LAST text of a turn; text before a tool call can be swallowed by the tool UI.
|
||||
- **why**: conversational deliverable (commands to copy-paste, a report) fails silently if any tool call follows the print — user sees "nothing displayed" while the transcript contains it. Structural fix: the deliverable IS the turn's final text; collect answers BEFORE printing, or let the reply arrive as the next user message.
|
||||
- **context**: 2026-07-05 /deploy run 2 (bchanot-cv). Skill patched same turn: checklist display-only (no NEXT.sh file at all — user: throwaway once deployed) + hand-back ends the turn, no tool call after.
|
||||
- **future application**: designing any skill/flow output meant to be read+used from the conversation — put it LAST; never sandwich a deliverable between tool calls; prefer plain-text report requests over blocking question tools after a deliverable.
|
||||
- **cousin**: [[LRN-100]] same skill lineage; CLAUDE.md communication doctrine (final message carries everything).
|
||||
@@ -1153,10 +1187,9 @@ rules:
|
||||
- **cousin**: [[BDR-058]] (this job's fix), darwin-skill's OVERSCOPED git-commit finding (job8 report — 3rd-party code, not patched, accepted risk under human-checkpoint gating, twin of [[LRN-105]]'s no-execute mandate for OUR read-only audits).
|
||||
|
||||
## LRN-110 — magic MCP `component_builder`'s local callback server = unauthenticated prompt-injection channel
|
||||
|
||||
- **context**: job8 audit read `dist/utils/callback-server.js:36` (+ `create-ui.js:35-38`) in the installed `@21st-dev/magic` package. `21st_magic_component_builder` opens a plain HTTP server on `127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin check, staying open up to 10 minutes per call. Whatever body a POST to `/data` carries gets injected VERBATIM into the tool result the model then consumes — any local process or an open browser tab on the same machine can win the race against the legitimate browser hand-back.
|
||||
- **context**: job8 audit read `dist/utils/callback-server.js:36` (+ `create-ui.js:35-38`) in the installed `@21st-dev/magic` package. `21st_magic_component_builder` opens a plain HTTP server on `127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin check, staying open up to 10 minutes per call. Any POST body to `/data` is injected VERBATIM into the tool result the model consumes — any local process or open browser tab on the machine can win the race against the legitimate browser hand-back.
|
||||
- **future application**: this is in the third-party package's code, not our config — don't try to patch a vendored/npx-installed dependency. The only real lever is on OUR side of the boundary: never allowlist a tool with this shape, keep it `ask`-gated so a human sees every invocation (see [[BDR-059]]). Applies to any MCP tool whose implementation opens a listener to receive async results, not just this one — check the listener's auth/origin scoping when auditing MCP server code, the tool's *description* text tells you nothing about it.
|
||||
- **cousin**: [[BDR-059]] (the settings fix), [[LRN-111]] (why the allowlist stays empty), job8 report §2 surface 1 finding A#0.
|
||||
- **cousin**: [[BDR-059]] (the settings fix), [[LRN-111]] (why the allowlist stays empty), job8 report §2 surface 1 finding A#0. Magic MCP retired 2026-09-22 ([[BDR-093]]).
|
||||
|
||||
## LRN-111 — empty allowlist is a valid, deliberate posture when real usage is zero, not a leftover gap
|
||||
|
||||
@@ -1225,9 +1258,9 @@ rules:
|
||||
- **cousin**: [[LRN-119]] (same GSC+CrUX build); SDD skill's own "never HEAD~1" warning (same base-selection bug class).
|
||||
|
||||
## LRN-121 — Shell allowlist validation: `grep -Eq` is fragile; use a whole-string POSIX `case`
|
||||
- **pattern**: guarding a user-supplied label to shell-safe ASCII with `printf '%s' "$v" | grep -Eq '^[A-Za-z0-9._-]+$'` failed 3 adversarial gate passes in a row: (1) command-injection framing (label interpolated into an agent-composed Bash line); (2) parser differential — the guard pre-scanned argv for the literal token `--label` while the downstream `argparse` ALSO accepts `--label=v` and abbreviations (`--labe`, `allow_abbrev=True`), so those forms reached the parser unchecked; (3) `grep -q` matches PER LINE, so a label with an embedded newline (`ok\nrm -rf`) passes because its FIRST line matches. Fix = replace the whole mechanism, don't patch again: `_label_safe() ( LC_ALL=C; case "$1" in ''|[!A-Za-z0-9]*|*[!A-Za-z0-9._-]*) exit 1;; esac )` — POSIX `case`, whole-string, C-locale subshell. No grep (no per-line), no regex, no second grammar to differ from; a newline is just a non-allowed byte caught by `*[!...]*`; `LC_ALL=C` stops UTF-8 collation widening `[A-Za-z0-9]` to homoglyphs (U+FF11, Kelvin U+212A).
|
||||
- **pattern**: guarding a user-supplied label to shell-safe ASCII with `printf '%s' "$v" | grep -Eq '^[A-Za-z0-9._-]+$'` failed 3 adversarial gate passes: (1) command-injection framing (label interpolated into an agent-composed Bash line); (2) parser differential — the guard pre-scanned argv for the literal `--label` while the downstream `argparse` ALSO accepts `--label=v` and abbreviations (`--labe`, `allow_abbrev=True`), those forms reached the parser unchecked; (3) `grep -q` matches PER LINE, a label with an embedded newline (`ok\nrm -rf`) passes on its FIRST line. Fix = replace the whole mechanism, don't patch again: `_label_safe() ( LC_ALL=C; case "$1" in ''|[!A-Za-z0-9]*|*[!A-Za-z0-9._-]*) exit 1;; esac )` — POSIX `case`, whole-string, C-locale subshell. No grep (no per-line), no regex, no second grammar to differ from; a newline is just a non-allowed byte caught by `*[!...]*`; `LC_ALL=C` stops UTF-8 collation widening `[A-Za-z0-9]` to homoglyphs (U+FF11, Kelvin U+212A).
|
||||
- **why it matters**: three distinct bypasses of the SAME guard = the approach was wrong, not each patch. `grep`'s line-orientation + locale-sensitive ranges, plus argv-prescan-vs-real-parser grammar drift, are the three classic ways an allowlist "passes" a string it shouldn't. Whole-string `case` in C locale closes all three at once. These were defense-in-depth (downstream used `"$2"`/`"$@"`/JSON-key, never `sh -c`/`eval` → not exploitable in the real exec chain) — but the backstop still took a categorical rewrite, and 3 security-gate BLOCKs to get there.
|
||||
- **future application**: validate shell input WHOLE-STRING (`case` or bash `[[ =~ ]]`), never `grep -q` (per-line). Set `LC_ALL=C` for byte-wise ranges. A guard that pre-scans argv must be STRICTER than the downstream parser (reject `=`-joined/abbrev) or validate post-parse against the value the parser settled on. When a fix is bypassed twice → STOP patching, replace the mechanism (re-plan, not whack-a-mole).
|
||||
- **future application**: validate shell input WHOLE-STRING (`case` or bash `[[ =~ ]]`), never `grep -q` (per-line). Set `LC_ALL=C` for byte-wise ranges. Argv pre-scan guard must be STRICTER than the downstream parser (reject `=`-joined/abbrev) or validate post-parse against the value the parser settled on. When a fix is bypassed twice → STOP patching, replace the mechanism (re-plan, not whack-a-mole).
|
||||
- **cousin**: [[LRN-119]] (fail-open engine this hardens), [[BDR-063]] (token store whose labels these guard), [[LRN-045]] (renaming-command leak-guard regexes — same charset-guard family).
|
||||
|
||||
---
|
||||
@@ -1265,10 +1298,9 @@ rules:
|
||||
- **cousin**: [[BDR-066]] (model routing: reflection/audit big, execution sonnet), [[LRN-113]] (consumer-staleness sweep on a pattern fix).
|
||||
|
||||
## LRN-126 — splitting a monolith agent severs every IMPLICIT data path; forward each consumed field through the handoff contract
|
||||
|
||||
- **pattern**: wave-4 redaction-only split (client-handover-writer monolith → reflection-parent + sonnet doc-writer child) silently dropped 2 inputs the extracted STEPs consumed. `DEPLOY_HINTS` (detected in parent STEP 2, consumed by child STEP 14) + `--skip-seo` flag (parsed from `$ARGUMENTS`, gated child STEP 13) worked in the monolith by shared scope; after the split they were dead — never added to the PACKAGE. Child rendered a §8 without platform tailoring; `--skip-seo` became a silent no-op. Caught only by the opus whole-branch review, not the census.
|
||||
- **why**: in a monolith, `$ARGUMENTS`, detected vars, and STEP-N side-outputs are all in one scope — a later STEP reads them for free. The split turns that free read into a data path that MUST cross the parent→child contract explicitly. Every implicit read becomes a severed wire unless forwarded.
|
||||
- **future application**: when splitting an agent, enumerate EVERY field the child reads (grep child for its input vocabulary — `PACKAGE.`, bare var names, `$ARGUMENTS` flags) and diff against what the parent SETS before dispatch. Any child-consumed field the parent never populates = severed path = renders a hole or a silent no-op. A census that checks shape (model pin, gate-free) will NOT catch this — needs a data-flow read.
|
||||
- **pattern**: wave-4 split (client-handover-writer monolith → reflection parent + sonnet doc-writer child) silently dropped 2 inputs the extracted STEPs consumed. `DEPLOY_HINTS` (detected in parent STEP 2, consumed by child STEP 14) + `--skip-seo` flag (parsed from `$ARGUMENTS`, gated child STEP 13) worked in the monolith by shared scope; after the split they were dead — never added to the PACKAGE. Child rendered a §8 without platform tailoring; `--skip-seo` became a silent no-op. Caught only by the opus whole-branch review, not the census.
|
||||
- **why**: monolith: `$ARGUMENTS`, detected vars, STEP-N side-outputs share one scope, later STEPs read them free. Split turns each free read into a data path that MUST cross the parent→child contract explicitly; every implicit read is a severed wire unless forwarded.
|
||||
- **future application**: splitting an agent: enumerate EVERY field the child reads (grep child for `PACKAGE.`, bare var names, `$ARGUMENTS` flags), diff against what the parent SETS before dispatch. Any child-consumed field the parent never populates = severed path = renders a hole or a silent no-op. A census that checks shape (model pin, gate-free) will NOT catch this — needs a data-flow read.
|
||||
- **cousin**: [[LRN-125]] (route consumer to right tier on a split), [[BDR-066]] (reflection/execution split), [[LRN-113]] (sweep ALL consumers). Distinct: 113/125 = WHICH agent/tier a consumer routes to; this = WHICH fields must cross the contract.
|
||||
|
||||
## LRN-127 — SDD implementers must not run destructive git ops on files outside their task scope
|
||||
@@ -1315,42 +1347,16 @@ rules:
|
||||
- **future**: the system already HAD the invariant (code-ceiling, §14 Annexe) but applied it in spots. Generalised it. A false signal is worse than a declared gap — the 4 features KILLED at measurement (B1/B2/B3/W2) beat 4 false-signal features. See [[LRN-131]]/[[LRN-132]] (same session, the verification discipline that feeds it).
|
||||
|
||||
## LRN-134 — resolve-then-pin in stdlib beats monkeypatching getaddrinfo — 2026-07-17
|
||||
- **pattern**: to close SSRF/DNS-rebinding on Python HTTP egress, resolve the
|
||||
host ONCE, validate every returned IP (`ipaddress`, dual-stack v4+v6), refuse
|
||||
if ANY is non-public (the multi-A vector), then connect to the exact pinned IP
|
||||
via an `http.client.HTTPSConnection` subclass whose `connect()` does
|
||||
`create_connection((pinned_ip, port))` and `wrap_socket(sock,
|
||||
server_hostname=real_host)` — SNI + cert stay bound to the real host. No
|
||||
second resolution to poison. `safe_fetch.py`.
|
||||
- **context**: the load-bearing property — classify the IP the OS RESOLVED
|
||||
(`sockaddr[0]`), NEVER the URL text. That defeats octal/hex/decimal literals,
|
||||
IPv4-mapped IPv6, NAT64, 6to4 structurally, not by enumeration (confirmed by
|
||||
the security review's fuzz). `is_global` is the decisive gate (catches CGNAT
|
||||
100.64/10 the per-flags miss); add a small extra-deny for special-use ranges
|
||||
it passes (192.88.99.0/24 6to4-relay). Redirects: re-validate EACH hop —
|
||||
urlopen followed them blind.
|
||||
- **future**: beats claude-seo url_safety.py on 3 axes — dual-stack (theirs
|
||||
IPv4-only), thread-safe by construction (theirs monkeypatches getaddrinfo
|
||||
behind a global lock), stdlib-only (theirs `requests`). A name-level guard
|
||||
(url-guard.sh) cannot see a rebind; this is the layer that can. Shell `curl`
|
||||
stays unpinnable from here → `curl --resolve`, separate.
|
||||
- **pattern**: close SSRF/DNS-rebinding on Python HTTP egress: resolve the host ONCE, validate every returned IP (`ipaddress`, dual-stack v4+v6), refuse if ANY is non-public (the multi-A vector), connect to the exact pinned IP via an `http.client.HTTPSConnection` subclass whose `connect()` does `create_connection((pinned_ip, port))` + `wrap_socket(sock, server_hostname=real_host)` — SNI + cert stay bound to the real host. No second resolution to poison. `safe_fetch.py`.
|
||||
- **context**: the load-bearing property — classify the IP the OS RESOLVED (`sockaddr[0]`), NEVER the URL text. That defeats octal/hex/decimal literals, IPv4-mapped IPv6, NAT64, 6to4 structurally, not by enumeration (confirmed by the security review's fuzz). `is_global` = decisive gate (catches CGNAT 100.64/10 the per-flags miss); small extra-deny for special-use ranges it passes (192.88.99.0/24 6to4-relay). Redirects: re-validate EACH hop — urlopen followed them blind.
|
||||
- **future**: beats claude-seo url_safety.py on 3 axes: dual-stack (theirs IPv4-only), thread-safe by construction (theirs monkeypatches getaddrinfo behind a global lock), stdlib-only (theirs `requests`). A name-level guard (url-guard.sh) cannot see a rebind; this is the layer that can. Shell `curl` stays unpinnable from here → `curl --resolve`, separate.
|
||||
|
||||
## LRN-135 — a prefix-only scan for a dangerous construct is bypassable by padding — 2026-07-17
|
||||
- **pattern**: to refuse a hostile construct (DTD, directive, marker) before
|
||||
parsing, scan the WHOLE document, never a bounded prefix.
|
||||
- **context**: `_refuse_dtd` (C1b) scanned only `raw[:4096]` → a sitemap with
|
||||
>4 KB of leading comment pushed `<!DOCTYPE` past the window while
|
||||
`ET.fromstring` still parsed AND EXPANDED the entities (`&lol2;` →
|
||||
"lollollollollol", proven). Billion-laughs reopened on my own already-merged
|
||||
code. Found by the security review of the rebinding diff, not by me — fixed
|
||||
there rather than filed (root-cause discipline).
|
||||
- **future**: over ≤20 MB a full `re.search` is microseconds — no perf excuse
|
||||
for a bounded scan. Corollary of [[LRN-133]]: if you refuse a construct,
|
||||
refuse it EVERYWHERE, not just where you look first. A fresh adversarial
|
||||
reviewer attacking diff A routinely surfaces a real hole in already-shipped
|
||||
code B — see [[EVAL-020]].
|
||||
- **pattern**: to refuse a hostile construct (DTD, directive, marker) before parsing, scan the WHOLE document, never a bounded prefix.
|
||||
- **context**: `_refuse_dtd` (C1b) scanned only `raw[:4096]` → sitemap with >4 KB of leading comment pushed `<!DOCTYPE` past the window while `ET.fromstring` parsed AND EXPANDED the entities (`&lol2;` → "lollollollollol", proven). Billion-laughs reopened on my own already-merged code. Found by the security review of the rebinding diff, not by me — fixed there rather than filed (root-cause discipline).
|
||||
- **future**: over ≤20 MB a full `re.search` is microseconds — no perf excuse for a bounded scan. Corollary of [[LRN-133]]: if you refuse a construct, refuse it EVERYWHERE, not just where you look first. Fresh adversarial reviewer attacking diff A routinely surfaces a real hole in already-shipped code B — see [[EVAL-020]].
|
||||
|
||||
### LRN-136 — config-protection live state follows checked-out branch's symlinked settings.json (2026-07-17)
|
||||
## LRN-136 — config-protection live state follows checked-out branch's symlinked settings.json (2026-07-17)
|
||||
~/.claude/settings.json is a SYMLINK to the repo settings.json; Claude Code hot-reloads settings on change → the config-protection PreToolUse hook's active/inactive state tracks the CURRENT branch's settings.json. On feature/drop-config-protection (hook deregistered) a protected edit passed silently, sentinel unconsumed; after gitflow-switch to a branch off develop (hook still registered) the SAME class of edit was blocked. Apply: a change that removes a settings-registered hook is live only on that branch until merged; use the one-shot sentinel for protected edits on any branch that still registers it. ([[BDR-074]] context.)
|
||||
|
||||
## LRN-137 — mode-based re-tiering beats file splits for mixed-tier agents
|
||||
@@ -1527,3 +1533,48 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
- **Pattern**: (a) code-only build 2.3 s, 0 tokens, 3141 nodes / 7241 edges / 199 communities; hubs correct without any LLM (Auth, Database, Router, PDO, PHPMailer). Incremental update 1.9 s. `graphify-out/` = 8 MB (graph.json 4.4 + graph.html 3.5) → gitignore it. (b) one `graphify query` ≈ 2000-3000 tokens (default budget 2000, over-budget answers spill; truncation at 70/245 nodes on broad questions) = the price of two file reads; it maps (name, file:line), it does not replace reading the file you edit. Value = localisation, not editing. (c) it indexed `.claude/` (contracts, PROCEDURE.md, registries): an "authentication" query surfaced a mobile-nav contract → `.graphifyignore` (`.claude/`, `docs/superpowers/`; gitignore semantics, can only exclude more). (d) 13 `.sql` files contributed nothing: `tree_sitter_sql` missing → `pipx inject graphifyy "graphifyy[sql]"`. (e) `update` refuses to write a graph with FEWER nodes unless `--force`/`GRAPHIFY_FORCE=1` → after a refactor that deletes code, an automated update goes stale silently. (f) `graphify hook install` targets the repo's hooks dir; under our global `core.hooksPath` it is inert → our generated post-commit hook is the only integration point. (g) `graphify claude install` = PreToolUse nudges on every Read/Glob + CLAUDE.md rewrite — the context tax itself. (h) the semantic pass (docs/papers) runs on the host agent = session tokens; code-only stays free.
|
||||
- **Future application**: measure a "context saver" before adopting it — build time, artifact size, tokens per use, noise sources. Threshold rule [[BDR-097]]: propose from 200 tracked code files, never below. Pilot recipe when the user says go: `graphify update .` + `.graphifyignore` + gitignore `graphify-out/` + `GRAPHIFY_FORCE=1 graphify update .` in the post-commit hook, guarded by `[ -f graphify-out/graph.json ]`.
|
||||
- **Reference**: [[BDR-097]], [[BDR-028]], `lib/graphify-gate.sh`, graphify 0.9.65 (`detect.py` `_SKIP_DIRS`, `.graphifyignore`; `hooks.py` core.hooksPath handling; `__main__.py` PreToolUse nudge payloads).
|
||||
|
||||
## LRN-163 — VS Code terminal instrumentation is per-terminal and unpredictable: pre-flight the pty before attaching
|
||||
- **Date**: 2026-09-24 (merge of [[LRN-147]] + [[LRN-148]], both 2026-09-03)
|
||||
- **Context**: notify-attention over Remote-SSH. Incident 1: re-attach from a RESTORED terminal → bell OK, toast dead; probe on that pty: OSC 777 unique + repeated + OSC 9 all silent, BEL rang ⇒ bytes arrive, ext not hooked to that terminal. LRN-147 blamed `terminal.integrated.enablePersistentSessions` (terminals restored BEFORE lazy ext activation). Incident 2, same day, refuted that as sole cause: two terminals, SAME window, pts/3 (born 01:58:33) instrumented, pts/7 (born 01:59:29, LATER) deaf; ext GLOBAL (marketplace Enable/Disable only), shells identical on every server-side measurable (`VSCODE_INJECTION=1`, TERM, TERM_PROGRAM, same `--init-file`). Trigger NOT identified.
|
||||
- **Pattern**: `wenbopan.vscode-terminal-osc-notifier` instruments a terminal only if it exists AFTER ext activation, and can still silently skip a later one. Treat instrumentation as a per-terminal property that fails for unknown reasons. Pre-flight before committing a long-lived session: `printf '\a\a\033]777;notify;NEUF;test\033\\'` typed IN that terminal. Toast → instrumented, attach. Bell only → deaf, open another. 5 s, replaces an hour of pty archaeology.
|
||||
- **Recovery**: deaf terminal never repairs. Fresh terminal, pre-flight, `dtach -a ~/.dtach/<session>`; dtach broadcasts, old client may stay, session never at risk. Client setting `"terminal.integrated.enablePersistentSessions": false` removes the restored-terminal case, not the unknown one.
|
||||
- **Diagnostic split (holds)**: bell alive + toast dead = terminal instrumentation. Toast alive + bell dead = client audio ([[BLK-020]] fault B). Neither = bytes never arrive. Check which channel survives first.
|
||||
- **Future application**: verify instrumentation on the ACTUAL attached pty after every restart; never assume yesterday's terminal. Do NOT assert the born-before-activation cause as established — it fits the first incident, not the second; unknown trigger is the honest state.
|
||||
- **Reference**: supersedes [[LRN-147]], [[LRN-148]] (bodies kept). Links [[BLK-019]], [[BLK-020]], [[BDR-087]], [[LRN-145]], [[LRN-146]], [[LRN-149]].
|
||||
|
||||
## LRN-164 — one fixed occurrence ≠ pattern closed: grep the whole surface, add a guard with teeth
|
||||
- **Date**: 2026-09-24 (merge of [[LRN-106]] 2026-07-06 + [[LRN-113]] 2026-07-08)
|
||||
- **Pattern**: fixer greps the reported line, fixes it, stops; twins survive one file or one agent over. job3-B1: `blockers-snapshot.md` fixture frozen, T2 repointed, "B1 UNBLOCKED", suite 20/20 GREEN — job4, same day, found T3 and T5 in the SAME FILE still reading live `$MEM/decisions.md`. Job1-9 review found 4 more: trailer stripped from commit-changer only (twins bugfixer/feater/hotfixer); YAML quoted elsewhere, seo/security-auditor left broken; attribution scrubbed on 3 skills, geo-analyzer missed; gitleaks added to the hook generator, installed hook not regenerated.
|
||||
- **Why**: "suite green" + "named finding fixed" don't imply "no other instance of the same root cause survives nearby." Nothing enumerates the pattern across the full surface at commit time; an adversarial review catches the twins later.
|
||||
- **Fix**: every pattern-fix ends with (1) a whole-surface grep proving zero residue (agents/ lib/ hooks/ templates/ skills/), (2) a deterministic make-test guard that REDs if any occurrence returns. Shipped `lib/tests/run-review-guards.sh`: G1 trailer, G2 false attribution, G3 strict-YAML, G4 reconcile hermeticity, G5 hook-drift, teeth-verified (planted violation REDs). job4 closure: T3/T5 repointed at `decisions-snapshot.md`, `$MEM` deleted, `grep -c '$MEM' == 0` gate.
|
||||
- **Future application**: after fixing one instance of a generic finding, grep the WHOLE FILE and the whole surface class before declaring the class closed; add or extend a review-guard with teeth.
|
||||
- **Reference**: `.audit/job4-report.md` J4-10, `lib/tests/run-reconcile.sh`, `lib/tests/run-review-guards.sh`. Supersedes [[LRN-106]], [[LRN-113]] (bodies kept). Cousins [[LRN-077]], [[LRN-114]], [[LRN-047]], [[BDR-041]].
|
||||
|
||||
## LRN-165 — a read-only sub-agent mandate constrains files, not tools: name the banned commands, ban copying secret values
|
||||
- **Date**: 2026-09-24 (merge of [[LRN-105]] 2026-07-06 + [[LRN-107]] 2026-07-07)
|
||||
- **Pattern**: "read-only" frames FILES; the model does not map it onto every tool call. (a) job3 docs-drift explorer (Bash + Read/Grep, "audit BODIES — do NOT modify any file") ran `graphify .` to check CLI behavior — a real build, stray `graphify-out/` at repo root. The prompt never named the command class to avoid; running the subject's own CLI read as investigation. Fixed only by a mid-run main-session correction. (b) job6 explorer under an explicit no-execute mandate copied the plaintext `MAGIC_API_KEY` into its own scratch file: copying a value into a NEW file mutates nothing that existed, so it passes the "don't mutate" mental model while creating a fresh copy of the secret ([[BDR-026]] class). Harness flagged it, main session redacted, contained to the scratchpad.
|
||||
- **Future application**: any sub-agent dispatch framed read-only / audit / verify that grants Bash → explicitly ban executing the subject-under-test's CLI/build/generator and name the safe alternative in the same sentence (read installed source, grep docs). Any mandate touching config or env files → explicitly ban copying a secret's VALUE into output or scratch: "reference by name/location, never paste the value"; filter env fields (`jq 'del(.. | .env?)'`) over raw `cat`. Don't rely on the word "read-only" alone.
|
||||
- **Reference**: `.audit/job3-report.md` A1/A2 header incident, `.audit/job6-report.md` "Incident (contained)", explorer-C.md redacted. Supersedes [[LRN-105]], [[LRN-107]] (bodies kept). Extended by [[LRN-160]] (prose guardrails are judgment). Cousins [[LRN-100]], [[BDR-026]].
|
||||
|
||||
## LRN-166 — structure and census locks are fixed single-line strings: a prose rewrap reds them with zero doctrine lost
|
||||
- **Date**: 2026-09-24 (merge of [[LRN-142]] 2026-08-24 + [[LRN-144]] 2026-08-26)
|
||||
- **Context**: contract-gates ([[BDR-083]]): editing lib/verify-secure-loop.md rewrapped 5 locked phrases across line breaks ("Max 3 conformity iterations", "Max 3 security iterations", "re-verify the REQUEST first", "always re-checked BEFORE security", "one verifier dispatch + one security dispatch") → loops-light.test.sh 30 pass / 5 fail, ZERO doctrine dropped. darwin 2026-08-26: hotfix RULES rewrap split "No verifier is dispatched at hotfix weight" → same lock RED, caught post-edit by make test.
|
||||
- **Pattern**: lib/tests/*.test.sh lock sentences verbatim, single-line. Locks cannot distinguish "clause deleted" from "clause rewrapped"; that conservative bias is correct — fuzzy matching would miss real deletions.
|
||||
- **Rule**: before editing prose under locks, grep lib/tests/ for the lock strings in the touched region, then re-flow AROUND them — each locked phrase stays on one unbroken line. Fix the DOC, not the lock, unless the doctrine genuinely changed. Run make test BEFORE dispatching judges, not after. Under locks: verify-secure-loop.md, contract-interview.md, verifier / security-auditor / plan-challenger agents, seo+geo (71 locks), hotfix RULES.
|
||||
- **Reference**: `lib/tests/loops-light.test.sh`, `lib/tests/seo-geo-contract.test.sh`. Supersedes [[LRN-142]], [[LRN-144]] (bodies kept). Links [[BDR-083]], [[LRN-093]], [[LRN-096]].
|
||||
|
||||
## LRN-167 — a release/develop fork strands CODE on develop: a "resolved" blocker or a parallel-merged feature can miss its fix
|
||||
- **Date**: 2026-09-24 (merge of [[LRN-116]] + [[LRN-117]], release/1.0.0 review, 2026-07)
|
||||
- **Pattern**: cutting release/1.0.0 while develop moved on, RC-branch fixes landed ONLY on release: rtk install bridge `e58037c`, find-skills drop `095d881`, make-update TTY guard `a1093ca`, rtk update-path guard `4c5e862`, SC1091 lint `e65796f`. Live-broken on develop for the whole fork (rtk compression dead, ~460K tokens/30d; `make update` dies non-interactively). BLK-016 read "resolved via e58037c" and backfilled cleanly into develop while the fix was absent there: a resolved status is a claim about CODE state on the target branch, safe only for the TEXT.
|
||||
- **Why it hides**: registry-sequence gaps (missing LRN/BLK/EVAL ids) are easy to detect; orphaned CODE has no sequence. A feature parallel-merged to both branches while its RC fix commit is never back-merged trips nothing.
|
||||
- **Fix**: before back-merging a resolved blocker, grep the target for the fix's code signature — e58037c ported to develop first, THEN BLK-016 backfilled. At release-finish / in /reconcile, list `develop..release/*` commits touching functional files (exclude merges, `.claude/**`, version.txt/CHANGELOG) for back-merge review. Advisory, NOT a hard make-test gate — cherry-picks land with new SHAs so the source commit stays in the range; automatic "already-ported?" equivalence is unreliable. Backlogged.
|
||||
- **Future application**: any long-lived fork → audit CODE divergence, not only declared/registry state ([[LRN-034]] narrated ≠ ground truth, applied to branches). `git cherry` before deleting a divergent branch ([[LRN-129]]).
|
||||
- **Reference**: `install-plugins.sh` rtk bridge, release/1.0.0..develop review. Supersedes [[LRN-116]], [[LRN-117]] (bodies kept). Cousins [[LRN-036]], [[LRN-047]], [[BDR-054]].
|
||||
|
||||
## LRN-168 — a relayed claim is not a fact: WebSearch consensus and sub-agent summaries both need a primary source or a live test
|
||||
- **Date**: 2026-09-24 (merge of [[LRN-131]] + [[LRN-132]], both 2026-07-17)
|
||||
- **Pattern**: plausible RECOMBINATION is the failure mode — what a model half-remembering a search produces, and what a sub-agent relays. "Cross-check via WebSearch" LAUNDERS blog consensus instead of catching it. Treat every relayed finding as a claim to verify against a primary source or a live test. A statistic reaches a client only as `<claim> — <source, year, venue|vendor> — measured: <what the source ACTUALLY measured> — <link>`; the `measured:` field catches the error.
|
||||
- **Context**: seo/geo 2026-07-17. "VSI (Visual Stability Index) — new 2026 Core Web Vital" sat in seo-analyzer as a threshold; it does NOT exist — absent from the CrUX API metric list AND web.dev, 10 SEO blogs cross-cited it into consensus. EVERY stat in agents/resources/ was real but grafted onto the wrong subject (Aggarwal 40% = ALL methods; AccuraCast 58.9% = Person-schema PREVALENCE pinned on QAPage lift, meaning inverted; LLMrefs 3x = brand-mentions-vs-backlinks pinned on freshness decay). 7 sub-agent claims disproven in one session: "Off-page has ZERO data" (brand mentions ARE gathered, STEP 6); "the stats drive axis weights" (no citations); "GSC Links API is available" (endpoint doesn't exist); "SPA §0 flag compensates" (never existed); "X/Twitter returns 403" (200, live-tested); Common Crawl "nearest free source" (17.3 GB dead end); the whole opening inventory behind the 20-point plan. The same error reproduced 3× while WRITING the fixes; contact with the REAL corrected it every time — sitemap, repo, curl, primary doc.
|
||||
- **Future application**: an API's metric list (developer.chrome.com/docs/crux) is decisive: a metric the API can't return is one you can't score. Measure-first before building on a relayed summary; never re-read the spec as verification. Corroborates [[LRN-074]] (watch the RED go red), [[LRN-034]] (narrated ≠ ground truth).
|
||||
- **Reference**: `agents/seo-analyzer.md`, `agents/resources/`, [[EVAL-025]]. Supersedes [[LRN-131]], [[LRN-132]] (bodies kept).
|
||||
|
||||
Reference in New Issue
Block a user