thumbgate 1.15.0 → 1.16.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.
Files changed (129) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +59 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +210 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +157 -8
  12. package/config/evals/agent-safety-eval.json +338 -22
  13. package/config/gates/routine.json +43 -0
  14. package/config/github-about.json +3 -3
  15. package/config/model-candidates.json +131 -0
  16. package/openapi/openapi.yaml +118 -2
  17. package/package.json +57 -49
  18. package/public/blog.html +7 -7
  19. package/public/codex-plugin.html +6 -6
  20. package/public/compare.html +29 -23
  21. package/public/dashboard.html +82 -10
  22. package/public/guide.html +28 -28
  23. package/public/index.html +216 -98
  24. package/public/learn.html +50 -22
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +17 -17
  27. package/public/pro.html +82 -18
  28. package/scripts/agent-audit-trace.js +55 -0
  29. package/scripts/agent-memory-lifecycle.js +96 -0
  30. package/scripts/agent-readiness-plan.js +118 -0
  31. package/scripts/agentic-data-pipeline.js +21 -1
  32. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  33. package/scripts/ai-org-governance.js +98 -0
  34. package/scripts/ai-search-distribution.js +43 -0
  35. package/scripts/artifact-agent-plan.js +81 -0
  36. package/scripts/billing.js +27 -8
  37. package/scripts/cli-schema.js +18 -2
  38. package/scripts/code-mode-mcp-plan.js +71 -0
  39. package/scripts/context-engine.js +1 -2
  40. package/scripts/context-manager.js +4 -1
  41. package/scripts/dashboard-render-spec.js +1 -1
  42. package/scripts/dashboard.js +275 -9
  43. package/scripts/decision-journal.js +13 -3
  44. package/scripts/document-workflow-governance.js +62 -0
  45. package/scripts/enterprise-agent-rollout.js +34 -0
  46. package/scripts/experience-replay-governance.js +69 -0
  47. package/scripts/export-hf-dataset.js +1 -1
  48. package/scripts/feedback-loop.js +92 -4
  49. package/scripts/feedback-to-rules.js +17 -23
  50. package/scripts/gates-engine.js +4 -6
  51. package/scripts/growth-campaigns.js +49 -0
  52. package/scripts/harness-selector.js +16 -4
  53. package/scripts/hybrid-supervisor-agent.js +64 -0
  54. package/scripts/inference-cache-policy.js +72 -0
  55. package/scripts/inference-economics.js +53 -0
  56. package/scripts/internal-agent-bootstrap.js +12 -2
  57. package/scripts/knowledge-layer-plan.js +108 -0
  58. package/scripts/lesson-inference.js +183 -44
  59. package/scripts/lesson-search.js +4 -1
  60. package/scripts/llm-client.js +157 -26
  61. package/scripts/mailer/resend-mailer.js +112 -1
  62. package/scripts/mcp-transport-strategy.js +66 -0
  63. package/scripts/memory-store-governance.js +60 -0
  64. package/scripts/meta-agent-loop.js +7 -13
  65. package/scripts/model-access-eligibility.js +38 -0
  66. package/scripts/model-migration-readiness.js +55 -0
  67. package/scripts/operational-integrity.js +96 -3
  68. package/scripts/otel-declarative-config.js +56 -0
  69. package/scripts/perplexity-client.js +1 -1
  70. package/scripts/post-training-governance.js +34 -0
  71. package/scripts/private-core-boundary.js +72 -0
  72. package/scripts/production-agent-readiness.js +40 -0
  73. package/scripts/prompt-eval.js +564 -32
  74. package/scripts/prompt-programs.js +93 -0
  75. package/scripts/provider-action-normalizer.js +585 -0
  76. package/scripts/scaling-law-claims.js +60 -0
  77. package/scripts/security-scanner.js +1 -1
  78. package/scripts/self-distill-agent.js +7 -32
  79. package/scripts/seo-gsd.js +232 -55
  80. package/scripts/skill-rag-router.js +53 -0
  81. package/scripts/spec-gate.js +1 -1
  82. package/scripts/student-consistent-training.js +73 -0
  83. package/scripts/synthetic-data-provenance.js +98 -0
  84. package/scripts/task-context-result.js +81 -0
  85. package/scripts/telemetry-analytics.js +149 -0
  86. package/scripts/thompson-sampling.js +2 -2
  87. package/scripts/token-savings.js +7 -6
  88. package/scripts/token-tco.js +46 -0
  89. package/scripts/tool-registry.js +63 -3
  90. package/scripts/verification-loop.js +10 -1
  91. package/scripts/verifier-scoring.js +71 -0
  92. package/scripts/workflow-sentinel.js +284 -28
  93. package/scripts/workspace-agent-routines.js +118 -0
  94. package/src/api/server.js +381 -120
  95. package/scripts/analytics-report.js +0 -328
  96. package/scripts/autonomous-workflow.js +0 -377
  97. package/scripts/billing-setup.js +0 -109
  98. package/scripts/creator-campaigns.js +0 -239
  99. package/scripts/cross-encoder-reranker.js +0 -235
  100. package/scripts/daemon-manager.js +0 -108
  101. package/scripts/decision-trace.js +0 -354
  102. package/scripts/delegation-runtime.js +0 -896
  103. package/scripts/dispatch-brief.js +0 -159
  104. package/scripts/distribution-surfaces.js +0 -110
  105. package/scripts/feedback-history-distiller.js +0 -382
  106. package/scripts/funnel-analytics.js +0 -35
  107. package/scripts/history-distiller.js +0 -200
  108. package/scripts/hosted-job-launcher.js +0 -256
  109. package/scripts/intent-router.js +0 -392
  110. package/scripts/lesson-reranker.js +0 -263
  111. package/scripts/lesson-retrieval.js +0 -148
  112. package/scripts/managed-lesson-agent.js +0 -183
  113. package/scripts/operational-dashboard.js +0 -103
  114. package/scripts/operational-summary.js +0 -129
  115. package/scripts/operator-artifacts.js +0 -608
  116. package/scripts/optimize-context.js +0 -17
  117. package/scripts/org-dashboard.js +0 -206
  118. package/scripts/partner-orchestration.js +0 -146
  119. package/scripts/predictive-insights.js +0 -356
  120. package/scripts/pulse.js +0 -80
  121. package/scripts/reflector-agent.js +0 -221
  122. package/scripts/sales-pipeline.js +0 -681
  123. package/scripts/session-episode-store.js +0 -329
  124. package/scripts/session-health-sensor.js +0 -242
  125. package/scripts/session-report.js +0 -120
  126. package/scripts/swarm-coordinator.js +0 -81
  127. package/scripts/tool-kpi-tracker.js +0 -12
  128. package/scripts/webhook-delivery.js +0 -62
  129. package/scripts/workflow-sprint-intake.js +0 -475
@@ -1,329 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Session Episode Store — episodic memory for agent sessions.
6
- *
7
- * Persists session health snapshots across conversations so the system
8
- * learns cross-session degradation patterns:
9
- * - Which times of day produce degraded sessions
10
- * - Which task categories trigger repeat errors
11
- * - How long sessions last before degradation onset
12
- * - Whether feedback is actually reducing repeat mistakes over time
13
- *
14
- * This is the "episodic experience" layer described in the harnessed-agent
15
- * framework (Memory = working context + semantic knowledge + episodic experience).
16
- * The session-health-sensor provides the real-time signal; this module provides
17
- * the longitudinal learning.
18
- */
19
-
20
- const crypto = require('node:crypto');
21
- const path = require('node:path');
22
- const { readJsonl, appendJsonl } = require('./fs-utils');
23
- const { resolveFeedbackDir } = require('./feedback-paths');
24
- const {
25
- computeSessionHealth,
26
- loadRecentFeedback,
27
- } = require('./session-health-sensor');
28
-
29
- const EPISODE_FILE = 'session-episodes.jsonl';
30
- const PATTERN_WINDOW_EPISODES = 20;
31
-
32
- // ---------------------------------------------------------------------------
33
- // Episode Recording
34
- // ---------------------------------------------------------------------------
35
-
36
- function getEpisodePath({ feedbackDir } = {}) {
37
- const dir = feedbackDir || resolveFeedbackDir();
38
- return path.join(dir, EPISODE_FILE);
39
- }
40
-
41
- function buildEpisode({
42
- sessionId = null,
43
- health = null,
44
- feedbackEntries = [],
45
- tags = [],
46
- durationMs = null,
47
- } = {}) {
48
- const now = new Date();
49
- const effectiveHealth = health || computeSessionHealth(feedbackEntries);
50
-
51
- const negativeEntries = feedbackEntries.filter((e) => e.signal === 'negative');
52
- const categories = extractCategories(feedbackEntries);
53
- const errorFingerprints = extractErrorFingerprints(negativeEntries);
54
-
55
- return {
56
- sessionId: sessionId || `session_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`,
57
- recordedAt: now.toISOString(),
58
- hourOfDay: now.getHours(),
59
- dayOfWeek: now.getDay(),
60
- score: effectiveHealth.score,
61
- grade: effectiveHealth.grade,
62
- signals: effectiveHealth.signals.map((s) => ({ signal: s.signal, severity: s.severity })),
63
- recommendation: effectiveHealth.recommendation,
64
- feedbackCount: feedbackEntries.length,
65
- negativeCount: negativeEntries.length,
66
- positiveCount: feedbackEntries.filter((e) => e.signal === 'positive').length,
67
- categories,
68
- errorFingerprints,
69
- durationMs,
70
- tags,
71
- };
72
- }
73
-
74
- function recordEpisode(episode, options = {}) {
75
- const episodePath = getEpisodePath(options);
76
- appendJsonl(episodePath, episode);
77
- return episode;
78
- }
79
-
80
- function captureAndRecordEpisode(options = {}) {
81
- const feedbackEntries = loadRecentFeedback(options);
82
- const episode = buildEpisode({
83
- sessionId: options.sessionId,
84
- feedbackEntries,
85
- tags: options.tags || [],
86
- durationMs: options.durationMs,
87
- });
88
- return recordEpisode(episode, options);
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // Episode Loading
93
- // ---------------------------------------------------------------------------
94
-
95
- function loadEpisodes(options = {}) {
96
- return readJsonl(getEpisodePath(options));
97
- }
98
-
99
- function loadRecentEpisodes(count = PATTERN_WINDOW_EPISODES, options = {}) {
100
- return readJsonl(getEpisodePath(options), { tail: true, maxLines: count });
101
- }
102
-
103
- // ---------------------------------------------------------------------------
104
- // Cross-Session Pattern Detection
105
- // ---------------------------------------------------------------------------
106
-
107
- function analyzeTimeOfDayPatterns(episodes) {
108
- const byHour = new Map();
109
- for (const ep of episodes) {
110
- const hour = ep.hourOfDay;
111
- if (hour === undefined || hour === null) continue;
112
- const bucket = byHour.get(hour) || { total: 0, degraded: 0, critical: 0, totalScore: 0 };
113
- bucket.total += 1;
114
- bucket.totalScore += ep.score || 0;
115
- if (ep.grade === 'degraded') bucket.degraded += 1;
116
- if (ep.grade === 'critical') bucket.critical += 1;
117
- byHour.set(hour, bucket);
118
- }
119
-
120
- const patterns = [];
121
- for (const [hour, bucket] of byHour) {
122
- if (bucket.total < 2) continue;
123
- const failRate = (bucket.degraded + bucket.critical) / bucket.total;
124
- const avgScore = Math.round(bucket.totalScore / bucket.total);
125
- if (failRate > 0.5) {
126
- patterns.push({
127
- type: 'time_of_day_risk',
128
- hour,
129
- failRate: Math.round(failRate * 100),
130
- avgScore,
131
- sessions: bucket.total,
132
- recommendation: `Sessions at ${formatHour(hour)} degrade ${Math.round(failRate * 100)}% of the time. Consider scheduling complex work at other hours.`,
133
- });
134
- }
135
- }
136
-
137
- return patterns.sort((a, b) => b.failRate - a.failRate);
138
- }
139
-
140
- function analyzeCategoryPatterns(episodes) {
141
- const byCategory = new Map();
142
- for (const ep of episodes) {
143
- for (const cat of ep.categories || []) {
144
- const bucket = byCategory.get(cat) || { total: 0, degraded: 0, totalScore: 0 };
145
- bucket.total += 1;
146
- bucket.totalScore += ep.score || 0;
147
- if (ep.grade === 'degraded' || ep.grade === 'critical') bucket.degraded += 1;
148
- byCategory.set(cat, bucket);
149
- }
150
- }
151
-
152
- const patterns = [];
153
- for (const [category, bucket] of byCategory) {
154
- if (bucket.total < 2) continue;
155
- const failRate = bucket.degraded / bucket.total;
156
- const avgScore = Math.round(bucket.totalScore / bucket.total);
157
- if (failRate > 0.4) {
158
- patterns.push({
159
- type: 'category_risk',
160
- category,
161
- failRate: Math.round(failRate * 100),
162
- avgScore,
163
- sessions: bucket.total,
164
- recommendation: `"${category}" tasks degrade ${Math.round(failRate * 100)}% of sessions. Break these into smaller chunks or add prevention rules.`,
165
- });
166
- }
167
- }
168
-
169
- return patterns.sort((a, b) => b.failRate - a.failRate);
170
- }
171
-
172
- function analyzeRecurringErrors(episodes) {
173
- const fingerprints = new Map();
174
- for (const ep of episodes) {
175
- for (const fp of ep.errorFingerprints || []) {
176
- const count = (fingerprints.get(fp) || 0) + 1;
177
- fingerprints.set(fp, count);
178
- }
179
- }
180
-
181
- const patterns = [];
182
- for (const [fingerprint, count] of fingerprints) {
183
- if (count < 2) continue;
184
- patterns.push({
185
- type: 'recurring_error',
186
- fingerprint,
187
- occurrences: count,
188
- recommendation: `Error "${fingerprint.slice(0, 80)}" has recurred across ${count} sessions. Promote to a prevention rule.`,
189
- });
190
- }
191
-
192
- return patterns.sort((a, b) => b.occurrences - a.occurrences);
193
- }
194
-
195
- function analyzeFeedbackEffectiveness(episodes) {
196
- if (episodes.length < 3) return null;
197
-
198
- const recentHalf = episodes.slice(Math.floor(episodes.length / 2));
199
- const olderHalf = episodes.slice(0, Math.floor(episodes.length / 2));
200
-
201
- const avgRecent = average(recentHalf.map((e) => e.score || 0));
202
- const avgOlder = average(olderHalf.map((e) => e.score || 0));
203
- const recentRepeatRate = average(recentHalf.map((e) => (e.errorFingerprints || []).length));
204
- const olderRepeatRate = average(olderHalf.map((e) => (e.errorFingerprints || []).length));
205
-
206
- const scoreTrend = avgRecent - avgOlder;
207
- const repeatTrend = recentRepeatRate - olderRepeatRate;
208
-
209
- return {
210
- type: 'feedback_effectiveness',
211
- olderAvgScore: Math.round(avgOlder),
212
- recentAvgScore: Math.round(avgRecent),
213
- scoreTrend: Math.round(scoreTrend),
214
- olderRepeatRate: round2(olderRepeatRate),
215
- recentRepeatRate: round2(recentRepeatRate),
216
- repeatTrend: round2(repeatTrend),
217
- improving: scoreTrend > 0 && repeatTrend <= 0,
218
- recommendation: scoreTrend > 0
219
- ? `Session health is improving (${Math.round(avgOlder)} → ${Math.round(avgRecent)}). Feedback loop is working.`
220
- : `Session health is declining (${Math.round(avgOlder)} → ${Math.round(avgRecent)}). Review prevention rules and consider a fresh context reset.`,
221
- };
222
- }
223
-
224
- function analyzePatterns(episodes) {
225
- const timePatterns = analyzeTimeOfDayPatterns(episodes);
226
- const categoryPatterns = analyzeCategoryPatterns(episodes);
227
- const recurringErrors = analyzeRecurringErrors(episodes);
228
- const effectiveness = analyzeFeedbackEffectiveness(episodes);
229
-
230
- return {
231
- timeOfDay: timePatterns,
232
- categories: categoryPatterns,
233
- recurringErrors,
234
- effectiveness,
235
- episodesAnalyzed: episodes.length,
236
- analyzedAt: new Date().toISOString(),
237
- };
238
- }
239
-
240
- // ---------------------------------------------------------------------------
241
- // Helpers
242
- // ---------------------------------------------------------------------------
243
-
244
- function extractCategories(entries) {
245
- const cats = new Set();
246
- for (const entry of entries) {
247
- if (Array.isArray(entry.tags)) {
248
- for (const tag of entry.tags) cats.add(tag);
249
- }
250
- if (entry.richContext && entry.richContext.domain) {
251
- cats.add(entry.richContext.domain);
252
- }
253
- }
254
- return Array.from(cats).slice(0, 20);
255
- }
256
-
257
- function extractErrorFingerprints(negativeEntries) {
258
- const fps = new Set();
259
- for (const entry of negativeEntries) {
260
- if (!entry.whatWentWrong) continue;
261
- const fp = entry.whatWentWrong
262
- .toLowerCase()
263
- .replace(/\b(line|col|column)\s*\d+/g, '')
264
- .replace(/\b\d+\b/g, 'N')
265
- .replace(/\s+/g, ' ')
266
- .trim()
267
- .slice(0, 200);
268
- if (fp) fps.add(fp);
269
- }
270
- return Array.from(fps).slice(0, 20);
271
- }
272
-
273
- function formatHour(hour) {
274
- const h = hour % 12 || 12;
275
- const ampm = hour < 12 ? 'AM' : 'PM';
276
- return `${h}${ampm}`;
277
- }
278
-
279
- function average(nums) {
280
- return nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
281
- }
282
-
283
- function round2(n) {
284
- return Math.round(n * 100) / 100;
285
- }
286
-
287
- // ---------------------------------------------------------------------------
288
- // CLI
289
- // ---------------------------------------------------------------------------
290
-
291
- function isCliInvocation(argv = process.argv) {
292
- const invokedPath = argv[1];
293
- return invokedPath ? path.resolve(invokedPath) === __filename : false;
294
- }
295
-
296
- if (isCliInvocation()) {
297
- const command = process.argv[2] || 'capture';
298
-
299
- if (command === 'capture') {
300
- const episode = captureAndRecordEpisode();
301
- console.log(JSON.stringify(episode, null, 2));
302
- } else if (command === 'patterns') {
303
- const episodes = loadEpisodes();
304
- const patterns = analyzePatterns(episodes);
305
- console.log(JSON.stringify(patterns, null, 2));
306
- } else if (command === 'history') {
307
- const episodes = loadRecentEpisodes(20);
308
- console.log(JSON.stringify(episodes, null, 2));
309
- } else {
310
- console.error(`Unknown command: ${command}. Use: capture, patterns, history`);
311
- process.exit(1);
312
- }
313
- }
314
-
315
- module.exports = {
316
- EPISODE_FILE,
317
- PATTERN_WINDOW_EPISODES,
318
- analyzePatterns,
319
- analyzeCategoryPatterns,
320
- analyzeFeedbackEffectiveness,
321
- analyzeRecurringErrors,
322
- analyzeTimeOfDayPatterns,
323
- buildEpisode,
324
- captureAndRecordEpisode,
325
- getEpisodePath,
326
- loadEpisodes,
327
- loadRecentEpisodes,
328
- recordEpisode,
329
- };
@@ -1,242 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Session Health Sensor
6
- *
7
- * Detects real-time agent session degradation by analyzing feedback patterns,
8
- * error recurrence, and context drift signals. Inspired by community research
9
- * showing that "context rot" — not model quality — is the primary cause of
10
- * perceived AI agent degradation on large projects.
11
- *
12
- * Signals tracked:
13
- * 1. Repeat error rate — same error recurring within a session window
14
- * 2. Negative feedback density — ratio of thumbs-down in recent window
15
- * 3. Stagnation — consecutive negative signals without recovery
16
- * 4. Context amnesia — feedback referencing "forgot", "again", "already told"
17
- *
18
- * Output: A session health score (0–100) and actionable degradation signals.
19
- *
20
- * Integration points:
21
- * - Thompson Sampling: feeds per-category reliability with session context
22
- * - Gates engine: health score can trigger "restart session" recommendation
23
- * - Self-heal: low health triggers diagnostic capture
24
- */
25
-
26
- const path = require('node:path');
27
- const { readJsonl } = require('./fs-utils');
28
- const { resolveFeedbackDir } = require('./feedback-paths');
29
-
30
- // ---------------------------------------------------------------------------
31
- // Constants
32
- // ---------------------------------------------------------------------------
33
-
34
- const SESSION_WINDOW_MS = 45 * 60 * 1000; // 45 minutes — aligned with community best practice
35
- const AMNESIA_PATTERNS = /\b(again|forgot|already told|repeated|same mistake|same error|keeps? (doing|making|breaking)|context (lost|drift|rot)|amnesia)\b/i;
36
- const STAGNATION_THRESHOLD = 4; // consecutive negatives without a positive
37
- const HEALTH_FLOOR = 0;
38
- const HEALTH_CEILING = 100;
39
-
40
- // ---------------------------------------------------------------------------
41
- // Data Loading
42
- // ---------------------------------------------------------------------------
43
-
44
- function loadRecentFeedback({ feedbackDir, windowMs = SESSION_WINDOW_MS, now = Date.now() } = {}) {
45
- const dir = feedbackDir || resolveFeedbackDir();
46
- const logPath = path.join(dir, 'feedback-log.jsonl');
47
- const entries = readJsonl(logPath, { tail: true, maxLines: 200 });
48
- const cutoff = now - windowMs;
49
-
50
- return entries.filter((entry) => {
51
- const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
52
- return Number.isFinite(ts) && ts >= cutoff;
53
- });
54
- }
55
-
56
- // ---------------------------------------------------------------------------
57
- // Signal Detectors
58
- // ---------------------------------------------------------------------------
59
-
60
- function detectRepeatErrors(entries) {
61
- const errorTexts = entries
62
- .filter((e) => e.signal === 'negative' && e.whatWentWrong)
63
- .map((e) => normalizeErrorText(e.whatWentWrong));
64
-
65
- const seen = new Map();
66
- let repeats = 0;
67
-
68
- for (const text of errorTexts) {
69
- const count = (seen.get(text) || 0) + 1;
70
- seen.set(text, count);
71
- if (count > 1) repeats += 1;
72
- }
73
-
74
- return {
75
- signal: 'repeat_errors',
76
- count: repeats,
77
- total: errorTexts.length,
78
- rate: errorTexts.length > 0 ? repeats / errorTexts.length : 0,
79
- severity: repeats >= 3 ? 'critical' : repeats >= 1 ? 'warning' : 'healthy',
80
- };
81
- }
82
-
83
- function detectNegativeDensity(entries) {
84
- if (entries.length === 0) {
85
- return { signal: 'negative_density', count: 0, total: 0, rate: 0, severity: 'healthy' };
86
- }
87
-
88
- const negatives = entries.filter((e) => e.signal === 'negative').length;
89
- const rate = negatives / entries.length;
90
-
91
- return {
92
- signal: 'negative_density',
93
- count: negatives,
94
- total: entries.length,
95
- rate,
96
- severity: rate > 0.7 ? 'critical' : rate > 0.4 ? 'warning' : 'healthy',
97
- };
98
- }
99
-
100
- function detectStagnation(entries) {
101
- let maxConsecutiveNegatives = 0;
102
- let current = 0;
103
-
104
- for (const entry of entries) {
105
- if (entry.signal === 'negative') {
106
- current += 1;
107
- maxConsecutiveNegatives = Math.max(maxConsecutiveNegatives, current);
108
- } else {
109
- current = 0;
110
- }
111
- }
112
-
113
- return {
114
- signal: 'stagnation',
115
- consecutiveNegatives: maxConsecutiveNegatives,
116
- threshold: STAGNATION_THRESHOLD,
117
- severity: maxConsecutiveNegatives >= STAGNATION_THRESHOLD * 2 ? 'critical'
118
- : maxConsecutiveNegatives >= STAGNATION_THRESHOLD ? 'warning'
119
- : 'healthy',
120
- };
121
- }
122
-
123
- function detectContextAmnesia(entries) {
124
- const amnesiaEntries = entries.filter((e) => {
125
- const text = [e.context, e.whatWentWrong, e.whatToChange].filter(Boolean).join(' ');
126
- return AMNESIA_PATTERNS.test(text);
127
- });
128
-
129
- return {
130
- signal: 'context_amnesia',
131
- count: amnesiaEntries.length,
132
- total: entries.length,
133
- severity: amnesiaEntries.length >= 3 ? 'critical'
134
- : amnesiaEntries.length >= 1 ? 'warning'
135
- : 'healthy',
136
- };
137
- }
138
-
139
- // ---------------------------------------------------------------------------
140
- // Health Score
141
- // ---------------------------------------------------------------------------
142
-
143
- const SEVERITY_WEIGHTS = { healthy: 0, warning: 15, critical: 30 };
144
-
145
- function computeSessionHealth(entries) {
146
- const signals = [
147
- detectRepeatErrors(entries),
148
- detectNegativeDensity(entries),
149
- detectStagnation(entries),
150
- detectContextAmnesia(entries),
151
- ];
152
-
153
- let penalty = 0;
154
- for (const signal of signals) {
155
- penalty += SEVERITY_WEIGHTS[signal.severity] || 0;
156
- }
157
-
158
- // Extra penalty for high negative density rate
159
- const density = signals.find((s) => s.signal === 'negative_density');
160
- if (density && density.rate > 0) {
161
- penalty += Math.round(density.rate * 20);
162
- }
163
-
164
- const score = Math.max(HEALTH_FLOOR, Math.min(HEALTH_CEILING, HEALTH_CEILING - penalty));
165
-
166
- return {
167
- score,
168
- grade: score >= 80 ? 'healthy' : score >= 50 ? 'degraded' : 'critical',
169
- signals,
170
- recommendation: buildRecommendation(score, signals),
171
- windowMs: SESSION_WINDOW_MS,
172
- entriesAnalyzed: entries.length,
173
- computedAt: new Date().toISOString(),
174
- };
175
- }
176
-
177
- function buildRecommendation(score, signals) {
178
- if (score >= 80) return null;
179
-
180
- const critical = signals.filter((s) => s.severity === 'critical');
181
- const parts = [];
182
-
183
- if (critical.some((s) => s.signal === 'context_amnesia')) {
184
- parts.push('Context drift detected. Start a fresh session with CLAUDE.md re-read.');
185
- }
186
- if (critical.some((s) => s.signal === 'repeat_errors')) {
187
- parts.push('Same errors recurring. Capture feedback and promote to prevention rule.');
188
- }
189
- if (critical.some((s) => s.signal === 'stagnation')) {
190
- parts.push('No recovery from failures. Break the task into smaller chunks or restart.');
191
- }
192
- if (score < 50 && parts.length === 0) {
193
- parts.push('Session health is critically low. Consider starting a fresh conversation.');
194
- }
195
-
196
- return parts.length > 0 ? parts.join(' ') : 'Session showing mild degradation. Monitor closely.';
197
- }
198
-
199
- // ---------------------------------------------------------------------------
200
- // Helpers
201
- // ---------------------------------------------------------------------------
202
-
203
- function normalizeErrorText(text) {
204
- if (!text) return '';
205
- return text
206
- .toLowerCase()
207
- .replace(/\b(line|col|column)\s*\d+/g, '')
208
- .replace(/\b\d+\b/g, 'N')
209
- .replace(/\s+/g, ' ')
210
- .trim()
211
- .slice(0, 200);
212
- }
213
-
214
- // ---------------------------------------------------------------------------
215
- // CLI
216
- // ---------------------------------------------------------------------------
217
-
218
- function isCliInvocation(argv = process.argv) {
219
- const invokedPath = argv[1];
220
- return invokedPath ? path.resolve(invokedPath) === __filename : false;
221
- }
222
-
223
- if (isCliInvocation()) {
224
- const entries = loadRecentFeedback();
225
- const health = computeSessionHealth(entries);
226
- console.log(JSON.stringify(health, null, 2));
227
- if (health.grade === 'critical') process.exit(1);
228
- if (health.grade === 'degraded') process.exit(2);
229
- }
230
-
231
- module.exports = {
232
- AMNESIA_PATTERNS,
233
- SESSION_WINDOW_MS,
234
- STAGNATION_THRESHOLD,
235
- computeSessionHealth,
236
- detectContextAmnesia,
237
- detectNegativeDensity,
238
- detectRepeatErrors,
239
- detectStagnation,
240
- loadRecentFeedback,
241
- normalizeErrorText,
242
- };
@@ -1,120 +0,0 @@
1
- 'use strict';
2
-
3
- const MIN_WINDOW_HOURS = 1;
4
- const MAX_WINDOW_HOURS = 24 * 30;
5
- const DEFAULT_WINDOW_HOURS = 24;
6
-
7
- function normalizeWindowHours(input) {
8
- if (input === null || input === undefined || input === '') return DEFAULT_WINDOW_HOURS;
9
- const n = Number(input);
10
- if (!Number.isFinite(n)) return DEFAULT_WINDOW_HOURS;
11
- if (n < MIN_WINDOW_HOURS) return MIN_WINDOW_HOURS;
12
- if (n > MAX_WINDOW_HOURS) return MAX_WINDOW_HOURS;
13
- return Math.floor(n);
14
- }
15
-
16
- function topNegativeTags(tags, limit = 5) {
17
- if (!tags || typeof tags !== 'object') return [];
18
- return Object.entries(tags)
19
- .map(([tag, counts]) => ({
20
- tag,
21
- negative: (counts && counts.negative) || 0,
22
- positive: (counts && counts.positive) || 0,
23
- total: (counts && counts.total) || 0,
24
- }))
25
- .filter((row) => row.negative > 0)
26
- .sort((a, b) => b.negative - a.negative)
27
- .slice(0, limit);
28
- }
29
-
30
- function topGates(byGate, limit = 5) {
31
- if (!byGate || typeof byGate !== 'object') return [];
32
- return Object.entries(byGate)
33
- .map(([gate, counts]) => ({
34
- gate,
35
- blocked: (counts && counts.blocked) || 0,
36
- warned: (counts && counts.warned) || 0,
37
- pendingApproval: (counts && counts.pendingApproval) || 0,
38
- }))
39
- .sort((a, b) => b.blocked - a.blocked || b.warned - a.warned)
40
- .slice(0, limit);
41
- }
42
-
43
- function summarizeProvenance(events, sinceMs) {
44
- if (!Array.isArray(events)) return { total: 0, byType: {} };
45
- const byType = {};
46
- let total = 0;
47
- for (const evt of events) {
48
- const ts = Date.parse(evt && evt.timestamp ? evt.timestamp : '');
49
- if (!Number.isFinite(ts) || ts < sinceMs) continue;
50
- total += 1;
51
- const type = (evt && evt.type) || 'unknown';
52
- byType[type] = (byType[type] || 0) + 1;
53
- }
54
- return { total, byType };
55
- }
56
-
57
- function buildSessionReport({ windowHours } = {}) {
58
- const hours = normalizeWindowHours(windowHours);
59
- const sinceMs = Date.now() - hours * 60 * 60 * 1000;
60
- const report = {
61
- generatedAt: new Date().toISOString(),
62
- windowHours: hours,
63
- since: new Date(sinceMs).toISOString(),
64
- feedback: { totalPositive: 0, totalNegative: 0, topNegativeTags: [] },
65
- gates: { blocked: 0, warned: 0, passed: 0, topGates: [] },
66
- provenance: { total: 0, byType: {} },
67
- errors: {},
68
- };
69
-
70
- try {
71
- const { analyzeFeedback } = require('./feedback-loop');
72
- const feedback = analyzeFeedback() || {};
73
- report.feedback = {
74
- totalPositive: feedback.totalPositive || 0,
75
- totalNegative: feedback.totalNegative || 0,
76
- topNegativeTags: topNegativeTags(feedback.tags || {}),
77
- };
78
- } catch (err) {
79
- report.errors.feedback = String(err && err.message ? err.message : err);
80
- }
81
-
82
- try {
83
- const { loadStats } = require('./gates-engine');
84
- const stats = loadStats() || {};
85
- report.gates = {
86
- blocked: stats.blocked || 0,
87
- warned: stats.warned || 0,
88
- passed: stats.passed || 0,
89
- pendingApproval: stats.pendingApproval || 0,
90
- topGates: topGates(stats.byGate || {}),
91
- };
92
- } catch (err) {
93
- report.errors.gates = String(err && err.message ? err.message : err);
94
- }
95
-
96
- try {
97
- const { getProvenance } = require('./contextfs');
98
- const events = getProvenance(500) || [];
99
- report.provenance = summarizeProvenance(events, sinceMs);
100
- } catch (err) {
101
- report.errors.provenance = String(err && err.message ? err.message : err);
102
- }
103
-
104
- if (Object.keys(report.errors).length === 0) {
105
- delete report.errors;
106
- }
107
-
108
- return report;
109
- }
110
-
111
- module.exports = {
112
- buildSessionReport,
113
- normalizeWindowHours,
114
- topNegativeTags,
115
- topGates,
116
- summarizeProvenance,
117
- MIN_WINDOW_HOURS,
118
- MAX_WINDOW_HOURS,
119
- DEFAULT_WINDOW_HOURS,
120
- };