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.
- package/.claude/commands/wogi-bug.md +49 -4
- package/.claude/commands/wogi-decide.md +25 -0
- package/.claude/commands/wogi-learn.md +12 -5
- package/.claude/commands/wogi-start-continuation.md +84 -0
- package/.claude/commands/wogi-start.md +94 -3
- package/.claude/commands/wogi-statusline-setup.md +18 -5
- package/.claude/docs/claude-code-compatibility.md +77 -1
- package/.claude/docs/explore-agents.md +21 -5
- package/lib/workspace-gates.js +149 -0
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +7 -1
- package/scripts/flow-context-estimator.js +26 -4
- package/scripts/flow-decision-authority.js +359 -0
- package/scripts/flow-health.js +180 -0
- package/scripts/flow-hypothesis-generator.js +63 -0
- package/scripts/flow-log-manager.js +38 -0
- package/scripts/flow-memory-db.js +53 -2
- package/scripts/flow-section-resolver.js +47 -0
- package/scripts/flow-session-state.js +37 -6
- package/scripts/flow-standards-gate.js +138 -5
- package/scripts/flow-statusline-setup.js +137 -20
- package/scripts/hooks/core/task-completed.js +77 -0
- package/scripts/hooks/entry/claude-code/session-start.js +8 -1
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...');
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|