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.
Files changed (65) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +12 -9
  4. package/SECURITY.md +5 -6
  5. package/VERSION +1 -1
  6. package/brand/concepts.svg +109 -0
  7. package/brand/convert.js +161 -0
  8. package/brand/favicon.svg +7 -0
  9. package/brand/fb-cover.svg +34 -0
  10. package/brand/fb-profile.svg +21 -0
  11. package/brand/kodelyth-dark.svg +24 -0
  12. package/brand/kodelyth-light.svg +24 -0
  13. package/brand/kodelyth-mark.svg +20 -0
  14. package/brand/package.json +12 -0
  15. package/bundles/enterprise.md +1 -1
  16. package/bundles/indie-hacker.md +1 -1
  17. package/bundles/red-team.md +1 -1
  18. package/hooks/hooks.json +13 -0
  19. package/hooks/memory/auto-resolve.js +68 -0
  20. package/hooks/memory/capture-correction.js +16 -0
  21. package/hooks/memory/capture-stop.js +70 -14
  22. package/package.json +1 -1
  23. package/rules/common/agents.md +1 -1
  24. package/rules/common/self-improvement-workflow.md +2 -2
  25. package/scripts/dashboard/data.js +1 -1
  26. package/scripts/dashboard/server.js +1 -1
  27. package/scripts/memory/instincts.js +282 -0
  28. package/scripts/memory/store.js +104 -0
  29. package/social/card-agents.svg +13 -1
  30. package/social/card-install.svg +13 -1
  31. package/social/card-main.svg +13 -1
  32. package/social/facebook-v150.svg +27 -13
  33. package/social/fb-ad-main.svg +128 -0
  34. package/social/fb-post-features.svg +118 -0
  35. package/social/fb-post-launch.svg +144 -0
  36. package/social/fb-post-platforms.svg +135 -0
  37. package/social/github-social-preview.svg +143 -139
  38. package/social/hype-compound-learning.svg +21 -9
  39. package/social/hype-devil-mode.svg +20 -8
  40. package/social/hype-mcp-server.svg +20 -8
  41. package/social/hype-parallel-agents.svg +20 -8
  42. package/social/hype-stats-hero.svg +15 -3
  43. package/social/og-image.svg +151 -0
  44. package/social/readme-agents.svg +14 -2
  45. package/social/readme-hero.svg +17 -4
  46. package/social/section-agents.svg +12 -0
  47. package/social/section-author.svg +12 -0
  48. package/social/section-dashboard.svg +12 -0
  49. package/social/section-devil.svg +12 -0
  50. package/social/section-hooks.svg +12 -0
  51. package/social/section-install.svg +12 -0
  52. package/social/section-learning.svg +12 -0
  53. package/social/section-mcp.svg +13 -1
  54. package/social/section-memory.svg +12 -0
  55. package/social/section-parallel.svg +12 -0
  56. package/social/section-routing.svg +12 -0
  57. package/social/twitter-threads.md +3 -3
  58. package/social/x-card-agents-grid.svg +14 -2
  59. package/social/x-card-free.svg +14 -2
  60. package/social/x-card-hook.svg +17 -5
  61. package/tests/memory/instincts.test.js +258 -0
  62. package/tests/memory/store.test.js +82 -0
  63. package/wiki/FAQ.md +1 -1
  64. package/wiki/Home.md +3 -3
  65. 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
- if (candidates.length === 0) {
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
- const lines = candidates.map(c => JSON.stringify({
52
- ...c,
53
- session_id: sessionId,
54
- project_path: data.cwd || process.cwd(),
55
- queued_at: new Date().toISOString(),
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
- fs.appendFileSync(queueFile, lines.join('\n') + '\n');
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.7.4",
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",
@@ -1,6 +1,6 @@
1
1
  # Agent Orchestration
2
2
 
3
- 62 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>`.
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 61 ECC specialist agents exist so the main context window stays clean:
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. 61 specialists. Zero setup.
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.7.4).
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/)
@@ -1,6 +1,6 @@
1
1
  // scripts/dashboard/server.js
2
2
  //
3
- // Local observability dashboard server (v1.7.4).
3
+ // Local observability dashboard server (v1.8.0).
4
4
  //
5
5
  // HTTP server using ONLY Node.js built-ins. Serves:
6
6
  // GET / → static index.html
@@ -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
+ };
@@ -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
  };