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.
@@ -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 };
@@ -714,6 +714,186 @@ function main() {
714
714
  }
715
715
  }
716
716
 
717
+ // Knowledge linting
718
+ console.log('');
719
+ printSection('Knowledge linting...');
720
+
721
+ // 1. Check section-index freshness
722
+ if (fileExists(PATHS.sectionIndex)) {
723
+ try {
724
+ const indexStat = fs.statSync(PATHS.sectionIndex);
725
+ const indexAge = Date.now() - indexStat.mtimeMs;
726
+ const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
727
+ if (indexAge > maxAge) {
728
+ const days = Math.round(indexAge / (24 * 60 * 60 * 1000));
729
+ warn(`section-index.json is ${days} days old — may be stale`);
730
+ console.log(` ${color('dim', "→ Run 'node scripts/flow-section-index.js --force' to regenerate")}`);
731
+ warnings++;
732
+ } else {
733
+ success(`section-index.json is fresh`);
734
+ }
735
+ } catch (_err) {
736
+ warn(`Could not check section-index.json age`);
737
+ warnings++;
738
+ }
739
+ } else {
740
+ warn(`section-index.json not found — knowledge navigation unavailable`);
741
+ console.log(` ${color('dim', "→ Run 'node scripts/flow-section-index.js --force' to generate")}`);
742
+ warnings++;
743
+ }
744
+
745
+ // 2. Check feedback-patterns.md for stale/duplicate entries
746
+ if (fileExists(PATHS.feedbackPatterns)) {
747
+ try {
748
+ const fpContent = fs.readFileSync(PATHS.feedbackPatterns, 'utf-8');
749
+ const fpLines = fpContent.split('\n').filter(l => l.startsWith('|') && !l.includes('---') && !l.includes('Date'));
750
+ const totalPatterns = fpLines.length;
751
+ const needsSkill = fpLines.filter(l => l.includes('#needs-skill')).length;
752
+ const promoted = fpLines.filter(l => l.includes('Fixed') || l.includes('Promoted')).length;
753
+ const stale = fpLines.filter(l => {
754
+ const dateMatch = l.match(/\d{4}-\d{2}-\d{2}/);
755
+ if (!dateMatch) return false;
756
+ const entryDate = new Date(dateMatch[0]);
757
+ const age = Date.now() - entryDate.getTime();
758
+ return age > 90 * 24 * 60 * 60 * 1000; // older than 90 days
759
+ }).length;
760
+
761
+ if (totalPatterns > 0) {
762
+ success(`feedback-patterns.md: ${totalPatterns} entries`);
763
+ if (needsSkill > totalPatterns * 0.7) {
764
+ warn(`${needsSkill}/${totalPatterns} entries are #needs-skill — skill gap detected`);
765
+ console.log(` ${color('dim', '→ Consider creating skills for common file patterns')}`);
766
+ warnings++;
767
+ }
768
+ if (stale > 0) {
769
+ warn(`${stale} entries older than 90 days — consider archiving`);
770
+ warnings++;
771
+ }
772
+ if (promoted > 0) {
773
+ console.log(` ${color('dim', 'ℹ')} ${promoted} patterns promoted/fixed`);
774
+ }
775
+ }
776
+ } catch (_err) {
777
+ warn(`Could not parse feedback-patterns.md`);
778
+ warnings++;
779
+ }
780
+ }
781
+
782
+ // 3. Check decisions.md references are still valid
783
+ if (fileExists(PATHS.decisions)) {
784
+ try {
785
+ const decContent = fs.readFileSync(PATHS.decisions, 'utf-8');
786
+ const fileRefs = decContent.match(/`[^`]*\.(js|ts|md|json)`/g) || [];
787
+ let orphanedRefs = 0;
788
+ const checkedRefs = new Set();
789
+
790
+ for (const ref of fileRefs) {
791
+ const filePath = ref.replace(/`/g, '');
792
+ if (checkedRefs.has(filePath)) continue;
793
+ checkedRefs.add(filePath);
794
+
795
+ // Resolve relative to project root, trying common prefixes
796
+ const candidates = [
797
+ path.join(PROJECT_ROOT, filePath),
798
+ path.join(PROJECT_ROOT, '.workflow', filePath),
799
+ path.join(PROJECT_ROOT, '.claude', filePath),
800
+ ];
801
+
802
+ const exists = candidates.some(c => fileExists(c));
803
+ if (!exists && !filePath.includes('*') && !filePath.includes('{')) {
804
+ orphanedRefs++;
805
+ }
806
+ }
807
+
808
+ if (orphanedRefs > 0) {
809
+ warn(`decisions.md has ${orphanedRefs} reference(s) to files that may no longer exist`);
810
+ warnings++;
811
+ } else if (fileRefs.length > 0) {
812
+ success(`decisions.md file references verified (${checkedRefs.size} checked)`);
813
+ }
814
+ } catch (_err) {
815
+ warn(`Could not lint decisions.md references`);
816
+ warnings++;
817
+ }
818
+ }
819
+
820
+ // 4. Check skill feedback loop — verify skills with learnings get loaded
821
+ try {
822
+ const skillsDir = path.join(PROJECT_ROOT, '.claude', 'skills');
823
+ if (dirExists(skillsDir)) {
824
+ const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
825
+ .filter(d => d.isDirectory() && d.name !== '_template' && d.name !== 'README.md');
826
+
827
+ let skillsWithLearnings = 0;
828
+ for (const dir of skillDirs) {
829
+ const learningsPath = path.join(skillsDir, dir.name, 'knowledge', 'learnings.md');
830
+ if (fileExists(learningsPath)) {
831
+ try {
832
+ const content = fs.readFileSync(learningsPath, 'utf-8').trim();
833
+ if (content.length > 50) { // Non-empty learnings
834
+ skillsWithLearnings++;
835
+ }
836
+ } catch (_err) {}
837
+ }
838
+ }
839
+
840
+ const config = getConfig();
841
+ const loadLearnings = config.skills?.loadLearnings !== false;
842
+
843
+ if (skillsWithLearnings > 0 && !loadLearnings) {
844
+ warn(`${skillsWithLearnings} skill(s) have learnings but skills.loadLearnings is disabled`);
845
+ console.log(` ${color('dim', '→ Set skills.loadLearnings: true in config.json to use accumulated learnings')}`);
846
+ warnings++;
847
+ } else if (skillsWithLearnings > 0) {
848
+ success(`${skillsWithLearnings} skill(s) with learnings — feedback loop active`);
849
+ } else if (skillDirs.length > 0) {
850
+ console.log(` ${color('dim', 'ℹ')} ${skillDirs.length} skill(s) installed — no learnings yet`);
851
+ }
852
+ }
853
+ } catch (_err) {
854
+ // Skills directory check is non-critical
855
+ }
856
+
857
+ // 5. Check registry maps for orphaned file references
858
+ try {
859
+ const { getActiveRegistries, STATE_DIR: stateDir } = require('./flow-utils');
860
+ const registries = getActiveRegistries();
861
+ let totalOrphans = 0;
862
+ let totalChecked = 0;
863
+
864
+ for (const reg of registries) {
865
+ const mapPath = path.join(stateDir, reg.mapFile);
866
+ if (!fileExists(mapPath)) continue;
867
+
868
+ try {
869
+ const mapContent = fs.readFileSync(mapPath, 'utf-8');
870
+ // Extract file paths from markdown table rows (typically in a "File" or "Path" column)
871
+ const pathRefs = mapContent.match(/(?:src|lib|scripts|components|pages|app)\/[\w/.-]+\.\w+/g) || [];
872
+ const unique = [...new Set(pathRefs)];
873
+
874
+ for (const ref of unique) {
875
+ totalChecked++;
876
+ const fullPath = path.join(PROJECT_ROOT, ref);
877
+ if (!fileExists(fullPath)) {
878
+ totalOrphans++;
879
+ }
880
+ }
881
+ } catch (_err) {
882
+ // Skip unreadable maps
883
+ }
884
+ }
885
+
886
+ if (totalOrphans > 0) {
887
+ warn(`Registry maps have ${totalOrphans} orphaned file reference(s) (${totalChecked} checked)`);
888
+ console.log(` ${color('dim', "→ Run 'flow registry-manager scan' to update maps")}`);
889
+ warnings++;
890
+ } else if (totalChecked > 0) {
891
+ success(`Registry map file references verified (${totalChecked} checked)`);
892
+ }
893
+ } catch (_err) {
894
+ // Registry system unavailable — skip silently
895
+ }
896
+
717
897
  // Check agents
718
898
  console.log('');
719
899
  printSection('Checking agents...');
@@ -658,12 +658,14 @@ Commands:
658
658
  archive Force archive old entries
659
659
  search <query> Search entries (current + archives)
660
660
  list-archives List archive files
661
+ rebuild-fts Rebuild FTS5 full-text search index from current log
661
662
  --help Show this help
662
663
 
663
664
  Examples:
664
665
  node scripts/flow-log-manager.js status
665
666
  node scripts/flow-log-manager.js search "#component:Button"
666
667
  node scripts/flow-log-manager.js archive
668
+ node scripts/flow-log-manager.js rebuild-fts
667
669
  `);
668
670
  }
669
671
 
@@ -757,6 +759,42 @@ if (require.main === module) {
757
759
  break;
758
760
  }
759
761
 
762
+ case 'rebuild-fts': {
763
+ if (!memoryDb) {
764
+ error('Memory DB not available — FTS rebuild requires flow-memory-db');
765
+ process.exit(1);
766
+ }
767
+
768
+ printHeader('Rebuilding FTS Index');
769
+ const rebuildContent = fileExists(LOG_PATH) ? readFile(LOG_PATH, '') : '';
770
+ const allEntries = parseEntries(rebuildContent);
771
+ let indexed = 0;
772
+
773
+ (async () => {
774
+ for (const entry of allEntries) {
775
+ try {
776
+ await memoryDb.addRequestLogEntry({
777
+ id: entry.id,
778
+ type: entry.type || 'other',
779
+ tags: entry.tags ? entry.tags.split(/\s+/) : [],
780
+ request: entry.request || '',
781
+ result: entry.result || '',
782
+ files: entry.files ? entry.files.split(/,\s*/) : [],
783
+ taskId: null
784
+ });
785
+ indexed++;
786
+ } catch (_err) {
787
+ // Skip duplicates
788
+ }
789
+ }
790
+ success(`Indexed ${indexed}/${allEntries.length} entries into FTS`);
791
+ })().catch(err => {
792
+ error(`FTS rebuild failed: ${err.message}`);
793
+ process.exit(1);
794
+ });
795
+ break;
796
+ }
797
+
760
798
  case '--help':
761
799
  case '-h':
762
800
  printUsage();
@@ -137,6 +137,7 @@ function safeParseArray(json) {
137
137
 
138
138
  let SQL = null;
139
139
  let db = null;
140
+ let ftsAvailable = false;
140
141
  let embedder = null;
141
142
  let initPromise = null;
142
143
 
@@ -294,6 +295,24 @@ async function initDatabase() {
294
295
  )
295
296
  `);
296
297
 
298
+ // FTS5 virtual table for full-text search over request log
299
+ // sql.js may not include FTS5 — silently skip if unavailable
300
+ try {
301
+ db.run(`
302
+ CREATE VIRTUAL TABLE IF NOT EXISTS request_log_fts USING fts5(
303
+ request, result, tags, files,
304
+ content='request_log',
305
+ content_rowid='rowid'
306
+ )
307
+ `);
308
+ ftsAvailable = true;
309
+ } catch (_err) {
310
+ // FTS5 not available in this sql.js build — fall back to LIKE queries
311
+ if (process.env.DEBUG) {
312
+ console.error('[memory-db] FTS5 not available — using LIKE fallback for search');
313
+ }
314
+ }
315
+
297
316
  // v10.0: Observations table for automatic tool use capture
298
317
  db.run(`
299
318
  CREATE TABLE IF NOT EXISTS observations (
@@ -1307,6 +1326,18 @@ async function addRequestLogEntry(entry) {
1307
1326
  entry.taskId || null
1308
1327
  ]);
1309
1328
 
1329
+ // Populate FTS index (skip if FTS5 not available to avoid exception overhead)
1330
+ if (ftsAvailable) {
1331
+ try {
1332
+ db.run(`
1333
+ INSERT INTO request_log_fts (rowid, request, result, tags, files)
1334
+ SELECT rowid, request, result, tags, files FROM request_log WHERE id = ?
1335
+ `, [id]);
1336
+ } catch (_err) {
1337
+ // FTS insert failure is non-critical
1338
+ }
1339
+ }
1340
+
1310
1341
  saveDatabase();
1311
1342
  return { id, stored: true };
1312
1343
  }
@@ -1345,8 +1376,28 @@ async function searchRequestLog(options = {}) {
1345
1376
  }
1346
1377
 
1347
1378
  if (query) {
1348
- sql += ' AND (request LIKE ? OR result LIKE ?)';
1349
- params.push(`%${query}%`, `%${query}%`);
1379
+ // Try FTS5 first for better ranking, fall back to LIKE
1380
+ try {
1381
+ const ftsCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='request_log_fts'");
1382
+ if (ftsCheck.length > 0 && ftsCheck[0].values.length > 0) {
1383
+ // Escape FTS special characters for safety
1384
+ const ftsQuery = query.replace(/['"]/g, '').replace(/[^\w\s#:-]/g, '').trim();
1385
+ if (ftsQuery) {
1386
+ sql += ' AND rowid IN (SELECT rowid FROM request_log_fts WHERE request_log_fts MATCH ?)';
1387
+ params.push(ftsQuery);
1388
+ } else {
1389
+ // Sanitized query is empty — fall back to LIKE
1390
+ sql += ' AND (request LIKE ? OR result LIKE ?)';
1391
+ params.push(`%${query}%`, `%${query}%`);
1392
+ }
1393
+ } else {
1394
+ sql += ' AND (request LIKE ? OR result LIKE ?)';
1395
+ params.push(`%${query}%`, `%${query}%`);
1396
+ }
1397
+ } catch (_err) {
1398
+ sql += ' AND (request LIKE ? OR result LIKE ?)';
1399
+ params.push(`%${query}%`, `%${query}%`);
1400
+ }
1350
1401
  }
1351
1402
 
1352
1403
  sql += ' ORDER BY timestamp DESC LIMIT ?';
@@ -428,6 +428,51 @@ async function main() {
428
428
  console.log(JSON.stringify(stats, null, 2));
429
429
  break;
430
430
 
431
+ case 'browse': {
432
+ // Human-readable knowledge index — outputs a browsable catalog of all indexed sections
433
+ await ensureIndex();
434
+
435
+ const indexData = readIndex();
436
+ if (!indexData) {
437
+ console.log('No section index found. Run: node scripts/flow-section-index.js --force');
438
+ break;
439
+ }
440
+
441
+ // Index stores sections per source: { sources: { "file.md": { sections: [...] } } }
442
+ const bySource = {};
443
+ let totalSections = 0;
444
+ const sources = indexData.sources || {};
445
+ for (const [sourceName, sourceData] of Object.entries(sources)) {
446
+ const sects = sourceData.sections || [];
447
+ if (sects.length > 0) {
448
+ bySource[sourceName] = sects;
449
+ totalSections += sects.length;
450
+ }
451
+ }
452
+
453
+ console.log('╔══════════════════════════════════════════════════╗');
454
+ console.log('║ KNOWLEDGE INDEX (browsable) ║');
455
+ console.log('╚══════════════════════════════════════════════════╝');
456
+ console.log(` ${totalSections} sections indexed from ${Object.keys(bySource).length} sources\n`);
457
+
458
+ for (const [source, sects] of Object.entries(bySource)) {
459
+ const shortSource = source.replace(/.*\/state\//, '').replace(/.*\/specs\//, 'specs/');
460
+ console.log(` ── ${shortSource} (${sects.length} sections) ──`);
461
+ for (const s of sects.slice(0, 20)) {
462
+ const pins = (s.pins || []).slice(0, 3).join(', ');
463
+ const title = s.title || s.id || '(untitled)';
464
+ console.log(` • ${title}${pins ? ` [${pins}]` : ''}`);
465
+ }
466
+ if (sects.length > 20) {
467
+ console.log(` ... and ${sects.length - 20} more`);
468
+ }
469
+ console.log('');
470
+ }
471
+
472
+ console.log(`Use 'find <pin>' to drill into a specific topic.`);
473
+ break;
474
+ }
475
+
431
476
  default:
432
477
  console.log(`
433
478
  Usage: node scripts/flow-section-resolver.js <command> [args]
@@ -437,12 +482,14 @@ Commands:
437
482
  get <section-id> Get a section by ID
438
483
  find <pins...> Find sections matching pins
439
484
  task "<description>" Find sections relevant to a task
485
+ browse Human-readable knowledge index catalog
440
486
  stats Show section statistics
441
487
 
442
488
  Examples:
443
489
  node scripts/flow-section-resolver.js get coding-standards:security-patterns-2026-01-11
444
490
  node scripts/flow-section-resolver.js find security error-handling
445
491
  node scripts/flow-section-resolver.js task "Add user authentication"
492
+ node scripts/flow-section-resolver.js browse
446
493
  `);
447
494
  }
448
495
  }
@@ -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.76+ stops auto-compaction after 3 consecutive failures. If context keeps growing, consider starting a new session.');
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
- context.relevantMemories = memories;
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
- // Key decisions
901
- if (ctx.keyDecisions && ctx.keyDecisions.length > 0) {
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, 5)) {
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, 3)) {
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, 3)) {
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