cold-shower 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1020 @@
1
+ ---
2
+ name: cold-shower
3
+ description: |
4
+ Three modes, one skill. Auto-triggers on what you say โ€” no commands to memorize.
5
+ ๐Ÿ” AUDIT: 6 parallel audits (LLM costs, AI security, code health, deps, prod readiness, git/devops) โ†’ Vibe Score 0-100.
6
+ ๐Ÿ“‹ PLAN-GATE: generates structured plan (files to touch, rollback, pre-mortem) then PreToolUse hook blocks all edits until user types APPROVED.
7
+ ๐Ÿง  RECALL: saves decisions + WHY, fragile file warnings, bug history to local brain files; grep-based retrieval across sessions.
8
+ Emergency mode auto-activates when app is actively failing under traffic.
9
+
10
+ Trigger on: "audit my codebase", "is this ready to ship", "cold shower", "reality check",
11
+ "something always breaks", "LLM bill too high", "is my AI secure", "clean up my deps",
12
+ "app crashed under traffic", "vibe code mess", "/cold-shower",
13
+ "implement X", "add X", "fix X", "refactor X", "build X",
14
+ "remember this", "what did we decide", "save this decision".
15
+ tools: Read, Write, Bash, Grep, Glob
16
+ ---
17
+
18
+ # cold-shower v2 โ€” Reality Check for Vibe-Coded Apps
19
+
20
+ Three modes, one skill:
21
+ - ๐Ÿ” AUDIT: 6 parallel audits (LLM costs, AI security, code health, deps, prod readiness, git/devops) โ†’ Vibe Score 0-100
22
+ - ๐Ÿ“‹ PLAN-GATE: structured implementation plan โ†’ PreToolUse hook blocks edits until approved
23
+ - ๐Ÿง  RECALL: persistent second brain โ†’ decisions, fragile files, bug history across sessions
24
+
25
+ ---
26
+
27
+ ## EMERGENCY CHECK โ€” Run This First
28
+
29
+ Before anything else, check if the app is actively on fire:
30
+
31
+ **Signs of emergency:** user mentions 500 errors, timeouts, "too many connections", DB overload,
32
+ app down, crashing under traffic, HN/Product Hunt spike.
33
+
34
+ **If emergency detected โ†’ jump to EMERGENCY MODE at the bottom of this file.**
35
+ Fix the bleeding first. Run full audit after app is stable.
36
+
37
+ ---
38
+
39
+ ## Phase 0: Stack Detection (30 seconds)
40
+
41
+ ```bash
42
+ mkdir -p .cold-shower && echo ".cold-shower/" >> .gitignore 2>/dev/null
43
+
44
+ [ -f package.json ] && echo "JS_PROJECT=1"
45
+ [ -f requirements.txt ] || [ -f pyproject.toml ] && echo "PY_PROJECT=1"
46
+
47
+ grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK=nextjs"
48
+ grep -q '"express"' package.json 2>/dev/null && echo "FRAMEWORK=express"
49
+ grep -q 'fastapi' requirements.txt pyproject.toml 2>/dev/null && echo "FRAMEWORK=fastapi"
50
+ grep -q 'django' requirements.txt pyproject.toml 2>/dev/null && echo "FRAMEWORK=django"
51
+
52
+ grep -rql 'openai\|@anthropic-ai\|anthropic\|langchain\|llamaindex' \
53
+ --include="*.ts" --include="*.js" --include="*.py" . 2>/dev/null \
54
+ && echo "HAS_AI=1"
55
+
56
+ grep -qE 'supabase|postgres|prisma|mongoose|mysql|sequelize|drizzle|sqlalchemy' \
57
+ package.json requirements.txt pyproject.toml 2>/dev/null \
58
+ && echo "HAS_DB=1"
59
+
60
+ [ -f pnpm-lock.yaml ] && echo "PM=pnpm"
61
+ [ -f yarn.lock ] && echo "PM=yarn"
62
+ [ -f bun.lockb ] && echo "PM=bun"
63
+ [ -f package-lock.json ] && echo "PM=npm"
64
+
65
+ node -e "const p=require('./package.json'); \
66
+ console.log('DEPS:', Object.keys(p.dependencies||{}).length, \
67
+ 'DEV_DEPS:', Object.keys(p.devDependencies||{}).length)" 2>/dev/null
68
+ ```
69
+
70
+ **Load previous Vibe Score (if exists):**
71
+ ```bash
72
+ mkdir -p .cold-shower
73
+ if [ -f .cold-shower/score-history.json ]; then
74
+ LAST=$(python3 -c "import json; h=json.load(open('.cold-shower/score-history.json')); e=h[-1]; print(f\"Last score: {e['score']}/100 ({e['grade']}) on {e['date']}\")" 2>/dev/null)
75
+ echo "๐Ÿ“Š $LAST โ€” comparing after this audit"
76
+ fi
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Phase 1: Five Audits in Parallel
82
+
83
+ ---
84
+
85
+ ### AUDIT A โ€” LLM Cost Scan (run if HAS_AI=1)
86
+
87
+ **Goal:** Find why the OpenAI/Anthropic bill is higher than it should be.
88
+
89
+ ```bash
90
+ # Find all LLM call sites
91
+ grep -rn "chat.completions.create\|messages.create\|openai.chat\|anthropic.messages\|ChatOpenAI\|ChatAnthropic" \
92
+ --include="*.ts" --include="*.js" --include="*.py" . \
93
+ > .cold-shower/ai-callsites.txt 2>/dev/null
94
+ echo "LLM call sites: $(wc -l < .cold-shower/ai-callsites.txt)"
95
+
96
+ # 1. Unbounded history accumulation
97
+ grep -rn "messages.push\|chat_history.append\|history +=" \
98
+ --include="*.ts" --include="*.js" --include="*.py" . >> .cold-shower/a-issues.txt
99
+
100
+ # 2. Hardcoded expensive model everywhere
101
+ grep -rn '"gpt-4"\|"claude-opus"\|"claude-3-opus"\|"gpt-4o"[^-]' \
102
+ --include="*.ts" --include="*.js" --include="*.py" . >> .cold-shower/a-issues.txt
103
+
104
+ # 3. No caching layer at all
105
+ grep -q 'redis\|upstash\|gptcache\|semantic-cache' \
106
+ package.json requirements.txt pyproject.toml 2>/dev/null \
107
+ || echo "NO_CACHE=1" >> .cold-shower/a-issues.txt
108
+
109
+ # 4. Retry loops without circuit breaker
110
+ grep -rn "for.*retry\|while.*retry\|attempts.*range\|maxRetries" \
111
+ --include="*.ts" --include="*.js" --include="*.py" . >> .cold-shower/a-issues.txt
112
+
113
+ # 5. No observability (flying blind on costs)
114
+ grep -rq 'helicone\|langfuse\|langsmith\|portkey' \
115
+ --include="*.ts" --include="*.js" --include="*.py" . 2>/dev/null \
116
+ || echo "NO_LLM_OBSERVABILITY=1" >> .cold-shower/a-issues.txt
117
+ ```
118
+
119
+ **Generate on fix request:**
120
+ - `lib/llm-cache.ts` โ€” Upstash semantic cache (40-70% call reduction)
121
+ - `lib/llm-router.ts` โ€” heuristic model router (gpt-4o-mini for simple queries = 20x cheaper)
122
+ - `lib/llm-client.ts` โ€” drop-in wrapper: history truncation + Helicone + cache + router
123
+
124
+ ---
125
+
126
+ ### AUDIT B โ€” AI Security Scan (run if HAS_AI=1)
127
+
128
+ **Goal:** Find AI endpoints exposed to real users with no protection.
129
+
130
+ ```bash
131
+ # Map all route handlers
132
+ grep -rn "router\.\(post\|get\)\|app\.\(post\|get\)\|export.*POST\|export.*GET" \
133
+ --include="*.ts" --include="*.js" . | grep -v node_modules > .cold-shower/b-endpoints.txt
134
+ grep -rn "@app\.\(post\|get\)\|@router\.\|path(" \
135
+ --include="*.py" . >> .cold-shower/b-endpoints.txt
136
+
137
+ # 1. Raw user input going straight to LLM (prompt injection risk)
138
+ grep -rn "req\.body\.\|request\.json\(\)\|await request\.body" \
139
+ --include="*.ts" --include="*.js" --include="*.py" . \
140
+ > .cold-shower/b-raw-inputs.txt
141
+
142
+ # 2. No rate limiting on AI endpoint
143
+ grep -rq 'rateLimit\|rate_limit\|slowapi\|RateLimiter\|@upstash/ratelimit' \
144
+ --include="*.ts" --include="*.js" --include="*.py" . 2>/dev/null \
145
+ || echo "NO_RATE_LIMIT=1" >> .cold-shower/b-issues.txt
146
+
147
+ # 3. No PII scrubbing before LLM
148
+ grep -rq 'presidio\|redact\|scrub\|anonymize' \
149
+ package.json requirements.txt pyproject.toml 2>/dev/null \
150
+ || echo "NO_PII_SCRUBBING=1" >> .cold-shower/b-issues.txt
151
+
152
+ # 4. Secrets inside system prompt
153
+ grep -rn 'SYSTEM_PROMPT\|systemPrompt\|system_prompt' \
154
+ --include="*.ts" --include="*.js" --include="*.py" . \
155
+ | grep -i 'api_key\|secret\|internal\|admin' \
156
+ >> .cold-shower/b-issues.txt
157
+
158
+ # 5. No per-user spend limit
159
+ grep -rq 'token.*budget\|usage.*limit\|user.*quota' \
160
+ --include="*.ts" --include="*.js" --include="*.py" . 2>/dev/null \
161
+ || echo "NO_USER_SPEND_LIMIT=1" >> .cold-shower/b-issues.txt
162
+ ```
163
+
164
+ **Generate on fix request:**
165
+ - `middleware/ai-guard.ts` โ€” injection pattern detection + input sanitization
166
+ - `middleware/ai-rate-limit.ts` โ€” Redis sliding window per-user token budget
167
+ - Canary token in system prompt (detects extraction attempts)
168
+
169
+ ---
170
+
171
+ ### AUDIT C โ€” Code Health Scan (always run)
172
+
173
+ **Goal:** Vibe Score โ€” how bad is the AI-generated rot?
174
+
175
+ ```bash
176
+ # Install tools if missing
177
+ command -v madge >/dev/null || npm install -g madge 2>/dev/null
178
+ command -v pylint >/dev/null || pip install pylint -q 2>/dev/null
179
+
180
+ # God files (>500 lines = warning, >1000 = critical)
181
+ find . \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.py" \) \
182
+ | grep -v node_modules | grep -v dist | grep -v ".cold-shower" \
183
+ | xargs wc -l 2>/dev/null | awk '$1 > 500 {print $1, $2}' | sort -rn \
184
+ > .cold-shower/c-god-files.txt
185
+ echo "God files (>500 lines): $(wc -l < .cold-shower/c-god-files.txt)"
186
+
187
+ # Circular dependencies
188
+ madge --circular --json src/ > .cold-shower/c-circular.json 2>/dev/null
189
+ echo "Circular dep chains: $(python3 -c \
190
+ 'import json; d=json.load(open(".cold-shower/c-circular.json")); print(len(d))' 2>/dev/null || echo 0)"
191
+
192
+ # Code duplication (>10% = failing grade, GitClear found AI code hits 12.3% avg)
193
+ npx --yes jscpd src/ --min-tokens 50 --reporters json \
194
+ --output .cold-shower/ >/dev/null 2>&1
195
+ echo "Duplication %: $(python3 -c \
196
+ 'import json; d=json.load(open(".cold-shower/jscpd-report.json")); \
197
+ print(round(d.get("statistics",{}).get("total",{}).get("percentage",0),1))' 2>/dev/null || echo "N/A")"
198
+
199
+ # Floating promises โ€” async calls with no error handling (silent crash sites)
200
+ npx eslint src/ \
201
+ --rule '{"@typescript-eslint/no-floating-promises":"error","no-async-promise-executor":"error"}' \
202
+ --format json > .cold-shower/c-async.json 2>/dev/null
203
+ echo "Floating promises: $(python3 -c \
204
+ 'import json; d=json.load(open(".cold-shower/c-async.json")); \
205
+ print(sum(len(f["messages"]) for f in d))' 2>/dev/null || echo 0)"
206
+
207
+ # Dead exports
208
+ npx --yes knip --reporter json > .cold-shower/c-knip.json 2>/dev/null
209
+ ```
210
+
211
+ **Vibe Score calculation (0-100):**
212
+ - Start: 100
213
+ - `-15` if duplication > 10%
214
+ - `-10` per circular dep chain (max -30)
215
+ - `-5` per god file >1000 lines (max -20)
216
+ - `-3` per god file >500 lines (max -15)
217
+ - `-2` per floating promise (max -20)
218
+
219
+ **Grades:** 90+=A | 75-89=B | 60-74=C | 40-59=D | <40=F (do not ship)
220
+
221
+ ---
222
+
223
+ ### AUDIT D โ€” Dependency Scan (always run)
224
+
225
+ **Goal:** Find what AI installed that's dead weight.
226
+
227
+ ```bash
228
+ # Unused deps (knip result reused from Audit C if already ran)
229
+ npx --yes knip --reporter json 2>/dev/null > .cold-shower/d-knip.json
230
+ node -e "
231
+ const r = JSON.parse(require('fs').readFileSync('.cold-shower/d-knip.json'));
232
+ const unused = [...new Set((r.issues||[]).flatMap(f =>
233
+ [...(f.dependencies||[]),...(f.devDependencies||[])].map(d=>d.name)))];
234
+ console.log('Unused deps:', unused.length);
235
+ require('fs').writeFileSync('.cold-shower/d-unused.json', JSON.stringify(unused,null,2));
236
+ " 2>/dev/null
237
+
238
+ # Python unused
239
+ command -v deptry >/dev/null && deptry . --json-output .cold-shower/d-deptry.json 2>/dev/null
240
+
241
+ # Security CVEs
242
+ npm audit --json > .cold-shower/d-audit.json 2>/dev/null
243
+ node -e "const r=require('./.cold-shower/d-audit.json'); \
244
+ const v=r.metadata?.vulnerabilities||{}; \
245
+ console.log('CVEs โ€” critical:',v.critical,'high:',v.high,'moderate:',v.moderate)" 2>/dev/null
246
+
247
+ # Bundle size for top 10 unused via bundlephobia (no API key needed)
248
+ node -e "
249
+ const unused=JSON.parse(require('fs').readFileSync('.cold-shower/d-unused.json')||'[]');
250
+ const pkg=require('./package.json');
251
+ const deps={...(pkg.dependencies||{}),...(pkg.devDependencies||{})};
252
+ unused.slice(0,10).forEach(n=>{
253
+ const v=(deps[n]||'latest').replace(/[^\d.]/g,'').split(' ')[0]||'latest';
254
+ console.log(n+'@'+v);
255
+ });
256
+ " 2>/dev/null | while read pkgver; do
257
+ gzip=$(curl -s "https://bundlephobia.com/api/size?package=${pkgver}" \
258
+ | python3 -c "import json,sys; print(json.load(sys.stdin).get('gzip',0))" 2>/dev/null)
259
+ echo "${pkgver} | ${gzip}B gzip" >> .cold-shower/d-sizes.txt
260
+ sleep 0.3
261
+ done
262
+
263
+ # Semantic duplicates โ€” same job, multiple packages
264
+ node -e "
265
+ const CATS={
266
+ http:['axios','got','superagent','node-fetch','ky','undici','request'],
267
+ date:['moment','date-fns','dayjs','luxon'],
268
+ util:['lodash','underscore','ramda','remeda','radash'],
269
+ validation:['joi','yup','zod','valibot','ajv'],
270
+ uuid:['uuid','nanoid','cuid','cuid2','ulid'],
271
+ logging:['winston','pino','bunyan','loglevel'],
272
+ state:['redux','zustand','mobx','jotai','valtio','recoil'],
273
+ };
274
+ const pkg=require('./package.json');
275
+ const inst=Object.keys({...(pkg.dependencies||{}),...(pkg.devDependencies||{})});
276
+ const dupes=Object.entries(CATS)
277
+ .map(([cat,pkgs])=>({cat,found:pkgs.filter(p=>inst.includes(p))}))
278
+ .filter(d=>d.found.length>1);
279
+ if(dupes.length){
280
+ require('fs').writeFileSync('.cold-shower/d-dupes.json',JSON.stringify(dupes,null,2));
281
+ dupes.forEach(d=>console.log('DUPE CATEGORY:',d.cat,'->',d.found.join(' + ')));
282
+ }
283
+ " 2>/dev/null
284
+ ```
285
+
286
+ ---
287
+
288
+ ### AUDIT F โ€” Git/GitHub/DevOps Hygiene (always run)
289
+
290
+ **Goal:** Find everything vibe coders skip in git setup that causes security incidents or broken deploys.
291
+
292
+ ```bash
293
+ # 1. .env committed to git? (CRITICAL โ€” rotate ALL secrets if yes)
294
+ git ls-files | grep -E "^\.env$|^\.env\.(local|production|staging|prod)$" \
295
+ && echo "CRITICAL_ENV_COMMITTED=1" >> .cold-shower/f-issues.txt
296
+
297
+ # 2. Secrets in git history
298
+ git log --all --diff-filter=A --name-only --format="" -- "*.env" "*.pem" "*.key" 2>/dev/null \
299
+ | head -5 >> .cold-shower/f-issues.txt
300
+
301
+ # 3. .gitignore completeness
302
+ [ -f .gitignore ] || echo "MISSING_GITIGNORE=1" >> .cold-shower/f-issues.txt
303
+ for entry in ".env" ".env.local" "node_modules" "dist/" "build/" ".next/" \
304
+ "__pycache__" ".DS_Store" "terraform.tfstate" "*.pem" "*.key"; do
305
+ grep -q "$entry" .gitignore 2>/dev/null \
306
+ || echo "GITIGNORE_MISSING_ENTRY: $entry" >> .cold-shower/f-issues.txt
307
+ done
308
+
309
+ # 4. Claude settings.local.json committed? (contains personal API keys)
310
+ git ls-files | grep -q "settings.local.json" \
311
+ && echo "CLAUDE_SETTINGS_LOCAL_COMMITTED=1" >> .cold-shower/f-issues.txt
312
+
313
+ # 5. CI workflows exist?
314
+ [ -d .github/workflows ] && ls .github/workflows/*.yml >/dev/null 2>&1 \
315
+ || echo "NO_CI_WORKFLOWS=1" >> .cold-shower/f-issues.txt
316
+
317
+ # 6. Typecheck in CI?
318
+ grep -rq "typecheck\|tsc --noEmit\|mypy\|pyright" .github/workflows/ 2>/dev/null \
319
+ || echo "NO_TYPECHECK_IN_CI=1" >> .cold-shower/f-issues.txt
320
+
321
+ # 7. Tests in CI?
322
+ grep -rq "npm test\|pytest\|jest\|vitest\|mocha" .github/workflows/ 2>/dev/null \
323
+ || echo "NO_TESTS_IN_CI=1" >> .cold-shower/f-issues.txt
324
+
325
+ # 8. Unpinned GitHub Actions (CVE-2025-30066: 23,000 repos compromised via floating tags)
326
+ grep -rE "uses: .+@(v[0-9]|main|master|latest)" .github/workflows/ 2>/dev/null \
327
+ && echo "UNPINNED_ACTIONS=1" >> .cold-shower/f-issues.txt
328
+
329
+ # 9. Workflow injection โ€” untrusted PR input interpolated into run: (command injection)
330
+ grep -rn '\${{ github.event.pull_request.title\|\${{ github.event.issue.title\|\${{ github.head_ref' \
331
+ .github/workflows/ 2>/dev/null >> .cold-shower/f-issues.txt
332
+
333
+ # 10. pull_request_target + fork checkout = "Pwn Request" attack
334
+ if grep -rq "pull_request_target" .github/workflows/ 2>/dev/null; then
335
+ grep -rq "head\.ref\|head\.sha" .github/workflows/ 2>/dev/null \
336
+ && echo "PWN_REQUEST_RISK=1" >> .cold-shower/f-issues.txt
337
+ fi
338
+
339
+ # 11. No explicit permissions block in workflows (default is write-all in older repos)
340
+ for f in .github/workflows/*.yml 2>/dev/null; do
341
+ grep -q "^permissions:" "$f" 2>/dev/null \
342
+ || echo "NO_PERMISSIONS_BLOCK: $f" >> .cold-shower/f-issues.txt
343
+ done
344
+
345
+ # 12. ACTIONS_RUNNER_DEBUG left on (dumps env vars + masked secrets to logs)
346
+ grep -rn "ACTIONS_RUNNER_DEBUG\|ACTIONS_STEP_DEBUG" .github/workflows/ 2>/dev/null \
347
+ >> .cold-shower/f-issues.txt
348
+
349
+ # 13. secrets: inherit in reusable workflows (exposes ALL repo secrets to called workflow)
350
+ grep -rn "secrets: inherit" .github/workflows/ 2>/dev/null \
351
+ >> .cold-shower/f-issues.txt
352
+
353
+ # 14. No env validation on startup (missing vars fail silently at runtime, not startup)
354
+ grep -rq "z\.object\|BaseSettings\|envalid\|dotenv-safe\|pydantic_settings" \
355
+ --include="*.ts" --include="*.js" --include="*.py" . 2>/dev/null \
356
+ || echo "NO_ENV_VALIDATION=1" >> .cold-shower/f-issues.txt
357
+
358
+ # 15. No .env.example (teammates can't onboard)
359
+ [ -f .env.example ] || [ -f .env.sample ] \
360
+ || echo "NO_ENV_EXAMPLE=1" >> .cold-shower/f-issues.txt
361
+
362
+ # 16. No Dependabot (CVEs accumulate silently between manual audits)
363
+ [ -f .github/dependabot.yml ] \
364
+ || echo "NO_DEPENDABOT=1" >> .cold-shower/f-issues.txt
365
+
366
+ # 17. Branch protection on main? (requires gh CLI)
367
+ if command -v gh >/dev/null 2>&1; then
368
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)
369
+ if [ -n "$REPO" ]; then
370
+ PROTECTED=$(gh api repos/$REPO/branches/main --jq '.protected' 2>/dev/null)
371
+ [ "$PROTECTED" = "true" ] \
372
+ || echo "NO_BRANCH_PROTECTION=1" >> .cold-shower/f-issues.txt
373
+ fi
374
+ fi
375
+
376
+ # 18. Python-specific
377
+ if [ -f requirements.txt ] || [ -f pyproject.toml ]; then
378
+ [ -f .python-version ] || echo "NO_PYTHON_VERSION_FILE=1" >> .cold-shower/f-issues.txt
379
+ grep -rq "pip-audit" .github/workflows/ 2>/dev/null \
380
+ || echo "NO_PIP_AUDIT_IN_CI=1" >> .cold-shower/f-issues.txt
381
+ UNPINNED=$(grep -E "^[a-zA-Z]" requirements.txt 2>/dev/null | grep -v "==" | wc -l)
382
+ [ "$UNPINNED" -gt 0 ] \
383
+ && echo "UNPINNED_PYTHON_DEPS: ${UNPINNED} packages" >> .cold-shower/f-issues.txt
384
+ fi
385
+
386
+ echo "Git/DevOps issues: $(wc -l < .cold-shower/f-issues.txt)"
387
+ ```
388
+
389
+ **Critical issues that require immediate action (before any other sprint):**
390
+ - `.env` committed โ†’ rotate ALL secrets NOW, then remove from history with `git filter-repo`
391
+ - Workflow injection pattern โ†’ fix before next PR
392
+ - `pull_request_target` Pwn Request โ†’ fix before repo goes public
393
+
394
+ **Generate on fix request:**
395
+ - `.github/workflows/ci.yml` โ€” minimum viable CI (lint + typecheck + test + build)
396
+ - `.github/dependabot.yml` โ€” weekly dep + actions updates
397
+ - `src/env.ts` or `src/env.py` โ€” startup env validation (zod/pydantic)
398
+ - Updated `.gitignore` with all missing entries
399
+
400
+ ---
401
+
402
+ ### AUDIT E โ€” Production Readiness (run if HAS_DB=1)
403
+
404
+ **Goal:** Will this survive its first real traffic spike?
405
+
406
+ ```bash
407
+ # Connection pool configured?
408
+ grep -rn "new Pool\|pool_size\|max:\s*[0-9]\|MAX_CONNECTIONS" \
409
+ --include="*.ts" --include="*.js" --include="*.py" . \
410
+ | grep -v node_modules > .cold-shower/e-pool.txt
411
+ [ ! -s .cold-shower/e-pool.txt ] && echo "NO_POOL_CONFIG=1" >> .cold-shower/e-issues.txt
412
+
413
+ # N+1 patterns โ€” DB calls inside loops
414
+ grep -rn "\.map.*await\|forEach.*await\|for.*await.*find\|for.*await.*query" \
415
+ --include="*.ts" --include="*.js" --include="*.py" . \
416
+ | grep -v node_modules > .cold-shower/e-n1.txt
417
+ N1=$(wc -l < .cold-shower/e-n1.txt)
418
+ [ "$N1" -gt 0 ] && echo "N1_SITES: ${N1}" >> .cold-shower/e-issues.txt
419
+
420
+ # API rate limiting present?
421
+ grep -rq 'express-rate-limit\|rateLimit\|@upstash/ratelimit\|slowapi\|Flask-Limiter' \
422
+ package.json requirements.txt pyproject.toml 2>/dev/null \
423
+ || echo "NO_API_RATE_LIMIT=1" >> .cold-shower/e-issues.txt
424
+
425
+ # SELECT * (fetches entire row when you need 2 columns)
426
+ grep -rn 'SELECT \*\|findMany()\|find({})' \
427
+ --include="*.ts" --include="*.js" --include="*.py" . \
428
+ | grep -v node_modules > .cold-shower/e-selectstar.txt
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Phase 2: Unified Health Report
434
+
435
+ Print to terminal AND save to `.cold-shower/REPORT.md`:
436
+
437
+ ```
438
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
439
+ โ•‘ COLD SHOWER โ€” [project] โ€” [date] โ•‘
440
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
441
+
442
+ VIBE SCORE: [XX]/100 Grade: [A/B/C/D/F]
443
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
444
+
445
+ [A] LLM COSTS โ€” [X issues / CLEAN]
446
+ [B] AI SECURITY โ€” [X issues / CLEAN]
447
+ [C] CODE HEALTH โ€” [X] god files | [X]% duplication | [X] floating promises
448
+ [D] DEPENDENCIES โ€” [X] unused | [X] CVEs | [X] semantic dupes
449
+ [E] PROD READINESS โ€” [READY / X issues]
450
+ [F] GIT/DEVOPS โ€” [X] gitignore gaps | [X] CI issues | [X] secrets risks
451
+
452
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
453
+ ๐Ÿ”ด CRITICAL (fix before shipping anything)
454
+ [specific findings with file:line]
455
+
456
+ ๐ŸŸก HIGH (fix this sprint)
457
+ [findings]
458
+
459
+ ๐ŸŸข QUICK WINS (<30 min each)
460
+ [findings with install command + code snippet]
461
+
462
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
463
+ RECOMMENDED FIX ORDER
464
+
465
+ Sprint 0.5 โ€” 15 min โ€” Rotate exposed secrets + fix .gitignore (IF .env committed)
466
+ Sprint 1 โ€” 30 min โ€” Dead deps + security headers + .env.example
467
+ Sprint 2 โ€” 2 hr โ€” Async errors + rate limiting + env validation
468
+ Sprint 3 โ€” 2 hr โ€” Connection pool + N+1 fixes
469
+ Sprint 4 โ€” 4 hr โ€” LLM cost middleware + caching
470
+ Sprint 5 โ€” 1 day โ€” God component surgery
471
+ Sprint 6 โ€” 1 hr โ€” CI workflow + branch protection + Dependabot
472
+ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
473
+ ```
474
+
475
+ **Save Vibe Score to history:**
476
+ ```bash
477
+ mkdir -p .cold-shower
478
+ DATE=$(date +%Y-%m-%d)
479
+ SCORE=<computed_score>
480
+ GRADE=<computed_grade>
481
+ python3 - <<'PYEOF'
482
+ import json, os
483
+ path = '.cold-shower/score-history.json'
484
+ history = []
485
+ try:
486
+ with open(path) as f:
487
+ history = json.load(f)
488
+ except:
489
+ pass
490
+ history.append({"date": os.environ.get("DATE",""), "score": int(os.environ.get("SCORE",0)), "grade": os.environ.get("GRADE","")})
491
+ with open(path, 'w') as f:
492
+ json.dump(history[-20:], f, indent=2) # keep last 20 entries
493
+ print(f"Score saved. History: {len(history)} entries.")
494
+ PYEOF
495
+ ```
496
+
497
+ Note: when running this, substitute the actual computed SCORE and GRADE values into `DATE`, `SCORE`, `GRADE` env vars before running the python block.
498
+
499
+ Ask user: "Which sprint to start? (1-5 or describe what to fix)"
500
+
501
+ ---
502
+
503
+ ## Phase 3: Fix Sprints
504
+
505
+ Commit after every fix: `git add -p && git commit -m "cold-shower: [description]"`
506
+
507
+ **After completing any sprint:** type `re-audit` or `check score` to re-run the audit and see if Vibe Score improved. Closing the loop is the point โ€” a sprint without a re-audit is unverified.
508
+
509
+ ### Sprint 0.5 โ€” Secrets Emergency (15 min, ONLY if .env committed)
510
+ ```bash
511
+ # 1. Rotate EVERY secret in the committed .env โ€” assume all compromised
512
+ # 2. Remove from git history
513
+ pip install git-filter-repo # or: brew install git-filter-repo
514
+ git filter-repo --path .env --invert-paths
515
+ # 3. Force push (required โ€” this is the one case where it's correct)
516
+ git push --force-with-lease origin main
517
+ # 4. Add to .gitignore immediately
518
+ echo ".env" >> .gitignore && echo ".env.*" >> .gitignore
519
+ git add .gitignore && git commit -m "cold-shower: add .env to gitignore"
520
+ ```
521
+ โš ๏ธ All collaborators must re-clone after force push.
522
+
523
+ ### Sprint 1 โ€” Dead Deps + Quick Wins (30 min)
524
+
525
+ **Dep removal loop โ€” always one at a time, never batch:**
526
+ ```bash
527
+ UNUSED=$(node -e "console.log(JSON.parse(require('fs').readFileSync('.cold-shower/d-unused.json')).join(' '))" 2>/dev/null)
528
+ for pkg in $UNUSED; do
529
+ echo "Removing: $pkg"
530
+ npm uninstall $pkg
531
+ if npm run build 2>/dev/null && npx tsc --noEmit 2>/dev/null; then
532
+ git add package.json package-lock.json
533
+ git commit -m "cold-shower: remove unused dep $pkg"
534
+ echo "โœ“ $pkg safely removed"
535
+ else
536
+ echo "โœ— $pkg broke build โ€” reverting"
537
+ git checkout -- package.json package-lock.json && npm install --silent
538
+ fi
539
+ done
540
+ ```
541
+
542
+ **Add `knip.json` to project root to suppress knip false positives:**
543
+ ```json
544
+ {
545
+ "$schema": "https://unpkg.com/knip@5/schema.json",
546
+ "ignoreDependencies": ["eslint-plugin-*", "jest-environment-*", "@types/node", "cross-env"],
547
+ "ignoreBinaries": ["tsc", "eslint", "prettier"]
548
+ }
549
+ ```
550
+
551
+ **Security headers:**
552
+ ```bash
553
+ npm install helmet
554
+ ```
555
+ ```typescript
556
+ import helmet from 'helmet'
557
+ app.use(helmet())
558
+ ```
559
+
560
+ - Add `.env.example` if missing (copy `.env`, blank out all values)
561
+
562
+ ### Sprint 2 โ€” Async + Rate Limiting (2 hr)
563
+ - Wrap each floating promise: try/catch, log error, return safe default
564
+ - Generate rate limiting middleware for all API routes
565
+ - Add per-user token budget middleware for AI endpoints
566
+
567
+ ### Sprint 3 โ€” Production DB Hardening (2 hr)
568
+ - Generate pool config for detected ORM (Prisma/pg/SQLAlchemy/Sequelize/Drizzle)
569
+ - Add `include`/`select_related`/`joinedLoad` at each N+1 site
570
+ - Add dev-only query logger to catch future N+1s early
571
+
572
+ ### Sprint 4 โ€” LLM Cost Middleware (4 hr)
573
+
574
+ Generate these 3 files. User only changes call sites from `openai.chat.completions.create` โ†’ `llmChat`.
575
+
576
+ **`lib/llm-cache.ts`** โ€” Upstash semantic cache (40-70% call reduction):
577
+ ```typescript
578
+ // npm install @upstash/semantic-cache @upstash/vector
579
+ import { SemanticCache } from '@upstash/semantic-cache'
580
+ import { Index } from '@upstash/vector'
581
+
582
+ const index = new Index({
583
+ url: process.env.UPSTASH_VECTOR_REST_URL!,
584
+ token: process.env.UPSTASH_VECTOR_REST_TOKEN!,
585
+ })
586
+ const cache = new SemanticCache({ index, minProximity: 0.85 })
587
+
588
+ export async function cachedCall(prompt: string, fn: () => Promise<string>): Promise<string> {
589
+ const hit = await cache.get(prompt)
590
+ if (hit) return hit
591
+ const result = await fn()
592
+ await cache.set(prompt, result)
593
+ return result
594
+ }
595
+ ```
596
+
597
+ **`lib/llm-router.ts`** โ€” heuristic model router (20x cheaper on simple queries):
598
+ ```typescript
599
+ export function routeModel(prompt: string): string {
600
+ const tokens = prompt.split(/\s+/).length
601
+ const hasCode = /```|function |class |def |import /.test(prompt)
602
+ const hasReasoning = /analyze|compare|explain why|step by step|summarize/i.test(prompt)
603
+ if (tokens < 100 && !hasCode && !hasReasoning) return 'gpt-4o-mini'
604
+ if (tokens > 500 || (hasCode && hasReasoning)) return 'gpt-4o'
605
+ return 'gpt-4o-mini'
606
+ }
607
+ ```
608
+
609
+ **`lib/llm-client.ts`** โ€” drop-in wrapper (cache + router + Helicone + history truncation):
610
+ ```typescript
611
+ import OpenAI from 'openai'
612
+ import { cachedCall } from './llm-cache'
613
+ import { routeModel } from './llm-router'
614
+
615
+ const client = new OpenAI({
616
+ baseURL: process.env.HELICONE_API_KEY ? 'https://oai.helicone.ai/v1' : undefined,
617
+ defaultHeaders: process.env.HELICONE_API_KEY
618
+ ? { 'Helicone-Auth': `Bearer ${process.env.HELICONE_API_KEY}` }
619
+ : undefined,
620
+ })
621
+
622
+ const MAX_HISTORY = 20
623
+
624
+ export async function llmChat(
625
+ messages: Array<{ role: string; content: string }>,
626
+ opts: { bypassCache?: boolean; model?: string } = {}
627
+ ): Promise<string> {
628
+ const system = messages.find(m => m.role === 'system')
629
+ const history = messages.filter(m => m.role !== 'system').slice(-MAX_HISTORY)
630
+ const truncated = [...(system ? [system] : []), ...history]
631
+ const userMsg = truncated.at(-1)?.content ?? ''
632
+ const model = opts.model ?? routeModel(userMsg)
633
+
634
+ const callFn = async () => {
635
+ const res = await client.chat.completions.create({ model, messages: truncated as any })
636
+ return res.choices[0]?.message?.content ?? ''
637
+ }
638
+ return opts.bypassCache ? callFn() : cachedCall(userMsg, callFn)
639
+ }
640
+ ```
641
+
642
+ **Add to `.env.example`:**
643
+ ```
644
+ UPSTASH_VECTOR_REST_URL=
645
+ UPSTASH_VECTOR_REST_TOKEN=
646
+ HELICONE_API_KEY= # get free at helicone.ai โ€” 1 line, instant cost visibility
647
+ ```
648
+
649
+ **Replace call sites:**
650
+ ```bash
651
+ # Find all direct openai call sites to migrate
652
+ grep -rn "chat.completions.create\|openai.chat" --include="*.ts" --include="*.js" src/
653
+ ```
654
+
655
+ ### Sprint 5 โ€” God Component Surgery (1 day)
656
+
657
+ **Non-negotiable rules โ€” break any of these and you'll break the app:**
658
+ 1. Write characterization tests BEFORE touching any code. Assert current behavior including weird parts.
659
+ 2. Extract pure presentational components first (JSX only, all handlers stay in parent).
660
+ 3. Extract custom hooks second (one concern per hook, define return interface before moving logic).
661
+ 4. Commit after every single extraction โ€” never batch two extractions in one commit.
662
+ 5. Never mix structural and behavioral changes in the same commit.
663
+
664
+ **Add to ESLint config to detect rot going forward:**
665
+ ```json
666
+ {
667
+ "rules": {
668
+ "complexity": ["warn", { "max": 10 }],
669
+ "max-lines": ["warn", { "max": 300 }],
670
+ "max-lines-per-function": ["warn", { "max": 50 }],
671
+ "max-params": ["warn", 4],
672
+ "@typescript-eslint/no-floating-promises": "error",
673
+ "@typescript-eslint/no-misused-promises": "error",
674
+ "no-async-promise-executor": "error"
675
+ }
676
+ }
677
+ ```
678
+
679
+ **Safe extraction order for a god file:**
680
+ ```
681
+ Phase A โ€” Lock current behavior
682
+ โ†’ Write characterization tests (assert outputs, not implementation)
683
+ โ†’ git commit "characterization tests for [ComponentName]"
684
+
685
+ Phase B โ€” Extract presentational components (safest)
686
+ โ†’ Move JSX that only needs props, no new state/effects
687
+ โ†’ Keep ALL handlers in parent, pass them down
688
+ โ†’ Run tests โ†’ git commit
689
+
690
+ Phase C โ€” Extract derived values
691
+ โ†’ Move derived values out of useState into useMemo or plain variables
692
+ โ†’ One at a time โ†’ tests โ†’ commit
693
+
694
+ Phase D โ€” Extract custom hooks (last)
695
+ โ†’ Group related useState/useEffect pairs with single purpose into useX
696
+ โ†’ Run tests โ†’ git commit
697
+
698
+ RULE: never proceed to next phase until current phase tests are green.
699
+ ```
700
+
701
+ **Duplication consolidation โ€” Rule of Three only:**
702
+ Only consolidate logic that appears 3+ times. Two similar functions is fine. Three = extract to `src/lib/`.
703
+ ```bash
704
+ # Find top duplicate blocks
705
+ npx jscpd src/ --min-tokens 50 --reporters console 2>/dev/null | head -40
706
+ ```
707
+
708
+ ---
709
+
710
+ ## EMERGENCY MODE
711
+
712
+ **App actively failing under traffic. Do these in order. Speed > perfection.**
713
+
714
+ ### Step 1 โ€” Rate Limit (2 min)
715
+
716
+ **Express:**
717
+ ```typescript
718
+ // npm install express-rate-limit
719
+ import rateLimit from 'express-rate-limit'
720
+ app.use(rateLimit({ windowMs: 60_000, max: 60, standardHeaders: true }))
721
+ ```
722
+
723
+ **Next.js โ€” create `middleware.ts` at project root:**
724
+ ```typescript
725
+ // npm install @upstash/ratelimit @upstash/redis
726
+ import { Ratelimit } from '@upstash/ratelimit'
727
+ import { Redis } from '@upstash/redis'
728
+ import { NextResponse } from 'next/server'
729
+ const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(60, '1m') })
730
+ export async function middleware(req: Request) {
731
+ const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
732
+ const { success } = await ratelimit.limit(ip)
733
+ return success ? NextResponse.next() : new NextResponse('Too Many Requests', { status: 429 })
734
+ }
735
+ ```
736
+
737
+ **FastAPI:**
738
+ ```python
739
+ # pip install slowapi
740
+ from slowapi import Limiter
741
+ from slowapi.util import get_remote_address
742
+ limiter = Limiter(key_func=get_remote_address)
743
+ app.state.limiter = limiter
744
+ @app.post("/api/chat")
745
+ @limiter.limit("60/minute")
746
+ async def chat(request: Request): ...
747
+ ```
748
+
749
+ ### Step 2 โ€” Fix Connection Pool (2-5 min)
750
+
751
+ **Supabase โ€” zero code, one env var change:**
752
+ ```
753
+ DATABASE_URL: change port 5432 โ†’ 6543 (enables built-in PgBouncer)
754
+ ```
755
+
756
+ **Prisma:**
757
+ ```
758
+ DATABASE_URL="postgresql://...?connection_limit=1&pgbouncer=true"
759
+ ```
760
+
761
+ **node-postgres:**
762
+ ```typescript
763
+ const pool = new Pool({ max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000 })
764
+ ```
765
+
766
+ **SQLAlchemy:**
767
+ ```python
768
+ engine = create_engine(DATABASE_URL, pool_size=20, max_overflow=10, pool_pre_ping=True)
769
+ ```
770
+
771
+ ### Step 3 โ€” In-Memory Cache (3 min) โ€” cuts DB load 60-80%
772
+
773
+ **Node.js:**
774
+ ```typescript
775
+ // npm install lru-cache
776
+ import { LRUCache } from 'lru-cache'
777
+ const cache = new LRUCache<string, any>({ max: 500, ttl: 1000 * 60 * 5 })
778
+ app.get('/api/posts', async (req, res) => {
779
+ if (cache.has('posts')) return res.json(cache.get('posts'))
780
+ const data = await db.query('SELECT id, title, created_at FROM posts LIMIT 50')
781
+ cache.set('posts', data.rows)
782
+ res.json(data.rows)
783
+ })
784
+ ```
785
+
786
+ **Python:**
787
+ ```python
788
+ # pip install cachetools
789
+ from cachetools import TTLCache, cached
790
+ cache = TTLCache(maxsize=500, ttl=300)
791
+ @cached(cache)
792
+ async def get_posts(): ...
793
+ ```
794
+
795
+ ### Step 4 โ€” Detect N+1 (1 min, no restart needed with hot reload)
796
+
797
+ ```typescript
798
+ // Add temporarily to db.ts
799
+ const queryCounts = new Map<string, number>()
800
+ const _query = pool.query.bind(pool)
801
+ pool.query = (text: any, values?: any) => {
802
+ const key = (typeof text === 'string' ? text : text.text ?? '').substring(0, 80)
803
+ const n = (queryCounts.get(key) || 0) + 1
804
+ queryCounts.set(key, n)
805
+ if (n > 10) console.warn(`[N+1 ALERT] Repeated ${n}x:`, key)
806
+ return _query(text, values)
807
+ }
808
+ ```
809
+
810
+ ### Step 5 โ€” Scale (last resort, costs money)
811
+
812
+ ```bash
813
+ heroku ps:scale web=2:standard-2x
814
+ railway scale --replicas 2
815
+ fly scale count 2
816
+ ```
817
+
818
+ **Once stable: run full `/cold-shower` to find root cause.**
819
+
820
+ ---
821
+
822
+ ### Sprint 6 โ€” CI + Branch Protection + Dependabot (1 hr)
823
+
824
+ **Generate `.github/workflows/ci.yml`:**
825
+ ```yaml
826
+ name: CI
827
+ on:
828
+ push:
829
+ branches: [main]
830
+ pull_request:
831
+ branches: [main]
832
+ concurrency:
833
+ group: ${{ github.workflow }}-${{ github.ref }}
834
+ cancel-in-progress: true
835
+ permissions:
836
+ contents: read
837
+ jobs:
838
+ ci:
839
+ runs-on: ubuntu-latest
840
+ steps:
841
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
842
+ with:
843
+ persist-credentials: false
844
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
845
+ with:
846
+ node-version-file: .nvmrc
847
+ cache: npm
848
+ - run: npm ci
849
+ - run: npm run lint
850
+ - run: npm run typecheck
851
+ - run: npm test -- --passWithNoTests
852
+ - run: npm run build
853
+ ```
854
+
855
+ **Generate `.github/dependabot.yml`:**
856
+ ```yaml
857
+ version: 2
858
+ updates:
859
+ - package-ecosystem: npm
860
+ directory: "/"
861
+ schedule:
862
+ interval: weekly
863
+ groups:
864
+ all-dependencies:
865
+ patterns: ["*"]
866
+ - package-ecosystem: github-actions
867
+ directory: "/"
868
+ schedule:
869
+ interval: weekly
870
+ ```
871
+
872
+ **Pin all unpinned Actions (run once):**
873
+ ```bash
874
+ npx pinact .github/workflows/ci.yml
875
+ ```
876
+
877
+ **Branch protection via gh CLI:**
878
+ ```bash
879
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
880
+ gh api repos/$REPO/branches/main/protection \
881
+ --method PUT \
882
+ --field required_status_checks='{"strict":true,"contexts":["ci"]}' \
883
+ --field enforce_admins=false \
884
+ --field required_pull_request_reviews='{"required_approving_review_count":1}' \
885
+ --field restrictions=null \
886
+ --field allow_force_pushes=false \
887
+ --field allow_deletions=false
888
+ ```
889
+
890
+ **Generate `src/env.ts` startup validation:**
891
+ ```typescript
892
+ import { z } from 'zod'
893
+ const EnvSchema = z.object({
894
+ DATABASE_URL: z.string().url(),
895
+ NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
896
+ PORT: z.coerce.number().default(3000),
897
+ })
898
+ export const env = EnvSchema.parse(process.env)
899
+ // Add your other required vars โ€” throws at startup if any are missing
900
+ ```
901
+
902
+ ---
903
+
904
+ ## PLAN-GATE โ€” Structured Planning Before Implementation
905
+
906
+ Auto-activates when user says: "implement X", "add X", "fix X", "create X", "refactor X", "build X"
907
+
908
+ ### When plan-gate is active:
909
+ - gate.js hook blocks all Edit/Write/MultiEdit tool calls
910
+ - Claude MUST generate a structured plan first
911
+ - Edits only allowed after user types `APPROVED`
912
+ - Plan stored at `.plan-gate/plan.md`
913
+
914
+ ### Structured plan format (generate this before any code):
915
+
916
+ ```markdown
917
+ ## Plan: [task name]
918
+
919
+ ### Understanding
920
+ - Problem being solved (the WHY, not just the WHAT)
921
+ - Definition of done (behavior, not code description)
922
+ - Out of scope (explicit exclusions)
923
+
924
+ ### Files to Touch
925
+ | File | Lines | Change | Reason |
926
+ |------|-------|--------|--------|
927
+
928
+ ### Files NOT to Touch
929
+ | File | Reason |
930
+ |------|--------|
931
+
932
+ ### Contracts That Cannot Change
933
+ (API signatures, HTTP response codes, DB columns callers depend on)
934
+
935
+ ### Dependency Order
936
+ 1. First change (unlocks others)
937
+ 2. Second change
938
+ ...
939
+
940
+ ### Risk Assessment
941
+ - HIGH: [specific failure mode]
942
+ - MEDIUM: [specific failure mode]
943
+
944
+ ### Pre-Mortem
945
+ "If this fails, the most likely cause is..."
946
+
947
+ ### Rollback
948
+ (exact steps โ€” does it need migration rollback? data backfill?)
949
+ ```
950
+
951
+ ### After plan is generated:
952
+ Ask user: **"Review the plan above. Type `APPROVED` to proceed with implementation."**
953
+
954
+ Do NOT write code, create files, or edit anything until user types APPROVED.
955
+
956
+ ### After implementation:
957
+ Remind user: `"Type re-audit to verify Vibe Score didn't drop."`
958
+
959
+ ### To skip plan-gate for a quick change:
960
+ User can say "skip plan" or delete `.plan-gate/ACTIVE`
961
+
962
+ ---
963
+
964
+ ## RECALL โ€” Persistent Second Brain
965
+
966
+ Replaces Obsidian for developers who want memory inside their coding workflow.
967
+
968
+ ### Brain file locations:
969
+ ```
970
+ ~/.claude/brain/ โ† global (all projects)
971
+ preferences.md โ† coding style, tool preferences
972
+
973
+ ~/.claude/projects/<project>/brain/ โ† project-scoped
974
+ decisions.md โ† architectural choices + WHY + rejected alternatives
975
+ avoid.md โ† fragile files/areas โ€” checked by PreToolUse hook before edits
976
+ bugs.md โ† fixed bugs + how to detect regression
977
+ context.md โ† domain knowledge, user base, compliance, business context
978
+ patterns.md โ† code patterns with project-specific examples
979
+ ```
980
+
981
+ ### Memory entry format (always include WHY and date):
982
+ ```markdown
983
+ ## 2026-06-29 ยท [one-line summary]
984
+ **Decision/Pattern/Bug/Context:** [the WHAT]
985
+ **Why:** [the reason โ€” constraints, rejected alternatives, incidents]
986
+ **Source:** [commit sha, issue #, or "session decision"]
987
+ ```
988
+
989
+ ### How to save memories:
990
+
991
+ **Manual:** User says "remember that..." or "save this decision..." โ†’ append to appropriate brain file
992
+
993
+ **Auto-capture:** Stop hook (capture.js) scans session at end, surfaces 3-5 suggestions
994
+
995
+ **Anti-regression:** gate.js PreToolUse hook warns before editing files listed in avoid.md
996
+
997
+ ### Recall commands:
998
+ - `"remember [X]"` โ†’ save to appropriate brain file
999
+ - `"what did we decide about [X]"` โ†’ grep brain files and return matches
1000
+ - `"show avoid list"` โ†’ read avoid.md
1001
+ - `"show context"` โ†’ read context.md
1002
+ - `"/recall review"` โ†’ show memories older than 90 days for staleness review
1003
+ - `"forget [X]"` โ†’ remove matching entry from brain files
1004
+
1005
+ ### When user says "remember [X]", classify and save:
1006
+ - Architectural choice โ†’ `decisions.md`
1007
+ - File/area to avoid โ†’ `avoid.md` (also triggers gate.js warning on future edits)
1008
+ - Bug pattern โ†’ `bugs.md`
1009
+ - Domain/business context โ†’ `context.md`
1010
+ - Code pattern โ†’ `patterns.md`
1011
+ - Personal style โ†’ `~/.claude/brain/preferences.md`
1012
+
1013
+ ### Hard limits (never exceed):
1014
+ - Each brain file: 50 lines max โ†’ archive oldest to `brain/archive/` when full
1015
+ - Session injection: 15 headlines max (~1,500 tokens)
1016
+ - Never inject full file content at session start โ€” headers only, full content on demand
1017
+
1018
+ ### Privacy note:
1019
+ Brain files are local only. Never commit `~/.claude/brain/` or `~/.claude/projects/*/brain/` to git.
1020
+ Add to .gitignore: `.claude/`