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/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
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -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
+ }