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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- 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
|
+
}
|