kodelyth-ecc 1.5.10 → 1.7.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.
Files changed (273) hide show
  1. package/.github/workflows/publish.yml +19 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +475 -0
  4. package/CLAUDE.md +48 -16
  5. package/README.md +285 -81
  6. package/VERSION +1 -1
  7. package/actions/ecc-review/README.md +243 -0
  8. package/actions/ecc-review/action.yml +158 -0
  9. package/actions/ecc-review/post-comment.js +171 -0
  10. package/actions/ecc-review/run-review.js +285 -0
  11. package/agents/backdoor-hunter.md +260 -0
  12. package/agents/chaos-engineer.md +251 -0
  13. package/agents/code-stealer-detector.md +228 -0
  14. package/agents/jailbreak-tester.md +222 -0
  15. package/agents/license-violation-finder.md +212 -0
  16. package/agents/prompt-injection-hunter.md +126 -0
  17. package/agents/secret-hunter.md +209 -0
  18. package/agents/supply-chain-auditor.md +195 -0
  19. package/bin/kodelyth-ecc.js +875 -1
  20. package/bundles/enterprise.md +172 -0
  21. package/bundles/indie-hacker.md +106 -0
  22. package/bundles/red-team.md +138 -0
  23. package/cat +0 -0
  24. package/commands/dashboard.md +67 -0
  25. package/commands/devil-mode.md +121 -0
  26. package/commands/memory-evolve.md +71 -0
  27. package/commands/replay.md +61 -0
  28. package/commands/route-model.md +48 -0
  29. package/commands/swarm.md +68 -0
  30. package/commands/verify-supply-chain.md +59 -0
  31. package/docs/dashboard.md +211 -0
  32. package/docs/evolve.md +303 -0
  33. package/docs/mcp-clients.md +167 -0
  34. package/docs/mcp.md +178 -0
  35. package/docs/replay.md +244 -0
  36. package/docs/supply-chain.md +207 -0
  37. package/docs/swarm.md +243 -0
  38. package/hooks/hooks.json +52 -0
  39. package/hooks/memory/auto-recall.js +29 -1
  40. package/hooks/safety/README.md +124 -0
  41. package/hooks/safety/lib/patterns.js +179 -0
  42. package/hooks/safety/prompt-injection-guard.js +179 -0
  43. package/hooks/safety/token-budget.js +229 -0
  44. package/install.ps1 +199 -2
  45. package/install.sh +285 -3
  46. package/package.json +30 -4
  47. package/rules/common/agent-intent-routing.md +182 -0
  48. package/rules/common/cost-aware-model-routing.md +152 -0
  49. package/scripts/dashboard/data.js +353 -0
  50. package/scripts/dashboard/server.js +324 -0
  51. package/scripts/dashboard/static/index.html +645 -0
  52. package/scripts/evolve/analyze.js +303 -0
  53. package/scripts/evolve/proposals.js +162 -0
  54. package/scripts/evolve/stats.js +219 -0
  55. package/scripts/mcp/catalog.js +244 -0
  56. package/scripts/mcp/client.js +181 -0
  57. package/scripts/mcp/prompts.js +133 -0
  58. package/scripts/mcp/resources.js +94 -0
  59. package/scripts/mcp/server.js +153 -0
  60. package/scripts/mcp/tools.js +465 -0
  61. package/scripts/replay/bundle.js +191 -0
  62. package/scripts/replay/replay.js +107 -0
  63. package/scripts/router/classify.js +232 -0
  64. package/scripts/supply-chain/manifest.js +155 -0
  65. package/scripts/supply-chain/sbom.js +202 -0
  66. package/scripts/supply-chain/verify.js +102 -0
  67. package/scripts/swarm/build-plan.js +193 -0
  68. package/skills/cost-aware-model-routing/SKILL.md +153 -0
  69. package/skills/kodelyth-quickstart/SKILL.md +7 -0
  70. package/skills/observability-dashboard/SKILL.md +119 -0
  71. package/skills/self-evolving-memory/SKILL.md +175 -0
  72. package/skills/session-replay/SKILL.md +199 -0
  73. package/skills/supply-chain-verification/SKILL.md +201 -0
  74. package/skills/swarm-orchestrator/SKILL.md +177 -0
  75. package/social/card-install.svg +1 -1
  76. package/social/facebook-group/POST.md +121 -0
  77. package/social/facebook-group/fb-1-3am-debug.png +0 -0
  78. package/social/facebook-group/fb-1-3am-debug.svg +97 -0
  79. package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
  80. package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
  81. package/social/facebook-group/fb-3-before-after.png +0 -0
  82. package/social/facebook-group/fb-3-before-after.svg +94 -0
  83. package/social/facebook-v150.svg +6 -6
  84. package/social/github-social-preview.svg +119 -100
  85. package/social/readme-hero.svg +12 -12
  86. package/social/section-agents.svg +57 -0
  87. package/social/section-author.svg +54 -0
  88. package/social/section-dashboard.svg +59 -0
  89. package/social/section-devil.svg +54 -0
  90. package/social/section-hooks.svg +51 -0
  91. package/social/section-install.svg +42 -0
  92. package/social/section-learning.svg +52 -0
  93. package/social/section-mcp.svg +46 -0
  94. package/social/section-memory.svg +57 -0
  95. package/social/section-parallel.svg +72 -0
  96. package/social/section-routing.svg +51 -0
  97. package/social/x-card-agents-grid.svg +6 -6
  98. package/tests/dashboard/data.test.js +235 -0
  99. package/tests/dashboard/server.test.js +240 -0
  100. package/tests/evolve/analyze.test.js +169 -0
  101. package/tests/evolve/proposals.test.js +173 -0
  102. package/tests/evolve/stats.test.js +167 -0
  103. package/tests/mcp/catalog.test.js +98 -0
  104. package/tests/mcp/client.test.js +113 -0
  105. package/tests/mcp/resources-prompts.test.js +70 -0
  106. package/tests/mcp/tools.test.js +159 -0
  107. package/tests/replay/bundle.test.js +181 -0
  108. package/tests/replay/replay.test.js +131 -0
  109. package/tests/router/classify.test.js +164 -0
  110. package/tests/safety/patterns.test.js +94 -0
  111. package/tests/safety/prompt-injection-guard.test.js +111 -0
  112. package/tests/safety/token-budget.test.js +119 -0
  113. package/tests/supply-chain/manifest.test.js +147 -0
  114. package/tests/supply-chain/sbom.test.js +170 -0
  115. package/tests/supply-chain/verify.test.js +146 -0
  116. package/tests/swarm/build-plan.test.js +188 -0
  117. package/wiki/Agent-Reference.md +58 -7
  118. package/wiki/FAQ.md +204 -7
  119. package/wiki/Home.md +104 -29
  120. package/wiki/Hook-Reference.md +1 -1
  121. package/wiki/Installation-Guide.md +109 -6
  122. package/wiki/Platform-Support.md +189 -72
  123. package/wiki/Skill-Reference.md +101 -6
  124. package/.agent/rules/README.md +0 -108
  125. package/.agent/rules/agents.md +0 -94
  126. package/.agent/rules/code-review.md +0 -124
  127. package/.agent/rules/coding-style.md +0 -122
  128. package/.agent/rules/design-quality.md +0 -63
  129. package/.agent/rules/development-workflow.md +0 -44
  130. package/.agent/rules/git-mastery.md +0 -366
  131. package/.agent/rules/git-workflow.md +0 -24
  132. package/.agent/rules/hooks.md +0 -25
  133. package/.agent/rules/kodelyth-always-on.md +0 -63
  134. package/.agent/rules/kodelyth-quickstart.md +0 -207
  135. package/.agent/rules/observability.md +0 -390
  136. package/.agent/rules/patterns.md +0 -50
  137. package/.agent/rules/performance.md +0 -178
  138. package/.agent/rules/security.md +0 -58
  139. package/.agent/rules/smart-debug.md +0 -241
  140. package/.agent/rules/soul.md +0 -32
  141. package/.agent/rules/testing.md +0 -46
  142. package/.agent/skills/api-guardian.md +0 -279
  143. package/.agent/skills/architect.md +0 -211
  144. package/.agent/skills/build-error-resolver.md +0 -114
  145. package/.agent/skills/chief-of-staff.md +0 -151
  146. package/.agent/skills/code-architect.md +0 -71
  147. package/.agent/skills/code-explorer.md +0 -69
  148. package/.agent/skills/code-reviewer.md +0 -237
  149. package/.agent/skills/code-simplifier.md +0 -47
  150. package/.agent/skills/comment-analyzer.md +0 -45
  151. package/.agent/skills/conversation-analyzer.md +0 -52
  152. package/.agent/skills/cpp-build-resolver.md +0 -90
  153. package/.agent/skills/cpp-reviewer.md +0 -72
  154. package/.agent/skills/csharp-reviewer.md +0 -101
  155. package/.agent/skills/dart-build-resolver.md +0 -201
  156. package/.agent/skills/database-reviewer.md +0 -91
  157. package/.agent/skills/debug-detective.md +0 -409
  158. package/.agent/skills/doc-updater.md +0 -107
  159. package/.agent/skills/docs-lookup.md +0 -68
  160. package/.agent/skills/e2e-runner.md +0 -107
  161. package/.agent/skills/flutter-reviewer.md +0 -243
  162. package/.agent/skills/gan-evaluator.md +0 -209
  163. package/.agent/skills/gan-generator.md +0 -131
  164. package/.agent/skills/gan-planner.md +0 -99
  165. package/.agent/skills/go-build-resolver.md +0 -94
  166. package/.agent/skills/go-reviewer.md +0 -76
  167. package/.agent/skills/harness-optimizer.md +0 -35
  168. package/.agent/skills/healthcare-reviewer.md +0 -83
  169. package/.agent/skills/java-build-resolver.md +0 -153
  170. package/.agent/skills/java-reviewer.md +0 -92
  171. package/.agent/skills/kodelyth-advisor.md +0 -172
  172. package/.agent/skills/kotlin-build-resolver.md +0 -118
  173. package/.agent/skills/kotlin-reviewer.md +0 -159
  174. package/.agent/skills/loop-operator.md +0 -36
  175. package/.agent/skills/migration-guide.md +0 -368
  176. package/.agent/skills/opensource-forker.md +0 -198
  177. package/.agent/skills/opensource-packager.md +0 -249
  178. package/.agent/skills/opensource-sanitizer.md +0 -188
  179. package/.agent/skills/pair-programmer.md +0 -260
  180. package/.agent/skills/performance-optimizer.md +0 -446
  181. package/.agent/skills/planner.md +0 -212
  182. package/.agent/skills/pr-test-analyzer.md +0 -45
  183. package/.agent/skills/python-reviewer.md +0 -98
  184. package/.agent/skills/pytorch-build-resolver.md +0 -120
  185. package/.agent/skills/refactor-cleaner.md +0 -85
  186. package/.agent/skills/rust-build-resolver.md +0 -148
  187. package/.agent/skills/rust-reviewer.md +0 -94
  188. package/.agent/skills/security-reviewer.md +0 -108
  189. package/.agent/skills/seo-specialist.md +0 -62
  190. package/.agent/skills/silent-failure-hunter.md +0 -50
  191. package/.agent/skills/tdd-guide.md +0 -91
  192. package/.agent/skills/type-design-analyzer.md +0 -41
  193. package/.agent/skills/typescript-reviewer.md +0 -112
  194. package/.agent/skills/ux-reviewer.md +0 -483
  195. package/.agent/workflows/agent-sort.md +0 -23
  196. package/.agent/workflows/aside.md +0 -164
  197. package/.agent/workflows/build-fix.md +0 -62
  198. package/.agent/workflows/checkpoint.md +0 -74
  199. package/.agent/workflows/claw.md +0 -23
  200. package/.agent/workflows/code-review.md +0 -289
  201. package/.agent/workflows/context-budget.md +0 -23
  202. package/.agent/workflows/cpp-build.md +0 -173
  203. package/.agent/workflows/cpp-review.md +0 -132
  204. package/.agent/workflows/cpp-test.md +0 -251
  205. package/.agent/workflows/devfleet.md +0 -23
  206. package/.agent/workflows/docs.md +0 -23
  207. package/.agent/workflows/e2e.md +0 -268
  208. package/.agent/workflows/eval.md +0 -23
  209. package/.agent/workflows/evolve.md +0 -178
  210. package/.agent/workflows/feature-dev.md +0 -49
  211. package/.agent/workflows/flutter-build.md +0 -164
  212. package/.agent/workflows/flutter-review.md +0 -116
  213. package/.agent/workflows/flutter-test.md +0 -144
  214. package/.agent/workflows/gan-build.md +0 -99
  215. package/.agent/workflows/gan-design.md +0 -35
  216. package/.agent/workflows/go-build.md +0 -183
  217. package/.agent/workflows/go-review.md +0 -148
  218. package/.agent/workflows/go-test.md +0 -268
  219. package/.agent/workflows/gradle-build.md +0 -70
  220. package/.agent/workflows/harness-audit.md +0 -73
  221. package/.agent/workflows/hookify-configure.md +0 -14
  222. package/.agent/workflows/hookify-help.md +0 -46
  223. package/.agent/workflows/hookify-list.md +0 -21
  224. package/.agent/workflows/hookify.md +0 -50
  225. package/.agent/workflows/instinct-export.md +0 -66
  226. package/.agent/workflows/instinct-import.md +0 -114
  227. package/.agent/workflows/instinct-status.md +0 -59
  228. package/.agent/workflows/jira.md +0 -106
  229. package/.agent/workflows/kotlin-build.md +0 -174
  230. package/.agent/workflows/kotlin-review.md +0 -140
  231. package/.agent/workflows/kotlin-test.md +0 -312
  232. package/.agent/workflows/learn-eval.md +0 -116
  233. package/.agent/workflows/learn.md +0 -70
  234. package/.agent/workflows/loop-start.md +0 -32
  235. package/.agent/workflows/loop-status.md +0 -24
  236. package/.agent/workflows/model-route.md +0 -26
  237. package/.agent/workflows/multi-backend.md +0 -158
  238. package/.agent/workflows/multi-execute.md +0 -315
  239. package/.agent/workflows/multi-frontend.md +0 -158
  240. package/.agent/workflows/multi-plan.md +0 -268
  241. package/.agent/workflows/multi-workflow.md +0 -191
  242. package/.agent/workflows/orchestrate.md +0 -135
  243. package/.agent/workflows/plan.md +0 -117
  244. package/.agent/workflows/pm2.md +0 -272
  245. package/.agent/workflows/projects.md +0 -39
  246. package/.agent/workflows/promote.md +0 -41
  247. package/.agent/workflows/prompt-optimize.md +0 -23
  248. package/.agent/workflows/prp-commit.md +0 -112
  249. package/.agent/workflows/prp-implement.md +0 -385
  250. package/.agent/workflows/prp-plan.md +0 -502
  251. package/.agent/workflows/prp-pr.md +0 -184
  252. package/.agent/workflows/prp-prd.md +0 -447
  253. package/.agent/workflows/prune.md +0 -31
  254. package/.agent/workflows/python-review.md +0 -297
  255. package/.agent/workflows/quality-gate.md +0 -29
  256. package/.agent/workflows/refactor-clean.md +0 -80
  257. package/.agent/workflows/resume-session.md +0 -156
  258. package/.agent/workflows/review-pr.md +0 -37
  259. package/.agent/workflows/rules-distill.md +0 -20
  260. package/.agent/workflows/rust-build.md +0 -187
  261. package/.agent/workflows/rust-review.md +0 -142
  262. package/.agent/workflows/rust-test.md +0 -308
  263. package/.agent/workflows/santa-loop.md +0 -175
  264. package/.agent/workflows/save-session.md +0 -275
  265. package/.agent/workflows/sessions.md +0 -333
  266. package/.agent/workflows/setup-pm.md +0 -80
  267. package/.agent/workflows/skill-create.md +0 -174
  268. package/.agent/workflows/skill-health.md +0 -54
  269. package/.agent/workflows/tdd.md +0 -231
  270. package/.agent/workflows/test-coverage.md +0 -69
  271. package/.agent/workflows/update-codemaps.md +0 -72
  272. package/.agent/workflows/update-docs.md +0 -84
  273. package/.agent/workflows/verify.md +0 -23
@@ -0,0 +1,303 @@
1
+ // scripts/evolve/analyze.js
2
+ //
3
+ // Phase 3.4 — Self-evolving memory: signal → proposal logic.
4
+ //
5
+ // Pure, deterministic functions. No I/O. Given:
6
+ // - reuse stats (from stats.getReuseStats)
7
+ // - routing-miss stats (from stats.getRoutingMissStats)
8
+ // - the memories themselves (from scripts/memory/store.listAll)
9
+ // - the current routing rule text (string)
10
+ //
11
+ // Produces a list of structured proposals. Two kinds:
12
+ //
13
+ // 1) skill-upgrade — a memory has been surfaced ≥ N times and across
14
+ // ≥ M distinct sessions → propose creating a draft
15
+ // SKILL.md from it.
16
+ //
17
+ // 2) routing-addition — ≥ K substantive prompts cluster on the same token
18
+ // set without any memory match → propose adding a
19
+ // new routing entry under the most-likely tier.
20
+ //
21
+ // Every proposal carries:
22
+ // - id (deterministic given evidence — stable across re-runs)
23
+ // - type
24
+ // - evidence (the raw signal data)
25
+ // - proposal: { kind, target_path, diff (markdown), rationale }
26
+ //
27
+ // Apply step writes the diff to disk as a draft file the user reviews
28
+ // before committing. NEVER auto-applied.
29
+ 'use strict';
30
+
31
+ const crypto = require('crypto');
32
+
33
+ const DEFAULT_REUSE_MIN_COUNT = 3;
34
+ const DEFAULT_REUSE_MIN_SESSIONS = 2;
35
+ const DEFAULT_MISS_MIN_COUNT = 3;
36
+ const DEFAULT_MISS_CLUSTER_MIN = 2; // distinct prompt-hashes per cluster
37
+
38
+ // ── helpers ──────────────────────────────────────────────────────────────────
39
+
40
+ function deterministicId(prefix, payload) {
41
+ const h = crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 10);
42
+ return `${prefix}-${h}`;
43
+ }
44
+
45
+ function slugify(s) {
46
+ return String(s || '')
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9]+/g, '-')
49
+ .replace(/^-+|-+$/g, '')
50
+ .slice(0, 60) || 'untitled';
51
+ }
52
+
53
+ // ── skill-upgrade proposals ─────────────────────────────────────────────────
54
+
55
+ function buildSkillUpgradeMarkdown(memory) {
56
+ const slug = slugify(memory.problem);
57
+ const tags = (memory.tags || []).slice(0, 8);
58
+ const desc = (memory.problem || '').slice(0, 180);
59
+ const approach = (memory.approach || '').trim();
60
+ const lang = memory.language || 'general';
61
+
62
+ return [
63
+ '---',
64
+ `name: ${slug}`,
65
+ `description: ${desc.replace(/[\r\n]+/g, ' ')} (auto-derived from memory ${memory.id} after repeated reuse)`,
66
+ `origin: kodelyth-evolve`,
67
+ `language: ${lang}`,
68
+ `tags:`,
69
+ ...tags.map(t => ` - ${t}`),
70
+ '---',
71
+ '',
72
+ `# Skill: ${slug}`,
73
+ '',
74
+ '> **Auto-derived draft.** Generated by `kodelyth-ecc evolve` after this memory was surfaced repeatedly across multiple sessions. Review, refine, and rename before committing.',
75
+ '',
76
+ '## Problem',
77
+ '',
78
+ memory.problem || '_(no problem statement)_',
79
+ '',
80
+ '## Approach',
81
+ '',
82
+ approach || '_(no approach text)_',
83
+ '',
84
+ memory.gotchas?.length ? '## Gotchas\n\n' + memory.gotchas.map(g => `- ${g}`).join('\n') + '\n' : '',
85
+ '## When to invoke',
86
+ '',
87
+ '_Replace this section with explicit trigger conditions. Examples that worked:_',
88
+ '',
89
+ '_(add 2-3 real prompts that historically benefitted from this approach)_',
90
+ '',
91
+ '## Origin',
92
+ '',
93
+ `- Memory id: \`${memory.id}\``,
94
+ `- Captured at: ${memory.captured_at || 'unknown'}`,
95
+ `- Source: ${memory.source || 'unknown'}`,
96
+ `- Auto-promoted by Phase 3.4 self-evolving memory.`,
97
+ '',
98
+ ].filter(Boolean).join('\n');
99
+ }
100
+
101
+ function analyzeReuseForProposals({
102
+ reuseStats,
103
+ memories,
104
+ thresholds: t = {},
105
+ } = {}) {
106
+ if (!reuseStats || !Array.isArray(memories)) return [];
107
+ const minCount = t.minCount ?? DEFAULT_REUSE_MIN_COUNT;
108
+ const minSessions = t.minSessions ?? DEFAULT_REUSE_MIN_SESSIONS;
109
+
110
+ const memoriesById = new Map(memories.map(m => [m.id, m]));
111
+ const proposals = [];
112
+
113
+ for (const entry of reuseStats.entries || []) {
114
+ if (entry.count < minCount) continue;
115
+ if ((entry.sessions || []).length < minSessions) continue;
116
+ const memory = memoriesById.get(entry.id);
117
+ if (!memory) continue;
118
+
119
+ const targetPath = `skills/${slugify(memory.problem)}/SKILL.md`;
120
+ const evidence = {
121
+ memoryId: entry.id,
122
+ reuseCount: entry.count,
123
+ sessions: entry.sessions,
124
+ projects: entry.projects || [],
125
+ firstSurfaced: entry.firstSurfaced,
126
+ lastSurfaced: entry.lastSurfaced,
127
+ };
128
+ proposals.push({
129
+ id: deterministicId('skill', evidence),
130
+ type: 'skill-upgrade',
131
+ evidence,
132
+ proposal: {
133
+ kind: 'create-skill',
134
+ target_path: targetPath,
135
+ diff: buildSkillUpgradeMarkdown(memory),
136
+ rationale:
137
+ `Memory "${memory.problem}" surfaced ${entry.count}x across ` +
138
+ `${(entry.sessions || []).length} sessions — promoting to a draft skill.`,
139
+ },
140
+ });
141
+ }
142
+ return proposals;
143
+ }
144
+
145
+ // ── routing-addition proposals ──────────────────────────────────────────────
146
+
147
+ const STOP = new Set([
148
+ 'the','a','an','to','of','in','on','for','and','or','but','if','is','are',
149
+ 'was','were','be','been','have','has','had','do','does','did','this','that',
150
+ 'it','its','i','you','we','they','my','your','our','can','will','should',
151
+ 'how','what','why','when','where','use','using','run','make','need','want',
152
+ ]);
153
+
154
+ function topTokens(tokens, k = 4) {
155
+ const filtered = (tokens || []).filter(t => t.length >= 3 && !STOP.has(t));
156
+ return filtered.slice(0, k);
157
+ }
158
+
159
+ /**
160
+ * Cluster routing-miss entries by their top-K tokens. Two entries are in
161
+ * the same cluster if their top-K token sets share at least 2 tokens.
162
+ * Greedy / O(n²) — fine for thousands of misses, this isn't BM25.
163
+ */
164
+ function clusterMisses(missEntries) {
165
+ const clusters = [];
166
+ for (const m of missEntries) {
167
+ const myTokens = topTokens(m.tokens);
168
+ if (myTokens.length === 0) continue;
169
+ let placed = false;
170
+ for (const c of clusters) {
171
+ const overlap = myTokens.filter(t => c.tokens.has(t)).length;
172
+ if (overlap >= 2) {
173
+ c.entries.push(m);
174
+ c.count += m.count;
175
+ myTokens.forEach(t => c.tokens.add(t));
176
+ placed = true;
177
+ break;
178
+ }
179
+ }
180
+ if (!placed) {
181
+ clusters.push({
182
+ tokens: new Set(myTokens),
183
+ entries: [m],
184
+ count: m.count,
185
+ });
186
+ }
187
+ }
188
+ return clusters
189
+ .map(c => ({
190
+ tokens: [...c.tokens],
191
+ entries: c.entries,
192
+ count: c.count,
193
+ }))
194
+ .sort((a, b) => b.count - a.count);
195
+ }
196
+
197
+ function buildRoutingProposalMarkdown(cluster) {
198
+ const sampleTokens = cluster.tokens.slice(0, 4).join(' / ');
199
+ const sampleLines = cluster.entries
200
+ .flatMap(e => e.samples || [])
201
+ .slice(0, 5)
202
+ .map(s => ` - "${s.replace(/\n/g, ' ').slice(0, 140)}"`)
203
+ .join('\n');
204
+ return [
205
+ '<!-- proposed addition to rules/common/agent-intent-routing.md -->',
206
+ '<!-- review by hand, decide on tier + agent, then merge -->',
207
+ '',
208
+ `### TODO-agent — covers prompts about ${sampleTokens}`,
209
+ '',
210
+ `Trigger if the user mentions ${cluster.tokens.slice(0, 6).map(t => `\`${t}\``).join(', ')}.`,
211
+ '',
212
+ '| Signal | Real human phrasing |',
213
+ '|---|---|',
214
+ `| repeated unrouted prompt cluster | ${cluster.tokens.slice(0, 5).join(', ')} |`,
215
+ '',
216
+ `**Origin:** Phase 3.4 self-evolving memory \u2014 ${cluster.count} prompts in this cluster were submitted with no memory match and (presumably) no specialist routing.`,
217
+ '',
218
+ 'Recent samples:',
219
+ sampleLines || ' _(none)_',
220
+ '',
221
+ '_(Pick the right tier in `rules/common/agent-intent-routing.md` before merging. Do NOT commit this block as-is — replace `TODO-agent` with the real agent name and slot under the correct priority tier.)_',
222
+ '',
223
+ ].join('\n');
224
+ }
225
+
226
+ function analyzeMissesForProposals({
227
+ missStats,
228
+ thresholds: t = {},
229
+ } = {}) {
230
+ if (!missStats) return [];
231
+ const minCount = t.minCount ?? DEFAULT_MISS_MIN_COUNT;
232
+ const minClusterDistinct = t.minClusterDistinct ?? DEFAULT_MISS_CLUSTER_MIN;
233
+
234
+ const clusters = clusterMisses(missStats.entries || []);
235
+ const proposals = [];
236
+
237
+ for (const c of clusters) {
238
+ if (c.count < minCount) continue;
239
+ if (c.entries.length < minClusterDistinct) continue;
240
+ const evidence = {
241
+ tokens: c.tokens,
242
+ occurrences: c.count,
243
+ distinct_prompts: c.entries.length,
244
+ sample_prompts: c.entries.flatMap(e => e.samples || []).slice(0, 5),
245
+ };
246
+ proposals.push({
247
+ id: deterministicId('route', { tokens: c.tokens.sort() }),
248
+ type: 'routing-addition',
249
+ evidence,
250
+ proposal: {
251
+ kind: 'add-routing-entry',
252
+ target_path: 'rules/common/agent-intent-routing.md',
253
+ diff: buildRoutingProposalMarkdown(c),
254
+ rationale:
255
+ `${c.count} substantive prompts (${c.entries.length} distinct) clustered on ` +
256
+ `tokens [${c.tokens.slice(0, 4).join(', ')}] without any memory match. ` +
257
+ `Likely a routing gap.`,
258
+ },
259
+ });
260
+ }
261
+ return proposals;
262
+ }
263
+
264
+ // ── single entry point ──────────────────────────────────────────────────────
265
+
266
+ function analyzeAll({
267
+ reuseStats,
268
+ missStats,
269
+ memories,
270
+ thresholds = {},
271
+ } = {}) {
272
+ return [
273
+ ...analyzeReuseForProposals({
274
+ reuseStats,
275
+ memories: memories || [],
276
+ thresholds: {
277
+ minCount: thresholds.reuseMinCount,
278
+ minSessions: thresholds.reuseMinSessions,
279
+ },
280
+ }),
281
+ ...analyzeMissesForProposals({
282
+ missStats,
283
+ thresholds: {
284
+ minCount: thresholds.missMinCount,
285
+ minClusterDistinct: thresholds.missMinClusterDistinct,
286
+ },
287
+ }),
288
+ ];
289
+ }
290
+
291
+ module.exports = {
292
+ DEFAULT_REUSE_MIN_COUNT,
293
+ DEFAULT_REUSE_MIN_SESSIONS,
294
+ DEFAULT_MISS_MIN_COUNT,
295
+ DEFAULT_MISS_CLUSTER_MIN,
296
+ analyzeReuseForProposals,
297
+ analyzeMissesForProposals,
298
+ analyzeAll,
299
+ buildSkillUpgradeMarkdown,
300
+ buildRoutingProposalMarkdown,
301
+ clusterMisses,
302
+ _internals: { topTokens, slugify, deterministicId },
303
+ };
@@ -0,0 +1,162 @@
1
+ // scripts/evolve/proposals.js
2
+ //
3
+ // Phase 3.4 — Self-evolving memory: proposal log.
4
+ //
5
+ // Append-only JSONL store of proposals with state transitions:
6
+ // pending → initial state when written
7
+ // accepted → user ran `evolve accept <id>`; draft file written to disk
8
+ // rejected → user ran `evolve reject <id>`
9
+ // applied → user committed the accepted draft (advisory, set manually)
10
+ //
11
+ // State changes are append-only (a new line per state event), so the full
12
+ // audit trail is preserved. Reads collapse to "latest state per id".
13
+ //
14
+ // Storage:
15
+ // ${KODELYTH_EVOLVE_DIR:-~/.kodelyth/evolve}/proposals.jsonl
16
+ //
17
+ // Pure where possible. Public functions accept dir explicitly.
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const os = require('os');
23
+
24
+ const DEFAULT_DIR = process.env.KODELYTH_EVOLVE_DIR
25
+ || path.join(os.homedir(), '.kodelyth', 'evolve');
26
+
27
+ const PROPOSALS_FILE = 'proposals.jsonl';
28
+
29
+ const VALID_STATUSES = new Set(['pending', 'accepted', 'rejected', 'applied']);
30
+
31
+ function ensureDir(dir) {
32
+ try {
33
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
34
+ return true;
35
+ } catch { return false; }
36
+ }
37
+
38
+ function readAll(dir = DEFAULT_DIR) {
39
+ const p = path.join(dir, PROPOSALS_FILE);
40
+ if (!fs.existsSync(p)) return [];
41
+ try {
42
+ return fs.readFileSync(p, 'utf8')
43
+ .split('\n')
44
+ .filter(Boolean)
45
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
46
+ .filter(Boolean);
47
+ } catch { return []; }
48
+ }
49
+
50
+ /**
51
+ * Reduce raw event log → latest state per proposal id.
52
+ * Returns an ordered array (insertion order of first event per id).
53
+ */
54
+ function readLatest(dir = DEFAULT_DIR) {
55
+ const events = readAll(dir);
56
+ const byId = new Map();
57
+ for (const ev of events) {
58
+ if (!ev.id) continue;
59
+ if (!byId.has(ev.id)) {
60
+ byId.set(ev.id, { ...ev });
61
+ } else {
62
+ const cur = byId.get(ev.id);
63
+ // Merge: keep original proposal/evidence, update mutable fields.
64
+ cur.status = ev.status || cur.status;
65
+ cur.last_updated = ev.timestamp || cur.last_updated;
66
+ cur.applied_path = ev.applied_path || cur.applied_path;
67
+ cur.note = ev.note ?? cur.note;
68
+ }
69
+ }
70
+ return [...byId.values()];
71
+ }
72
+
73
+ function appendEvent(event, dir = DEFAULT_DIR) {
74
+ if (!ensureDir(dir)) return false;
75
+ try {
76
+ fs.appendFileSync(path.join(dir, PROPOSALS_FILE), JSON.stringify(event) + '\n');
77
+ return true;
78
+ } catch { return false; }
79
+ }
80
+
81
+ /**
82
+ * Append a fresh proposal with status=pending. Idempotent on `id`:
83
+ * if a proposal with this id already exists in the log, this is a no-op.
84
+ *
85
+ * @returns the proposal (existing or new)
86
+ */
87
+ function appendProposal(proposal, dir = DEFAULT_DIR) {
88
+ if (!proposal || !proposal.id || !proposal.type || !proposal.proposal) {
89
+ throw new Error('appendProposal: proposal must include id, type, and proposal{}');
90
+ }
91
+ const existing = readLatest(dir).find(p => p.id === proposal.id);
92
+ if (existing) return existing;
93
+
94
+ const event = {
95
+ ...proposal,
96
+ status: 'pending',
97
+ created_at: new Date().toISOString(),
98
+ timestamp: new Date().toISOString(),
99
+ };
100
+ appendEvent(event, dir);
101
+ return event;
102
+ }
103
+
104
+ function setStatus(id, status, dir = DEFAULT_DIR, extras = {}) {
105
+ if (!VALID_STATUSES.has(status)) {
106
+ throw new Error(`setStatus: invalid status "${status}" (expected one of ${[...VALID_STATUSES].join(', ')})`);
107
+ }
108
+ const current = readLatest(dir).find(p => p.id === id);
109
+ if (!current) return null;
110
+ const event = {
111
+ id,
112
+ status,
113
+ timestamp: new Date().toISOString(),
114
+ ...extras,
115
+ };
116
+ appendEvent(event, dir);
117
+ return { ...current, ...event };
118
+ }
119
+
120
+ function listByStatus(status = null, dir = DEFAULT_DIR) {
121
+ const all = readLatest(dir);
122
+ if (!status) return all;
123
+ return all.filter(p => p.status === status);
124
+ }
125
+
126
+ function findById(id, dir = DEFAULT_DIR) {
127
+ return readLatest(dir).find(p => p.id === id) || null;
128
+ }
129
+
130
+ /**
131
+ * Write the proposal's diff to its target_path (relative to repoRoot).
132
+ * Defensive: refuses to overwrite an existing file unless overwrite=true.
133
+ *
134
+ * @returns { written, path } on success
135
+ * @throws on conflict / write failure
136
+ */
137
+ function applyProposalToDisk(proposal, { repoRoot, overwrite = false } = {}) {
138
+ if (!proposal?.proposal?.diff || !proposal.proposal.target_path) {
139
+ throw new Error('applyProposalToDisk: proposal lacks diff or target_path');
140
+ }
141
+ if (!repoRoot) throw new Error('applyProposalToDisk: repoRoot is required');
142
+ const abs = path.resolve(repoRoot, proposal.proposal.target_path);
143
+ if (fs.existsSync(abs) && !overwrite) {
144
+ throw new Error(`applyProposalToDisk: refusing to overwrite existing file at ${abs} (pass overwrite=true)`);
145
+ }
146
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
147
+ fs.writeFileSync(abs, proposal.proposal.diff);
148
+ return { written: true, path: abs };
149
+ }
150
+
151
+ module.exports = {
152
+ DEFAULT_DIR,
153
+ PROPOSALS_FILE,
154
+ VALID_STATUSES,
155
+ appendProposal,
156
+ setStatus,
157
+ listByStatus,
158
+ findById,
159
+ readAll,
160
+ readLatest,
161
+ applyProposalToDisk,
162
+ };
@@ -0,0 +1,219 @@
1
+ // scripts/evolve/stats.js
2
+ //
3
+ // Phase 3.4 — Self-evolving memory: signal recording.
4
+ //
5
+ // Two signal streams are tracked, both purely local, zero telemetry:
6
+ //
7
+ // 1) reuse signals — memory IDs surfaced by auto-recall, counted
8
+ // per memory + per session. High counts → the
9
+ // memory is being repeatedly useful → candidate
10
+ // for promotion to a skill.
11
+ //
12
+ // 2) routing misses — substantive UserPromptSubmit prompts where
13
+ // memory recall returned NOTHING. Clustered later
14
+ // by tokens to surface "we keep getting asked
15
+ // about X but have no agent / skill / memory for it".
16
+ //
17
+ // Storage layout (default ${HOME}/.kodelyth/evolve/):
18
+ // reuse.json { byMemory: { id: { count, sessions[], lastSurfaced } } }
19
+ // routing-misses.jsonl append-only — one prompt per line
20
+ //
21
+ // Override the directory with $KODELYTH_EVOLVE_DIR.
22
+ //
23
+ // Pure functions — every function takes its dir explicitly. No global state.
24
+ // Safe to call from a hook: every public function swallows internal errors
25
+ // and returns either a sane default or `false`/`null`. Hooks must never crash.
26
+ 'use strict';
27
+
28
+ const fs = require('fs');
29
+ const os = require('os');
30
+ const path = require('path');
31
+ const crypto = require('crypto');
32
+
33
+ const DEFAULT_DIR = process.env.KODELYTH_EVOLVE_DIR
34
+ || path.join(os.homedir(), '.kodelyth', 'evolve');
35
+
36
+ const REUSE_FILE = 'reuse.json';
37
+ const MISSES_FILE = 'routing-misses.jsonl';
38
+
39
+ // ── helpers ──────────────────────────────────────────────────────────────────
40
+
41
+ function ensureDir(dir) {
42
+ try {
43
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
44
+ return true;
45
+ } catch { return false; }
46
+ }
47
+
48
+ function safeReadJson(p, fallback) {
49
+ try {
50
+ if (!fs.existsSync(p)) return fallback;
51
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
52
+ } catch { return fallback; }
53
+ }
54
+
55
+ function safeWriteJson(p, data) {
56
+ try {
57
+ fs.writeFileSync(p, JSON.stringify(data, null, 2));
58
+ return true;
59
+ } catch { return false; }
60
+ }
61
+
62
+ function safeAppendLine(p, line) {
63
+ try {
64
+ fs.appendFileSync(p, line.replace(/\n+$/, '') + '\n');
65
+ return true;
66
+ } catch { return false; }
67
+ }
68
+
69
+ function tokenize(text) {
70
+ if (!text) return [];
71
+ return String(text)
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9_\-/.\s]/g, ' ')
74
+ .split(/\s+/)
75
+ .filter(Boolean);
76
+ }
77
+
78
+ function hashPrompt(prompt) {
79
+ return crypto.createHash('sha256').update(String(prompt)).digest('hex').slice(0, 12);
80
+ }
81
+
82
+ // ── reuse signals ────────────────────────────────────────────────────────────
83
+
84
+ function emptyReuse() { return { byMemory: {}, lastUpdated: null }; }
85
+
86
+ function readReuse(dir = DEFAULT_DIR) {
87
+ return safeReadJson(path.join(dir, REUSE_FILE), emptyReuse());
88
+ }
89
+
90
+ /**
91
+ * Record that a memory was surfaced to the user.
92
+ * Idempotent per (memoryId, sessionId): if the same memory has already
93
+ * been counted this session, count is NOT bumped again. This matches the
94
+ * existing auto-recall semantics ("never re-surface the same memory twice
95
+ * in a session").
96
+ *
97
+ * @returns true on success, false on any error (never throws).
98
+ */
99
+ function recordSurface({ memoryId, sessionId, projectRoot, timestamp } = {}, dir = DEFAULT_DIR) {
100
+ if (!memoryId) return false;
101
+ if (!ensureDir(dir)) return false;
102
+ const data = readReuse(dir);
103
+ const ts = timestamp || new Date().toISOString();
104
+ const sid = String(sessionId || 'unknown');
105
+
106
+ const cur = data.byMemory[memoryId] || {
107
+ count: 0,
108
+ sessions: [],
109
+ projects: [],
110
+ firstSurfaced: ts,
111
+ lastSurfaced: ts,
112
+ };
113
+
114
+ if (!cur.sessions.includes(sid)) {
115
+ cur.count += 1;
116
+ cur.sessions.push(sid);
117
+ }
118
+ if (projectRoot && !cur.projects.includes(projectRoot)) {
119
+ cur.projects.push(projectRoot);
120
+ }
121
+ cur.lastSurfaced = ts;
122
+
123
+ data.byMemory[memoryId] = cur;
124
+ data.lastUpdated = ts;
125
+ return safeWriteJson(path.join(dir, REUSE_FILE), data);
126
+ }
127
+
128
+ function getReuseStats(dir = DEFAULT_DIR) {
129
+ const data = readReuse(dir);
130
+ const entries = Object.entries(data.byMemory).map(([id, v]) => ({ id, ...v }));
131
+ entries.sort((a, b) => b.count - a.count);
132
+ return {
133
+ total_memories_tracked: entries.length,
134
+ total_surfaces: entries.reduce((s, e) => s + e.count, 0),
135
+ last_updated: data.lastUpdated,
136
+ entries,
137
+ };
138
+ }
139
+
140
+ // ── routing miss signals ─────────────────────────────────────────────────────
141
+
142
+ function readMisses(dir = DEFAULT_DIR) {
143
+ const p = path.join(dir, MISSES_FILE);
144
+ if (!fs.existsSync(p)) return [];
145
+ try {
146
+ return fs.readFileSync(p, 'utf8')
147
+ .split('\n')
148
+ .filter(Boolean)
149
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
150
+ .filter(Boolean);
151
+ } catch { return []; }
152
+ }
153
+
154
+ /**
155
+ * Record a substantive prompt where memory recall returned nothing.
156
+ * Hook layer decides what counts as "substantive" — this function is dumb
157
+ * persistence. Stores the prompt itself (capped) plus tokens for clustering.
158
+ *
159
+ * @returns true on success, false on any error (never throws).
160
+ */
161
+ function recordRoutingMiss({ prompt, sessionId, projectRoot, timestamp } = {}, dir = DEFAULT_DIR) {
162
+ if (!prompt || typeof prompt !== 'string') return false;
163
+ if (!ensureDir(dir)) return false;
164
+
165
+ // Cap stored prompt to avoid log bloat from accidental paste of huge files.
166
+ const trimmed = prompt.slice(0, 1000);
167
+ const entry = {
168
+ hash: hashPrompt(trimmed),
169
+ prompt: trimmed,
170
+ tokens: tokenize(trimmed).slice(0, 32),
171
+ session_id: String(sessionId || 'unknown'),
172
+ project: projectRoot || null,
173
+ recorded_at: timestamp || new Date().toISOString(),
174
+ };
175
+ return safeAppendLine(path.join(dir, MISSES_FILE), JSON.stringify(entry));
176
+ }
177
+
178
+ function getRoutingMissStats(dir = DEFAULT_DIR) {
179
+ const all = readMisses(dir);
180
+ const byHash = new Map();
181
+ for (const m of all) {
182
+ const cur = byHash.get(m.hash) || { hash: m.hash, count: 0, samples: [], firstSeen: m.recorded_at, lastSeen: m.recorded_at, tokens: m.tokens };
183
+ cur.count += 1;
184
+ if (cur.samples.length < 3) cur.samples.push(m.prompt);
185
+ cur.lastSeen = m.recorded_at;
186
+ byHash.set(m.hash, cur);
187
+ }
188
+ const entries = [...byHash.values()].sort((a, b) => b.count - a.count);
189
+ return {
190
+ total_misses: all.length,
191
+ unique_prompts: entries.length,
192
+ entries,
193
+ };
194
+ }
195
+
196
+ // ── reset (test helper) ──────────────────────────────────────────────────────
197
+
198
+ function resetAll(dir = DEFAULT_DIR) {
199
+ try {
200
+ if (fs.existsSync(path.join(dir, REUSE_FILE))) fs.unlinkSync(path.join(dir, REUSE_FILE));
201
+ if (fs.existsSync(path.join(dir, MISSES_FILE))) fs.unlinkSync(path.join(dir, MISSES_FILE));
202
+ return true;
203
+ } catch { return false; }
204
+ }
205
+
206
+ module.exports = {
207
+ DEFAULT_DIR,
208
+ REUSE_FILE,
209
+ MISSES_FILE,
210
+ recordSurface,
211
+ readReuse,
212
+ getReuseStats,
213
+ recordRoutingMiss,
214
+ readMisses,
215
+ getRoutingMissStats,
216
+ resetAll,
217
+ // exposed for tests
218
+ _internals: { tokenize, hashPrompt },
219
+ };