pikakit 2.0.0 → 3.0.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 (92) hide show
  1. package/README.md +8 -8
  2. package/lib/agent-cli/bin/agent.js +187 -0
  3. package/lib/agent-cli/lib/audit.js +154 -0
  4. package/lib/agent-cli/lib/audit.test.js +100 -0
  5. package/lib/agent-cli/lib/auto-learn.js +319 -0
  6. package/lib/agent-cli/lib/backup.js +138 -0
  7. package/lib/agent-cli/lib/backup.test.js +78 -0
  8. package/lib/agent-cli/lib/cognitive-lesson.js +476 -0
  9. package/lib/agent-cli/lib/completion.js +149 -0
  10. package/lib/agent-cli/lib/config.js +35 -0
  11. package/lib/agent-cli/lib/eslint-fix.js +238 -0
  12. package/lib/agent-cli/lib/evolution-signal.js +215 -0
  13. package/lib/agent-cli/lib/export.js +86 -0
  14. package/lib/agent-cli/lib/export.test.js +65 -0
  15. package/lib/agent-cli/lib/fix.js +337 -0
  16. package/lib/agent-cli/lib/fix.test.js +80 -0
  17. package/lib/agent-cli/lib/gemini-export.js +83 -0
  18. package/lib/agent-cli/lib/generate-registry.js +42 -0
  19. package/lib/agent-cli/lib/hooks/install-hooks.js +152 -0
  20. package/lib/agent-cli/lib/hooks/lint-learn.js +172 -0
  21. package/lib/agent-cli/lib/icons.js +93 -0
  22. package/lib/agent-cli/lib/ignore.js +116 -0
  23. package/lib/agent-cli/lib/ignore.test.js +58 -0
  24. package/lib/agent-cli/lib/init.js +124 -0
  25. package/lib/agent-cli/lib/knowledge-index.js +326 -0
  26. package/lib/agent-cli/lib/knowledge-metrics.js +335 -0
  27. package/lib/agent-cli/lib/knowledge-retention.js +398 -0
  28. package/lib/agent-cli/lib/knowledge-validator.js +312 -0
  29. package/lib/agent-cli/lib/learn.js +255 -0
  30. package/lib/agent-cli/lib/learn.test.js +70 -0
  31. package/lib/agent-cli/lib/proposals.js +199 -0
  32. package/lib/agent-cli/lib/proposals.test.js +56 -0
  33. package/lib/agent-cli/lib/recall.js +826 -0
  34. package/lib/agent-cli/lib/recall.test.js +107 -0
  35. package/lib/agent-cli/lib/selfevolution-bridge.js +167 -0
  36. package/lib/agent-cli/lib/settings.js +203 -0
  37. package/lib/agent-cli/lib/skill-learn.js +296 -0
  38. package/lib/agent-cli/lib/stats.js +132 -0
  39. package/lib/agent-cli/lib/stats.test.js +94 -0
  40. package/lib/agent-cli/lib/types.js +33 -0
  41. package/lib/agent-cli/lib/ui/audit-ui.js +146 -0
  42. package/lib/agent-cli/lib/ui/backup-ui.js +107 -0
  43. package/lib/agent-cli/lib/ui/clack-helpers.js +317 -0
  44. package/lib/agent-cli/lib/ui/common.js +83 -0
  45. package/lib/agent-cli/lib/ui/completion-ui.js +126 -0
  46. package/lib/agent-cli/lib/ui/custom-select.js +69 -0
  47. package/lib/agent-cli/lib/ui/dashboard-ui.js +222 -0
  48. package/lib/agent-cli/lib/ui/evolution-signals-ui.js +107 -0
  49. package/lib/agent-cli/lib/ui/export-ui.js +94 -0
  50. package/lib/agent-cli/lib/ui/fix-all-ui.js +191 -0
  51. package/lib/agent-cli/lib/ui/help-ui.js +49 -0
  52. package/lib/agent-cli/lib/ui/index.js +199 -0
  53. package/lib/agent-cli/lib/ui/init-ui.js +56 -0
  54. package/lib/agent-cli/lib/ui/knowledge-ui.js +55 -0
  55. package/lib/agent-cli/lib/ui/learn-ui.js +706 -0
  56. package/lib/agent-cli/lib/ui/lessons-ui.js +148 -0
  57. package/lib/agent-cli/lib/ui/pretty.js +145 -0
  58. package/lib/agent-cli/lib/ui/proposals-ui.js +99 -0
  59. package/lib/agent-cli/lib/ui/recall-ui.js +342 -0
  60. package/lib/agent-cli/lib/ui/routing-demo.js +79 -0
  61. package/lib/agent-cli/lib/ui/routing-ui.js +325 -0
  62. package/lib/agent-cli/lib/ui/settings-ui.js +381 -0
  63. package/lib/agent-cli/lib/ui/stats-ui.js +123 -0
  64. package/lib/agent-cli/lib/ui/watch-ui.js +236 -0
  65. package/lib/agent-cli/lib/watcher.js +181 -0
  66. package/lib/agent-cli/lib/watcher.test.js +85 -0
  67. package/lib/agent-cli/src/MIGRATION.md +418 -0
  68. package/lib/agent-cli/src/README.md +367 -0
  69. package/lib/agent-cli/src/core/evolution/evolution-signal.js +42 -0
  70. package/lib/agent-cli/src/core/evolution/index.js +17 -0
  71. package/lib/agent-cli/src/core/evolution/review-gate.js +40 -0
  72. package/lib/agent-cli/src/core/evolution/signal-detector.js +137 -0
  73. package/lib/agent-cli/src/core/evolution/signal-queue.js +79 -0
  74. package/lib/agent-cli/src/core/evolution/threshold-checker.js +79 -0
  75. package/lib/agent-cli/src/core/index.js +15 -0
  76. package/lib/agent-cli/src/core/learning/cognitive-enhancer.js +282 -0
  77. package/lib/agent-cli/src/core/learning/index.js +12 -0
  78. package/lib/agent-cli/src/core/learning/lesson-synthesizer.js +83 -0
  79. package/lib/agent-cli/src/core/scanning/index.js +14 -0
  80. package/lib/agent-cli/src/data/index.js +13 -0
  81. package/lib/agent-cli/src/data/repositories/index.js +8 -0
  82. package/lib/agent-cli/src/data/repositories/lesson-repository.js +130 -0
  83. package/lib/agent-cli/src/data/repositories/signal-repository.js +119 -0
  84. package/lib/agent-cli/src/data/storage/index.js +8 -0
  85. package/lib/agent-cli/src/data/storage/json-storage.js +64 -0
  86. package/lib/agent-cli/src/data/storage/yaml-storage.js +66 -0
  87. package/lib/agent-cli/src/infrastructure/index.js +13 -0
  88. package/lib/agent-cli/src/presentation/formatters/skill-formatter.js +232 -0
  89. package/lib/agent-cli/src/services/export-service.js +162 -0
  90. package/lib/agent-cli/src/services/index.js +13 -0
  91. package/lib/agent-cli/src/services/learning-service.js +99 -0
  92. package/package.json +5 -3
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smart Learning Script - ESM Version (Production-Ready)
4
+ *
5
+ * The "Teacher" script. Adds new lessons to the system memory.
6
+ *
7
+ * Features:
8
+ * - Manual lesson addition
9
+ * - Category tagging
10
+ * - Source tracking (manual, eslint, test-failure)
11
+ *
12
+ * Usage:
13
+ * agent learn --add --pattern "regex" --message "why bad"
14
+ * agent learn --list
15
+ * agent learn --remove <id>
16
+ */
17
+
18
+ import fs from "fs";
19
+ import path from "path";
20
+ import yaml from "js-yaml";
21
+ import { KNOWLEDGE_DIR, LESSONS_PATH, DEBUG, VERSION } from "./config.js";
22
+
23
+ // ============================================================================
24
+ // CORE FUNCTIONS
25
+ // ============================================================================
26
+
27
+ /**
28
+ * Load knowledge base from YAML file
29
+ * @returns {{ lessons: Array, version?: number }}
30
+ */
31
+ function loadKnowledge() {
32
+ try {
33
+ if (!fs.existsSync(LESSONS_PATH)) {
34
+ const initial = { lessons: [], version: 1 };
35
+ fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
36
+ fs.writeFileSync(LESSONS_PATH, yaml.dump(initial), "utf8");
37
+ return initial;
38
+ }
39
+ const content = fs.readFileSync(LESSONS_PATH, "utf8");
40
+ return yaml.load(content) || { lessons: [], version: 1 };
41
+ } catch (error) {
42
+ console.error("❌ Failed to load knowledge base:", error.message);
43
+ if (DEBUG) console.error(error.stack);
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Save knowledge base to YAML file
50
+ * @param {{ lessons: Array, version?: number }} data
51
+ */
52
+ function saveKnowledge(data) {
53
+ try {
54
+ fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
55
+ const str = yaml.dump(data, { lineWidth: -1, quotingType: '"' });
56
+ fs.writeFileSync(LESSONS_PATH, str, "utf8");
57
+ console.log("✅ Knowledge base updated successfully.");
58
+ } catch (error) {
59
+ console.error("❌ Failed to save knowledge base:", error.message);
60
+ if (DEBUG) console.error(error.stack);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Add a new lesson to the knowledge base
67
+ * @param {string} pattern - Regex pattern
68
+ * @param {string} message - Explanation message
69
+ * @param {string} severity - WARNING or ERROR
70
+ * @param {string} category - Category tag
71
+ */
72
+ function addLesson(pattern, message, severity = "WARNING", category = "general") {
73
+ const db = loadKnowledge();
74
+
75
+ // Validate Regex
76
+ try {
77
+ new RegExp(pattern);
78
+ } catch (e) {
79
+ console.error("❌ Invalid Regex pattern:", e.message);
80
+ process.exit(1);
81
+ }
82
+
83
+ // Validate severity
84
+ if (!["WARNING", "ERROR"].includes(severity.toUpperCase())) {
85
+ console.error("❌ Invalid severity. Must be WARNING or ERROR");
86
+ process.exit(1);
87
+ }
88
+
89
+ // Check for duplicates
90
+ const exists = db.lessons.some(l => l.pattern === pattern);
91
+ if (exists) {
92
+ console.log("⚠️ Pattern already exists in knowledge base.");
93
+ process.exit(0);
94
+ }
95
+
96
+ const id = `LEARN-${String(db.lessons.length + 1).padStart(3, "0")}`;
97
+
98
+ const lesson = {
99
+ id,
100
+ pattern,
101
+ message,
102
+ severity: severity.toUpperCase(),
103
+ category,
104
+ source: "manual",
105
+ hitCount: 0,
106
+ lastHit: null,
107
+ autoEscalated: false,
108
+ addedAt: new Date().toISOString()
109
+ };
110
+
111
+ db.lessons.push(lesson);
112
+ saveKnowledge(db);
113
+
114
+ console.log(`\n🎓 Lesson Learned: [${id}]`);
115
+ console.log(` Pattern: /${pattern}/`);
116
+ console.log(` Message: ${message}`);
117
+ console.log(` Severity: ${severity.toUpperCase()}`);
118
+ console.log(` Category: ${category}\n`);
119
+ }
120
+
121
+ /**
122
+ * Remove a lesson by ID
123
+ * @param {string} lessonId
124
+ */
125
+ function removeLesson(lessonId) {
126
+ const db = loadKnowledge();
127
+ const idx = db.lessons.findIndex(l => l.id === lessonId.toUpperCase());
128
+
129
+ if (idx === -1) {
130
+ console.log(`❌ Lesson not found: ${lessonId}`);
131
+ process.exit(1);
132
+ }
133
+
134
+ const removed = db.lessons.splice(idx, 1)[0];
135
+ saveKnowledge(db);
136
+
137
+ console.log(`\n🗑️ Removed lesson: [${removed.id}]`);
138
+ console.log(` Pattern: /${removed.pattern}/\n`);
139
+ }
140
+
141
+ /**
142
+ * List all learned lessons
143
+ * @param {string} category - Filter by category
144
+ */
145
+ function listLessons(category = null) {
146
+ const db = loadKnowledge();
147
+
148
+ if (!db.lessons || db.lessons.length === 0) {
149
+ console.log("\nℹ️ No lessons learned yet.");
150
+ console.log(" Use: agent learn --add --pattern \"pat\" --message \"msg\"\n");
151
+ return;
152
+ }
153
+
154
+ let lessons = db.lessons;
155
+ if (category) {
156
+ lessons = lessons.filter(l => l.category === category);
157
+ }
158
+
159
+ console.log(`\n🧠 PikaKit Knowledge Base (${lessons.length} lesson(s))\n`);
160
+ console.log("─".repeat(60));
161
+
162
+ lessons.forEach(l => {
163
+ const icon = l.severity === "ERROR" ? "❌" : "⚠️";
164
+ const hits = l.hitCount ? ` (${l.hitCount} hits)` : "";
165
+ const escalated = l.autoEscalated ? " ⚡" : "";
166
+
167
+ console.log(`${icon} [${l.id}] ${l.message}${hits}${escalated}`);
168
+ console.log(` Pattern: /${l.pattern}/`);
169
+ console.log(` Category: ${l.category || "general"} | Source: ${l.source || "manual"}`);
170
+ console.log("");
171
+ });
172
+ }
173
+
174
+ // ============================================================================
175
+ // CLI
176
+ // ============================================================================
177
+
178
+ function printHelp() {
179
+ console.log(`
180
+ 🎓 Smart Learning Tool v${VERSION}
181
+
182
+ USAGE:
183
+ agent learn --add --pattern "..." --message "..."
184
+ agent learn --list [--category <cat>]
185
+ agent learn --remove <ID>
186
+
187
+ OPTIONS:
188
+ --add Add a new lesson
189
+ --pattern Regex pattern to flag
190
+ --message Explanation message
191
+ --severity WARNING (default) or ERROR
192
+ --category Category tag (default: general)
193
+ --list List all lessons
194
+ --remove Remove lesson by ID
195
+ --help Show this help
196
+
197
+ EXAMPLES:
198
+ agent learn --add --pattern "console\\.log" --message "No console.log in production" --severity ERROR
199
+ agent learn --list
200
+ agent learn --remove LEARN-001
201
+ `);
202
+ }
203
+
204
+ function main() {
205
+ const args = process.argv.slice(2);
206
+
207
+ if (args.includes("--help") || args.length === 0) {
208
+ printHelp();
209
+ process.exit(0);
210
+ }
211
+
212
+ if (args.includes("--list")) {
213
+ const catIdx = args.indexOf("--category");
214
+ const category = catIdx !== -1 ? args[catIdx + 1] : null;
215
+ listLessons(category);
216
+ process.exit(0);
217
+ }
218
+
219
+ if (args.includes("--remove")) {
220
+ const removeIdx = args.indexOf("--remove");
221
+ const lessonId = args[removeIdx + 1];
222
+ if (!lessonId) {
223
+ console.error("❌ Missing lesson ID for --remove");
224
+ process.exit(1);
225
+ }
226
+ removeLesson(lessonId);
227
+ process.exit(0);
228
+ }
229
+
230
+ if (args.includes("--add")) {
231
+ const pIdx = args.indexOf("--pattern");
232
+ const mIdx = args.indexOf("--message");
233
+ const sIdx = args.indexOf("--severity");
234
+ const cIdx = args.indexOf("--category");
235
+
236
+ if (pIdx === -1 || mIdx === -1) {
237
+ console.error("❌ --pattern and --message are required for --add");
238
+ process.exit(1);
239
+ }
240
+
241
+ const pattern = args[pIdx + 1];
242
+ const message = args[mIdx + 1];
243
+ const severity = sIdx !== -1 ? args[sIdx + 1] : "WARNING";
244
+ const category = cIdx !== -1 ? args[cIdx + 1] : "general";
245
+
246
+ if (!pattern || !message) {
247
+ console.error("❌ Missing values for pattern/message");
248
+ process.exit(1);
249
+ }
250
+
251
+ addLesson(pattern, message, severity, category);
252
+ }
253
+ }
254
+
255
+ main();
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Tests for learn.js functionality
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+
9
+ // Mock config để test
10
+ const TEST_DIR = path.join(os.tmpdir(), "agent-skill-kit-test");
11
+ const KNOWLEDGE_DIR = path.join(TEST_DIR, ".agent", "knowledge");
12
+ const LESSONS_PATH = path.join(KNOWLEDGE_DIR, "lessons-learned.yaml");
13
+
14
+ describe("Knowledge Base Operations", () => {
15
+ beforeEach(() => {
16
+ fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
17
+ });
18
+
19
+ afterEach(() => {
20
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
21
+ });
22
+
23
+ it("creates initial knowledge file if missing", () => {
24
+ // Simulate missing file scenario
25
+ expect(fs.existsSync(LESSONS_PATH)).toBe(false);
26
+
27
+ // Create initial file
28
+ const initial = { lessons: [] };
29
+ fs.writeFileSync(LESSONS_PATH, JSON.stringify(initial), "utf8");
30
+
31
+ expect(fs.existsSync(LESSONS_PATH)).toBe(true);
32
+ });
33
+
34
+ it("can write and read lessons", () => {
35
+ const lesson = {
36
+ id: "LEARN-001",
37
+ pattern: "console\\.log",
38
+ message: "No console.log in production",
39
+ severity: "WARNING",
40
+ addedAt: new Date().toISOString()
41
+ };
42
+
43
+ const data = { lessons: [lesson] };
44
+ fs.writeFileSync(LESSONS_PATH, JSON.stringify(data), "utf8");
45
+
46
+ const content = fs.readFileSync(LESSONS_PATH, "utf8");
47
+ const parsed = JSON.parse(content);
48
+
49
+ expect(parsed.lessons).toHaveLength(1);
50
+ expect(parsed.lessons[0].id).toBe("LEARN-001");
51
+ expect(parsed.lessons[0].pattern).toBe("console\\.log");
52
+ });
53
+
54
+ it("validates regex patterns", () => {
55
+ const validPattern = "console\\.log";
56
+ const invalidPattern = "[invalid(";
57
+
58
+ expect(() => new RegExp(validPattern)).not.toThrow();
59
+ expect(() => new RegExp(invalidPattern)).toThrow();
60
+ });
61
+
62
+ it("validates severity values", () => {
63
+ const validSeverities = ["WARNING", "ERROR"];
64
+ const invalidSeverity = "INFO";
65
+
66
+ expect(validSeverities.includes("WARNING")).toBe(true);
67
+ expect(validSeverities.includes("ERROR")).toBe(true);
68
+ expect(validSeverities.includes(invalidSeverity)).toBe(false);
69
+ });
70
+ });
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @fileoverview Proposal Generator for Auto-Updating Flow
3
+ * Generates markdown instructions for AI agents to update skills
4
+ */
5
+
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import yaml from "js-yaml";
9
+ import { KNOWLEDGE_DIR, LESSONS_PATH, AGENT_DIR } from "./config.js";
10
+ import { loadSettings } from "./settings.js";
11
+ import { loadKnowledge } from "./recall.js";
12
+
13
+ /** Proposals directory */
14
+ const PROPOSALS_DIR = path.join(KNOWLEDGE_DIR, "proposals");
15
+
16
+ /** Dismissed proposals file */
17
+ const DISMISSED_FILE = path.join(PROPOSALS_DIR, "dismissed.yaml");
18
+
19
+ /**
20
+ * Get lessons that qualify for proposals (hitCount >= threshold)
21
+ * @returns {Array<{ lesson: object, proposalPath: string }>}
22
+ */
23
+ export function getQualifyingLessons() {
24
+ const settings = loadSettings();
25
+ const threshold = settings.updateThreshold || 5;
26
+ const db = loadKnowledge();
27
+
28
+ if (!db.lessons) return [];
29
+
30
+ const dismissed = loadDismissed();
31
+
32
+ return db.lessons
33
+ .filter(l => (l.hitCount || 0) >= threshold && !dismissed.has(l.id))
34
+ .map(lesson => ({
35
+ lesson,
36
+ proposalPath: path.join(PROPOSALS_DIR, `${lesson.id}.md`)
37
+ }));
38
+ }
39
+
40
+ /**
41
+ * Load dismissed proposal IDs
42
+ * @returns {Set<string>}
43
+ */
44
+ function loadDismissed() {
45
+ try {
46
+ if (fs.existsSync(DISMISSED_FILE)) {
47
+ const data = yaml.load(fs.readFileSync(DISMISSED_FILE, "utf8")) || {};
48
+ return new Set(data.dismissed || []);
49
+ }
50
+ } catch (e) { }
51
+ return new Set();
52
+ }
53
+
54
+ /**
55
+ * Dismiss a proposal
56
+ * @param {string} lessonId
57
+ */
58
+ export function dismissProposal(lessonId) {
59
+ fs.mkdirSync(PROPOSALS_DIR, { recursive: true });
60
+ const dismissed = loadDismissed();
61
+ dismissed.add(lessonId);
62
+ fs.writeFileSync(DISMISSED_FILE, yaml.dump({ dismissed: [...dismissed] }), "utf8");
63
+ }
64
+
65
+ /**
66
+ * Generate proposal markdown for AI agent
67
+ * @param {object} lesson - Lesson object
68
+ * @returns {string} Markdown content
69
+ */
70
+ export function generateProposalMarkdown(lesson) {
71
+ const skillHint = guessSkillFromLesson(lesson);
72
+
73
+ return `# 🤖 Skill Update Proposal
74
+
75
+ ## Pattern Detected
76
+ | Field | Value |
77
+ |-------|-------|
78
+ | **ID** | \`${lesson.id}\` |
79
+ | **Hit Count** | ${lesson.hitCount || 0} |
80
+ | **Severity** | ${lesson.severity || "WARNING"} |
81
+ | **Last Hit** | ${lesson.lastHit || "N/A"} |
82
+
83
+ ## Message
84
+ > ${lesson.message}
85
+
86
+ ## Pattern (Regex)
87
+ \`\`\`
88
+ ${lesson.pattern}
89
+ \`\`\`
90
+
91
+ ---
92
+
93
+ ## 📝 Suggested Action for AI Agent
94
+
95
+ Please update the relevant skill file to include this rule:
96
+
97
+ ### Target File
98
+ \`${skillHint}\`
99
+
100
+ ### Add to Rules Section
101
+ \`\`\`markdown
102
+ ### ${lesson.message}
103
+ - Pattern: \`${lesson.pattern}\`
104
+ - Severity: ${lesson.severity || "WARNING"}
105
+ - Auto-learned from violations
106
+ \`\`\`
107
+
108
+ ---
109
+
110
+ ## How to Apply
111
+
112
+ 1. **Copy this entire proposal**
113
+ 2. **Paste to your AI coding agent** (Claude, Gemini, etc.)
114
+ 3. **Ask:** "Update the skill file with this rule"
115
+
116
+ The AI agent will modify the appropriate \`.agent/skills/*/SKILL.md\` file.
117
+
118
+ ---
119
+
120
+ *Generated by PikaKit v2.1.0*
121
+ `;
122
+ }
123
+
124
+ /**
125
+ * Guess which skill file this lesson relates to
126
+ * @param {object} lesson
127
+ * @returns {string}
128
+ */
129
+ function guessSkillFromLesson(lesson) {
130
+ const pattern = (lesson.pattern || "").toLowerCase();
131
+ const message = (lesson.message || "").toLowerCase();
132
+ const combined = pattern + " " + message;
133
+
134
+ // Frontend patterns
135
+ if (combined.includes("react") || combined.includes("component") || combined.includes("hook") || combined.includes("usestate") || combined.includes("useeffect")) {
136
+ return ".agent/skills/react-patterns/SKILL.md";
137
+ }
138
+ if (combined.includes("next") || combined.includes("getserverside") || combined.includes("getstaticprops") || combined.includes("app router")) {
139
+ return ".agent/skills/nextjs-best-practices/SKILL.md";
140
+ }
141
+ if (combined.includes("css") || combined.includes("style") || combined.includes("tailwind") || combined.includes("classname")) {
142
+ return ".agent/skills/frontend-design/SKILL.md";
143
+ }
144
+
145
+ // Backend patterns
146
+ if (combined.includes("api") || combined.includes("fetch") || combined.includes("http") || combined.includes("endpoint") || combined.includes("rest")) {
147
+ return ".agent/skills/api-patterns/SKILL.md";
148
+ }
149
+ if (combined.includes("database") || combined.includes("sql") || combined.includes("prisma") || combined.includes("query") || combined.includes("schema")) {
150
+ return ".agent/skills/database-design/SKILL.md";
151
+ }
152
+ if (combined.includes("node") || combined.includes("express") || combined.includes("server") || combined.includes("middleware")) {
153
+ return ".agent/skills/nodejs-best-practices/SKILL.md";
154
+ }
155
+
156
+ // Testing patterns
157
+ if (combined.includes("test") || combined.includes("mock") || combined.includes("jest") || combined.includes("vitest") || combined.includes("expect")) {
158
+ return ".agent/skills/testing-patterns/SKILL.md";
159
+ }
160
+
161
+ // TypeScript patterns
162
+ if (combined.includes("typescript") || combined.includes("type") || combined.includes("interface") || combined.includes("generic") || combined.includes("any")) {
163
+ return ".agent/skills/typescript-expert/SKILL.md";
164
+ }
165
+
166
+ // Security patterns
167
+ if (combined.includes("security") || combined.includes("auth") || combined.includes("password") || combined.includes("token") || combined.includes("xss") || combined.includes("injection")) {
168
+ return ".agent/skills/vulnerability-scanner/SKILL.md";
169
+ }
170
+
171
+ // Performance patterns
172
+ if (combined.includes("performance") || combined.includes("memo") || combined.includes("lazy") || combined.includes("cache") || combined.includes("optimize")) {
173
+ return ".agent/skills/performance-profiling/SKILL.md";
174
+ }
175
+
176
+ // Git patterns
177
+ if (combined.includes("commit") || combined.includes("branch") || combined.includes("merge") || combined.includes("git")) {
178
+ return ".agent/skills/git-conventions/SKILL.md";
179
+ }
180
+
181
+ // Default to clean-code for general patterns
182
+ return ".agent/skills/clean-code/SKILL.md";
183
+ }
184
+
185
+ /**
186
+ * Count pending proposals
187
+ * @returns {number}
188
+ */
189
+ export function countPendingProposals() {
190
+ return getQualifyingLessons().length;
191
+ }
192
+
193
+ export default {
194
+ getQualifyingLessons,
195
+ dismissProposal,
196
+ generateProposalMarkdown,
197
+ countPendingProposals,
198
+ PROPOSALS_DIR
199
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @fileoverview Tests for proposals module
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+
7
+ // Test proposal markdown generation without mocking
8
+ describe("proposals", () => {
9
+ describe("generateProposalMarkdown format", () => {
10
+ it("creates markdown with required sections", () => {
11
+ const lesson = {
12
+ id: "LEARN-001",
13
+ pattern: "console\\.log",
14
+ message: "No console.log in production",
15
+ hitCount: 5,
16
+ severity: "ERROR"
17
+ };
18
+
19
+ // Simulate markdown generation
20
+ const md = `# 🤖 Skill Update Proposal\n\n## Pattern Detected\n| Field | Value |\n|-------|-------|\n| **ID** | \`${lesson.id}\` |\n| **Hit Count** | ${lesson.hitCount || 0} |\n\n## Message\n> ${lesson.message}`;
21
+
22
+ expect(md).toContain("LEARN-001");
23
+ expect(md).toContain("No console.log");
24
+ expect(md).toContain("5");
25
+ });
26
+
27
+ it("includes AI agent instructions", () => {
28
+ const md = `## How to Apply\n1. **Copy this entire proposal**\n2. **Paste to your AI coding agent**`;
29
+
30
+ expect(md).toContain("Copy");
31
+ expect(md).toContain("Paste");
32
+ expect(md).toContain("AI");
33
+ });
34
+ });
35
+
36
+ describe("proposal threshold", () => {
37
+ it("default threshold is 5", () => {
38
+ const defaultThreshold = 5;
39
+ expect(defaultThreshold).toBe(5);
40
+ });
41
+
42
+ it("lesson qualifies when hitCount >= threshold", () => {
43
+ const threshold = 5;
44
+ const lessons = [
45
+ { id: "L1", hitCount: 5 },
46
+ { id: "L2", hitCount: 3 },
47
+ { id: "L3", hitCount: 10 }
48
+ ];
49
+
50
+ const qualifying = lessons.filter(l => l.hitCount >= threshold);
51
+ expect(qualifying).toHaveLength(2);
52
+ expect(qualifying.map(l => l.id)).toContain("L1");
53
+ expect(qualifying.map(l => l.id)).toContain("L3");
54
+ });
55
+ });
56
+ });