kodelyth-ecc 2.4.3 → 2.4.5
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/CHANGELOG.md +50 -0
- package/VERSION +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/safety/prompt-injection-guard.js +7 -2
- package/package.json +1 -1
- package/scripts/memory/extract.js +57 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kodelyth ECC are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.4.5 — Real-user audit: auto-capture never captured anything (July 2026)
|
|
6
|
+
|
|
7
|
+
Third "installed but dummy" feature found in the audit — and the biggest. **The entire memory-capture side was dead.**
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`scripts/memory/extract.js` mined ZERO candidates from real transcripts.** Real Claude Code transcript events nest `role`, `content`, and tool calls under `.message` (e.g. `event.message.role`, `content[].type === 'tool_use'`). `extractCandidates()` read flat `ev.role` / `ev.tool_name` / `ev.tool_input`, so it skipped every event and never captured a single memory. The 52 memories in the store came from seeds and MCP `capture_memory` — the automatic Stop-hook capture had **never worked** on real data.
|
|
12
|
+
- **Fix**: `readTranscript()` now normalizes every raw event to the flat shape downstream code expects — resolving `role` from `message.role`, surfacing the first `tool_use` as `tool_name`/`tool_input`, and exposing `tool_result` output for success scoring. Verified: a real transcript now mines **12 candidates** (was 0); the full `capture-stop` hook queues 3 to `pending-review.jsonl` end-to-end.
|
|
13
|
+
- **Bonus quality fix**: `approach` extraction now finds the last assistant message *with text*, skipping trailing `tool_use`/`tool_result` events that carry no explanation. Previously a fix followed by a tool call produced an empty approach and got dropped.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`tests/memory/extract.test.js`** — `extract.js` was completely untested, which is exactly how this shipped. New tests cover: nested-message mining, no-success-signal → no capture, legacy flat shape still works, and empty/malformed safety.
|
|
18
|
+
|
|
19
|
+
### Impact
|
|
20
|
+
|
|
21
|
+
Memory now works **both ways** for real users: recall (fixed in 2.4.3) AND capture (fixed here). Before this session, a fresh install's memory system was effectively write-nothing / read-crash.
|
|
22
|
+
|
|
23
|
+
## v2.4.4 — Real-user audit: prompt-injection guard was a silent no-op (July 2026)
|
|
24
|
+
|
|
25
|
+
Phase 1 of the real-user audit — firing every hook with realistic input instead of fixtures. Found a second "installed but dummy" feature.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Prompt-injection guard defaulted to `MODE=off`** — so on every fresh install the hook was registered, fired on each prompt, and did **absolutely nothing** unless the user knew to set `KODELYTH_PI_GUARD=warn`. A safety feature nobody knew to turn on is a safety feature that doesn't exist.
|
|
30
|
+
- **Fix**: default is now `warn` — the guard scans every prompt + tool response and surfaces injection findings to stderr (visible in transcript), but **never blocks** (exit 0 always). Measured **0 false positives** across 8 realistic legit prompts containing trigger-adjacent words ("ignore", "override", "system prompt", "bypass", "reveal"). Set `KODELYTH_PI_GUARD=off` to silence, `=block` to hard-block critical patterns (still opt-in, since block can halt on false positives).
|
|
31
|
+
|
|
32
|
+
### Verified (real-user audit, this round)
|
|
33
|
+
|
|
34
|
+
Fired all 8 hooks with realistic Claude Code payloads:
|
|
35
|
+
|
|
36
|
+
| Hook | Result |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `inject-start` | ✓ outputs memory context block |
|
|
39
|
+
| `read-lessons` | ✓ exit 0 |
|
|
40
|
+
| `auto-recall` | ✓ recalls 3 real memories (post-2.4.3 fix) |
|
|
41
|
+
| `capture-stop` | ✓ exit 0 |
|
|
42
|
+
| `capture-correction` | ✓ exit 0 |
|
|
43
|
+
| `auto-resolve` | ✓ exit 0 |
|
|
44
|
+
| `prompt-injection-guard` | ✗→✓ was silent no-op, now warns by default |
|
|
45
|
+
| `token-budget` | ✓ exit 0 |
|
|
46
|
+
|
|
47
|
+
Also verified on real data: `route` (correctly classified a production outage as hard→opus), `evolve analyze` (scanned 195 reuse + 3 miss signals → 5 proposals).
|
|
48
|
+
|
|
49
|
+
### Tests
|
|
50
|
+
|
|
51
|
+
- Updated `mode=off` test to set the env var explicitly (default is no longer off)
|
|
52
|
+
- Added 2 tests: default-no-env warns on injection + stays silent on legit prompts
|
|
53
|
+
- Full suite: 0 failures across 25 files
|
|
54
|
+
|
|
5
55
|
## v2.4.3 — Memory recall crash fix (the "still dummy" bug) (July 2026)
|
|
6
56
|
|
|
7
57
|
Found by testing ECC as a real user actually experiences it, not just in the test harness. **Memory recall was silently crashing on every prompt** for any user whose `~/.kodelythecc/memory/index.json` was written by an older/foreign BM25 schema.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.4.
|
|
1
|
+
2.4.5
|
package/hooks/hooks.json
CHANGED
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"timeout": 3
|
|
164
164
|
}
|
|
165
165
|
],
|
|
166
|
-
"description": "Kodelyth Safety: scan user prompts for jailbreak / instruction-override patterns.
|
|
166
|
+
"description": "Kodelyth Safety: scan user prompts for jailbreak / instruction-override patterns. Scans by default in warn mode (logs, never blocks). KODELYTH_PI_GUARD=off to silence, =block to hard-block critical.",
|
|
167
167
|
"id": "kodelyth:user-prompt:prompt-injection-guard"
|
|
168
168
|
}
|
|
169
169
|
],
|
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
"timeout": 3
|
|
388
388
|
}
|
|
389
389
|
],
|
|
390
|
-
"description": "Kodelyth Safety: scan Read/WebFetch/MCP tool responses for indirect prompt-injection.
|
|
390
|
+
"description": "Kodelyth Safety: scan Read/WebFetch/MCP tool responses for indirect prompt-injection. Scans by default in warn mode (logs, never blocks). KODELYTH_PI_GUARD=off to silence, =block to hard-block critical.",
|
|
391
391
|
"id": "kodelyth:post-tool:prompt-injection-guard"
|
|
392
392
|
}
|
|
393
393
|
],
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// injection from external content)
|
|
11
11
|
//
|
|
12
12
|
// Modes (env var KODELYTH_PI_GUARD):
|
|
13
|
-
// off — never run
|
|
13
|
+
// off — never run
|
|
14
14
|
// warn — exit 0 always; print findings to stderr (visible in transcript)
|
|
15
15
|
// block — exit 2 (block) on critical; warn on high; pass on medium
|
|
16
16
|
//
|
|
@@ -28,7 +28,12 @@ const path = require('path');
|
|
|
28
28
|
|
|
29
29
|
const { scan, maxSeverity } = require('./lib/patterns');
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Default is 'warn' (v2.4.4+): the guard scans and surfaces findings to stderr
|
|
32
|
+
// but NEVER blocks — exit 0 always. This makes the safety feature actually run
|
|
33
|
+
// on a fresh install instead of being a silent no-op. Measured 0 false positives
|
|
34
|
+
// on realistic legit prompts. Set KODELYTH_PI_GUARD=off to silence entirely, or
|
|
35
|
+
// =block to hard-block on critical patterns (opt-in, can halt on false positives).
|
|
36
|
+
const MODE = String(process.env.KODELYTH_PI_GUARD || 'warn').toLowerCase();
|
|
32
37
|
const MAX_INPUT = Number(process.env.KODELYTH_PI_GUARD_MAX_INPUT || 20000);
|
|
33
38
|
const LOG_PATH = process.env.KODELYTH_PI_GUARD_LOG || null;
|
|
34
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
4
4
|
"description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 194 skills, 97 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
|
|
5
5
|
"author": "Kodelyth <github.com/sifxprime>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,12 +36,54 @@ const FAILURE_PHRASES = [
|
|
|
36
36
|
/\bsame error\b/i,
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
+
// Real Claude Code transcript events nest role/content/tool info under
|
|
40
|
+
// `.message`; tool calls live as `content[].type === 'tool_use'` and tool
|
|
41
|
+
// results come back in the next user message as `content[].type === 'tool_result'`.
|
|
42
|
+
// Downstream code reads flat `ev.role`, `ev.tool_name`, `ev.tool_input`,
|
|
43
|
+
// `ev.content` — so we normalise every raw event to that flat shape. Without
|
|
44
|
+
// this, extractCandidates() skips every event and auto-capture never fires.
|
|
45
|
+
function normalizeEvent(raw) {
|
|
46
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
47
|
+
const msg = raw.message && typeof raw.message === 'object' ? raw.message : null;
|
|
48
|
+
|
|
49
|
+
// role: prefer message.role, fall back to the top-level `type` (user/assistant)
|
|
50
|
+
const role = (msg && msg.role) || (raw.role) ||
|
|
51
|
+
(raw.type === 'user' || raw.type === 'assistant' ? raw.type : undefined);
|
|
52
|
+
|
|
53
|
+
// content: keep whatever extractText already understands (string | array | message)
|
|
54
|
+
const content = (msg && msg.content !== undefined) ? msg.content : raw.content;
|
|
55
|
+
|
|
56
|
+
const out = { ...raw, role, content };
|
|
57
|
+
|
|
58
|
+
// tool_use: surface the FIRST tool call in an assistant message as
|
|
59
|
+
// ev.tool_name / ev.tool_input so the flat readers see it.
|
|
60
|
+
if (Array.isArray(content)) {
|
|
61
|
+
const toolUse = content.find(c => c && c.type === 'tool_use');
|
|
62
|
+
if (toolUse) {
|
|
63
|
+
out.tool_name = toolUse.name;
|
|
64
|
+
out.tool_input = toolUse.input || {};
|
|
65
|
+
}
|
|
66
|
+
// tool_result carries command output (exit codes, "tests passed", etc.)
|
|
67
|
+
const toolResult = content.find(c => c && c.type === 'tool_result');
|
|
68
|
+
if (toolResult && !out.tool_name) {
|
|
69
|
+
const rc = toolResult.content;
|
|
70
|
+
out._toolResultText = typeof rc === 'string'
|
|
71
|
+
? rc
|
|
72
|
+
: Array.isArray(rc) ? rc.map(x => (typeof x === 'string' ? x : x?.text || '')).join('\n') : '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
function readTranscript(jsonlPath) {
|
|
40
79
|
if (!fs.existsSync(jsonlPath)) return [];
|
|
41
80
|
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\n').filter(Boolean);
|
|
42
81
|
const events = [];
|
|
43
82
|
for (const line of lines) {
|
|
44
|
-
try {
|
|
83
|
+
try {
|
|
84
|
+
const norm = normalizeEvent(JSON.parse(line));
|
|
85
|
+
if (norm) events.push(norm);
|
|
86
|
+
} catch { /* skip malformed */ }
|
|
45
87
|
}
|
|
46
88
|
return events;
|
|
47
89
|
}
|
|
@@ -49,11 +91,15 @@ function readTranscript(jsonlPath) {
|
|
|
49
91
|
function extractText(event) {
|
|
50
92
|
if (typeof event.content === 'string') return event.content;
|
|
51
93
|
if (Array.isArray(event.content)) {
|
|
52
|
-
|
|
94
|
+
const text = event.content
|
|
53
95
|
.filter(c => c && (c.type === 'text' || typeof c.text === 'string'))
|
|
54
96
|
.map(c => c.text || '')
|
|
55
97
|
.join('\n');
|
|
98
|
+
if (text) return text;
|
|
99
|
+
// tool_result content (command output) — used for success scoring
|
|
100
|
+
if (event._toolResultText) return event._toolResultText;
|
|
56
101
|
}
|
|
102
|
+
if (event._toolResultText) return event._toolResultText;
|
|
57
103
|
if (event.message?.content) return extractText({ content: event.message.content });
|
|
58
104
|
return '';
|
|
59
105
|
}
|
|
@@ -148,8 +194,15 @@ function extractCandidates(jsonlPath) {
|
|
|
148
194
|
.filter(Boolean)
|
|
149
195
|
)).slice(0, 5);
|
|
150
196
|
|
|
151
|
-
|
|
152
|
-
|
|
197
|
+
// Find the last assistant message that actually has TEXT — skip trailing
|
|
198
|
+
// tool_use/tool_result events which carry no explanation of the fix.
|
|
199
|
+
const reversed = window.slice().reverse();
|
|
200
|
+
let approach = null;
|
|
201
|
+
for (const e of reversed) {
|
|
202
|
+
if (e.role !== 'assistant') continue;
|
|
203
|
+
const t = extractText(e).trim();
|
|
204
|
+
if (t) { approach = t.slice(0, 600); break; }
|
|
205
|
+
}
|
|
153
206
|
|
|
154
207
|
if (!problem || !approach) continue;
|
|
155
208
|
|