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.
|
|
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.
|
|
121
|
-
4.
|
|
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:
|
|
129
|
-
If option 4:
|
|
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
package/scripts/flow-health.js
CHANGED
|
@@ -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
|
-
|
|
1349
|
-
|
|
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');
|