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.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +42 -0
- package/README.md +264 -0
- package/bin/cold-shower.js +120 -0
- package/hooks/activate.js +40 -0
- package/hooks/capture.js +132 -0
- package/hooks/gate.js +181 -0
- package/hooks/package.json +3 -0
- package/hooks/trigger.js +164 -0
- package/package.json +34 -0
- package/skills/cold-shower/SKILL.md +1020 -0
|
@@ -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/`
|