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
package/hooks/gate.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook — two jobs:
|
|
3
|
+
// Job 1 (Plan-gate): Block edit tools until a plan is approved via .plan-gate/APPROVED.
|
|
4
|
+
// Job 2 (Anti-regression): Warn when a file being edited is listed in brain/avoid.md.
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const fs = require('fs')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
const os = require('os')
|
|
11
|
+
|
|
12
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
|
|
13
|
+
|
|
14
|
+
const PLAN_GATE_BLOCK = `PLAN-GATE: No approved plan found.
|
|
15
|
+
|
|
16
|
+
Cold-shower requires a plan before editing files.
|
|
17
|
+
Claude should generate the implementation plan first, then ask the user to type APPROVED.
|
|
18
|
+
|
|
19
|
+
To skip plan-gate for this change: delete .plan-gate/ACTIVE`
|
|
20
|
+
|
|
21
|
+
function encodeCwd(cwd) {
|
|
22
|
+
// /Users/pradipt/Code/hukumai → Users-pradipt-Code-hukumai
|
|
23
|
+
return cwd.replace(/^\//, '').replace(/\//g, '-')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fileExists(p) {
|
|
27
|
+
try { return fs.existsSync(p) } catch { return false }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readFile(p) {
|
|
31
|
+
try { return fs.readFileSync(p, 'utf8') } catch { return null }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse avoid.md for file paths.
|
|
35
|
+
// Looks for:
|
|
36
|
+
// - Lines that start with a path-like token (starts with / or ./ or contains /)
|
|
37
|
+
// - Lines that contain backtick-wrapped paths: `path/to/file`
|
|
38
|
+
// Returns array of { filePath, reason } objects.
|
|
39
|
+
function parseAvoidMd(content) {
|
|
40
|
+
const entries = []
|
|
41
|
+
const lines = content.split('\n')
|
|
42
|
+
|
|
43
|
+
// Group lines: a "path line" followed by subsequent non-path lines form one entry.
|
|
44
|
+
let i = 0
|
|
45
|
+
while (i < lines.length) {
|
|
46
|
+
const line = lines[i]
|
|
47
|
+
|
|
48
|
+
// Check for backtick path(s) anywhere on the line
|
|
49
|
+
const backtickMatches = [...line.matchAll(/`([^`]+)`/g)]
|
|
50
|
+
const backtickPaths = backtickMatches
|
|
51
|
+
.map(m => m[1])
|
|
52
|
+
.filter(p => p.includes('/') || p.endsWith('.js') || p.endsWith('.ts') ||
|
|
53
|
+
p.endsWith('.py') || p.endsWith('.rb') || p.endsWith('.go') ||
|
|
54
|
+
p.endsWith('.rs') || p.endsWith('.java') || p.endsWith('.kt') ||
|
|
55
|
+
p.endsWith('.swift') || p.endsWith('.cs') || p.endsWith('.tsx') ||
|
|
56
|
+
p.endsWith('.jsx') || p.endsWith('.vue') || p.endsWith('.php'))
|
|
57
|
+
|
|
58
|
+
if (backtickPaths.length > 0) {
|
|
59
|
+
// Treat rest of line (after the backtick) as reason
|
|
60
|
+
const reason = line.replace(/`[^`]+`/g, '').replace(/^[\s\-*#:]+/, '').trim() || line.trim()
|
|
61
|
+
for (const fp of backtickPaths) {
|
|
62
|
+
entries.push({ filePath: fp, reason: reason || line.trim() })
|
|
63
|
+
}
|
|
64
|
+
i++
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for a line that starts like a path
|
|
69
|
+
const trimmed = line.trim()
|
|
70
|
+
const pathMatch = trimmed.match(/^([\/.]?[^\s]+\.[a-zA-Z0-9]{1,10})(\s+(.*))?$/)
|
|
71
|
+
if (pathMatch && (pathMatch[1].includes('/') || pathMatch[1].startsWith('.'))) {
|
|
72
|
+
const fp = pathMatch[1]
|
|
73
|
+
let reason = (pathMatch[3] || '').trim()
|
|
74
|
+
// Gather following indented lines as part of reason
|
|
75
|
+
let j = i + 1
|
|
76
|
+
while (j < lines.length) {
|
|
77
|
+
const next = lines[j]
|
|
78
|
+
if (next.match(/^\s+\S/) || next.match(/^[>*\-]\s/)) {
|
|
79
|
+
if (!reason) reason = next.trim()
|
|
80
|
+
j++
|
|
81
|
+
} else {
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
entries.push({ filePath: fp, reason: reason || trimmed })
|
|
86
|
+
i = j
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
i++
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return entries
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Returns a warning string if filePath matches any avoid entry, otherwise null.
|
|
97
|
+
function checkAvoid(filePath, avoidEntries) {
|
|
98
|
+
if (!filePath) return null
|
|
99
|
+
const basename = path.basename(filePath)
|
|
100
|
+
const normalised = filePath.replace(/\\/g, '/')
|
|
101
|
+
|
|
102
|
+
for (const entry of avoidEntries) {
|
|
103
|
+
const ep = entry.filePath.replace(/\\/g, '/')
|
|
104
|
+
const epBasename = path.basename(ep)
|
|
105
|
+
|
|
106
|
+
// Match on full path suffix, basename, or exact match
|
|
107
|
+
if (
|
|
108
|
+
normalised.endsWith(ep) ||
|
|
109
|
+
normalised === ep ||
|
|
110
|
+
(epBasename && basename === epBasename && epBasename.length > 3) ||
|
|
111
|
+
normalised.includes(ep)
|
|
112
|
+
) {
|
|
113
|
+
const reason = entry.reason || '(no reason given)'
|
|
114
|
+
return `RECALL WARNING: ${basename} is marked fragile in recall memory. Reason: ${reason}. Proceed carefully.`
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function main(data) {
|
|
122
|
+
const toolName = data.tool_name || ''
|
|
123
|
+
const toolInput = data.tool_input || {}
|
|
124
|
+
|
|
125
|
+
if (!EDIT_TOOLS.has(toolName)) {
|
|
126
|
+
// Not an edit tool — nothing to do
|
|
127
|
+
process.exit(0)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cwd = process.cwd()
|
|
131
|
+
const filePath = toolInput.file_path || toolInput.path || ''
|
|
132
|
+
|
|
133
|
+
// ── Job 1: Plan-gate ──────────────────────────────────────────────────────
|
|
134
|
+
const activePath = path.join(cwd, '.plan-gate', 'ACTIVE')
|
|
135
|
+
const approvedPath = path.join(cwd, '.plan-gate', 'APPROVED')
|
|
136
|
+
|
|
137
|
+
if (fileExists(activePath) && !fileExists(approvedPath)) {
|
|
138
|
+
process.stderr.write(PLAN_GATE_BLOCK + '\n')
|
|
139
|
+
process.exit(2)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Job 2: Anti-regression warn ──────────────────────────────────────────
|
|
143
|
+
const encodedCwd = encodeCwd(cwd)
|
|
144
|
+
const homeDir = os.homedir()
|
|
145
|
+
|
|
146
|
+
const avoidPaths = [
|
|
147
|
+
path.join(homeDir, '.claude', 'projects', encodedCwd, 'brain', 'avoid.md'),
|
|
148
|
+
path.join(homeDir, '.claude', 'brain', 'avoid-global.md'),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
let avoidEntries = []
|
|
152
|
+
for (const ap of avoidPaths) {
|
|
153
|
+
const content = readFile(ap)
|
|
154
|
+
if (content) {
|
|
155
|
+
avoidEntries = avoidEntries.concat(parseAvoidMd(content))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (avoidEntries.length > 0) {
|
|
160
|
+
const warning = checkAvoid(filePath, avoidEntries)
|
|
161
|
+
if (warning) {
|
|
162
|
+
process.stderr.write(warning + '\n')
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Allow the edit
|
|
167
|
+
process.exit(0)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Read stdin fully, then process
|
|
171
|
+
let input = ''
|
|
172
|
+
process.stdin.on('data', chunk => { input += chunk })
|
|
173
|
+
process.stdin.on('end', () => {
|
|
174
|
+
try {
|
|
175
|
+
const data = JSON.parse(input)
|
|
176
|
+
main(data)
|
|
177
|
+
} catch {
|
|
178
|
+
// Silent fail — never break user session
|
|
179
|
+
process.exit(0)
|
|
180
|
+
}
|
|
181
|
+
})
|
package/hooks/trigger.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// UserPromptSubmit hook — detects cold-shower trigger phrases in the user's prompt.
|
|
3
|
+
// When matched, reinforces the skill context so Claude activates it automatically.
|
|
4
|
+
|
|
5
|
+
let input = ''
|
|
6
|
+
process.stdin.on('data', chunk => { input += chunk })
|
|
7
|
+
process.stdin.on('end', () => {
|
|
8
|
+
try {
|
|
9
|
+
const data = JSON.parse(input)
|
|
10
|
+
const prompt = (data.prompt || '').toLowerCase()
|
|
11
|
+
|
|
12
|
+
const triggers = [
|
|
13
|
+
'cold shower', 'cold-shower',
|
|
14
|
+
'audit my codebase', 'audit my app', 'audit my project', 'audit this',
|
|
15
|
+
'is this ready to ship', 'ready to ship', 'ready to deploy', 'can i ship',
|
|
16
|
+
'reality check',
|
|
17
|
+
'something always breaks', 'always breaks when i',
|
|
18
|
+
'llm bill', 'api bill', 'openai bill', 'anthropic bill',
|
|
19
|
+
'costs too high', 'spending too much', 'api costs',
|
|
20
|
+
'is my ai secure', 'ai endpoint', 'prompt injection',
|
|
21
|
+
'clean up my deps', 'unused packages', 'too many packages', 'remove packages',
|
|
22
|
+
'app crashed', 'app is down', '500 errors', 'getting 500s',
|
|
23
|
+
'too many connections', 'database overload', 'db is slow',
|
|
24
|
+
'went viral', 'hacker news', 'product hunt', 'traffic spike',
|
|
25
|
+
'vibe code mess', 'vibe coding debt', 'my code is a mess', 'codebase is a mess',
|
|
26
|
+
'god component', 'god file', 'file is too long',
|
|
27
|
+
'circular dep', 'duplicate code', 'technical debt',
|
|
28
|
+
// Audit F — git/github/CI triggers
|
|
29
|
+
'github setup', 'ci setup', 'github actions', 'branch protection',
|
|
30
|
+
'gitignore', '.gitignore', 'secrets in git', 'env committed',
|
|
31
|
+
'no ci', 'missing ci', 'no pipeline', 'devops setup',
|
|
32
|
+
'staging environment', 'production environment', 'preview deployment',
|
|
33
|
+
'dependabot', 'unpinned actions', 'workflow security',
|
|
34
|
+
'git hygiene', 'git setup', 'repo setup', 'missing workflow',
|
|
35
|
+
// Pre-deploy gate triggers
|
|
36
|
+
'about to deploy', 'about to push', 'about to ship',
|
|
37
|
+
'pushing to prod', 'pushing to production', 'merging to main',
|
|
38
|
+
'going live', 'going to prod', 'before i push', 'before i deploy',
|
|
39
|
+
'ready to merge', 'shipping today', 'deploy today',
|
|
40
|
+
// Re-audit / score check triggers
|
|
41
|
+
're-audit', 'reaudit', 'check score', 'check my score',
|
|
42
|
+
'did score improve', 'run audit again', 'audit again',
|
|
43
|
+
// Plan-gate triggers — implementation intent
|
|
44
|
+
'implement', 'add feature', 'add endpoint', 'add support for',
|
|
45
|
+
'build this', 'create component', 'create a new', 'create the',
|
|
46
|
+
'fix this bug', 'fix the bug', 'fix the issue', 'fix the error',
|
|
47
|
+
'refactor', 'migrate', 'rewrite', 'update the', 'modify the',
|
|
48
|
+
// Recall triggers — memory commands
|
|
49
|
+
'remember this', 'remember that', 'save this', 'dont forget',
|
|
50
|
+
"don't forget", 'recall', 'what did we decide', 'brain dump',
|
|
51
|
+
'/recall', '/plan',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const matched = triggers.some(t => prompt.includes(t))
|
|
55
|
+
|
|
56
|
+
if (!matched) return
|
|
57
|
+
|
|
58
|
+
// Detect which mode triggered
|
|
59
|
+
const planTriggers = ['implement', 'add feature', 'add endpoint', 'add support for',
|
|
60
|
+
'build this', 'create component', 'create a new', 'create the',
|
|
61
|
+
'fix this bug', 'fix the bug', 'fix the issue', 'fix the error',
|
|
62
|
+
'refactor', 'migrate', 'rewrite', 'update the', 'modify the']
|
|
63
|
+
const recallTriggers = ['remember this', 'remember that', 'save this', 'dont forget',
|
|
64
|
+
"don't forget", 'recall', 'what did we decide', 'brain dump', '/recall']
|
|
65
|
+
const auditTriggers = ['cold shower', 'cold-shower', 'audit', 'vibe score',
|
|
66
|
+
'ready to ship', 'ready to deploy', 'about to deploy', 'about to push',
|
|
67
|
+
're-audit', 'reaudit', 'check score']
|
|
68
|
+
|
|
69
|
+
const isPlan = planTriggers.some(t => prompt.includes(t))
|
|
70
|
+
const isRecall = recallTriggers.some(t => prompt.includes(t))
|
|
71
|
+
const isAudit = auditTriggers.some(t => prompt.includes(t))
|
|
72
|
+
|
|
73
|
+
// Load score history for audit mode
|
|
74
|
+
const scoreHistoryPath = require('path').join(process.cwd(), '.cold-shower', 'score-history.json')
|
|
75
|
+
let scoreHistory = []
|
|
76
|
+
try { scoreHistory = JSON.parse(require('fs').readFileSync(scoreHistoryPath, 'utf8')) } catch {}
|
|
77
|
+
const lastScore = scoreHistory.length > 0 ? scoreHistory[scoreHistory.length - 1] : null
|
|
78
|
+
const scoreContext = lastScore
|
|
79
|
+
? `Last Vibe Score: ${lastScore.score}/100 (${lastScore.grade}) on ${lastScore.date}.`
|
|
80
|
+
: 'No previous Vibe Score.'
|
|
81
|
+
|
|
82
|
+
// Load recall memories
|
|
83
|
+
const path = require('path')
|
|
84
|
+
const os = require('os')
|
|
85
|
+
const projectName = path.basename(process.cwd())
|
|
86
|
+
const brainDir = path.join(os.homedir(), '.claude', 'brain')
|
|
87
|
+
const projectBrainDir = path.join(os.homedir(), '.claude', 'projects', projectName, 'brain')
|
|
88
|
+
let recallContext = ''
|
|
89
|
+
try {
|
|
90
|
+
const files = ['context.md', 'decisions.md', 'avoid.md', 'bugs.md']
|
|
91
|
+
const lines = []
|
|
92
|
+
for (const f of files) {
|
|
93
|
+
try {
|
|
94
|
+
const content = require('fs').readFileSync(path.join(projectBrainDir, f), 'utf8')
|
|
95
|
+
lines.push(...content.split('\n').filter(l => l.startsWith('##')).slice(0, 3))
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
if (lines.length > 0) recallContext = '\nRECALL CONTEXT:\n' + lines.slice(0, 8).join('\n')
|
|
99
|
+
} catch {}
|
|
100
|
+
|
|
101
|
+
// Create plan-gate ACTIVE marker if implementation intent
|
|
102
|
+
if (isPlan && !isAudit) {
|
|
103
|
+
try {
|
|
104
|
+
const fs = require('fs')
|
|
105
|
+
fs.mkdirSync(path.join(process.cwd(), '.plan-gate'), { recursive: true })
|
|
106
|
+
fs.writeFileSync(path.join(process.cwd(), '.plan-gate', 'ACTIVE'), new Date().toISOString())
|
|
107
|
+
// Remove any stale approval
|
|
108
|
+
try { fs.unlinkSync(path.join(process.cwd(), '.plan-gate', 'APPROVED')) } catch {}
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let additionalContext = ''
|
|
113
|
+
|
|
114
|
+
if (isRecall && !isPlan && !isAudit) {
|
|
115
|
+
additionalContext = [
|
|
116
|
+
'COLD-SHOWER RECALL MODE',
|
|
117
|
+
recallContext,
|
|
118
|
+
'User wants to manage memories. Available commands:',
|
|
119
|
+
'- "remember [decision/pattern/bug/context]" → save to brain',
|
|
120
|
+
'- "what did we decide about X" → search brain files',
|
|
121
|
+
'- "show avoid list" → show fragile files',
|
|
122
|
+
'- "/recall review" → review memories older than 90 days',
|
|
123
|
+
'Brain files: ~/.claude/brain/ (global) and ~/.claude/projects/' + projectName + '/brain/ (project)',
|
|
124
|
+
].join('\n')
|
|
125
|
+
} else if (isPlan && !isAudit) {
|
|
126
|
+
additionalContext = [
|
|
127
|
+
'COLD-SHOWER PLAN-GATE ACTIVATED',
|
|
128
|
+
recallContext,
|
|
129
|
+
'User wants to implement something. Generate a structured plan BEFORE writing any code:',
|
|
130
|
+
'',
|
|
131
|
+
'## Plan: [task name]',
|
|
132
|
+
'### Understanding — problem, definition of done, out of scope',
|
|
133
|
+
'### Files to Touch — table: file | lines | change type | reason',
|
|
134
|
+
'### Files NOT to Touch — table: file | reason (check recall avoid.md for fragile files)',
|
|
135
|
+
'### Contracts That Cannot Change — API signatures, response shapes callers depend on',
|
|
136
|
+
'### Dependency Order — numbered, which changes unlock others',
|
|
137
|
+
'### Risk Assessment — HIGH/MEDIUM/LOW with specific failure mode',
|
|
138
|
+
'### Pre-Mortem — "If this fails, most likely cause is..."',
|
|
139
|
+
'### Rollback — exact steps to undo, does it need migration rollback?',
|
|
140
|
+
'',
|
|
141
|
+
'After generating the plan: ask user "Type APPROVED to proceed with implementation."',
|
|
142
|
+
'Do NOT write any code or edit any files until user types APPROVED.',
|
|
143
|
+
'Plan-gate is active — edits are blocked until approved.',
|
|
144
|
+
].join('\n')
|
|
145
|
+
} else {
|
|
146
|
+
// Audit mode (default)
|
|
147
|
+
additionalContext = [
|
|
148
|
+
'COLD-SHOWER SKILL ACTIVATED',
|
|
149
|
+
scoreContext,
|
|
150
|
+
recallContext,
|
|
151
|
+
'Run the audit workflow:',
|
|
152
|
+
'1. Emergency check — app actively failing? Jump to EMERGENCY MODE.',
|
|
153
|
+
'2. Phase 0: detect stack.',
|
|
154
|
+
'3. Phase 1: run audits A-F in parallel.',
|
|
155
|
+
'4. Phase 2: compute Vibe Score, write to .cold-shower/score-history.json.',
|
|
156
|
+
'5. Phase 3: ask which fix sprint. After sprint: remind user to type re-audit.',
|
|
157
|
+
].join('\n')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.stdout.write(JSON.stringify({ additionalContext }))
|
|
161
|
+
} catch {
|
|
162
|
+
// Silent fail — never break the user session
|
|
163
|
+
}
|
|
164
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cold-shower",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Reality check for vibe-coded apps. 6 audits + plan-gate + second brain for Claude Code.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cold-shower": "bin/cold-shower.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"skills/",
|
|
11
|
+
"hooks/",
|
|
12
|
+
".claude-plugin/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude-code",
|
|
16
|
+
"claude",
|
|
17
|
+
"skill",
|
|
18
|
+
"vibe-coding",
|
|
19
|
+
"ai",
|
|
20
|
+
"security",
|
|
21
|
+
"audit",
|
|
22
|
+
"plan-gate"
|
|
23
|
+
],
|
|
24
|
+
"author": "PradiptaPutra",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/PradiptaPutra/cold-shower.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/PradiptaPutra/cold-shower",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
}
|
|
34
|
+
}
|