wogiflow 2.9.0 → 2.10.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.
@@ -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...');
@@ -551,6 +551,64 @@ function formatHypothesisTree(tree) {
551
551
  return lines.join('\n');
552
552
  }
553
553
 
554
+ // ============================================================
555
+ // Hypothesis Verification Gate (v2.10)
556
+ // ============================================================
557
+
558
+ const VERIFICATION_STATE_PATH = path.join(PATHS.state, 'hypothesis-verification.json');
559
+
560
+ /**
561
+ * Record a hypothesis verification attempt.
562
+ * Called by the AI during bug investigation to track the
563
+ * hypothesis → verify → confirm cycle.
564
+ *
565
+ * @param {object} params
566
+ * @param {string} params.taskId - Bug task ID
567
+ * @param {string} params.hypothesis - The root cause claim
568
+ * @param {string} params.method - Verification method: 'code-trace' | 'test' | 'data-query' | 'reproduction'
569
+ * @param {string} params.result - 'confirmed' | 'refuted' | 'inconclusive'
570
+ * @param {string} params.evidence - What was observed
571
+ */
572
+ function recordHypothesisVerification({ taskId, hypothesis, method, result, evidence }) {
573
+ const state = readJson(VERIFICATION_STATE_PATH, { verifications: [] });
574
+ state.verifications.push({
575
+ taskId,
576
+ hypothesis,
577
+ method,
578
+ result,
579
+ evidence,
580
+ timestamp: new Date().toISOString()
581
+ });
582
+ writeJson(VERIFICATION_STATE_PATH, state);
583
+ if (result === 'confirmed') {
584
+ success(`Hypothesis verified (${method}): ${hypothesis}`);
585
+ } else if (result === 'refuted') {
586
+ warn(`Hypothesis refuted (${method}): ${hypothesis}`);
587
+ } else {
588
+ info(`Hypothesis inconclusive (${method}): ${hypothesis}`);
589
+ }
590
+ return state;
591
+ }
592
+
593
+ /**
594
+ * Check whether a hypothesis has been verified for a given task.
595
+ * Used by the gate to determine if "fixed" claims are allowed.
596
+ *
597
+ * @param {string} taskId
598
+ * @returns {{ verified: boolean, hypothesis: string|null, evidence: string|null }}
599
+ */
600
+ function isHypothesisVerified(taskId) {
601
+ const state = readJson(VERIFICATION_STATE_PATH, { verifications: [] });
602
+ const confirmed = state.verifications.find(
603
+ v => v.taskId === taskId && v.result === 'confirmed'
604
+ );
605
+ return {
606
+ verified: !!confirmed,
607
+ hypothesis: confirmed?.hypothesis ?? null,
608
+ evidence: confirmed?.evidence ?? null
609
+ };
610
+ }
611
+
554
612
  // ============================================================
555
613
  // Exports
556
614
  // ============================================================
@@ -574,6 +632,11 @@ module.exports = {
574
632
  saveHypothesisTree,
575
633
  loadHypothesisTree,
576
634
 
635
+ // Hypothesis verification gate (v2.10)
636
+ VERIFICATION_STATE_PATH,
637
+ recordHypothesisVerification,
638
+ isHypothesisVerified,
639
+
577
640
  // Formatting
578
641
  formatHypotheses,
579
642
  formatHypothesisTree
@@ -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
  }
@@ -85,7 +85,8 @@ function getDefaultState() {
85
85
  tasksCompleted: 0,
86
86
  filesModified: 0,
87
87
  errorsEncountered: 0,
88
- sessionCount: 0
88
+ sessionCount: 0,
89
+ sessionTasksStarted: 0 // Tasks started in THIS session (resets on new session)
89
90
  },
90
91
  lastSessionSummary: null,
91
92
  // Bypass tracking for enforcement
@@ -106,10 +107,7 @@ function getDefaultState() {
106
107
  * Returns default state if file doesn't exist or is invalid
107
108
  */
108
109
  function loadSessionState() {
109
- if (!fileExists(SESSION_PATH)) {
110
- return getDefaultState();
111
- }
112
-
110
+ // Use try-catch only, no fileExists (prevents TOCTOU race condition)
113
111
  try {
114
112
  const state = readJson(SESSION_PATH, null);
115
113
  if (!state) return getDefaultState();
@@ -245,13 +243,44 @@ function getCliSessionId() {
245
243
  * Track task start
246
244
  */
247
245
  function trackTaskStart(taskId, taskTitle, metadata = {}) {
248
- // saveSessionState already loads current state and merges
246
+ const current = loadSessionState();
247
+ const metrics = current.metrics || {};
249
248
  return saveSessionState({
250
249
  currentTask: {
251
250
  id: taskId,
252
251
  title: taskTitle,
253
252
  startedAt: new Date().toISOString(),
254
253
  ...metadata
254
+ },
255
+ metrics: {
256
+ ...metrics,
257
+ sessionTasksStarted: (metrics.sessionTasksStarted || 0) + 1
258
+ }
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Check if this is a continuation task (2nd+ in session)
264
+ * Used by /wogi-start to select compressed vs full prompt.
265
+ * MUST be called BEFORE trackTaskStart() for the current task.
266
+ * @returns {boolean}
267
+ */
268
+ function isContinuationTask() {
269
+ const state = loadSessionState();
270
+ return (state.metrics?.sessionTasksStarted ?? 0) >= 1;
271
+ }
272
+
273
+ /**
274
+ * Reset session task counter. Called by SessionStart hook
275
+ * to ensure the first task of a new session uses the full prompt.
276
+ */
277
+ function resetSessionTaskCounter() {
278
+ const current = loadSessionState();
279
+ const metrics = current.metrics || {};
280
+ return saveSessionState({
281
+ metrics: {
282
+ ...metrics,
283
+ sessionTasksStarted: 0
255
284
  }
256
285
  });
257
286
  }
@@ -912,6 +941,8 @@ module.exports = {
912
941
 
913
942
  // Task tracking
914
943
  trackTaskStart,
944
+ isContinuationTask,
945
+ resetSessionTaskCounter,
915
946
  trackTaskComplete,
916
947
  trackTaskCompleteAsync,
917
948
  getCurrentTask,