wogiflow 2.9.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,7 +105,10 @@ Use `AskUserQuestion` to let user select.
105
105
  - What was the root cause?
106
106
  - What should have been done differently?
107
107
 
108
- 4. Propose a rule:
108
+ 4. **Auto-route via knowledge router** (NEW):
109
+ Run `node -e "const kr = require('wogiflow/scripts/flow-knowledge-router'); const routes = kr.detectKnowledgeRoute('[correction text]', { currentModel: process.env.CLAUDE_MODEL }); console.log(JSON.stringify(routes))"` to get routing suggestions. Use the highest-confidence route as the default option.
110
+
111
+ 5. Propose a rule with routing recommendation:
109
112
 
110
113
  ```
111
114
  Based on recent work, here's what I found:
@@ -114,19 +117,23 @@ Incident: [description from request-log/corrections]
114
117
  Root Cause: [analysis]
115
118
  Proposed Rule: "[rule statement]"
116
119
 
120
+ Knowledge Router suggests: [route type] (confidence: XX%)
121
+
117
122
  What should we do with this learning?
118
123
  1. Create a project rule (routes to /wogi-decide flow)
119
124
  2. Fix WogiFlow product behavior (edit command/script/template)
120
- 3. Add to feedback-patterns for monitoring first
121
- 4. Skip not a recurring issue
125
+ 3. Store as skill learning (auto-routes to matching skill)
126
+ 4. Add to feedback-patterns for monitoring first
127
+ 5. Skip — not a recurring issue
122
128
  ```
123
129
 
124
130
  Use `AskUserQuestion` to present options.
125
131
 
126
132
  If option 1: Invoke `/wogi-decide --from-pattern` with the proposed rule (uses streamlined path, writes to project `decisions.md`). If user cancels within the /wogi-decide sub-flow, return to wogi-learn and display "Rule creation cancelled. Pattern not promoted."
127
133
  If option 2: Apply the product-level fix directly — edit the relevant command `.md` file, script, or template. If the fix needs investigation, add to `.workflow/state/product-feedback.md`. Do NOT write to `decisions.md` (product fixes don't belong in per-project state).
128
- If option 3: Add to `feedback-patterns.md` Pending Patterns section with count 1.
129
- If option 4: No action taken.
134
+ If option 3: Route via knowledge router: `node -e "const kr = require('wogiflow/scripts/flow-knowledge-router'); kr.storeByRoute(correction, route, context)"` stores to the matching skill's `knowledge/learnings.md`.
135
+ If option 4: Add to `feedback-patterns.md` Pending Patterns section with count 1.
136
+ If option 5: No action taken.
130
137
 
131
138
  ### Mode C: Bulk Promotion
132
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -714,6 +714,186 @@ function main() {
714
714
  }
715
715
  }
716
716
 
717
+ // Knowledge linting
718
+ console.log('');
719
+ printSection('Knowledge linting...');
720
+
721
+ // 1. Check section-index freshness
722
+ if (fileExists(PATHS.sectionIndex)) {
723
+ try {
724
+ const indexStat = fs.statSync(PATHS.sectionIndex);
725
+ const indexAge = Date.now() - indexStat.mtimeMs;
726
+ const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
727
+ if (indexAge > maxAge) {
728
+ const days = Math.round(indexAge / (24 * 60 * 60 * 1000));
729
+ warn(`section-index.json is ${days} days old — may be stale`);
730
+ console.log(` ${color('dim', "→ Run 'node scripts/flow-section-index.js --force' to regenerate")}`);
731
+ warnings++;
732
+ } else {
733
+ success(`section-index.json is fresh`);
734
+ }
735
+ } catch (_err) {
736
+ warn(`Could not check section-index.json age`);
737
+ warnings++;
738
+ }
739
+ } else {
740
+ warn(`section-index.json not found — knowledge navigation unavailable`);
741
+ console.log(` ${color('dim', "→ Run 'node scripts/flow-section-index.js --force' to generate")}`);
742
+ warnings++;
743
+ }
744
+
745
+ // 2. Check feedback-patterns.md for stale/duplicate entries
746
+ if (fileExists(PATHS.feedbackPatterns)) {
747
+ try {
748
+ const fpContent = fs.readFileSync(PATHS.feedbackPatterns, 'utf-8');
749
+ const fpLines = fpContent.split('\n').filter(l => l.startsWith('|') && !l.includes('---') && !l.includes('Date'));
750
+ const totalPatterns = fpLines.length;
751
+ const needsSkill = fpLines.filter(l => l.includes('#needs-skill')).length;
752
+ const promoted = fpLines.filter(l => l.includes('Fixed') || l.includes('Promoted')).length;
753
+ const stale = fpLines.filter(l => {
754
+ const dateMatch = l.match(/\d{4}-\d{2}-\d{2}/);
755
+ if (!dateMatch) return false;
756
+ const entryDate = new Date(dateMatch[0]);
757
+ const age = Date.now() - entryDate.getTime();
758
+ return age > 90 * 24 * 60 * 60 * 1000; // older than 90 days
759
+ }).length;
760
+
761
+ if (totalPatterns > 0) {
762
+ success(`feedback-patterns.md: ${totalPatterns} entries`);
763
+ if (needsSkill > totalPatterns * 0.7) {
764
+ warn(`${needsSkill}/${totalPatterns} entries are #needs-skill — skill gap detected`);
765
+ console.log(` ${color('dim', '→ Consider creating skills for common file patterns')}`);
766
+ warnings++;
767
+ }
768
+ if (stale > 0) {
769
+ warn(`${stale} entries older than 90 days — consider archiving`);
770
+ warnings++;
771
+ }
772
+ if (promoted > 0) {
773
+ console.log(` ${color('dim', 'ℹ')} ${promoted} patterns promoted/fixed`);
774
+ }
775
+ }
776
+ } catch (_err) {
777
+ warn(`Could not parse feedback-patterns.md`);
778
+ warnings++;
779
+ }
780
+ }
781
+
782
+ // 3. Check decisions.md references are still valid
783
+ if (fileExists(PATHS.decisions)) {
784
+ try {
785
+ const decContent = fs.readFileSync(PATHS.decisions, 'utf-8');
786
+ const fileRefs = decContent.match(/`[^`]*\.(js|ts|md|json)`/g) || [];
787
+ let orphanedRefs = 0;
788
+ const checkedRefs = new Set();
789
+
790
+ for (const ref of fileRefs) {
791
+ const filePath = ref.replace(/`/g, '');
792
+ if (checkedRefs.has(filePath)) continue;
793
+ checkedRefs.add(filePath);
794
+
795
+ // Resolve relative to project root, trying common prefixes
796
+ const candidates = [
797
+ path.join(PROJECT_ROOT, filePath),
798
+ path.join(PROJECT_ROOT, '.workflow', filePath),
799
+ path.join(PROJECT_ROOT, '.claude', filePath),
800
+ ];
801
+
802
+ const exists = candidates.some(c => fileExists(c));
803
+ if (!exists && !filePath.includes('*') && !filePath.includes('{')) {
804
+ orphanedRefs++;
805
+ }
806
+ }
807
+
808
+ if (orphanedRefs > 0) {
809
+ warn(`decisions.md has ${orphanedRefs} reference(s) to files that may no longer exist`);
810
+ warnings++;
811
+ } else if (fileRefs.length > 0) {
812
+ success(`decisions.md file references verified (${checkedRefs.size} checked)`);
813
+ }
814
+ } catch (_err) {
815
+ warn(`Could not lint decisions.md references`);
816
+ warnings++;
817
+ }
818
+ }
819
+
820
+ // 4. Check skill feedback loop — verify skills with learnings get loaded
821
+ try {
822
+ const skillsDir = path.join(PROJECT_ROOT, '.claude', 'skills');
823
+ if (dirExists(skillsDir)) {
824
+ const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
825
+ .filter(d => d.isDirectory() && d.name !== '_template' && d.name !== 'README.md');
826
+
827
+ let skillsWithLearnings = 0;
828
+ for (const dir of skillDirs) {
829
+ const learningsPath = path.join(skillsDir, dir.name, 'knowledge', 'learnings.md');
830
+ if (fileExists(learningsPath)) {
831
+ try {
832
+ const content = fs.readFileSync(learningsPath, 'utf-8').trim();
833
+ if (content.length > 50) { // Non-empty learnings
834
+ skillsWithLearnings++;
835
+ }
836
+ } catch (_err) {}
837
+ }
838
+ }
839
+
840
+ const config = getConfig();
841
+ const loadLearnings = config.skills?.loadLearnings !== false;
842
+
843
+ if (skillsWithLearnings > 0 && !loadLearnings) {
844
+ warn(`${skillsWithLearnings} skill(s) have learnings but skills.loadLearnings is disabled`);
845
+ console.log(` ${color('dim', '→ Set skills.loadLearnings: true in config.json to use accumulated learnings')}`);
846
+ warnings++;
847
+ } else if (skillsWithLearnings > 0) {
848
+ success(`${skillsWithLearnings} skill(s) with learnings — feedback loop active`);
849
+ } else if (skillDirs.length > 0) {
850
+ console.log(` ${color('dim', 'ℹ')} ${skillDirs.length} skill(s) installed — no learnings yet`);
851
+ }
852
+ }
853
+ } catch (_err) {
854
+ // Skills directory check is non-critical
855
+ }
856
+
857
+ // 5. Check registry maps for orphaned file references
858
+ try {
859
+ const { getActiveRegistries, STATE_DIR: stateDir } = require('./flow-utils');
860
+ const registries = getActiveRegistries();
861
+ let totalOrphans = 0;
862
+ let totalChecked = 0;
863
+
864
+ for (const reg of registries) {
865
+ const mapPath = path.join(stateDir, reg.mapFile);
866
+ if (!fileExists(mapPath)) continue;
867
+
868
+ try {
869
+ const mapContent = fs.readFileSync(mapPath, 'utf-8');
870
+ // Extract file paths from markdown table rows (typically in a "File" or "Path" column)
871
+ const pathRefs = mapContent.match(/(?:src|lib|scripts|components|pages|app)\/[\w/.-]+\.\w+/g) || [];
872
+ const unique = [...new Set(pathRefs)];
873
+
874
+ for (const ref of unique) {
875
+ totalChecked++;
876
+ const fullPath = path.join(PROJECT_ROOT, ref);
877
+ if (!fileExists(fullPath)) {
878
+ totalOrphans++;
879
+ }
880
+ }
881
+ } catch (_err) {
882
+ // Skip unreadable maps
883
+ }
884
+ }
885
+
886
+ if (totalOrphans > 0) {
887
+ warn(`Registry maps have ${totalOrphans} orphaned file reference(s) (${totalChecked} checked)`);
888
+ console.log(` ${color('dim', "→ Run 'flow registry-manager scan' to update maps")}`);
889
+ warnings++;
890
+ } else if (totalChecked > 0) {
891
+ success(`Registry map file references verified (${totalChecked} checked)`);
892
+ }
893
+ } catch (_err) {
894
+ // Registry system unavailable — skip silently
895
+ }
896
+
717
897
  // Check agents
718
898
  console.log('');
719
899
  printSection('Checking agents...');
@@ -658,12 +658,14 @@ Commands:
658
658
  archive Force archive old entries
659
659
  search <query> Search entries (current + archives)
660
660
  list-archives List archive files
661
+ rebuild-fts Rebuild FTS5 full-text search index from current log
661
662
  --help Show this help
662
663
 
663
664
  Examples:
664
665
  node scripts/flow-log-manager.js status
665
666
  node scripts/flow-log-manager.js search "#component:Button"
666
667
  node scripts/flow-log-manager.js archive
668
+ node scripts/flow-log-manager.js rebuild-fts
667
669
  `);
668
670
  }
669
671
 
@@ -757,6 +759,42 @@ if (require.main === module) {
757
759
  break;
758
760
  }
759
761
 
762
+ case 'rebuild-fts': {
763
+ if (!memoryDb) {
764
+ error('Memory DB not available — FTS rebuild requires flow-memory-db');
765
+ process.exit(1);
766
+ }
767
+
768
+ printHeader('Rebuilding FTS Index');
769
+ const rebuildContent = fileExists(LOG_PATH) ? readFile(LOG_PATH, '') : '';
770
+ const allEntries = parseEntries(rebuildContent);
771
+ let indexed = 0;
772
+
773
+ (async () => {
774
+ for (const entry of allEntries) {
775
+ try {
776
+ await memoryDb.addRequestLogEntry({
777
+ id: entry.id,
778
+ type: entry.type || 'other',
779
+ tags: entry.tags ? entry.tags.split(/\s+/) : [],
780
+ request: entry.request || '',
781
+ result: entry.result || '',
782
+ files: entry.files ? entry.files.split(/,\s*/) : [],
783
+ taskId: null
784
+ });
785
+ indexed++;
786
+ } catch (_err) {
787
+ // Skip duplicates
788
+ }
789
+ }
790
+ success(`Indexed ${indexed}/${allEntries.length} entries into FTS`);
791
+ })().catch(err => {
792
+ error(`FTS rebuild failed: ${err.message}`);
793
+ process.exit(1);
794
+ });
795
+ break;
796
+ }
797
+
760
798
  case '--help':
761
799
  case '-h':
762
800
  printUsage();
@@ -137,6 +137,7 @@ function safeParseArray(json) {
137
137
 
138
138
  let SQL = null;
139
139
  let db = null;
140
+ let ftsAvailable = false;
140
141
  let embedder = null;
141
142
  let initPromise = null;
142
143
 
@@ -294,6 +295,24 @@ async function initDatabase() {
294
295
  )
295
296
  `);
296
297
 
298
+ // FTS5 virtual table for full-text search over request log
299
+ // sql.js may not include FTS5 — silently skip if unavailable
300
+ try {
301
+ db.run(`
302
+ CREATE VIRTUAL TABLE IF NOT EXISTS request_log_fts USING fts5(
303
+ request, result, tags, files,
304
+ content='request_log',
305
+ content_rowid='rowid'
306
+ )
307
+ `);
308
+ ftsAvailable = true;
309
+ } catch (_err) {
310
+ // FTS5 not available in this sql.js build — fall back to LIKE queries
311
+ if (process.env.DEBUG) {
312
+ console.error('[memory-db] FTS5 not available — using LIKE fallback for search');
313
+ }
314
+ }
315
+
297
316
  // v10.0: Observations table for automatic tool use capture
298
317
  db.run(`
299
318
  CREATE TABLE IF NOT EXISTS observations (
@@ -1307,6 +1326,18 @@ async function addRequestLogEntry(entry) {
1307
1326
  entry.taskId || null
1308
1327
  ]);
1309
1328
 
1329
+ // Populate FTS index (skip if FTS5 not available to avoid exception overhead)
1330
+ if (ftsAvailable) {
1331
+ try {
1332
+ db.run(`
1333
+ INSERT INTO request_log_fts (rowid, request, result, tags, files)
1334
+ SELECT rowid, request, result, tags, files FROM request_log WHERE id = ?
1335
+ `, [id]);
1336
+ } catch (_err) {
1337
+ // FTS insert failure is non-critical
1338
+ }
1339
+ }
1340
+
1310
1341
  saveDatabase();
1311
1342
  return { id, stored: true };
1312
1343
  }
@@ -1345,8 +1376,28 @@ async function searchRequestLog(options = {}) {
1345
1376
  }
1346
1377
 
1347
1378
  if (query) {
1348
- sql += ' AND (request LIKE ? OR result LIKE ?)';
1349
- params.push(`%${query}%`, `%${query}%`);
1379
+ // Try FTS5 first for better ranking, fall back to LIKE
1380
+ try {
1381
+ const ftsCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='request_log_fts'");
1382
+ if (ftsCheck.length > 0 && ftsCheck[0].values.length > 0) {
1383
+ // Escape FTS special characters for safety
1384
+ const ftsQuery = query.replace(/['"]/g, '').replace(/[^\w\s#:-]/g, '').trim();
1385
+ if (ftsQuery) {
1386
+ sql += ' AND rowid IN (SELECT rowid FROM request_log_fts WHERE request_log_fts MATCH ?)';
1387
+ params.push(ftsQuery);
1388
+ } else {
1389
+ // Sanitized query is empty — fall back to LIKE
1390
+ sql += ' AND (request LIKE ? OR result LIKE ?)';
1391
+ params.push(`%${query}%`, `%${query}%`);
1392
+ }
1393
+ } else {
1394
+ sql += ' AND (request LIKE ? OR result LIKE ?)';
1395
+ params.push(`%${query}%`, `%${query}%`);
1396
+ }
1397
+ } catch (_err) {
1398
+ sql += ' AND (request LIKE ? OR result LIKE ?)';
1399
+ params.push(`%${query}%`, `%${query}%`);
1400
+ }
1350
1401
  }
1351
1402
 
1352
1403
  sql += ' ORDER BY timestamp DESC LIMIT ?';
@@ -428,6 +428,51 @@ async function main() {
428
428
  console.log(JSON.stringify(stats, null, 2));
429
429
  break;
430
430
 
431
+ case 'browse': {
432
+ // Human-readable knowledge index — outputs a browsable catalog of all indexed sections
433
+ await ensureIndex();
434
+
435
+ const indexData = readIndex();
436
+ if (!indexData) {
437
+ console.log('No section index found. Run: node scripts/flow-section-index.js --force');
438
+ break;
439
+ }
440
+
441
+ // Index stores sections per source: { sources: { "file.md": { sections: [...] } } }
442
+ const bySource = {};
443
+ let totalSections = 0;
444
+ const sources = indexData.sources || {};
445
+ for (const [sourceName, sourceData] of Object.entries(sources)) {
446
+ const sects = sourceData.sections || [];
447
+ if (sects.length > 0) {
448
+ bySource[sourceName] = sects;
449
+ totalSections += sects.length;
450
+ }
451
+ }
452
+
453
+ console.log('╔══════════════════════════════════════════════════╗');
454
+ console.log('║ KNOWLEDGE INDEX (browsable) ║');
455
+ console.log('╚══════════════════════════════════════════════════╝');
456
+ console.log(` ${totalSections} sections indexed from ${Object.keys(bySource).length} sources\n`);
457
+
458
+ for (const [source, sects] of Object.entries(bySource)) {
459
+ const shortSource = source.replace(/.*\/state\//, '').replace(/.*\/specs\//, 'specs/');
460
+ console.log(` ── ${shortSource} (${sects.length} sections) ──`);
461
+ for (const s of sects.slice(0, 20)) {
462
+ const pins = (s.pins || []).slice(0, 3).join(', ');
463
+ const title = s.title || s.id || '(untitled)';
464
+ console.log(` • ${title}${pins ? ` [${pins}]` : ''}`);
465
+ }
466
+ if (sects.length > 20) {
467
+ console.log(` ... and ${sects.length - 20} more`);
468
+ }
469
+ console.log('');
470
+ }
471
+
472
+ console.log(`Use 'find <pin>' to drill into a specific topic.`);
473
+ break;
474
+ }
475
+
431
476
  default:
432
477
  console.log(`
433
478
  Usage: node scripts/flow-section-resolver.js <command> [args]
@@ -437,12 +482,14 @@ Commands:
437
482
  get <section-id> Get a section by ID
438
483
  find <pins...> Find sections matching pins
439
484
  task "<description>" Find sections relevant to a task
485
+ browse Human-readable knowledge index catalog
440
486
  stats Show section statistics
441
487
 
442
488
  Examples:
443
489
  node scripts/flow-section-resolver.js get coding-standards:security-patterns-2026-01-11
444
490
  node scripts/flow-section-resolver.js find security error-handling
445
491
  node scripts/flow-section-resolver.js task "Add user authentication"
492
+ node scripts/flow-section-resolver.js browse
446
493
  `);
447
494
  }
448
495
  }
@@ -311,6 +311,83 @@ async function handleTaskCompleted(input) {
311
311
  // Workspace notifications are handled by the Stop hook (via HTTP to manager port).
312
312
  // Removed duplicate file-based notification here to prevent double messages (finding-004).
313
313
 
314
+ // Compound from success — capture positive patterns (fire-and-forget)
315
+ if (result.completed) {
316
+ try {
317
+ const config = getConfig();
318
+ if (config.skillLearning?.enabled) {
319
+ const { writeToFeedbackPatterns } = require('../../flow-learning-orchestrator');
320
+ const taskType = completedTask.type || 'unknown';
321
+ const changedFiles = input.changedFiles || [];
322
+ const criteriaCount = input.scenarioCount || completedTask.criteria || 0;
323
+ const firstPass = input.firstAttemptPass !== false;
324
+
325
+ // Only record success patterns for non-trivial tasks that passed on first attempt
326
+ if (firstPass && changedFiles.length >= 2 && criteriaCount >= 2) {
327
+ const today = new Date().toISOString().split('T')[0];
328
+ const filesSummary = changedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
329
+ const entryText = `success-pattern: ${taskType} task (${criteriaCount} criteria, ${changedFiles.length} files) completed first-pass. Files: ${filesSummary}`;
330
+ const tableRow = `| ${today} | ${entryText} | 1 | - | #success |`;
331
+
332
+ writeToFeedbackPatterns({
333
+ content: tableRow,
334
+ entryText,
335
+ caller: 'task-completed-success',
336
+ }).catch(() => {
337
+ // Non-critical
338
+ });
339
+ }
340
+ }
341
+ } catch (_err) {
342
+ // Non-critical — success pattern capture may not be available
343
+ }
344
+ }
345
+
346
+ // Skill learning extraction (fire-and-forget)
347
+ if (result.completed) {
348
+ try {
349
+ const { isLearningEnabled, extractLearningContext, matchFilesToSkills, appendLearning, discoverSkills, ensureKnowledgeDir, formatSemanticChanges } = require('../../flow-skill-learn');
350
+ const config = getConfig();
351
+ if (isLearningEnabled(config, 'task')) {
352
+ const changedFiles = input.changedFiles || [];
353
+ if (changedFiles.length > 0) {
354
+ const skills = discoverSkills();
355
+ const { matches: skillMap } = matchFilesToSkills(changedFiles, skills);
356
+ const context = extractLearningContext(changedFiles, 'task');
357
+
358
+ // Enrich context with task info
359
+ context.summary = `Task ${completedTask.id}: ${completedTask.title || ''}`;
360
+ context.taskType = completedTask.type || 'unknown';
361
+
362
+ for (const [skillName, matchedFiles] of skillMap) {
363
+ if (matchedFiles.length > 0) {
364
+ const skill = skills.find(s => s.name === skillName);
365
+ const skillDir = skill?.path;
366
+ if (skillDir) {
367
+ ensureKnowledgeDir(skillDir);
368
+ const entry = [
369
+ `### ${context.summary}`,
370
+ `**Type**: ${context.type} | **Trigger**: task-complete`,
371
+ `**Files**: ${matchedFiles.join(', ')}`,
372
+ ];
373
+ if (context.semanticChanges.length > 0) {
374
+ entry.push(`**Changes**: ${formatSemanticChanges(context.semanticChanges).slice(0, 200)}`);
375
+ }
376
+ entry.push('');
377
+ appendLearning(skillDir, entry.join('\n'));
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ } catch (_err) {
384
+ // Non-critical — skill learning may not be available
385
+ if (process.env.DEBUG) {
386
+ console.error(`[Task Completed] Skill learning failed: ${_err.message}`);
387
+ }
388
+ }
389
+ }
390
+
314
391
  // Check pending queue — notify user if items are waiting
315
392
  try {
316
393
  const { getPendingCount } = require('../../flow-pending');