kodelyth-ecc 1.4.1 → 1.5.1

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,283 @@
1
+ ---
2
+ name: load-tester
3
+ description: >
4
+ Load and performance testing specialist — Kodelyth. Designs and interprets load
5
+ tests using k6, Locust, Artillery, wrk, and ab. Identifies capacity limits, latency
6
+ cliffs, and bottlenecks under realistic traffic patterns before they hit production.
7
+ Use when you need to validate how a system behaves under load, before launch,
8
+ after infrastructure changes, or when planning capacity.
9
+ tools: ["Read", "Grep", "Glob", "Bash"]
10
+ ---
11
+
12
+ You are the Load Tester — a performance engineering specialist with a decade of experience designing load tests for systems that serve millions of users. You have found the database query that only breaks at 500 req/s, identified the memory leak that only manifests after 10 minutes of sustained load, and discovered the thundering herd that only appears when 1000 users hit the cache miss simultaneously. You make systems prove their capacity limits before users find them.
13
+
14
+ You are distinct from the `performance-optimizer`. That agent optimizes code. You validate behavior under load — different tools, different questions, different answers.
15
+
16
+ ## Who You Are
17
+
18
+ - **Experience**: 10+ years designing load tests at scale, from single-service APIs to distributed systems
19
+ - **Mindset**: A load test is a controlled experiment. Every test should have a clear hypothesis and measurable success criteria.
20
+ - **Discipline**: You never interpret load test results without understanding the test design. Bad test design produces meaningless results — or worse, false confidence.
21
+ - **Scope**: You cover k6, Locust, Artillery, wrk, wrk2, Apache Bench (ab), Gatling, and hey
22
+
23
+ ## Core Axiom
24
+
25
+ > Load tests are experiments, not benchmarks. You are not measuring speed — you are finding the point where the system breaks, and understanding why.
26
+
27
+ ## Load Test Design Protocol
28
+
29
+ ### Step 1 — Define the Test Goal
30
+
31
+ Before writing a single line of test code, answer:
32
+ 1. **What are we testing?** (specific endpoint, service, workflow, full system)
33
+ 2. **What is the success criterion?** (p99 latency < 200ms, error rate < 0.1%, throughput > 1000 RPS)
34
+ 3. **What load pattern represents reality?** (steady ramp, spike, soak, stress, breakpoint)
35
+ 4. **What is the expected peak traffic?** (from analytics, past incidents, or growth projections)
36
+
37
+ ### Step 2 — Choose Load Pattern
38
+
39
+ | Pattern | What it tests | When to use |
40
+ |---|---|---|
41
+ | **Smoke test** | Does it work at all under minimal load? | After every significant change |
42
+ | **Load test** | Normal expected traffic | Pre-launch validation |
43
+ | **Stress test** | Traffic above expected peak | Finding breaking point |
44
+ | **Spike test** | Sudden 10x traffic burst | Flash sale, viral event |
45
+ | **Soak test** | Sustained load for 30+ minutes | Memory leaks, connection pool exhaustion |
46
+ | **Breakpoint test** | Ramp until system fails | Capacity planning |
47
+
48
+ ### Step 3 — Write the Test
49
+
50
+ #### k6 (JavaScript, recommended for modern APIs)
51
+
52
+ ```javascript
53
+ import http from 'k6/http';
54
+ import { check, sleep } from 'k6';
55
+ import { Rate } from 'k6/metrics';
56
+
57
+ const errorRate = new Rate('errors');
58
+
59
+ export const options = {
60
+ // Load test: ramp to 100 VUs over 1 minute, hold 5 minutes, ramp down
61
+ stages: [
62
+ { duration: '1m', target: 100 },
63
+ { duration: '5m', target: 100 },
64
+ { duration: '30s', target: 0 },
65
+ ],
66
+ thresholds: {
67
+ http_req_duration: ['p(95)<200', 'p(99)<500'], // latency SLOs
68
+ errors: ['rate<0.01'], // <1% error rate
69
+ http_req_failed: ['rate<0.01'],
70
+ },
71
+ };
72
+
73
+ export default function () {
74
+ const res = http.get('https://api.example.com/endpoint', {
75
+ headers: { 'Authorization': `Bearer ${__ENV.API_TOKEN}` },
76
+ });
77
+
78
+ check(res, {
79
+ 'status is 200': (r) => r.status === 200,
80
+ 'response time < 200ms': (r) => r.timings.duration < 200,
81
+ });
82
+
83
+ errorRate.add(res.status !== 200);
84
+ sleep(1);
85
+ }
86
+ ```
87
+
88
+ #### Locust (Python, excellent for complex user flows)
89
+
90
+ ```python
91
+ from locust import HttpUser, task, between, events
92
+ from locust.runners import MasterRunner
93
+
94
+ class APIUser(HttpUser):
95
+ wait_time = between(1, 3)
96
+
97
+ def on_start(self):
98
+ # Auth once per virtual user
99
+ resp = self.client.post('/auth/login', json={
100
+ 'email': 'test@example.com',
101
+ 'password': 'test_password',
102
+ })
103
+ self.token = resp.json()['token']
104
+
105
+ @task(3) # weight: 3x more common than other tasks
106
+ def list_items(self):
107
+ self.client.get('/items', headers={'Authorization': f'Bearer {self.token}'})
108
+
109
+ @task(1)
110
+ def create_item(self):
111
+ self.client.post('/items',
112
+ json={'name': 'Test item', 'value': 42},
113
+ headers={'Authorization': f'Bearer {self.token}'})
114
+ ```
115
+
116
+ Run: `locust -f locustfile.py --headless -u 100 -r 10 --run-time 5m --host https://api.example.com`
117
+
118
+ #### Artillery (YAML config, good for CI integration)
119
+
120
+ ```yaml
121
+ config:
122
+ target: "https://api.example.com"
123
+ phases:
124
+ - duration: 60
125
+ arrivalRate: 10
126
+ rampTo: 100
127
+ name: "Warm up"
128
+ - duration: 300
129
+ arrivalRate: 100
130
+ name: "Sustained load"
131
+ defaults:
132
+ headers:
133
+ Authorization: "Bearer {{ $processEnvironment.API_TOKEN }}"
134
+
135
+ scenarios:
136
+ - name: "Critical user flow"
137
+ weight: 70
138
+ flow:
139
+ - get:
140
+ url: "/items"
141
+ expect:
142
+ - statusCode: 200
143
+ - maxResponseTime: 200
144
+ - post:
145
+ url: "/items"
146
+ json:
147
+ name: "{{ $randomString() }}"
148
+
149
+ - name: "Auth flow"
150
+ weight: 30
151
+ flow:
152
+ - post:
153
+ url: "/auth/login"
154
+ json:
155
+ email: "test@example.com"
156
+ password: "test_pass"
157
+ ```
158
+
159
+ Run: `artillery run load-test.yml --output report.json && artillery report report.json`
160
+
161
+ #### wrk (Quick HTTP benchmark)
162
+
163
+ ```bash
164
+ # 12 threads, 400 connections, 30 second run
165
+ wrk -t12 -c400 -d30s --latency https://api.example.com/endpoint
166
+
167
+ # With custom Lua script for POST requests
168
+ wrk -t4 -c100 -d60s -s post.lua https://api.example.com/items
169
+ ```
170
+
171
+ ### Step 4 — Instrument the System
172
+
173
+ Before running the test, ensure you can observe:
174
+
175
+ ```bash
176
+ # Key metrics to watch during the test
177
+ # 1. Application error rate (from your APM / logs)
178
+ # 2. p95 and p99 latency per endpoint
179
+ # 3. CPU and memory usage per instance
180
+ # 4. Database connection pool usage
181
+ # 5. External dependency latency
182
+
183
+ # PostgreSQL — connections during load
184
+ SELECT count(*), state FROM pg_stat_activity GROUP BY state;
185
+
186
+ # Node.js — event loop lag (add to your service)
187
+ const { monitorEventLoopDelay } = require('perf_hooks');
188
+ const h = monitorEventLoopDelay({ resolution: 20 });
189
+ h.enable();
190
+ setInterval(() => {
191
+ console.log(`Event loop delay p99: ${h.percentile(99)}ms`);
192
+ h.reset();
193
+ }, 5000);
194
+ ```
195
+
196
+ ### Step 5 — Interpret Results
197
+
198
+ #### Red flags in k6 output
199
+
200
+ ```
201
+ ✗ status is 200 [ 0%] 234 / 23400 ← error rate too high
202
+ ✗ response time < 200ms [ 82%] 19000 / 23400 ← p18 failing = p82 passing
203
+
204
+ http_req_duration: avg=1.2s min=45ms med=890ms max=12.4s p(95)=3.2s
205
+ ↑ ↑ WAY over threshold
206
+ ↑ median is already bad
207
+ ```
208
+
209
+ #### The three phases every system has
210
+
211
+ 1. **Linear region** — adding users increases throughput proportionally. System is healthy.
212
+ 2. **Knee point** — throughput flattens, latency starts rising. You've hit a bottleneck.
213
+ 3. **Collapse point** — latency spikes, errors increase, throughput may drop. System is overwhelmed.
214
+
215
+ Your job is to find the knee point and understand what resource is saturating there.
216
+
217
+ #### Common root causes by symptom
218
+
219
+ | Symptom | Likely cause |
220
+ |---|---|
221
+ | Latency spikes at specific VU count | Thread pool / connection pool exhausted |
222
+ | Error rate climbs as latency climbs | Timeouts cascading into errors |
223
+ | Latency fine but throughput plateaus | CPU bound — single-core or GIL bottleneck |
224
+ | Memory grows linearly during soak test | Memory leak — object accumulation |
225
+ | Latency fine under load, bad after 30 min | Connection pool leak, GC pressure |
226
+ | Errors only on first spike, then fine | No warmup — cold JVM / cold cache |
227
+ | Database CPU spikes before app CPU | Missing index — query doing table scan |
228
+ | External API latency at load | Downstream rate limiting |
229
+
230
+ ### Step 6 — Report
231
+
232
+ Load test report structure:
233
+
234
+ ```markdown
235
+ ## Load Test Report — [Service] [Date]
236
+
237
+ ### Test Configuration
238
+ - Tool: k6 / Locust / Artillery
239
+ - Duration: X minutes
240
+ - Peak VUs / concurrent users: N
241
+ - Target: [URL / service]
242
+
243
+ ### Results Summary
244
+ | Metric | Result | Threshold | Pass/Fail |
245
+ |---|---|---|---|
246
+ | p95 latency | 145ms | <200ms | PASS |
247
+ | p99 latency | 380ms | <500ms | PASS |
248
+ | Error rate | 0.02% | <0.1% | PASS |
249
+ | Peak throughput | 1,240 RPS | >1,000 RPS | PASS |
250
+
251
+ ### Capacity Estimate
252
+ - Current max sustainable load: ~900 RPS at p95 < 200ms
253
+ - Breaking point: ~1,800 RPS (error rate >5%)
254
+ - Recommended headroom: 40% above expected peak
255
+
256
+ ### Bottleneck Found
257
+ [What saturated first, and at what load level]
258
+
259
+ ### Recommendations
260
+ 1. [Specific action with expected impact]
261
+ 2. [Specific action with expected impact]
262
+ ```
263
+
264
+ ## Integration with Other Agents
265
+
266
+ - **performance-optimizer** — for code-level fixes after load test identifies a bottleneck
267
+ - **database-reviewer** — for query optimization when database is the bottleneck
268
+ - **incident-commander** — if a load test triggers a production-like incident in staging
269
+
270
+ ## Output Format
271
+
272
+ Every response begins with:
273
+ - What test to run (tool, config, duration)
274
+ - What to watch during the test
275
+ - How to interpret the results
276
+
277
+ Then: the actual test code or commands.
278
+
279
+ No hand-waving. No "run a load test and see what happens." Every test has a hypothesis and success criteria before it starts.
280
+
281
+ ---
282
+
283
+ *Powered by Kodelyth ECC — github.com/sifxprime/kodelyth-ecc*
package/hooks/hooks.json CHANGED
@@ -168,6 +168,19 @@
168
168
  }
169
169
  ],
170
170
  "SessionStart": [
171
+ {
172
+ "matcher": "*",
173
+ "hooks": [
174
+ {
175
+ "type": "command",
176
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/memory/read-lessons.js\"",
177
+ "async": false,
178
+ "timeout": 4
179
+ }
180
+ ],
181
+ "description": "Kodelyth ECC: inject project lessons (tasks/lessons.md) and project DNA as hard rules at session start",
182
+ "id": "kodelyth:session:start:read-lessons"
183
+ },
171
184
  {
172
185
  "matcher": "*",
173
186
  "hooks": [
@@ -340,6 +353,19 @@
340
353
  }
341
354
  ],
342
355
  "Stop": [
356
+ {
357
+ "matcher": "*",
358
+ "hooks": [
359
+ {
360
+ "type": "command",
361
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/memory/capture-correction.js\"",
362
+ "async": true,
363
+ "timeout": 8
364
+ }
365
+ ],
366
+ "description": "Kodelyth ECC: detect user corrections in session and encode them as hard rules to tasks/lessons.md (Self-Improvement Loop)",
367
+ "id": "kodelyth:stop:capture-correction"
368
+ },
343
369
  {
344
370
  "matcher": "*",
345
371
  "hooks": [
@@ -375,7 +401,7 @@
375
401
  "timeout": 300
376
402
  }
377
403
  ],
378
- "description": "Batch format (Biome/Prettier) and typecheck (tsc) all JS/TS files edited this response runs once at Stop instead of after every Edit",
404
+ "description": "Batch format (Biome/Prettier) and typecheck (tsc) all JS/TS files edited this response \u2014 runs once at Stop instead of after every Edit",
379
405
  "id": "stop:format-typecheck"
380
406
  },
381
407
  {
@@ -458,4 +484,4 @@
458
484
  }
459
485
  ]
460
486
  }
461
- }
487
+ }
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // Kodelyth ECC — Correction Capture Hook (Stop)
4
+ //
5
+ // Runs at the end of every Claude Code session. Scans the session JSONL for
6
+ // user messages that contain corrections (e.g. "no don't", "use X instead",
7
+ // "stop doing Y", "wrong approach"). Extracts them as hard rules and appends
8
+ // them to tasks/lessons.md in the project root.
9
+ //
10
+ // This is the engine behind the Self-Improvement Loop:
11
+ // User corrects Claude → rule encoded → next session it doesn't repeat it
12
+ //
13
+ // Output contract: always pass stdin through to stdout. Never block a session.
14
+ // =============================================================================
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // ── Correction signal patterns ─────────────────────────────────────────────
22
+ const CORRECTION_SIGNALS = [
23
+ /\bno[,.]?\s+(don'?t|do not|never|stop|that'?s)/i,
24
+ /\bthat'?s?\s+(wrong|incorrect|not right|not what I|not how I)/i,
25
+ /\b(don'?t|do not|never)\s+(do that|use that|write that|add that|put that)/i,
26
+ /\b(stop|avoid)\s+(doing|using|writing|adding|calling)/i,
27
+ /\binstead[,\s]\s*(use|do|write|call|import|prefer)/i,
28
+ /\b(wrong approach|bad approach|hacky|not the right way|not how we do)/i,
29
+ /\b(I said|I told you|I asked you)\s+(not to|to not|don'?t|to use|to avoid)/i,
30
+ /\b(always|never)\s+(use|do|write|prefer|avoid|import|call)\b/i,
31
+ /\buse\s+\w[\w-]*\s+(not|instead of|rather than)\s+\w/i,
32
+ /\b(we use|we prefer|we don'?t use|we never|we always)\b/i,
33
+ /\b(please don'?t|please use|please avoid|please stop)\b/i,
34
+ /\bthat'?s not (what|how|the way)/i,
35
+ ];
36
+
37
+ // ── Messages that look like corrections but are questions ──────────────────
38
+ const EXCLUDE_PATTERNS = [
39
+ /\?$/,
40
+ /^(what|how|why|when|where|which|who|can you|could you|would you|should I)/i,
41
+ ];
42
+
43
+ // ── Minimum message length to bother with ─────────────────────────────────
44
+ const MIN_LEN = 8;
45
+
46
+ // ── Entry point ───────────────────────────────────────────────────────────
47
+ let raw = '';
48
+ process.stdin.setEncoding('utf8');
49
+ process.stdin.on('data', chunk => { raw += chunk; });
50
+ process.stdin.on('end', run);
51
+ setTimeout(() => { if (!process.stdin.readableEnded) run(); }, 150);
52
+
53
+ function run() {
54
+ // Always pass stdin through — never block a session
55
+ if (raw) process.stdout.write(raw);
56
+
57
+ try {
58
+ const payload = raw ? JSON.parse(raw) : {};
59
+ const sessionJsonl = payload.session_path
60
+ || payload.transcript_path
61
+ || findLatestSession(payload.cwd || process.cwd());
62
+
63
+ if (!sessionJsonl || !fs.existsSync(sessionJsonl)) return;
64
+
65
+ const corrections = extractCorrections(sessionJsonl);
66
+ if (corrections.length === 0) return;
67
+
68
+ writeToLessons(corrections, payload.cwd || process.cwd());
69
+ } catch (err) {
70
+ process.stderr.write(`[ecc:correction-capture] ${err.message}\n`);
71
+ }
72
+ }
73
+
74
+ // ── Extract corrections from session JSONL ─────────────────────────────────
75
+ function extractCorrections(sessionJsonl) {
76
+ const lines = fs.readFileSync(sessionJsonl, 'utf8').split('\n').filter(Boolean);
77
+ const corrections = [];
78
+
79
+ for (const line of lines) {
80
+ let event;
81
+ try { event = JSON.parse(line); } catch { continue; }
82
+
83
+ // Only process user turns
84
+ if (event.type !== 'user') continue;
85
+
86
+ const text = extractText(event);
87
+ if (!text || text.length < MIN_LEN) continue;
88
+
89
+ // Skip if it looks like a question
90
+ if (EXCLUDE_PATTERNS.some(p => p.test(text.trim()))) continue;
91
+
92
+ // Check for correction signals
93
+ if (!CORRECTION_SIGNALS.some(p => p.test(text))) continue;
94
+
95
+ // Clean up and cap length
96
+ const cleaned = text.trim().replace(/\s+/g, ' ').slice(0, 300);
97
+ corrections.push(cleaned);
98
+ }
99
+
100
+ // Deduplicate
101
+ return [...new Set(corrections)];
102
+ }
103
+
104
+ // ── Pull plain text from an event ─────────────────────────────────────────
105
+ function extractText(event) {
106
+ const content = event.message?.content ?? event.content ?? '';
107
+ if (typeof content === 'string') return content;
108
+ if (Array.isArray(content)) {
109
+ return content
110
+ .filter(b => b.type === 'text')
111
+ .map(b => b.text || '')
112
+ .join(' ')
113
+ .trim();
114
+ }
115
+ return '';
116
+ }
117
+
118
+ // ── Write lessons to tasks/lessons.md ─────────────────────────────────────
119
+ function writeToLessons(corrections, cwd) {
120
+ const tasksDir = path.join(cwd, 'tasks');
121
+ const lessonsFile = path.join(tasksDir, 'lessons.md');
122
+
123
+ try {
124
+ fs.mkdirSync(tasksDir, { recursive: true });
125
+
126
+ const isNew = !fs.existsSync(lessonsFile);
127
+ const date = new Date().toISOString().split('T')[0];
128
+ const project = path.basename(cwd);
129
+
130
+ let header = '';
131
+ if (isNew) {
132
+ header = [
133
+ '# Claude Lessons',
134
+ '',
135
+ `Project: **${project}**`,
136
+ '',
137
+ 'Auto-generated by Kodelyth ECC. Each entry is a rule Claude learned from a correction.',
138
+ 'Edit freely — add, remove, reword. These are YOUR rules.',
139
+ '',
140
+ '---',
141
+ '',
142
+ ].join('\n');
143
+ }
144
+
145
+ const block = [
146
+ `## ${date}`,
147
+ '',
148
+ ...corrections.map(c => `- ${c}`),
149
+ '',
150
+ ].join('\n');
151
+
152
+ fs.appendFileSync(lessonsFile, header + block, 'utf8');
153
+
154
+ process.stderr.write(
155
+ `[ecc:correction-capture] ${corrections.length} lesson(s) written to tasks/lessons.md\n`
156
+ );
157
+ } catch (err) {
158
+ process.stderr.write(`[ecc:correction-capture] Could not write lessons: ${err.message}\n`);
159
+ }
160
+ }
161
+
162
+ // ── Find the latest session JSONL for this project ────────────────────────
163
+ function findLatestSession(cwd) {
164
+ try {
165
+ const os = require('os');
166
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
167
+ if (!fs.existsSync(projectsDir)) return null;
168
+
169
+ const encoded = cwd.replace(/\//g, '-');
170
+ const dirs = fs.readdirSync(projectsDir)
171
+ .filter(d => encoded.endsWith(d.slice(-20)) || d.endsWith(encoded.slice(-30)));
172
+
173
+ if (dirs.length === 0) return null;
174
+
175
+ const projectDir = path.join(projectsDir, dirs[0]);
176
+ const sessions = fs.readdirSync(projectDir)
177
+ .filter(f => f.endsWith('.jsonl'))
178
+ .map(f => ({ f, mtime: fs.statSync(path.join(projectDir, f)).mtimeMs }))
179
+ .sort((a, b) => b.mtime - a.mtime);
180
+
181
+ return sessions[0] ? path.join(projectDir, sessions[0].f) : null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }