wogiflow 1.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,880 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Skill Learning Engine
5
+ *
6
+ * Extracts learnings from work and updates skills automatically.
7
+ * Called by:
8
+ * - Pre-commit hook (--trigger=commit)
9
+ * - Task completion (--trigger=task)
10
+ * - Context compaction (--trigger=compact)
11
+ * - Manual invocation (--trigger=manual)
12
+ *
13
+ * Usage:
14
+ * flow skill-learn # Auto-detect trigger
15
+ * flow skill-learn --trigger=commit # Called from pre-commit
16
+ * flow skill-learn --skill=nestjs # Target specific skill
17
+ * flow skill-learn --dry-run # Show what would be updated
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { execSync } = require('child_process');
23
+ const { getProjectRoot, getConfig, colors } = require('./flow-utils');
24
+
25
+ const PROJECT_ROOT = getProjectRoot();
26
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
27
+ const SKILLS_DIR = path.join(PROJECT_ROOT, '.claude', 'skills');
28
+ const STATE_DIR = path.join(WORKFLOW_DIR, 'state');
29
+
30
+ function log(color, ...args) {
31
+ console.log(colors[color] + args.join(' ') + colors.reset);
32
+ }
33
+
34
+ // Alias getConfig as loadConfig for minimal code changes
35
+ const loadConfig = getConfig;
36
+
37
+ function isLearningEnabled(config, trigger) {
38
+ if (!config?.skillLearning?.enabled) return false;
39
+ if (!config?.skillLearning?.autoExtract) return false;
40
+
41
+ const triggers = config.skillLearning.triggers || {};
42
+ switch (trigger) {
43
+ case 'commit': return triggers.onCommit !== false;
44
+ case 'task': return triggers.onTaskComplete !== false;
45
+ case 'compact': return triggers.onCompact !== false;
46
+ case 'manual': return true;
47
+ default: return true;
48
+ }
49
+ }
50
+
51
+ // ============================================================
52
+ // Skill Discovery
53
+ // ============================================================
54
+
55
+ function discoverSkills() {
56
+ const skills = [];
57
+
58
+ if (!fs.existsSync(SKILLS_DIR)) {
59
+ return skills;
60
+ }
61
+
62
+ const dirs = fs.readdirSync(SKILLS_DIR);
63
+
64
+ for (const dir of dirs) {
65
+ if (dir.startsWith('_')) continue; // Skip templates
66
+
67
+ const skillPath = path.join(SKILLS_DIR, dir);
68
+ const skillMdPath = path.join(skillPath, 'skill.md');
69
+ const legacySkillMdPath = path.join(skillPath, 'SKILL.md');
70
+
71
+ if (fs.statSync(skillPath).isDirectory()) {
72
+ const mdPath = fs.existsSync(skillMdPath) ? skillMdPath :
73
+ fs.existsSync(legacySkillMdPath) ? legacySkillMdPath : null;
74
+
75
+ if (mdPath) {
76
+ const content = fs.readFileSync(mdPath, 'utf-8');
77
+ const skill = parseSkillMd(content, dir, skillPath);
78
+ skills.push(skill);
79
+ }
80
+ }
81
+ }
82
+
83
+ return skills;
84
+ }
85
+
86
+ function parseSkillMd(content, name, skillPath) {
87
+ const skill = {
88
+ name,
89
+ path: skillPath,
90
+ version: '1.0.0',
91
+ description: '',
92
+ filePatterns: [],
93
+ hasKnowledge: fs.existsSync(path.join(skillPath, 'knowledge'))
94
+ };
95
+
96
+ // Parse YAML frontmatter
97
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
98
+ if (frontmatterMatch) {
99
+ const yaml = frontmatterMatch[1];
100
+ const versionMatch = yaml.match(/version:\s*["']?([^"'\n]+)/);
101
+ const descMatch = yaml.match(/description:\s*["']?([^"'\n]+)/);
102
+
103
+ if (versionMatch) skill.version = versionMatch[1].trim();
104
+ if (descMatch) skill.description = descMatch[1].trim();
105
+ }
106
+
107
+ // Extract file patterns
108
+ const patternsMatch = content.match(/## File Patterns[\s\S]*?(?=\n## |$)/);
109
+ if (patternsMatch) {
110
+ const patterns = patternsMatch[0].match(/`([^`]+)`/g);
111
+ if (patterns) {
112
+ skill.filePatterns = patterns.map(p => p.replace(/`/g, ''));
113
+ }
114
+ }
115
+
116
+ // Infer patterns from skill name if none found
117
+ if (skill.filePatterns.length === 0) {
118
+ const inferredPatterns = {
119
+ 'nestjs': ['*.module.ts', '*.controller.ts', '*.service.ts', '*.entity.ts'],
120
+ 'react': ['*.tsx', '*.jsx', 'use*.ts'],
121
+ 'python': ['*.py'],
122
+ 'typescript': ['*.ts', '*.tsx']
123
+ };
124
+ skill.filePatterns = inferredPatterns[name] || [];
125
+ }
126
+
127
+ return skill;
128
+ }
129
+
130
+ // ============================================================
131
+ // Change Analysis
132
+ // ============================================================
133
+
134
+ function getChangedFiles(staged = false) {
135
+ try {
136
+ const cmd = staged
137
+ ? 'git diff --cached --name-only'
138
+ : 'git diff HEAD --name-only';
139
+ const output = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
140
+ return output.trim().split('\n').filter(Boolean);
141
+ } catch (err) {
142
+ return [];
143
+ }
144
+ }
145
+
146
+ function getRecentCommitFiles(count = 1) {
147
+ try {
148
+ const cmd = `git diff HEAD~${count} --name-only`;
149
+ const output = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
150
+ return output.trim().split('\n').filter(Boolean);
151
+ } catch (err) {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ function matchFilesToSkills(files, skills) {
157
+ const matches = new Map(); // skill -> files
158
+ const unmatched = [];
159
+
160
+ for (const file of files) {
161
+ let matched = false;
162
+
163
+ for (const skill of skills) {
164
+ for (const pattern of skill.filePatterns) {
165
+ if (matchPattern(file, pattern)) {
166
+ if (!matches.has(skill.name)) {
167
+ matches.set(skill.name, []);
168
+ }
169
+ matches.get(skill.name).push(file);
170
+ matched = true;
171
+ break;
172
+ }
173
+ }
174
+ if (matched) break;
175
+ }
176
+
177
+ if (!matched) {
178
+ unmatched.push(file);
179
+ }
180
+ }
181
+
182
+ return { matches, unmatched };
183
+ }
184
+
185
+ function matchPattern(file, pattern) {
186
+ // Simple glob matching
187
+ const regex = pattern
188
+ .replace(/\./g, '\\.')
189
+ .replace(/\*/g, '.*')
190
+ .replace(/\?/g, '.');
191
+ return new RegExp(regex).test(file);
192
+ }
193
+
194
+ // ============================================================
195
+ // Semantic Diff Analysis
196
+ // ============================================================
197
+
198
+ /**
199
+ * Extract semantic changes from git diff
200
+ * Analyzes what actually changed, not just which files
201
+ */
202
+ function extractSemanticChanges(files, staged = false) {
203
+ const changes = [];
204
+
205
+ for (const file of files) {
206
+ try {
207
+ const cmd = staged
208
+ ? `git diff --cached -U3 -- "${file}"`
209
+ : `git diff HEAD -U3 -- "${file}"`;
210
+ const diff = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
211
+
212
+ if (!diff.trim()) continue;
213
+
214
+ const fileChanges = analyzeDiff(diff, file);
215
+ if (fileChanges.length > 0) {
216
+ changes.push({ file, changes: fileChanges });
217
+ }
218
+ } catch (err) {
219
+ // Skip files that can't be diffed
220
+ }
221
+ }
222
+
223
+ return changes;
224
+ }
225
+
226
+ /**
227
+ * Analyze a diff to extract semantic patterns
228
+ */
229
+ function analyzeDiff(diff, filename) {
230
+ const changes = [];
231
+ const lines = diff.split('\n');
232
+ const added = lines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
233
+ const removed = lines.filter(l => l.startsWith('-') && !l.startsWith('---'));
234
+
235
+ // Detect function signature changes
236
+ const funcSigChanges = detectFunctionSignatureChanges(added, removed);
237
+ if (funcSigChanges.length > 0) {
238
+ changes.push(...funcSigChanges);
239
+ }
240
+
241
+ // Detect import changes
242
+ const importChanges = detectImportChanges(added, removed);
243
+ if (importChanges.length > 0) {
244
+ changes.push(...importChanges);
245
+ }
246
+
247
+ // Detect type annotation changes (TypeScript)
248
+ const typeChanges = detectTypeChanges(added, removed);
249
+ if (typeChanges.length > 0) {
250
+ changes.push(...typeChanges);
251
+ }
252
+
253
+ // Detect pattern adoption (error handling, null checks, etc.)
254
+ const patternChanges = detectPatternChanges(added, removed);
255
+ if (patternChanges.length > 0) {
256
+ changes.push(...patternChanges);
257
+ }
258
+
259
+ // Detect naming convention changes
260
+ const namingChanges = detectNamingChanges(added, removed, filename);
261
+ if (namingChanges.length > 0) {
262
+ changes.push(...namingChanges);
263
+ }
264
+
265
+ return changes;
266
+ }
267
+
268
+ function detectFunctionSignatureChanges(added, removed) {
269
+ const changes = [];
270
+ const funcRegex = /(?:function|const|let|var)\s+(\w+)\s*(?:=\s*)?(?:\([^)]*\)|async\s*\([^)]*\))/;
271
+ const arrowRegex = /(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\([^)]*\)\s*(?:=>|:)/;
272
+
273
+ const removedFuncs = new Map();
274
+ const addedFuncs = new Map();
275
+
276
+ for (const line of removed) {
277
+ const match = line.match(funcRegex) || line.match(arrowRegex);
278
+ if (match) {
279
+ removedFuncs.set(match[1], line.slice(1).trim());
280
+ }
281
+ }
282
+
283
+ for (const line of added) {
284
+ const match = line.match(funcRegex) || line.match(arrowRegex);
285
+ if (match) {
286
+ addedFuncs.set(match[1], line.slice(1).trim());
287
+ }
288
+ }
289
+
290
+ // Find functions that were modified (same name, different signature)
291
+ for (const [name, oldSig] of removedFuncs) {
292
+ if (addedFuncs.has(name)) {
293
+ const newSig = addedFuncs.get(name);
294
+ if (oldSig !== newSig) {
295
+ changes.push({
296
+ type: 'function_signature',
297
+ name,
298
+ before: oldSig,
299
+ after: newSig
300
+ });
301
+ }
302
+ }
303
+ }
304
+
305
+ return changes;
306
+ }
307
+
308
+ function detectImportChanges(added, removed) {
309
+ const changes = [];
310
+ const importRegex = /import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/;
311
+ const requireRegex = /(?:const|let|var)\s+(?:{[^}]+}|\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/;
312
+
313
+ const removedImports = new Set();
314
+ const addedImports = new Set();
315
+
316
+ for (const line of removed) {
317
+ const match = line.match(importRegex) || line.match(requireRegex);
318
+ if (match) {
319
+ removedImports.add(match[1]);
320
+ }
321
+ }
322
+
323
+ for (const line of added) {
324
+ const match = line.match(importRegex) || line.match(requireRegex);
325
+ if (match) {
326
+ addedImports.add(match[1]);
327
+ }
328
+ }
329
+
330
+ // New imports
331
+ for (const imp of addedImports) {
332
+ if (!removedImports.has(imp)) {
333
+ changes.push({ type: 'import_added', module: imp });
334
+ }
335
+ }
336
+
337
+ // Removed imports
338
+ for (const imp of removedImports) {
339
+ if (!addedImports.has(imp)) {
340
+ changes.push({ type: 'import_removed', module: imp });
341
+ }
342
+ }
343
+
344
+ return changes;
345
+ }
346
+
347
+ function detectTypeChanges(added, removed) {
348
+ const changes = [];
349
+ const typeAnnotationRegex = /:\s*([\w<>\[\]|&\s,]+)(?:\s*[=;,)])/;
350
+
351
+ // Check for type annotation additions
352
+ const addedTypes = added.filter(l => typeAnnotationRegex.test(l));
353
+ const removedTypes = removed.filter(l => typeAnnotationRegex.test(l));
354
+
355
+ if (addedTypes.length > removedTypes.length) {
356
+ changes.push({
357
+ type: 'type_annotation',
358
+ pattern: 'Added type annotations',
359
+ count: addedTypes.length - removedTypes.length
360
+ });
361
+ }
362
+
363
+ // Check for specific type improvements (any -> specific type)
364
+ for (const line of removed) {
365
+ if (line.includes(': any') || line.includes(':any')) {
366
+ for (const addLine of added) {
367
+ if (!addLine.includes(': any') && !addLine.includes(':any')) {
368
+ const match = addLine.match(typeAnnotationRegex);
369
+ if (match && match[1] !== 'any') {
370
+ changes.push({
371
+ type: 'type_improvement',
372
+ pattern: 'Replaced any with specific type',
373
+ newType: match[1].trim()
374
+ });
375
+ break;
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ return changes;
383
+ }
384
+
385
+ function detectPatternChanges(added, removed) {
386
+ const changes = [];
387
+ const patterns = [
388
+ { name: 'null_check', regex: /\?\.|if\s*\([^)]*(?:===?\s*null|!==?\s*null|!=\s*undefined)/ },
389
+ { name: 'error_handling', regex: /try\s*{|catch\s*\(|\.catch\(/ },
390
+ { name: 'async_await', regex: /async\s+(?:function|\()|\bawait\s/ },
391
+ { name: 'optional_chaining', regex: /\?\.\w+|\?\[/ },
392
+ { name: 'nullish_coalescing', regex: /\?\?/ },
393
+ { name: 'early_return', regex: /^\s*if\s*\([^)]+\)\s*return/ }
394
+ ];
395
+
396
+ for (const { name, regex } of patterns) {
397
+ const addedCount = added.filter(l => regex.test(l)).length;
398
+ const removedCount = removed.filter(l => regex.test(l)).length;
399
+
400
+ if (addedCount > removedCount) {
401
+ changes.push({
402
+ type: 'pattern_adoption',
403
+ pattern: name,
404
+ added: addedCount - removedCount
405
+ });
406
+ } else if (removedCount > addedCount) {
407
+ changes.push({
408
+ type: 'pattern_removal',
409
+ pattern: name,
410
+ removed: removedCount - addedCount
411
+ });
412
+ }
413
+ }
414
+
415
+ return changes;
416
+ }
417
+
418
+ function detectNamingChanges(added, removed, filename) {
419
+ const changes = [];
420
+
421
+ // Check for consistent naming pattern adoption
422
+ const camelCaseRegex = /\b[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*\b/g;
423
+ const snakeCaseRegex = /\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b/g;
424
+ const kebabCaseRegex = /['"][a-z][a-z0-9]*-[a-z][a-z0-9-]*['"]/g;
425
+
426
+ const addedCamel = added.join(' ').match(camelCaseRegex)?.length || 0;
427
+ const removedCamel = removed.join(' ').match(camelCaseRegex)?.length || 0;
428
+ const addedSnake = added.join(' ').match(snakeCaseRegex)?.length || 0;
429
+ const removedSnake = removed.join(' ').match(snakeCaseRegex)?.length || 0;
430
+
431
+ // Detect migration from snake_case to camelCase
432
+ if (addedCamel > removedCamel && removedSnake > addedSnake) {
433
+ changes.push({
434
+ type: 'naming_convention',
435
+ pattern: 'Migrated from snake_case to camelCase'
436
+ });
437
+ }
438
+
439
+ return changes;
440
+ }
441
+
442
+ /**
443
+ * Format semantic changes for human-readable output
444
+ */
445
+ function formatSemanticChanges(semanticChanges) {
446
+ if (!semanticChanges || semanticChanges.length === 0) {
447
+ return '';
448
+ }
449
+
450
+ const lines = [];
451
+
452
+ for (const { file, changes } of semanticChanges) {
453
+ if (changes.length === 0) continue;
454
+
455
+ for (const change of changes) {
456
+ switch (change.type) {
457
+ case 'function_signature':
458
+ lines.push(`- **Function signature changed**: \`${change.name}\``);
459
+ lines.push(` - Before: \`${change.before.slice(0, 60)}${change.before.length > 60 ? '...' : ''}\``);
460
+ lines.push(` - After: \`${change.after.slice(0, 60)}${change.after.length > 60 ? '...' : ''}\``);
461
+ break;
462
+ case 'import_added':
463
+ lines.push(`- **New import**: \`${change.module}\``);
464
+ break;
465
+ case 'import_removed':
466
+ lines.push(`- **Removed import**: \`${change.module}\``);
467
+ break;
468
+ case 'type_annotation':
469
+ lines.push(`- **Added ${change.count} type annotation(s)**`);
470
+ break;
471
+ case 'type_improvement':
472
+ lines.push(`- **Improved typing**: Replaced \`any\` with \`${change.newType}\``);
473
+ break;
474
+ case 'pattern_adoption':
475
+ lines.push(`- **Adopted pattern**: ${change.pattern.replace(/_/g, ' ')} (+${change.added})`);
476
+ break;
477
+ case 'pattern_removal':
478
+ lines.push(`- **Removed pattern**: ${change.pattern.replace(/_/g, ' ')} (-${change.removed})`);
479
+ break;
480
+ case 'naming_convention':
481
+ lines.push(`- **Naming**: ${change.pattern}`);
482
+ break;
483
+ }
484
+ }
485
+ }
486
+
487
+ return lines.length > 0 ? lines.join('\n') : '';
488
+ }
489
+
490
+ // ============================================================
491
+ // Learning Extraction
492
+ // ============================================================
493
+
494
+ function extractLearningContext(files, trigger, staged = false) {
495
+ const context = {
496
+ trigger,
497
+ timestamp: new Date().toISOString(),
498
+ files,
499
+ summary: '',
500
+ type: 'observation', // observation | correction | pattern | anti-pattern
501
+ semanticChanges: []
502
+ };
503
+
504
+ // Try to get commit message for context
505
+ if (trigger === 'commit') {
506
+ try {
507
+ const msg = execSync('git log -1 --format=%B', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
508
+ context.summary = msg.trim().split('\n')[0];
509
+ } catch (err) {
510
+ context.summary = `Changed ${files.length} files`;
511
+ }
512
+ } else {
513
+ context.summary = `${trigger}: Changed ${files.length} files`;
514
+ }
515
+
516
+ // Detect if this looks like a fix/correction
517
+ const lowerSummary = context.summary.toLowerCase();
518
+ if (lowerSummary.includes('fix') || lowerSummary.includes('bug') || lowerSummary.includes('error')) {
519
+ context.type = 'correction';
520
+ } else if (lowerSummary.includes('refactor') || lowerSummary.includes('improve')) {
521
+ context.type = 'pattern';
522
+ }
523
+
524
+ // Extract semantic changes from the diff
525
+ context.semanticChanges = extractSemanticChanges(files, staged);
526
+
527
+ return context;
528
+ }
529
+
530
+ // ============================================================
531
+ // Knowledge Updates
532
+ // ============================================================
533
+
534
+ function ensureKnowledgeDir(skillPath) {
535
+ const knowledgeDir = path.join(skillPath, 'knowledge');
536
+ if (!fs.existsSync(knowledgeDir)) {
537
+ fs.mkdirSync(knowledgeDir, { recursive: true });
538
+
539
+ // Copy templates
540
+ const templateDir = path.join(SKILLS_DIR, '_template', 'knowledge');
541
+ if (fs.existsSync(templateDir)) {
542
+ for (const file of ['learnings.md', 'patterns.md', 'anti-patterns.md']) {
543
+ const src = path.join(templateDir, file);
544
+ const dest = path.join(knowledgeDir, file);
545
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
546
+ fs.copyFileSync(src, dest);
547
+ }
548
+ }
549
+ }
550
+ }
551
+ return knowledgeDir;
552
+ }
553
+
554
+ function appendLearning(skillPath, context) {
555
+ const knowledgeDir = ensureKnowledgeDir(skillPath);
556
+ const learningsPath = path.join(knowledgeDir, 'learnings.md');
557
+
558
+ const date = context.timestamp.split('T')[0];
559
+
560
+ // Format semantic changes for output
561
+ const semanticOutput = formatSemanticChanges(context.semanticChanges);
562
+
563
+ const entry = `
564
+ ### ${date} - ${context.summary}
565
+
566
+ **Context**: ${context.trigger} trigger
567
+ **Trigger**: ${context.trigger}
568
+ **Type**: ${context.type}
569
+ **Files**: ${context.files.slice(0, 5).join(', ')}${context.files.length > 5 ? ` (+${context.files.length - 5} more)` : ''}
570
+ ${semanticOutput ? `\n**Semantic Changes**:\n${semanticOutput}\n` : ''}
571
+ ---
572
+ `;
573
+
574
+ if (fs.existsSync(learningsPath)) {
575
+ let content = fs.readFileSync(learningsPath, 'utf-8');
576
+
577
+ // Find the "Recent Learnings" section and append after it
578
+ const marker = '## Recent Learnings';
579
+ const idx = content.indexOf(marker);
580
+ if (idx !== -1) {
581
+ const insertPoint = content.indexOf('\n', idx + marker.length) + 1;
582
+ content = content.slice(0, insertPoint) + entry + content.slice(insertPoint);
583
+ } else {
584
+ content += '\n## Recent Learnings\n' + entry;
585
+ }
586
+
587
+ fs.writeFileSync(learningsPath, content);
588
+ return true;
589
+ }
590
+
591
+ return false;
592
+ }
593
+
594
+ function updateSkillVersion(skillPath) {
595
+ const skillMdPath = path.join(skillPath, 'skill.md');
596
+ const legacyPath = path.join(skillPath, 'SKILL.md');
597
+ const mdPath = fs.existsSync(skillMdPath) ? skillMdPath : legacyPath;
598
+
599
+ if (!fs.existsSync(mdPath)) return;
600
+
601
+ let content = fs.readFileSync(mdPath, 'utf-8');
602
+
603
+ // Update lastUpdated
604
+ const today = new Date().toISOString().split('T')[0];
605
+ if (content.includes('lastUpdated:')) {
606
+ content = content.replace(/lastUpdated:\s*[\d-]+/, `lastUpdated: ${today}`);
607
+ }
608
+
609
+ // Increment learningCount
610
+ const countMatch = content.match(/learningCount:\s*(\d+)/);
611
+ if (countMatch) {
612
+ const newCount = parseInt(countMatch[1]) + 1;
613
+ content = content.replace(/learningCount:\s*\d+/, `learningCount: ${newCount}`);
614
+ }
615
+
616
+ fs.writeFileSync(mdPath, content);
617
+ }
618
+
619
+ // ============================================================
620
+ // Feedback Patterns Integration
621
+ // ============================================================
622
+
623
+ /**
624
+ * Extract file extensions/types from a list of files
625
+ */
626
+ function getFileSignature(files) {
627
+ const exts = files.map(f => {
628
+ const ext = path.extname(f);
629
+ // Group common patterns
630
+ if (f.includes('.module.')) return '.module.*';
631
+ if (f.includes('.controller.')) return '.controller.*';
632
+ if (f.includes('.service.')) return '.service.*';
633
+ if (f.includes('.entity.')) return '.entity.*';
634
+ if (f.includes('.test.') || f.includes('.spec.')) return '.test.*';
635
+ return ext || 'no-ext';
636
+ });
637
+ return [...new Set(exts)].sort().join(',');
638
+ }
639
+
640
+ /**
641
+ * Log unmatched files to feedback-patterns.md with proper count tracking
642
+ */
643
+ function logToFeedbackPatterns(context, unmatchedFiles) {
644
+ const feedbackPath = path.join(STATE_DIR, 'feedback-patterns.md');
645
+
646
+ if (!fs.existsSync(feedbackPath)) return;
647
+
648
+ let content = fs.readFileSync(feedbackPath, 'utf-8');
649
+ const date = context.timestamp.split('T')[0];
650
+
651
+ // Get signature for deduplication
652
+ const signature = getFileSignature(unmatchedFiles);
653
+ const filesPreview = unmatchedFiles.slice(0, 3).join(', ');
654
+
655
+ // Look for existing entry with same pattern (by #needs-skill tag and similar file types)
656
+ const tableMarker = '| Date | Correction | Count |';
657
+ const idx = content.indexOf(tableMarker);
658
+ if (idx === -1) return;
659
+
660
+ // Find all existing #needs-skill entries
661
+ const lines = content.split('\n');
662
+ let foundExisting = false;
663
+ let existingLineIdx = -1;
664
+ let existingCount = 0;
665
+
666
+ for (let i = 0; i < lines.length; i++) {
667
+ const line = lines[i];
668
+ if (line.includes('#needs-skill') && line.includes('Files changed with no matching skill')) {
669
+ // Check if same file signature
670
+ const lineSignature = getFileSignature(
671
+ (line.match(/Files changed with no matching skill: ([^|]+)/)?.[1] || '')
672
+ .split(', ')
673
+ .map(f => f.trim())
674
+ .filter(f => f)
675
+ );
676
+
677
+ if (lineSignature === signature || lineSignature === '' || signature === '') {
678
+ // Found existing entry with same pattern - increment count
679
+ const countMatch = line.match(/\|\s*(\d+)\s*\|/);
680
+ if (countMatch) {
681
+ existingCount = parseInt(countMatch[1]);
682
+ existingLineIdx = i;
683
+ foundExisting = true;
684
+ break;
685
+ }
686
+ }
687
+ }
688
+ }
689
+
690
+ if (foundExisting && existingLineIdx >= 0) {
691
+ // Update existing entry with new count and date
692
+ const newCount = existingCount + 1;
693
+ const newEntry = `| ${date} | Files changed with no matching skill: ${filesPreview} | ${newCount} | - | #needs-skill |`;
694
+ lines[existingLineIdx] = newEntry;
695
+ content = lines.join('\n');
696
+ } else {
697
+ // Add new entry with count 1
698
+ const entry = `| ${date} | Files changed with no matching skill: ${filesPreview} | 1 | - | #needs-skill |`;
699
+ const headerEnd = content.indexOf('\n', content.indexOf('\n', idx) + 1) + 1;
700
+ content = content.slice(0, headerEnd) + entry + '\n' + content.slice(headerEnd);
701
+ }
702
+
703
+ fs.writeFileSync(feedbackPath, content);
704
+ }
705
+
706
+ // ============================================================
707
+ // Main CLI
708
+ // ============================================================
709
+
710
+ function parseArgs() {
711
+ const args = process.argv.slice(2);
712
+ const options = {
713
+ trigger: 'manual',
714
+ skill: null,
715
+ dryRun: false,
716
+ verbose: false,
717
+ help: false
718
+ };
719
+
720
+ for (const arg of args) {
721
+ if (arg.startsWith('--trigger=')) {
722
+ options.trigger = arg.split('=')[1];
723
+ } else if (arg.startsWith('--skill=')) {
724
+ options.skill = arg.split('=')[1];
725
+ } else if (arg === '--dry-run') {
726
+ options.dryRun = true;
727
+ } else if (arg === '--verbose' || arg === '-v') {
728
+ options.verbose = true;
729
+ } else if (arg === '--help' || arg === '-h') {
730
+ options.help = true;
731
+ }
732
+ }
733
+
734
+ return options;
735
+ }
736
+
737
+ function showHelp() {
738
+ console.log(`
739
+ Wogi Flow Skill Learning Engine
740
+
741
+ Usage:
742
+ flow skill-learn [options]
743
+
744
+ Options:
745
+ --trigger=TYPE Trigger type: commit, task, compact, manual (default: manual)
746
+ --skill=NAME Target specific skill only
747
+ --dry-run Show what would be updated without making changes
748
+ --verbose, -v Show detailed output
749
+ --help, -h Show this help
750
+
751
+ Examples:
752
+ flow skill-learn # Manual learning extraction
753
+ flow skill-learn --trigger=commit # Called from pre-commit hook
754
+ flow skill-learn --skill=nestjs # Update only nestjs skill
755
+ flow skill-learn --dry-run # Preview changes
756
+ `);
757
+ }
758
+
759
+ async function main() {
760
+ const options = parseArgs();
761
+
762
+ if (options.help) {
763
+ showHelp();
764
+ process.exit(0);
765
+ }
766
+
767
+ const config = loadConfig();
768
+
769
+ if (!isLearningEnabled(config, options.trigger)) {
770
+ if (options.verbose) {
771
+ log('dim', 'Skill learning disabled for this trigger');
772
+ }
773
+ process.exit(0);
774
+ }
775
+
776
+ log('cyan', `\nšŸ“š Skill Learning (${options.trigger})\n`);
777
+
778
+ // Get changed files
779
+ const files = options.trigger === 'commit'
780
+ ? getChangedFiles(true)
781
+ : getChangedFiles(false);
782
+
783
+ if (files.length === 0) {
784
+ log('dim', 'No changed files to analyze');
785
+ process.exit(0);
786
+ }
787
+
788
+ log('white', `Found ${files.length} changed files`);
789
+
790
+ // Discover skills
791
+ let skills = discoverSkills();
792
+
793
+ if (options.skill) {
794
+ skills = skills.filter(s => s.name === options.skill);
795
+ if (skills.length === 0) {
796
+ log('yellow', `Skill '${options.skill}' not found`);
797
+ process.exit(1);
798
+ }
799
+ }
800
+
801
+ log('dim', `Checking against ${skills.length} skills`);
802
+
803
+ // Match files to skills
804
+ const { matches, unmatched } = matchFilesToSkills(files, skills);
805
+
806
+ // Extract learning context
807
+ const context = extractLearningContext(files, options.trigger);
808
+
809
+ // Update matched skills
810
+ let updatedCount = 0;
811
+
812
+ for (const [skillName, skillFiles] of matches) {
813
+ const skill = skills.find(s => s.name === skillName);
814
+
815
+ log('green', `\n āœ“ ${skillName}: ${skillFiles.length} files`);
816
+
817
+ if (options.verbose) {
818
+ for (const f of skillFiles.slice(0, 3)) {
819
+ log('dim', ` - ${f}`);
820
+ }
821
+ if (skillFiles.length > 3) {
822
+ log('dim', ` ... +${skillFiles.length - 3} more`);
823
+ }
824
+ }
825
+
826
+ if (!options.dryRun) {
827
+ const skillContext = { ...context, files: skillFiles };
828
+ appendLearning(skill.path, skillContext);
829
+ updateSkillVersion(skill.path);
830
+ updatedCount++;
831
+ }
832
+ }
833
+
834
+ // Handle unmatched files
835
+ if (unmatched.length > 0) {
836
+ log('yellow', `\n ⚠ ${unmatched.length} files with no matching skill`);
837
+
838
+ if (options.verbose) {
839
+ for (const f of unmatched.slice(0, 5)) {
840
+ log('dim', ` - ${f}`);
841
+ }
842
+ }
843
+
844
+ if (!options.dryRun) {
845
+ logToFeedbackPatterns(context, unmatched);
846
+ }
847
+ }
848
+
849
+ // Summary
850
+ console.log('');
851
+ if (options.dryRun) {
852
+ log('yellow', `Dry run: Would update ${matches.size} skill(s)`);
853
+ } else {
854
+ log('green', `āœ… Updated ${updatedCount} skill(s)`);
855
+ }
856
+ console.log('');
857
+ }
858
+
859
+ // ============================================================
860
+ // Exports (for use by other modules like knowledge-router)
861
+ // ============================================================
862
+
863
+ module.exports = {
864
+ discoverSkills,
865
+ matchFilesToSkills,
866
+ appendLearning,
867
+ ensureKnowledgeDir,
868
+ extractLearningContext,
869
+ isLearningEnabled,
870
+ // Semantic diff analysis
871
+ extractSemanticChanges,
872
+ formatSemanticChanges
873
+ };
874
+
875
+ if (require.main === module) {
876
+ main().catch(e => {
877
+ log('red', `Error: ${err.message}`);
878
+ process.exit(1);
879
+ });
880
+ }