wogiflow 2.8.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.
- package/.claude/commands/wogi-learn.md +12 -5
- package/lib/workspace-gates.js +87 -0
- package/lib/workspace-session.js +308 -0
- package/lib/workspace.js +39 -3
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +27 -4
- package/scripts/flow-config-migrate.js +270 -0
- package/scripts/flow-context-manifest.js +322 -0
- package/scripts/flow-done-gates.js +76 -0
- package/scripts/flow-done.js +14 -0
- package/scripts/flow-gate-latch.js +119 -0
- package/scripts/flow-health.js +180 -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/hooks/core/post-compact.js +11 -1
- package/scripts/hooks/core/session-context.js +51 -7
- package/scripts/hooks/core/task-completed.js +103 -0
- package/scripts/postinstall.js +20 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Gate Latch
|
|
5
|
+
*
|
|
6
|
+
* Records when quality gates have passed for a task.
|
|
7
|
+
* The TaskCompleted hook checks for this latch before allowing
|
|
8
|
+
* a task to move to recentlyCompleted.
|
|
9
|
+
*
|
|
10
|
+
* Without the latch, agents can call TaskUpdate(status: "completed")
|
|
11
|
+
* and bypass all quality gates. The latch ensures that the only path
|
|
12
|
+
* to completion goes through the quality gate pipeline.
|
|
13
|
+
*
|
|
14
|
+
* Flow:
|
|
15
|
+
* 1. flow-done.js runs quality gates → gates pass → writes latch
|
|
16
|
+
* 2. Agent calls TaskUpdate → TaskCompleted hook fires
|
|
17
|
+
* 3. Hook checks latch → if present and recent → allows completion
|
|
18
|
+
* 4. If no latch → blocks completion with actionable error message
|
|
19
|
+
*
|
|
20
|
+
* Latch file: .workflow/state/.gates-passed.json
|
|
21
|
+
* TTL: 30 minutes (stale latches are ignored)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const { PATHS, safeJsonParse } = require('./flow-utils');
|
|
27
|
+
|
|
28
|
+
/** Latch time-to-live in milliseconds (30 minutes) */
|
|
29
|
+
const LATCH_TTL_MS = 30 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
/** Path to the gate latch file */
|
|
32
|
+
const LATCH_PATH = path.join(PATHS.state, '.gates-passed.json');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record that quality gates have passed for a task.
|
|
36
|
+
* Called by flow-done.js after all gates pass.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} taskId - The task ID that passed gates
|
|
39
|
+
* @param {string[]} gatesPassed - Names of gates that passed
|
|
40
|
+
* @returns {{ written: boolean, path: string }}
|
|
41
|
+
*/
|
|
42
|
+
function setGateLatch(taskId, gatesPassed = []) {
|
|
43
|
+
try {
|
|
44
|
+
const latch = {
|
|
45
|
+
taskId,
|
|
46
|
+
gatesPassed,
|
|
47
|
+
passedAt: new Date().toISOString(),
|
|
48
|
+
pid: process.pid
|
|
49
|
+
};
|
|
50
|
+
fs.writeFileSync(LATCH_PATH, JSON.stringify(latch, null, 2));
|
|
51
|
+
return { written: true, path: LATCH_PATH };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (process.env.DEBUG) {
|
|
54
|
+
console.error(`[gate-latch] Failed to write latch: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
return { written: false, path: LATCH_PATH };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if quality gates have passed for a task.
|
|
62
|
+
* Returns the latch data if valid, null if no latch or expired.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} taskId - The task ID to check
|
|
65
|
+
* @returns {{ valid: boolean, latch: Object|null, reason: string }}
|
|
66
|
+
*/
|
|
67
|
+
function checkGateLatch(taskId) {
|
|
68
|
+
const latch = safeJsonParse(LATCH_PATH, null);
|
|
69
|
+
|
|
70
|
+
if (!latch) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
latch: null,
|
|
74
|
+
reason: 'No gate latch found. Quality gates have not been run for this task.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check task ID matches
|
|
79
|
+
if (latch.taskId !== taskId) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
latch,
|
|
83
|
+
reason: `Gate latch is for task ${latch.taskId}, not ${taskId}.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check TTL
|
|
88
|
+
const passedAt = new Date(latch.passedAt).getTime();
|
|
89
|
+
const age = Date.now() - passedAt;
|
|
90
|
+
if (age > LATCH_TTL_MS) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
latch,
|
|
94
|
+
reason: `Gate latch expired (${Math.round(age / 60000)} min old, TTL is ${LATCH_TTL_MS / 60000} min).`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
valid: true,
|
|
100
|
+
latch,
|
|
101
|
+
reason: `Gates passed at ${latch.passedAt} (${latch.gatesPassed.length} gates)`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear the gate latch after task completion.
|
|
107
|
+
* Prevents stale latches from persisting.
|
|
108
|
+
*/
|
|
109
|
+
function clearGateLatch() {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(LATCH_PATH)) {
|
|
112
|
+
fs.unlinkSync(LATCH_PATH);
|
|
113
|
+
}
|
|
114
|
+
} catch (_err) {
|
|
115
|
+
// Non-critical
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { setGateLatch, checkGateLatch, clearGateLatch, LATCH_PATH };
|
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
|
}
|
|
@@ -198,7 +198,17 @@ function handlePostCompact() {
|
|
|
198
198
|
fs.writeFileSync(compactStatePath, JSON.stringify(tracker, null, 2));
|
|
199
199
|
|
|
200
200
|
if (tracker.count >= 3) {
|
|
201
|
-
contextParts.push('**WARNING**: Multiple compactions detected in quick succession. Claude Code 2.1.
|
|
201
|
+
contextParts.push('**WARNING**: Multiple compactions detected in quick succession. Claude Code 2.1.89+ stops auto-compaction after 3 consecutive thrash cycles. If context keeps growing, consider starting a new session.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// CC 2.1.89 context-budget awareness: when 2+ compactions happened in quick succession,
|
|
205
|
+
// instruct Claude to load context incrementally to avoid the thrash loop.
|
|
206
|
+
if (tracker.count >= 2) {
|
|
207
|
+
contextParts.push('**Context budget: LOW** — 2+ compactions in quick succession. To avoid a thrash loop:\n' +
|
|
208
|
+
'- Load context ON DEMAND only (Read specific files when needed, not bulk)\n' +
|
|
209
|
+
'- Use the manifest in SessionStart context to know what exists\n' +
|
|
210
|
+
'- Prefer targeted `Read` of specific sections over loading entire registry files\n' +
|
|
211
|
+
'- Skip non-essential context (community knowledge, memory recall)');
|
|
202
212
|
}
|
|
203
213
|
} catch (err) {
|
|
204
214
|
if (process.env.DEBUG) {
|
|
@@ -18,6 +18,7 @@ const setupCheck = require('./setup-check');
|
|
|
18
18
|
const { findParallelizable, getParallelConfig } = require('../../flow-parallel');
|
|
19
19
|
const { getBypassTracking } = require('../../flow-session-state');
|
|
20
20
|
const { loadCheckpoint, clearCheckpoint } = require('../../flow-task-checkpoint');
|
|
21
|
+
const { generateManifest, formatManifestForInjection, hasContent } = require('../../flow-context-manifest');
|
|
21
22
|
|
|
22
23
|
// ============================================================
|
|
23
24
|
// State Folder Hygiene — Whitelist + Age-Based Cleanup
|
|
@@ -119,6 +120,9 @@ const KNOWN_STATE_FILES = new Set([
|
|
|
119
120
|
'pending-skill.json',
|
|
120
121
|
'pending-setup.json',
|
|
121
122
|
|
|
123
|
+
// Context manifest (tiered context T2)
|
|
124
|
+
'session-manifest.md',
|
|
125
|
+
|
|
122
126
|
// Archive
|
|
123
127
|
'completed-archive.json',
|
|
124
128
|
|
|
@@ -132,6 +136,7 @@ const KNOWN_STATE_FILES = new Set([
|
|
|
132
136
|
'.claude-md-regen-version',
|
|
133
137
|
'.routing-pending',
|
|
134
138
|
'.routing-cleared',
|
|
139
|
+
'.gates-passed.json',
|
|
135
140
|
]);
|
|
136
141
|
|
|
137
142
|
/** Known directory names within state/ (not files) */
|
|
@@ -668,6 +673,7 @@ async function gatherSessionContext(options = {}) {
|
|
|
668
673
|
}
|
|
669
674
|
|
|
670
675
|
// Memory pipeline recall (surface relevant memories for current task)
|
|
676
|
+
// Capped at 2KB to prevent unbounded growth (CC 2.1.89 saves >50K hook output to disk)
|
|
671
677
|
try {
|
|
672
678
|
const currentTask = context.currentTask;
|
|
673
679
|
if (currentTask) {
|
|
@@ -677,7 +683,10 @@ async function gatherSessionContext(options = {}) {
|
|
|
677
683
|
currentTask.type || ''
|
|
678
684
|
);
|
|
679
685
|
if (memories) {
|
|
680
|
-
|
|
686
|
+
const MAX_MEMORY_CHARS = 2048;
|
|
687
|
+
context.relevantMemories = typeof memories === 'string' && memories.length > MAX_MEMORY_CHARS
|
|
688
|
+
? memories.substring(0, MAX_MEMORY_CHARS) + '\n... (truncated — load full via memory DB)'
|
|
689
|
+
: memories;
|
|
681
690
|
}
|
|
682
691
|
}
|
|
683
692
|
} catch (err) {
|
|
@@ -732,11 +741,12 @@ async function gatherSessionContext(options = {}) {
|
|
|
732
741
|
// v7.0: Real-time correction surfacing
|
|
733
742
|
// When 2+ corrections of the same type are detected in the same session,
|
|
734
743
|
// surface a hint telling Claude to consider recording the pattern.
|
|
744
|
+
// Capped at 5 types to prevent unbounded growth (CC 2.1.89 >50K disk save)
|
|
735
745
|
try {
|
|
736
746
|
const { getRepeatedCorrectionTypes } = require('../../flow-correction-detector');
|
|
737
747
|
const repeatedTypes = getRepeatedCorrectionTypes();
|
|
738
748
|
if (repeatedTypes.length > 0) {
|
|
739
|
-
context.correctionSurfacing = repeatedTypes;
|
|
749
|
+
context.correctionSurfacing = repeatedTypes.slice(0, 5);
|
|
740
750
|
}
|
|
741
751
|
} catch (_err) {
|
|
742
752
|
// Non-critical
|
|
@@ -768,6 +778,20 @@ async function gatherSessionContext(options = {}) {
|
|
|
768
778
|
// Non-critical — community sync module may not be available
|
|
769
779
|
}
|
|
770
780
|
|
|
781
|
+
// T2: Context manifest — compact inventory of all registries
|
|
782
|
+
// Lets Claude know what coding rules, components, utilities, and APIs exist
|
|
783
|
+
// without injecting full content (loaded on-demand via Read)
|
|
784
|
+
try {
|
|
785
|
+
const manifest = generateManifest();
|
|
786
|
+
if (hasContent(manifest)) {
|
|
787
|
+
context.contextManifest = manifest;
|
|
788
|
+
}
|
|
789
|
+
} catch (err) {
|
|
790
|
+
if (process.env.DEBUG) {
|
|
791
|
+
console.error(`[session-context] Manifest generation failed: ${err.message}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
771
795
|
return {
|
|
772
796
|
enabled: true,
|
|
773
797
|
context
|
|
@@ -897,8 +921,16 @@ function formatContextForInjection(context) {
|
|
|
897
921
|
output += `\nConsider running these tasks in parallel for faster completion.\n\n`;
|
|
898
922
|
}
|
|
899
923
|
|
|
900
|
-
//
|
|
901
|
-
|
|
924
|
+
// T2: Context manifest — replaces key decisions with full registry inventory
|
|
925
|
+
// Provides one-line summaries so Claude knows WHAT EXISTS without full injection
|
|
926
|
+
if (ctx.contextManifest) {
|
|
927
|
+
const manifestText = formatManifestForInjection(ctx.contextManifest);
|
|
928
|
+
if (manifestText) {
|
|
929
|
+
output += `### Available Context (load on demand)\n`;
|
|
930
|
+
output += manifestText + '\n\n';
|
|
931
|
+
}
|
|
932
|
+
} else if (ctx.keyDecisions && ctx.keyDecisions.length > 0) {
|
|
933
|
+
// Fallback: legacy key decisions format (manifest generation failed or unavailable)
|
|
902
934
|
output += `### Key Decisions\n`;
|
|
903
935
|
for (const decision of ctx.keyDecisions) {
|
|
904
936
|
output += `- **${decision.title}**: ${decision.summary}\n`;
|
|
@@ -984,13 +1016,14 @@ function formatContextForInjection(context) {
|
|
|
984
1016
|
}
|
|
985
1017
|
|
|
986
1018
|
// Community knowledge (pulled from server)
|
|
1019
|
+
// Capped: 3 model intelligence + 2 error strategies + 2 patterns (CC 2.1.89 >50K disk save)
|
|
987
1020
|
if (ctx.communityKnowledge && typeof ctx.communityKnowledge === 'object') {
|
|
988
1021
|
const ck = ctx.communityKnowledge;
|
|
989
1022
|
const items = [];
|
|
990
1023
|
|
|
991
1024
|
// Model intelligence
|
|
992
1025
|
if (Array.isArray(ck.modelIntelligence)) {
|
|
993
|
-
for (const item of ck.modelIntelligence.slice(0,
|
|
1026
|
+
for (const item of ck.modelIntelligence.slice(0, 3)) {
|
|
994
1027
|
if (item.model && (item.strengths || item.adjustments)) {
|
|
995
1028
|
const detail = item.adjustments || item.strengths;
|
|
996
1029
|
items.push(`Community: ${item.model} — ${detail}`);
|
|
@@ -1000,7 +1033,7 @@ function formatContextForInjection(context) {
|
|
|
1000
1033
|
|
|
1001
1034
|
// Error strategies
|
|
1002
1035
|
if (Array.isArray(ck.errorStrategies)) {
|
|
1003
|
-
for (const item of ck.errorStrategies.slice(0,
|
|
1036
|
+
for (const item of ck.errorStrategies.slice(0, 2)) {
|
|
1004
1037
|
if (item.category && item.strategy) {
|
|
1005
1038
|
items.push(`Community: ${item.category} — ${item.strategy}`);
|
|
1006
1039
|
}
|
|
@@ -1009,7 +1042,7 @@ function formatContextForInjection(context) {
|
|
|
1009
1042
|
|
|
1010
1043
|
// Patterns
|
|
1011
1044
|
if (Array.isArray(ck.patterns)) {
|
|
1012
|
-
for (const item of ck.patterns.slice(0,
|
|
1045
|
+
for (const item of ck.patterns.slice(0, 2)) {
|
|
1013
1046
|
if (item.description) {
|
|
1014
1047
|
items.push(`Community: ${item.description}`);
|
|
1015
1048
|
}
|
|
@@ -1025,6 +1058,17 @@ function formatContextForInjection(context) {
|
|
|
1025
1058
|
}
|
|
1026
1059
|
}
|
|
1027
1060
|
|
|
1061
|
+
// CC 2.1.89: Hook output >50K chars gets saved to disk with file path + preview.
|
|
1062
|
+
// Cap total output to stay under threshold and keep full context in-session.
|
|
1063
|
+
// If we exceed this, the most important context (T1: task state, routing) is at
|
|
1064
|
+
// the top and will appear in the preview. T2 (manifest) and lower-priority sections
|
|
1065
|
+
// are trimmed first.
|
|
1066
|
+
const MAX_OUTPUT_CHARS = 45000; // Stay safely under 50K
|
|
1067
|
+
if (output.length > MAX_OUTPUT_CHARS) {
|
|
1068
|
+
output = output.substring(0, MAX_OUTPUT_CHARS) +
|
|
1069
|
+
'\n\n*[Context truncated to stay within injection limit. Load full context via `/wogi-context` or Read registry files directly.]*\n';
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1028
1072
|
return output;
|
|
1029
1073
|
}
|
|
1030
1074
|
|