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.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +110 -0
- package/CLAUDE.md +1 -1
- package/KODELYTH.md +46 -8
- package/README.md +20 -7
- package/SECURITY.md +6 -2
- package/VERSION +1 -1
- package/agents/incident-commander.md +227 -0
- package/agents/load-tester.md +283 -0
- package/hooks/hooks.json +28 -2
- package/hooks/memory/capture-correction.js +185 -0
- package/hooks/memory/read-lessons.js +190 -0
- package/install.sh +1 -1
- package/package.json +4 -2
- package/rules/common/agent-intent-routing.md +33 -1
- package/rules/common/self-improvement-workflow.md +126 -0
- package/social/card-agents.svg +71 -76
- package/social/card-install.svg +43 -77
- package/social/card-main.svg +36 -109
- package/social/facebook-v150.svg +119 -0
- package/social/github-social-preview.svg +110 -103
- package/social/readme-agents.svg +125 -138
- package/social/readme-hero.svg +91 -92
- package/social/x-card-agents-grid.svg +94 -70
- package/social/x-card-free.svg +37 -83
- package/social/x-card-hook.svg +37 -66
- package/wiki/Agent-Reference.md +398 -199
- package/wiki/FAQ.md +229 -142
- package/wiki/Home.md +114 -50
- package/wiki/Hook-Reference.md +197 -101
- package/wiki/Installation-Guide.md +208 -126
- package/wiki/Platform-Support.md +187 -106
- package/wiki/Skill-Reference.md +211 -103
|
@@ -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
|
|
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
|
+
}
|