kodelyth-ecc 1.7.4 → 1.8.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/CHANGELOG.md +18 -1
- package/CLAUDE.md +1 -1
- package/README.md +12 -9
- package/SECURITY.md +5 -6
- package/VERSION +1 -1
- package/brand/concepts.svg +109 -0
- package/brand/convert.js +161 -0
- package/brand/favicon.svg +7 -0
- package/brand/fb-cover.svg +34 -0
- package/brand/fb-profile.svg +21 -0
- package/brand/kodelyth-dark.svg +24 -0
- package/brand/kodelyth-light.svg +24 -0
- package/brand/kodelyth-mark.svg +20 -0
- package/brand/package.json +12 -0
- package/bundles/enterprise.md +1 -1
- package/bundles/indie-hacker.md +1 -1
- package/bundles/red-team.md +1 -1
- package/hooks/hooks.json +13 -0
- package/hooks/memory/auto-resolve.js +68 -0
- package/hooks/memory/capture-correction.js +16 -0
- package/hooks/memory/capture-stop.js +70 -14
- package/package.json +1 -1
- package/rules/common/agents.md +1 -1
- package/rules/common/self-improvement-workflow.md +2 -2
- package/scripts/dashboard/data.js +1 -1
- package/scripts/dashboard/server.js +1 -1
- package/scripts/memory/instincts.js +282 -0
- package/scripts/memory/store.js +104 -0
- package/social/card-agents.svg +13 -1
- package/social/card-install.svg +13 -1
- package/social/card-main.svg +13 -1
- package/social/facebook-v150.svg +27 -13
- package/social/fb-ad-main.svg +128 -0
- package/social/fb-post-features.svg +118 -0
- package/social/fb-post-launch.svg +144 -0
- package/social/fb-post-platforms.svg +135 -0
- package/social/github-social-preview.svg +143 -139
- package/social/hype-compound-learning.svg +21 -9
- package/social/hype-devil-mode.svg +20 -8
- package/social/hype-mcp-server.svg +20 -8
- package/social/hype-parallel-agents.svg +20 -8
- package/social/hype-stats-hero.svg +15 -3
- package/social/og-image.svg +151 -0
- package/social/readme-agents.svg +14 -2
- package/social/readme-hero.svg +17 -4
- package/social/section-agents.svg +12 -0
- package/social/section-author.svg +12 -0
- package/social/section-dashboard.svg +12 -0
- package/social/section-devil.svg +12 -0
- package/social/section-hooks.svg +12 -0
- package/social/section-install.svg +12 -0
- package/social/section-learning.svg +12 -0
- package/social/section-mcp.svg +13 -1
- package/social/section-memory.svg +12 -0
- package/social/section-parallel.svg +12 -0
- package/social/section-routing.svg +12 -0
- package/social/twitter-threads.md +3 -3
- package/social/x-card-agents-grid.svg +14 -2
- package/social/x-card-free.svg +14 -2
- package/social/x-card-hook.svg +17 -5
- package/tests/memory/instincts.test.js +258 -0
- package/tests/memory/store.test.js +82 -0
- package/wiki/FAQ.md +1 -1
- package/wiki/Home.md +3 -3
- package/wiki/Installation-Guide.md +2 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Kodelyth ECC — Memory Auto-Resolve Hook (PostToolUse: Write|Edit|MultiEdit)
|
|
4
|
+
//
|
|
5
|
+
// Improvement C — Outcome Tracking
|
|
6
|
+
//
|
|
7
|
+
// Fires after every file write or edit. Checks if the edited file appears in
|
|
8
|
+
// any unresolved memory's files[] list. If it does, that memory's outcome is
|
|
9
|
+
// marked resolved:false — a follow-up edit implies the previous solution
|
|
10
|
+
// didn't fully stick.
|
|
11
|
+
//
|
|
12
|
+
// The same event is propagated to instincts.js so that any structued instinct
|
|
13
|
+
// sourced from the same session gets its confidence downgraded (−0.30),
|
|
14
|
+
// enabling automatic pruning of bad patterns over time.
|
|
15
|
+
//
|
|
16
|
+
// This hook is deliberately silent:
|
|
17
|
+
// - It never emits output to stdout (would pollute the AI's context).
|
|
18
|
+
// - It always exits 0 (never blocks a session).
|
|
19
|
+
// - All writes are atomic enough for the append-only JSONL format.
|
|
20
|
+
//
|
|
21
|
+
// Output contract: pass stdin through to stdout unchanged. Never block.
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
let raw = '';
|
|
30
|
+
process.stdin.setEncoding('utf8');
|
|
31
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
32
|
+
process.stdin.on('end', run);
|
|
33
|
+
setTimeout(() => { if (!process.stdin.readableEnded) run(); }, 150);
|
|
34
|
+
|
|
35
|
+
function run() {
|
|
36
|
+
// Always echo stdin — never block the tool result from reaching the AI
|
|
37
|
+
if (raw) process.stdout.write(raw);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const payload = raw ? JSON.parse(raw) : {};
|
|
41
|
+
const toolName = payload.tool_name || payload.name || '';
|
|
42
|
+
const toolInput = payload.tool_input || payload.input || {};
|
|
43
|
+
const cwd = payload.cwd || process.cwd();
|
|
44
|
+
|
|
45
|
+
// Extract the file path from Write, Edit, or MultiEdit inputs
|
|
46
|
+
let filePath = toolInput.file_path || toolInput.path || null;
|
|
47
|
+
if (!filePath && Array.isArray(toolInput.edits) && toolInput.edits[0]) {
|
|
48
|
+
filePath = toolInput.edits[0].file_path || toolInput.edits[0].path || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!filePath) return; // no file path — nothing to resolve
|
|
52
|
+
|
|
53
|
+
const storePath = path.join(__dirname, '..', '..', 'scripts', 'memory', 'store.js');
|
|
54
|
+
if (!fs.existsSync(storePath)) return;
|
|
55
|
+
|
|
56
|
+
const store = require(storePath);
|
|
57
|
+
const resolved = store.autoResolveOnEdit(filePath, cwd);
|
|
58
|
+
|
|
59
|
+
if (resolved.length > 0) {
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
`[ecc:auto-resolve] Marked ${resolved.length} memory/memories resolved:false` +
|
|
62
|
+
` after edit to ${path.basename(filePath)}\n`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
process.stderr.write(`[ecc:auto-resolve] ${err.message}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -154,6 +154,22 @@ function writeToLessons(corrections, cwd) {
|
|
|
154
154
|
process.stderr.write(
|
|
155
155
|
`[ecc:correction-capture] ${corrections.length} lesson(s) written to tasks/lessons.md\n`
|
|
156
156
|
);
|
|
157
|
+
|
|
158
|
+
// ── Improvement B: also write to structured instinct schema ──────────────
|
|
159
|
+
try {
|
|
160
|
+
const instinctsPath = path.join(__dirname, '..', '..', 'scripts', 'memory', 'instincts.js');
|
|
161
|
+
if (fs.existsSync(instinctsPath)) {
|
|
162
|
+
const instincts = require(instinctsPath);
|
|
163
|
+
for (const correction of corrections) {
|
|
164
|
+
instincts.captureFromCorrection(correction, cwd, 'project');
|
|
165
|
+
}
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`[ecc:correction-capture] ${corrections.length} instinct(s) written to structured schema\n`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} catch (instErr) {
|
|
171
|
+
process.stderr.write(`[ecc:correction-capture] Instinct schema write skipped: ${instErr.message}\n`);
|
|
172
|
+
}
|
|
157
173
|
} catch (err) {
|
|
158
174
|
process.stderr.write(`[ecc:correction-capture] Could not write lessons: ${err.message}\n`);
|
|
159
175
|
}
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
// /memory review-pending
|
|
11
11
|
// or:
|
|
12
12
|
// node scripts/memory/cli.js list-pending
|
|
13
|
+
//
|
|
14
|
+
// Improvement A: detects meaningful file writes / git commits in the session
|
|
15
|
+
// and emits a contextual "should I save what we learned?" prompt.
|
|
16
|
+
//
|
|
17
|
+
// Improvement D: runs instinct decay check — flags instincts unused 30+ days.
|
|
13
18
|
// =============================================================================
|
|
14
19
|
|
|
15
20
|
'use strict';
|
|
@@ -37,7 +42,11 @@ function main() {
|
|
|
37
42
|
|
|
38
43
|
const { extractCandidates } = require(path.join(__dirname, '..', '..', 'scripts', 'memory', 'extract'));
|
|
39
44
|
const candidates = extractCandidates(sessionJsonl);
|
|
40
|
-
|
|
45
|
+
|
|
46
|
+
// ── Improvement A: detect meaningful file writes or git commits ───────────
|
|
47
|
+
const hadFileWrites = detectFileWrites(sessionJsonl);
|
|
48
|
+
|
|
49
|
+
if (candidates.length === 0 && !hadFileWrites) {
|
|
41
50
|
process.exit(0);
|
|
42
51
|
}
|
|
43
52
|
|
|
@@ -45,22 +54,46 @@ function main() {
|
|
|
45
54
|
|| path.join(os.homedir(), '.kodelyth', 'memory');
|
|
46
55
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
47
56
|
|
|
48
|
-
const queueFile = path.join(dir, 'pending-review.jsonl');
|
|
49
57
|
const sessionId = data.session_id || path.basename(sessionJsonl, '.jsonl');
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
if (candidates.length > 0) {
|
|
60
|
+
const queueFile = path.join(dir, 'pending-review.jsonl');
|
|
61
|
+
const lines = candidates.map(c => JSON.stringify({
|
|
62
|
+
...c,
|
|
63
|
+
session_id: sessionId,
|
|
64
|
+
project_path: data.cwd || process.cwd(),
|
|
65
|
+
queued_at: new Date().toISOString(),
|
|
66
|
+
}));
|
|
67
|
+
fs.appendFileSync(queueFile, lines.join('\n') + '\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Improvement A: build contextual advisory from session activity ────────
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (candidates.length > 0) {
|
|
73
|
+
parts.push(`${candidates.length} memory candidate(s) queued — run "/memory review-pending" to confirm`);
|
|
74
|
+
}
|
|
75
|
+
if (hadFileWrites) {
|
|
76
|
+
parts.push('files written this session — run "/memory save" or "/learn-eval" to save what we learned');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Improvement D: decay check — surface stale instincts for review ───────
|
|
80
|
+
try {
|
|
81
|
+
const instinctsPath = path.join(__dirname, '..', '..', 'scripts', 'memory', 'instincts.js');
|
|
82
|
+
if (fs.existsSync(instinctsPath)) {
|
|
83
|
+
const instincts = require(instinctsPath);
|
|
84
|
+
const stale = instincts.runDecayCheck();
|
|
85
|
+
if (stale.length > 0) {
|
|
86
|
+
parts.push(`${stale.length} instinct(s) unused 30+ days — run "/skill-health" to review stale rules`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch { /* decay check is best-effort, never block a session */ }
|
|
57
90
|
|
|
58
|
-
|
|
91
|
+
if (parts.length > 0) {
|
|
92
|
+
process.stdout.write(JSON.stringify({
|
|
93
|
+
message: `Kodelyth Memory: ${parts.join(' | ')}`,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
59
96
|
|
|
60
|
-
// Emit advisory message so user sees something happened
|
|
61
|
-
process.stdout.write(JSON.stringify({
|
|
62
|
-
message: `Kodelyth Memory: ${candidates.length} candidate(s) queued for review. Run "/memory review-pending" to confirm.`,
|
|
63
|
-
}));
|
|
64
97
|
process.exit(0);
|
|
65
98
|
} catch (err) {
|
|
66
99
|
process.stderr.write(`kodelyth-memory capture: ${err.message}\n`);
|
|
@@ -68,11 +101,34 @@ function main() {
|
|
|
68
101
|
}
|
|
69
102
|
}
|
|
70
103
|
|
|
104
|
+
// ── Improvement A: scan session JSONL for Write/Edit tool uses or git commits ─
|
|
105
|
+
function detectFileWrites(sessionJsonl) {
|
|
106
|
+
try {
|
|
107
|
+
const lines = fs.readFileSync(sessionJsonl, 'utf8').split('\n').filter(Boolean);
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
let event;
|
|
110
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
111
|
+
|
|
112
|
+
// Tool use events that write files
|
|
113
|
+
const toolName = event.name || event.tool_name || '';
|
|
114
|
+
if (/^(Write|Edit|MultiEdit|NotebookEdit)$/i.test(toolName)) return true;
|
|
115
|
+
|
|
116
|
+
// Bash events containing git commit
|
|
117
|
+
if (/^Bash$/i.test(toolName)) {
|
|
118
|
+
const input = JSON.stringify(event.input || event.tool_input || '');
|
|
119
|
+
if (/git\s+commit|git\s+push/i.test(input)) return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
71
128
|
function findLatestClaudeSession(cwd) {
|
|
72
129
|
try {
|
|
73
130
|
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
74
131
|
if (!fs.existsSync(projectsDir)) return null;
|
|
75
|
-
// Project dirs are encoded paths
|
|
76
132
|
const encoded = '-' + cwd.replace(/\//g, '-');
|
|
77
133
|
const matches = fs.readdirSync(projectsDir).filter(d => d.endsWith(encoded.slice(-30)));
|
|
78
134
|
if (matches.length === 0) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
package/rules/common/agents.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Orchestration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
70 specialist agents are available. Intent routing is always-on — describe your problem in plain words and the right agent is invoked automatically. You can also invoke explicitly: `use <agent-name>`.
|
|
4
4
|
|
|
5
5
|
## Kodelyth Exclusives
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ Enter plan mode for ANY non-trivial task (3+ steps, architectural decisions, or
|
|
|
16
16
|
|
|
17
17
|
## 2. Subagent Strategy
|
|
18
18
|
|
|
19
|
-
The
|
|
19
|
+
The 70 ECC specialist agents exist so the main context window stays clean:
|
|
20
20
|
|
|
21
21
|
- Offload research, exploration, and parallel analysis to subagents
|
|
22
22
|
- For complex problems: throw more compute via agents, not more tokens in main context
|
|
@@ -124,7 +124,7 @@ ECC uses three compounding memory layers — together they make Claude increasin
|
|
|
124
124
|
- Example: "Last time you had a CORS issue in Express, you added this middleware..."
|
|
125
125
|
|
|
126
126
|
### Layer 3 — Intent Routing (`rules/common/agent-intent-routing.md`)
|
|
127
|
-
- Always-on.
|
|
127
|
+
- Always-on. 70 specialists. Zero setup.
|
|
128
128
|
- Routes your message to the right expert from the first word
|
|
129
129
|
- No agent names needed — just describe the problem
|
|
130
130
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// scripts/dashboard/data.js
|
|
2
2
|
//
|
|
3
|
-
// Local observability dashboard: pure aggregators (v1.
|
|
3
|
+
// Local observability dashboard: pure aggregators (v1.8.0).
|
|
4
4
|
//
|
|
5
5
|
// All data the dashboard renders comes from:
|
|
6
6
|
// - The MCP catalog (filesystem reads of agents/, skills/, commands/, rules/, bundles/)
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Kodelyth ECC — Structured Instinct Schema (Improvement B)
|
|
3
|
+
//
|
|
4
|
+
// Stores learned instincts as typed JSON records in:
|
|
5
|
+
// ~/.kodelyth/memory/instincts.jsonl
|
|
6
|
+
//
|
|
7
|
+
// Each instinct has: pattern, trigger, confidence, last_used, outcome, decay.
|
|
8
|
+
// This replaces free-form markdown bullets for machine-readable learning.
|
|
9
|
+
// The lessons.md file is still written for human readability — this runs
|
|
10
|
+
// in parallel, enabling deduplication, conflict detection, and decay.
|
|
11
|
+
//
|
|
12
|
+
// Schema:
|
|
13
|
+
// id — deterministic hash (stable across re-runs)
|
|
14
|
+
// pattern — short slug: "use-pnpm-not-npm", "no-console-logs"
|
|
15
|
+
// rule — full human-readable text of the rule
|
|
16
|
+
// scope — "project" | "global"
|
|
17
|
+
// trigger — keywords/phrases that activate this instinct
|
|
18
|
+
// confidence — 0.0–1.0 (starts at 0.7, confirmed → 1.0, decays with age)
|
|
19
|
+
// source — "correction" | "manual" | "evolved"
|
|
20
|
+
// created — ISO date
|
|
21
|
+
// last_used — ISO date
|
|
22
|
+
// use_count — how many sessions this was applied
|
|
23
|
+
// outcome — null | "success" | "failure"
|
|
24
|
+
// project_path — absolute path of origin project
|
|
25
|
+
// stale — true if unused for 30+ days (set by decay check)
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const crypto = require('crypto');
|
|
34
|
+
|
|
35
|
+
const MEMORY_DIR = process.env.KODELYTH_MEMORY_DIR
|
|
36
|
+
|| path.join(os.homedir(), '.kodelyth', 'memory');
|
|
37
|
+
|
|
38
|
+
const INSTINCTS_FILE = path.join(MEMORY_DIR, 'instincts.jsonl');
|
|
39
|
+
const STALE_DAYS = 30;
|
|
40
|
+
|
|
41
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function ensureDir() {
|
|
44
|
+
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function slugify(text) {
|
|
48
|
+
return String(text || '')
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '')
|
|
52
|
+
.slice(0, 60) || 'rule';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deterministicId(rule, projectPath) {
|
|
56
|
+
return crypto
|
|
57
|
+
.createHash('sha256')
|
|
58
|
+
.update(`${rule}::${projectPath || ''}`)
|
|
59
|
+
.digest('hex')
|
|
60
|
+
.slice(0, 12);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractTriggers(ruleText) {
|
|
64
|
+
// Heuristic: extract key words that would activate this rule
|
|
65
|
+
const FILLER = new Set(['use','don\'t','never','always','please','stop','avoid',
|
|
66
|
+
'instead','of','not','the','a','an','and','or','in','on','at','to','is',
|
|
67
|
+
'are','was','were','do','does','did','should','would','could']);
|
|
68
|
+
return ruleText
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
71
|
+
.split(/\s+/)
|
|
72
|
+
.filter(w => w.length >= 3 && !FILLER.has(w))
|
|
73
|
+
.slice(0, 6);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function today() {
|
|
77
|
+
return new Date().toISOString().split('T')[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function daysSince(isoDate) {
|
|
81
|
+
if (!isoDate) return 999;
|
|
82
|
+
return Math.floor((Date.now() - new Date(isoDate).getTime()) / 86400000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Read / Write ──────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function loadAll() {
|
|
88
|
+
if (!fs.existsSync(INSTINCTS_FILE)) return [];
|
|
89
|
+
return fs.readFileSync(INSTINCTS_FILE, 'utf8')
|
|
90
|
+
.split('\n')
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function saveAll(instincts) {
|
|
97
|
+
ensureDir();
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
INSTINCTS_FILE,
|
|
100
|
+
instincts.map(i => JSON.stringify(i)).join('\n') + '\n',
|
|
101
|
+
'utf8'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add a new instinct from a correction string.
|
|
109
|
+
* If a record with the same id already exists, increments use_count.
|
|
110
|
+
* Returns the instinct record.
|
|
111
|
+
*/
|
|
112
|
+
function captureFromCorrection(ruleText, projectPath, scope = 'project') {
|
|
113
|
+
ensureDir();
|
|
114
|
+
const id = deterministicId(ruleText, projectPath);
|
|
115
|
+
const all = loadAll();
|
|
116
|
+
|
|
117
|
+
const existing = all.find(i => i.id === id);
|
|
118
|
+
if (existing) {
|
|
119
|
+
existing.use_count = (existing.use_count || 1) + 1;
|
|
120
|
+
existing.last_used = today();
|
|
121
|
+
existing.confidence = Math.min(1.0, (existing.confidence || 0.7) + 0.1);
|
|
122
|
+
existing.stale = false;
|
|
123
|
+
saveAll(all);
|
|
124
|
+
return existing;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const instinct = {
|
|
128
|
+
id,
|
|
129
|
+
pattern: slugify(ruleText),
|
|
130
|
+
rule: ruleText.trim(),
|
|
131
|
+
scope,
|
|
132
|
+
trigger: extractTriggers(ruleText),
|
|
133
|
+
confidence: 0.7,
|
|
134
|
+
source: 'correction',
|
|
135
|
+
created: today(),
|
|
136
|
+
last_used: today(),
|
|
137
|
+
use_count: 1,
|
|
138
|
+
outcome: null,
|
|
139
|
+
project_path: projectPath || null,
|
|
140
|
+
stale: false,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
fs.appendFileSync(INSTINCTS_FILE, JSON.stringify(instinct) + '\n', 'utf8');
|
|
144
|
+
return instinct;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Record the outcome of an instinct (success/failure) by id.
|
|
149
|
+
* A failure reduces confidence; a success boosts it.
|
|
150
|
+
*/
|
|
151
|
+
function recordOutcome(id, outcome) {
|
|
152
|
+
const all = loadAll();
|
|
153
|
+
const instinct = all.find(i => i.id === id);
|
|
154
|
+
if (!instinct) return null;
|
|
155
|
+
|
|
156
|
+
instinct.outcome = outcome;
|
|
157
|
+
instinct.last_used = today();
|
|
158
|
+
|
|
159
|
+
if (outcome === 'success') {
|
|
160
|
+
instinct.confidence = Math.min(1.0, (instinct.confidence || 0.7) + 0.15);
|
|
161
|
+
} else if (outcome === 'failure') {
|
|
162
|
+
instinct.confidence = Math.max(0.0, (instinct.confidence || 0.7) - 0.3);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
saveAll(all);
|
|
166
|
+
return instinct;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Decay check (Improvement D).
|
|
171
|
+
* Marks instincts unused for STALE_DAYS+ as stale: true.
|
|
172
|
+
* Returns list of newly-stale instincts for review prompting.
|
|
173
|
+
*/
|
|
174
|
+
function runDecayCheck() {
|
|
175
|
+
const all = loadAll();
|
|
176
|
+
const newlyStale = [];
|
|
177
|
+
|
|
178
|
+
for (const instinct of all) {
|
|
179
|
+
const age = daysSince(instinct.last_used);
|
|
180
|
+
const wasStale = instinct.stale;
|
|
181
|
+
instinct.stale = age >= STALE_DAYS;
|
|
182
|
+
if (instinct.stale && !wasStale) {
|
|
183
|
+
newlyStale.push(instinct);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (newlyStale.length > 0) saveAll(all);
|
|
188
|
+
return newlyStale;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return all instincts, optionally filtered.
|
|
193
|
+
* Filters: { scope, stale, minConfidence, projectPath }
|
|
194
|
+
*/
|
|
195
|
+
function list({ scope, stale, minConfidence, projectPath } = {}) {
|
|
196
|
+
let all = loadAll();
|
|
197
|
+
if (scope !== undefined) all = all.filter(i => i.scope === scope);
|
|
198
|
+
if (stale !== undefined) all = all.filter(i => Boolean(i.stale) === stale);
|
|
199
|
+
if (minConfidence !== undefined) all = all.filter(i => (i.confidence || 0) >= minConfidence);
|
|
200
|
+
if (projectPath !== undefined) all = all.filter(i => i.project_path === projectPath);
|
|
201
|
+
return all;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Prune instincts with confidence below threshold (auto-cleanup of bad patterns).
|
|
206
|
+
*/
|
|
207
|
+
function pruneWeak(threshold = 0.2) {
|
|
208
|
+
const all = loadAll();
|
|
209
|
+
const kept = all.filter(i => (i.confidence || 0.7) >= threshold);
|
|
210
|
+
const pruned = all.length - kept.length;
|
|
211
|
+
if (pruned > 0) saveAll(kept);
|
|
212
|
+
return pruned;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Improvement C: outcome tracking via store.js bridge.
|
|
217
|
+
*
|
|
218
|
+
* Called when a follow-up file edit implies a previous instinct didn't stick.
|
|
219
|
+
* Finds instincts that match both the project_path AND a fragment of the
|
|
220
|
+
* problem text, then records a failure outcome on each match.
|
|
221
|
+
*
|
|
222
|
+
* This is intentionally fuzzy: a partial match (>20% token overlap between
|
|
223
|
+
* problemHint and instinct.rule) is enough to trigger a downgrade. The goal
|
|
224
|
+
* is to surface bad patterns, not to be surgically precise.
|
|
225
|
+
*
|
|
226
|
+
* Returns the list of downgraded instinct ids.
|
|
227
|
+
*/
|
|
228
|
+
function recordOutcomeByProject(projectPath, problemHint, outcome) {
|
|
229
|
+
if (!projectPath && !problemHint) return [];
|
|
230
|
+
const all = loadAll();
|
|
231
|
+
const changed = [];
|
|
232
|
+
|
|
233
|
+
const hintTokens = problemHint
|
|
234
|
+
? new Set(
|
|
235
|
+
String(problemHint)
|
|
236
|
+
.toLowerCase()
|
|
237
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
238
|
+
.split(/\s+/)
|
|
239
|
+
.filter(t => t.length >= 3)
|
|
240
|
+
)
|
|
241
|
+
: null;
|
|
242
|
+
|
|
243
|
+
for (const instinct of all) {
|
|
244
|
+
// Project match (when provided)
|
|
245
|
+
if (projectPath && instinct.project_path && instinct.project_path !== projectPath) continue;
|
|
246
|
+
|
|
247
|
+
// Problem text fuzzy match (when provided)
|
|
248
|
+
if (hintTokens && hintTokens.size > 0) {
|
|
249
|
+
const ruleTokens = String(instinct.rule || '')
|
|
250
|
+
.toLowerCase()
|
|
251
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
252
|
+
.split(/\s+/)
|
|
253
|
+
.filter(t => t.length >= 3);
|
|
254
|
+
const overlap = ruleTokens.filter(t => hintTokens.has(t)).length;
|
|
255
|
+
const ratio = overlap / Math.max(hintTokens.size, 1);
|
|
256
|
+
if (ratio < 0.2) continue; // < 20% overlap — not a match
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
instinct.outcome = outcome;
|
|
260
|
+
instinct.last_used = today();
|
|
261
|
+
if (outcome === 'success') {
|
|
262
|
+
instinct.confidence = Math.min(1.0, (instinct.confidence || 0.7) + 0.15);
|
|
263
|
+
} else if (outcome === false || outcome === 'failure') {
|
|
264
|
+
instinct.confidence = Math.max(0.0, (instinct.confidence || 0.7) - 0.3);
|
|
265
|
+
}
|
|
266
|
+
changed.push(instinct.id);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (changed.length > 0) saveAll(all);
|
|
270
|
+
return changed;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
captureFromCorrection,
|
|
275
|
+
recordOutcome,
|
|
276
|
+
recordOutcomeByProject,
|
|
277
|
+
runDecayCheck,
|
|
278
|
+
list,
|
|
279
|
+
pruneWeak,
|
|
280
|
+
INSTINCTS_FILE,
|
|
281
|
+
STALE_DAYS,
|
|
282
|
+
};
|
package/scripts/memory/store.js
CHANGED
|
@@ -257,6 +257,106 @@ function forget(memoryId) {
|
|
|
257
257
|
return found;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// ── Improvement C: outcome tracking ──────────────────────────────────────────
|
|
261
|
+
// Mark a memory as resolved:true (success) or resolved:false (failure).
|
|
262
|
+
// Called automatically by the PostToolUse hook when an edited file matches
|
|
263
|
+
// a memory's files[] list — a follow-up fix implies the previous approach
|
|
264
|
+
// didn't fully work, so resolved=false. The user can also call this manually.
|
|
265
|
+
//
|
|
266
|
+
// Side-effect: also propagates to instincts.js if the instinct module exists,
|
|
267
|
+
// so low-confidence instincts sourced from the same session get downgraded.
|
|
268
|
+
function resolveMemory(memoryId, resolved) {
|
|
269
|
+
if (!fs.existsSync(PATHS.log)) return false;
|
|
270
|
+
const lines = fs.readFileSync(PATHS.log, 'utf8').split('\n').filter(Boolean);
|
|
271
|
+
let found = false;
|
|
272
|
+
const updated = lines.map(line => {
|
|
273
|
+
try {
|
|
274
|
+
const m = JSON.parse(line);
|
|
275
|
+
if (m.id === memoryId && !m.deleted) {
|
|
276
|
+
found = true;
|
|
277
|
+
return JSON.stringify({
|
|
278
|
+
...m,
|
|
279
|
+
resolved,
|
|
280
|
+
resolved_at: new Date().toISOString(),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return line;
|
|
284
|
+
} catch {
|
|
285
|
+
return line;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
if (found) {
|
|
289
|
+
fs.writeFileSync(PATHS.log, updated.join('\n') + '\n');
|
|
290
|
+
}
|
|
291
|
+
return found;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Find unresolved memories whose files[] overlap with the given file path.
|
|
295
|
+
// Returns [ { id, problem, files, captured_at } ] — caller decides what to do.
|
|
296
|
+
function findMemoriesForFile(filePath, options = {}) {
|
|
297
|
+
const { projectRoot = null, limit = 5 } = options;
|
|
298
|
+
if (!fs.existsSync(PATHS.log)) return [];
|
|
299
|
+
|
|
300
|
+
const normFile = path.normalize(filePath);
|
|
301
|
+
const lines = fs.readFileSync(PATHS.log, 'utf8').split('\n').filter(Boolean);
|
|
302
|
+
const matches = [];
|
|
303
|
+
|
|
304
|
+
for (const line of lines) {
|
|
305
|
+
let m;
|
|
306
|
+
try { m = JSON.parse(line); } catch { continue; }
|
|
307
|
+
if (m.deleted || m.resolved !== undefined) continue; // skip already resolved
|
|
308
|
+
if (!Array.isArray(m.files) || m.files.length === 0) continue;
|
|
309
|
+
if (projectRoot && m.project_path && m.project_path !== projectRoot) continue;
|
|
310
|
+
|
|
311
|
+
const hit = m.files.some(f => {
|
|
312
|
+
const normF = path.normalize(String(f));
|
|
313
|
+
return normF === normFile || normFile.endsWith(normF) || normF.endsWith(normFile);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (hit) {
|
|
317
|
+
matches.push({
|
|
318
|
+
id: m.id,
|
|
319
|
+
problem: m.problem,
|
|
320
|
+
approach: m.approach,
|
|
321
|
+
files: m.files,
|
|
322
|
+
captured_at: m.captured_at,
|
|
323
|
+
project_path: m.project_path,
|
|
324
|
+
});
|
|
325
|
+
if (matches.length >= limit) break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return matches;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Auto-resolve hook: called by PostToolUse (Write|Edit) with the file just edited.
|
|
333
|
+
// Marks any unresolved memory that listed this file as resolved:false, then
|
|
334
|
+
// nudges the instinct schema to downgrade the matching instinct's confidence.
|
|
335
|
+
function autoResolveOnEdit(filePath, projectRoot = null) {
|
|
336
|
+
const affected = findMemoriesForFile(filePath, { projectRoot });
|
|
337
|
+
if (affected.length === 0) return [];
|
|
338
|
+
|
|
339
|
+
const resolved = [];
|
|
340
|
+
for (const m of affected) {
|
|
341
|
+
const ok = resolveMemory(m.id, false);
|
|
342
|
+
if (ok) resolved.push(m);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Propagate to instinct schema — best-effort only
|
|
346
|
+
try {
|
|
347
|
+
const instinctsPath = path.join(__dirname, 'instincts.js');
|
|
348
|
+
if (fs.existsSync(instinctsPath)) {
|
|
349
|
+
const instincts = require(instinctsPath);
|
|
350
|
+
for (const m of resolved) {
|
|
351
|
+
// Match by project_path + approximate problem text
|
|
352
|
+
instincts.recordOutcomeByProject(m.project_path, m.problem, false);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} catch { /* never block */ }
|
|
356
|
+
|
|
357
|
+
return resolved;
|
|
358
|
+
}
|
|
359
|
+
|
|
260
360
|
function rebuildIndex() {
|
|
261
361
|
const memories = readMemories();
|
|
262
362
|
let index = { tokens: {}, docCount: 0, avgDocLength: 0, totalLength: 0 };
|
|
@@ -297,4 +397,8 @@ module.exports = {
|
|
|
297
397
|
stats,
|
|
298
398
|
tokenise,
|
|
299
399
|
projectHash,
|
|
400
|
+
// Improvement C
|
|
401
|
+
resolveMemory,
|
|
402
|
+
findMemoriesForFile,
|
|
403
|
+
autoResolveOnEdit,
|
|
300
404
|
};
|