thumbgate 0.9.10
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-plugin/README.md +134 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +18 -0
- package/.claude-plugin/bundle/server/index.js +24 -0
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.well-known/mcp/server-card.json +231 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/adapters/README.md +9 -0
- package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
- package/adapters/chatgpt/INSTALL.md +83 -0
- package/adapters/chatgpt/openapi.yaml +1281 -0
- package/adapters/claude/.mcp.json +14 -0
- package/adapters/codex/config.toml +9 -0
- package/adapters/gemini/function-declarations.json +224 -0
- package/adapters/mcp/server-stdio.js +788 -0
- package/adapters/opencode/opencode.json +15 -0
- package/bin/cli.js +1484 -0
- package/bin/memory.sh +64 -0
- package/bin/obsidian-sync.sh +20 -0
- package/bin/postinstall.js +37 -0
- package/config/build-metadata.json +4 -0
- package/config/e2e-critical-flows.json +45 -0
- package/config/gate-templates.json +77 -0
- package/config/gates/claim-verification.json +29 -0
- package/config/gates/computer-use.json +39 -0
- package/config/gates/default.json +117 -0
- package/config/github-about.json +25 -0
- package/config/mcp-allowlists.json +135 -0
- package/config/model-tiers.json +33 -0
- package/config/partner-routing.json +132 -0
- package/config/policy-bundles/constrained-v1.json +64 -0
- package/config/policy-bundles/default-v1.json +91 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/skill-packs/react-testing.json +23 -0
- package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
- package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
- package/config/skill-specs/pr-reviewer.json +9 -0
- package/config/skill-specs/release-status.json +9 -0
- package/config/skill-specs/ticket-triage.json +9 -0
- package/config/subagent-profiles.json +32 -0
- package/config/tessl-tiles.json +29 -0
- package/config/thumbgate-settings.managed.json +12 -0
- package/openapi/openapi.yaml +1281 -0
- package/package.json +283 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +64 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
- package/plugins/claude-codex-bridge/.mcp.json +12 -0
- package/plugins/claude-codex-bridge/INSTALL.md +43 -0
- package/plugins/claude-codex-bridge/README.md +46 -0
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
- package/plugins/codex-profile/.mcp.json +12 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +66 -0
- package/plugins/codex-profile/README.md +37 -0
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
- package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
- package/plugins/cursor-marketplace/LICENSE +21 -0
- package/plugins/cursor-marketplace/README.md +124 -0
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
- package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
- package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
- package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
- package/plugins/cursor-marketplace/mcp.json +12 -0
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
- package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
- package/plugins/gemini-extension/INSTALL.md +92 -0
- package/plugins/gemini-extension/gemini_prompt.txt +14 -0
- package/plugins/gemini-extension/tool_contract.json +45 -0
- package/plugins/opencode-profile/INSTALL.md +57 -0
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +400 -0
- package/public/dashboard.html +1093 -0
- package/public/guide.html +317 -0
- package/public/index.html +1014 -0
- package/public/learn/agent-harness-pattern.html +180 -0
- package/public/learn/ai-agent-persistent-memory.html +202 -0
- package/public/learn/learn.css +45 -0
- package/public/learn/mcp-pre-action-gates-explained.html +172 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +213 -0
- package/public/lessons.html +650 -0
- package/public/vercel.json +8 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/access-anomaly-detector.js +12 -0
- package/scripts/adk-consolidator.js +266 -0
- package/scripts/agent-readiness.js +220 -0
- package/scripts/agent-security-hardening.js +227 -0
- package/scripts/agentic-data-pipeline.js +847 -0
- package/scripts/analytics-report.js +328 -0
- package/scripts/analytics-window.js +158 -0
- package/scripts/async-job-runner.js +1001 -0
- package/scripts/audit-trail.js +398 -0
- package/scripts/auto-promote-gates.js +299 -0
- package/scripts/auto-wire-hooks.js +312 -0
- package/scripts/autonomous-sales-agent.js +39 -0
- package/scripts/autoresearch-runner.js +216 -0
- package/scripts/background-agent-governance.js +237 -0
- package/scripts/behavioral-extraction.js +97 -0
- package/scripts/belief-update.js +84 -0
- package/scripts/billing.js +2438 -0
- package/scripts/bot-detector.js +50 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/build-claude-mcpb.js +189 -0
- package/scripts/build-metadata.js +97 -0
- package/scripts/check-congruence.js +322 -0
- package/scripts/cli-feedback.js +135 -0
- package/scripts/cli-telemetry.js +87 -0
- package/scripts/cloudflare-dynamic-sandbox.js +315 -0
- package/scripts/code-reasoning.js +350 -0
- package/scripts/codegraph-context.js +466 -0
- package/scripts/commercial-offer.js +56 -0
- package/scripts/computer-use-firewall.js +250 -0
- package/scripts/context-engine.js +694 -0
- package/scripts/contextfs.js +1287 -0
- package/scripts/conversation-context.js +119 -0
- package/scripts/creator-campaigns.js +239 -0
- package/scripts/daemon-manager.js +108 -0
- package/scripts/daily-digest.js +11 -0
- package/scripts/dashboard-render-spec.js +395 -0
- package/scripts/dashboard.js +1058 -0
- package/scripts/data-governance.js +173 -0
- package/scripts/delegation-runtime.js +900 -0
- package/scripts/deploy-gcp.sh +44 -0
- package/scripts/deploy-policy.js +263 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/dispatch-brief.js +159 -0
- package/scripts/distribution-surfaces.js +44 -0
- package/scripts/dpo-optimizer.js +209 -0
- package/scripts/ephemeral-agent-store.js +219 -0
- package/scripts/eval-harness.js +56 -0
- package/scripts/evolution-state.js +241 -0
- package/scripts/experiment-tracker.js +267 -0
- package/scripts/export-databricks-bundle.js +242 -0
- package/scripts/export-dpo-pairs.js +345 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/export-training.js +448 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +111 -0
- package/scripts/feedback-history-distiller.js +391 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +1887 -0
- package/scripts/feedback-paths.js +145 -0
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-root-consolidator.js +238 -0
- package/scripts/feedback-schema.js +426 -0
- package/scripts/feedback-session.js +286 -0
- package/scripts/feedback-to-memory.js +185 -0
- package/scripts/feedback-to-rules.js +163 -0
- package/scripts/filesystem-search.js +404 -0
- package/scripts/funnel-analytics.js +35 -0
- package/scripts/gate-satisfy.js +42 -0
- package/scripts/gate-stats.js +116 -0
- package/scripts/gate-templates.js +70 -0
- package/scripts/gates-engine.js +816 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/generate-pretool-hook.sh +40 -0
- package/scripts/github-about.js +350 -0
- package/scripts/github-outreach.js +65 -0
- package/scripts/gtm-revenue-loop.js +520 -0
- package/scripts/hallucination-detector.js +226 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/history-distiller.js +200 -0
- package/scripts/hook-auto-capture.sh +95 -0
- package/scripts/hook-stop-pr-thread-check.sh +68 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/hook-stop-verify-deploy.sh +31 -0
- package/scripts/hook-thumbgate-cache-updater.js +48 -0
- package/scripts/hook-verify-before-done.sh +20 -0
- package/scripts/hosted-config.js +170 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/install-mcp.js +159 -0
- package/scripts/intent-router.js +392 -0
- package/scripts/internal-agent-bootstrap.js +490 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/lesson-db.js +613 -0
- package/scripts/lesson-inference.js +315 -0
- package/scripts/lesson-retrieval.js +95 -0
- package/scripts/lesson-rotation.js +137 -0
- package/scripts/lesson-search.js +644 -0
- package/scripts/lesson-synthesis.js +196 -0
- package/scripts/license.js +50 -0
- package/scripts/local-model-profile.js +383 -0
- package/scripts/markdown-escape.js +12 -0
- package/scripts/marketing-experiment.js +671 -0
- package/scripts/mcp-config.js +149 -0
- package/scripts/mcp-policy.js +99 -0
- package/scripts/memalign-recall.js +111 -0
- package/scripts/memory-firewall.js +222 -0
- package/scripts/memory-migration.js +296 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/metered-billing.js +16 -0
- package/scripts/model-tier-router.js +301 -0
- package/scripts/money-watcher.js +71 -0
- package/scripts/multi-hop-recall.js +240 -0
- package/scripts/natural-language-harness.js +330 -0
- package/scripts/obsidian-export.js +712 -0
- package/scripts/operational-dashboard.js +103 -0
- package/scripts/operational-summary.js +93 -0
- package/scripts/optimize-context.js +17 -0
- package/scripts/org-dashboard.js +201 -0
- package/scripts/partner-orchestration.js +146 -0
- package/scripts/per-step-scoring.js +165 -0
- package/scripts/perplexity-marketing.js +466 -0
- package/scripts/pii-scanner.js +153 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/post-everywhere.js +308 -0
- package/scripts/post-to-x-retry.sh +22 -0
- package/scripts/post-to-x.js +369 -0
- package/scripts/pr-manager.js +236 -0
- package/scripts/predictive-insights.js +356 -0
- package/scripts/principle-extractor.js +162 -0
- package/scripts/pro-features.js +40 -0
- package/scripts/pro-local-dashboard.js +174 -0
- package/scripts/problem-detail.js +53 -0
- package/scripts/product-feedback.js +134 -0
- package/scripts/profile-router.js +245 -0
- package/scripts/prompt-dlp.js +221 -0
- package/scripts/prompt-guard.js +83 -0
- package/scripts/prove-adapters.js +863 -0
- package/scripts/prove-attribution.js +365 -0
- package/scripts/prove-automation.js +653 -0
- package/scripts/prove-autoresearch.js +304 -0
- package/scripts/prove-claim-verification.js +277 -0
- package/scripts/prove-cloudflare-sandbox.js +163 -0
- package/scripts/prove-data-pipeline.js +410 -0
- package/scripts/prove-data-quality.js +227 -0
- package/scripts/prove-evolution.js +352 -0
- package/scripts/prove-harnesses.js +287 -0
- package/scripts/prove-intelligence.js +259 -0
- package/scripts/prove-lancedb.js +371 -0
- package/scripts/prove-local-intelligence.js +342 -0
- package/scripts/prove-loop-closure.js +263 -0
- package/scripts/prove-predictive-insights.js +357 -0
- package/scripts/prove-runtime.js +350 -0
- package/scripts/prove-seo-gsd.js +234 -0
- package/scripts/prove-settings.js +279 -0
- package/scripts/prove-subway-upgrades.js +277 -0
- package/scripts/prove-tessl.js +229 -0
- package/scripts/prove-training-export.js +327 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/prove-xmemory.js +332 -0
- package/scripts/publish-decision.js +133 -0
- package/scripts/pulse.js +80 -0
- package/scripts/rate-limiter.js +125 -0
- package/scripts/reddit-dm-outreach.js +182 -0
- package/scripts/reddit-monitor-cron.sh +26 -0
- package/scripts/reflector-agent.js +221 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/revenue-status.js +472 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/schedule-manager.js +251 -0
- package/scripts/secret-scanner.js +414 -0
- package/scripts/self-heal.js +147 -0
- package/scripts/self-healing-check.js +188 -0
- package/scripts/semantic-layer.js +98 -0
- package/scripts/seo-gsd.js +1153 -0
- package/scripts/settings-hierarchy.js +214 -0
- package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
- package/scripts/skill-exporter.js +262 -0
- package/scripts/skill-generator.js +446 -0
- package/scripts/skill-materializer.js +134 -0
- package/scripts/skill-packs.js +136 -0
- package/scripts/skill-proposer.js +99 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/slo-alert-engine.js +14 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/social-analytics/db/schema.sql +32 -0
- package/scripts/social-analytics/digest.js +256 -0
- package/scripts/social-analytics/generate-instagram-card.js +97 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
- package/scripts/social-analytics/mcp-server.js +289 -0
- package/scripts/social-analytics/normalizer.js +580 -0
- package/scripts/social-analytics/notify.js +162 -0
- package/scripts/social-analytics/poll-all.js +107 -0
- package/scripts/social-analytics/pollers/github.js +195 -0
- package/scripts/social-analytics/pollers/instagram.js +253 -0
- package/scripts/social-analytics/pollers/linkedin.js +330 -0
- package/scripts/social-analytics/pollers/plausible.js +247 -0
- package/scripts/social-analytics/pollers/reddit.js +306 -0
- package/scripts/social-analytics/pollers/threads.js +233 -0
- package/scripts/social-analytics/pollers/tiktok.js +203 -0
- package/scripts/social-analytics/pollers/x.js +227 -0
- package/scripts/social-analytics/pollers/youtube.js +304 -0
- package/scripts/social-analytics/pollers/zernio.js +180 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
- package/scripts/social-analytics/publishers/devto.js +122 -0
- package/scripts/social-analytics/publishers/instagram.js +317 -0
- package/scripts/social-analytics/publishers/linkedin.js +294 -0
- package/scripts/social-analytics/publishers/reddit.js +390 -0
- package/scripts/social-analytics/publishers/threads.js +275 -0
- package/scripts/social-analytics/publishers/tiktok.js +217 -0
- package/scripts/social-analytics/publishers/x.js +259 -0
- package/scripts/social-analytics/publishers/youtube.js +223 -0
- package/scripts/social-analytics/publishers/zernio.js +209 -0
- package/scripts/social-analytics/run-digest.js +34 -0
- package/scripts/social-analytics/store.js +257 -0
- package/scripts/social-analytics/utm.js +143 -0
- package/scripts/social-pipeline.js +2628 -0
- package/scripts/social-quality-gate.js +18 -0
- package/scripts/social-reply-monitor.js +445 -0
- package/scripts/status-dashboard.js +155 -0
- package/scripts/statusline-lesson.js +16 -0
- package/scripts/statusline-tower.js +8 -0
- package/scripts/statusline.sh +116 -0
- package/scripts/stripe-live-status.js +115 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +70 -0
- package/scripts/sync-github-about.js +52 -0
- package/scripts/sync-version.js +451 -0
- package/scripts/synthetic-dpo.js +234 -0
- package/scripts/telemetry-analytics.js +821 -0
- package/scripts/tessl-export.js +371 -0
- package/scripts/test-coverage.js +120 -0
- package/scripts/thompson-sampling.js +417 -0
- package/scripts/thumbgate-search.js +189 -0
- package/scripts/tool-kpi-tracker.js +12 -0
- package/scripts/tool-registry.js +811 -0
- package/scripts/train_from_feedback.py +910 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +198 -0
- package/scripts/verification-loop.js +291 -0
- package/scripts/verify-obsidian-setup.sh +269 -0
- package/scripts/verify-run.js +269 -0
- package/scripts/webhook-delivery.js +62 -0
- package/scripts/weekly-auto-post.js +124 -0
- package/scripts/workflow-runs.js +154 -0
- package/scripts/workflow-sprint-intake.js +475 -0
- package/scripts/workspace-evolver.js +374 -0
- package/scripts/x-autonomous-marketing.js +139 -0
- package/scripts/xmemory-lite.js +405 -0
- package/skills/agent-memory/SKILL.md +97 -0
- package/skills/solve-architecture-autonomy/SKILL.md +17 -0
- package/skills/solve-architecture-autonomy/tool.js +33 -0
- package/skills/thumbgate/SKILL.md +114 -0
- package/skills/thumbgate-feedback/SKILL.md +49 -0
- package/src/api/server.js +4208 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson Inference — surrounding message context extraction + lesson linking.
|
|
6
|
+
*
|
|
7
|
+
* When a user gives thumbs up/down, this module:
|
|
8
|
+
* 1. Reads the surrounding conversation context (prior + following messages)
|
|
9
|
+
* 2. Infers what the lesson is from that context
|
|
10
|
+
* 3. Creates a structured lesson with a stable link
|
|
11
|
+
* 4. Provides data for the statusbar to show the most recent lesson
|
|
12
|
+
*
|
|
13
|
+
* Competing with Mem0: our advantage is local-first + structured inference,
|
|
14
|
+
* not just raw storage.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
|
+
const {
|
|
21
|
+
buildStableId,
|
|
22
|
+
extractFilePaths,
|
|
23
|
+
extractToolCalls,
|
|
24
|
+
extractErrors,
|
|
25
|
+
} = require('./conversation-context');
|
|
26
|
+
|
|
27
|
+
const LESSONS_FILE = 'lessons-index.jsonl';
|
|
28
|
+
const RECENT_LESSON_FILE = 'recent-lesson.json';
|
|
29
|
+
|
|
30
|
+
function getFeedbackDir() {
|
|
31
|
+
return resolveFeedbackDir();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getLessonBaseUrl() {
|
|
35
|
+
const configuredOrigin = String(process.env.THUMBGATE_PUBLIC_APP_ORIGIN || '').trim().replace(/\/+$/, '');
|
|
36
|
+
return configuredOrigin || 'http://localhost:3456';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getLessonsPath() { return path.join(getFeedbackDir(), LESSONS_FILE); }
|
|
40
|
+
function getRecentLessonPath() { return path.join(getFeedbackDir(), RECENT_LESSON_FILE); }
|
|
41
|
+
|
|
42
|
+
function readJsonl(fp) {
|
|
43
|
+
if (!fs.existsSync(fp)) return [];
|
|
44
|
+
const raw = fs.readFileSync(fp, 'utf-8').trim();
|
|
45
|
+
if (!raw) return [];
|
|
46
|
+
return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureDir(p) { const d = path.dirname(p); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 1. Surrounding Message Context Extraction
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract lesson context from surrounding messages.
|
|
57
|
+
* Takes the conversation turns before/after the feedback signal.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} opts
|
|
60
|
+
* @param {Array} opts.priorMessages - Messages before the feedback (most recent first)
|
|
61
|
+
* @param {Array} opts.followingMessages - Messages after the feedback
|
|
62
|
+
* @param {string} opts.signal - 'positive' or 'negative'
|
|
63
|
+
* @param {string} opts.feedbackContext - User-provided context string
|
|
64
|
+
* @returns {{ inferredLesson, triggerMessage, priorSummary, confidence }}
|
|
65
|
+
*/
|
|
66
|
+
function inferFromSurroundingMessages({ priorMessages = [], followingMessages = [], signal, feedbackContext = '' } = {}) {
|
|
67
|
+
const prior = priorMessages.slice(0, 5); // Last 5 messages before feedback
|
|
68
|
+
const following = followingMessages.slice(0, 3); // Next 3 messages after
|
|
69
|
+
|
|
70
|
+
// The trigger message is typically the last assistant message before feedback
|
|
71
|
+
const triggerMessage = prior.find((m) => m.role === 'assistant') || prior[0] || null;
|
|
72
|
+
const triggerText = triggerMessage ? (triggerMessage.content || triggerMessage.text || '') : '';
|
|
73
|
+
|
|
74
|
+
// Extract what the agent did
|
|
75
|
+
const actionPatterns = [
|
|
76
|
+
{ regex: /(?:edited|modified|changed|updated)\s+(.+?)(?:\.|$)/i, type: 'edit' },
|
|
77
|
+
{ regex: /(?:created|wrote|added)\s+(.+?)(?:\.|$)/i, type: 'create' },
|
|
78
|
+
{ regex: /(?:ran|executed|running)\s+(.+?)(?:\.|$)/i, type: 'command' },
|
|
79
|
+
{ regex: /(?:fixed|resolved|patched)\s+(.+?)(?:\.|$)/i, type: 'fix' },
|
|
80
|
+
{ regex: /(?:deployed|pushed|merged)\s+(.+?)(?:\.|$)/i, type: 'deploy' },
|
|
81
|
+
{ regex: /(?:deleted|removed|dropped)\s+(.+?)(?:\.|$)/i, type: 'delete' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
let inferredAction = null;
|
|
85
|
+
for (const ap of actionPatterns) {
|
|
86
|
+
const match = triggerText.match(ap.regex);
|
|
87
|
+
if (match) { inferredAction = { type: ap.type, target: match[1].trim().slice(0, 100) }; break; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build the lesson
|
|
91
|
+
const isNegative = signal === 'negative' || signal === 'down';
|
|
92
|
+
let inferredLesson;
|
|
93
|
+
|
|
94
|
+
if (isNegative && inferredAction) {
|
|
95
|
+
inferredLesson = `Avoid: ${inferredAction.type} on ${inferredAction.target}. ${feedbackContext || 'User signaled this approach failed.'}`;
|
|
96
|
+
} else if (!isNegative && inferredAction) {
|
|
97
|
+
inferredLesson = `Repeat: ${inferredAction.type} on ${inferredAction.target}. ${feedbackContext || 'User confirmed this approach works.'}`;
|
|
98
|
+
} else if (feedbackContext) {
|
|
99
|
+
inferredLesson = feedbackContext;
|
|
100
|
+
} else {
|
|
101
|
+
inferredLesson = `${isNegative ? 'Negative' : 'Positive'} signal on agent output. No specific action inferred.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Summarize prior context
|
|
105
|
+
const priorSummary = prior.slice(0, 3).map((m) => {
|
|
106
|
+
const role = m.role || 'unknown';
|
|
107
|
+
const text = (m.content || m.text || '').slice(0, 80);
|
|
108
|
+
return `[${role}] ${text}`;
|
|
109
|
+
}).join(' → ');
|
|
110
|
+
|
|
111
|
+
// Confidence: higher if we have more context
|
|
112
|
+
const contextSignals = [
|
|
113
|
+
feedbackContext.length > 10,
|
|
114
|
+
!!inferredAction,
|
|
115
|
+
prior.length >= 2,
|
|
116
|
+
!!triggerMessage,
|
|
117
|
+
];
|
|
118
|
+
const confidence = Math.round((contextSignals.filter(Boolean).length / contextSignals.length) * 100);
|
|
119
|
+
|
|
120
|
+
return { inferredLesson, triggerMessage: triggerText.slice(0, 200), priorSummary, inferredAction, confidence, signal };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// 2. Lesson Index & Linking
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a lesson record with a stable link and store in the index.
|
|
129
|
+
*/
|
|
130
|
+
function createLesson({ feedbackId, signal, inferredLesson, triggerMessage, priorSummary, confidence, tags = [], metadata = {} } = {}) {
|
|
131
|
+
const lesson = {
|
|
132
|
+
id: buildStableId('lesson'),
|
|
133
|
+
feedbackId: feedbackId || null,
|
|
134
|
+
signal: signal || 'unknown',
|
|
135
|
+
lesson: inferredLesson || '',
|
|
136
|
+
triggerMessage: triggerMessage || '',
|
|
137
|
+
priorSummary: priorSummary || '',
|
|
138
|
+
confidence: confidence || 0,
|
|
139
|
+
tags,
|
|
140
|
+
metadata,
|
|
141
|
+
createdAt: new Date().toISOString(),
|
|
142
|
+
link: null, // populated below
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Stable link: dashboard deep-link to this lesson
|
|
146
|
+
lesson.link = `${getLessonBaseUrl()}/lessons#${lesson.id}`;
|
|
147
|
+
|
|
148
|
+
const lessonsPath = getLessonsPath();
|
|
149
|
+
ensureDir(lessonsPath);
|
|
150
|
+
fs.appendFileSync(lessonsPath, JSON.stringify(lesson) + '\n');
|
|
151
|
+
|
|
152
|
+
// Update recent lesson for statusbar
|
|
153
|
+
const recentPath = getRecentLessonPath();
|
|
154
|
+
fs.writeFileSync(recentPath, JSON.stringify(lesson, null, 2) + '\n');
|
|
155
|
+
|
|
156
|
+
return lesson;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the most recent lesson (for statusbar display).
|
|
161
|
+
*/
|
|
162
|
+
function getRecentLesson() {
|
|
163
|
+
const p = getRecentLessonPath();
|
|
164
|
+
if (!fs.existsSync(p)) return null;
|
|
165
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Search lessons by query text.
|
|
170
|
+
*/
|
|
171
|
+
function searchLessons({ query = '', limit = 10, signal } = {}) {
|
|
172
|
+
const lessons = readJsonl(getLessonsPath());
|
|
173
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
174
|
+
|
|
175
|
+
return lessons
|
|
176
|
+
.filter((l) => !signal || l.signal === signal)
|
|
177
|
+
.map((l) => {
|
|
178
|
+
const haystack = `${l.lesson} ${l.triggerMessage} ${l.priorSummary} ${(l.tags || []).join(' ')}`.toLowerCase();
|
|
179
|
+
let score = 0;
|
|
180
|
+
for (const t of tokens) { if (t.length > 2 && haystack.includes(t)) score += 1; }
|
|
181
|
+
return { ...l, _score: score };
|
|
182
|
+
})
|
|
183
|
+
.filter((l) => tokens.length === 0 || l._score > 0)
|
|
184
|
+
.sort((a, b) => b._score - a._score || new Date(b.createdAt) - new Date(a.createdAt))
|
|
185
|
+
.slice(0, limit);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get lesson stats.
|
|
190
|
+
*/
|
|
191
|
+
function getLessonStats() {
|
|
192
|
+
const lessons = readJsonl(getLessonsPath());
|
|
193
|
+
const positive = lessons.filter((l) => l.signal === 'positive' || l.signal === 'up').length;
|
|
194
|
+
const negative = lessons.filter((l) => l.signal === 'negative' || l.signal === 'down').length;
|
|
195
|
+
const avgConfidence = lessons.length > 0 ? Math.round(lessons.reduce((s, l) => s + (l.confidence || 0), 0) / lessons.length) : 0;
|
|
196
|
+
return { total: lessons.length, positive, negative, avgConfidence };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// 3. Statusbar Data Provider
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get data for the Claude Code statusbar.
|
|
205
|
+
* Returns the most recent lesson with link, formatted for display.
|
|
206
|
+
*/
|
|
207
|
+
function getStatusbarLessonData() {
|
|
208
|
+
const recent = getRecentLesson();
|
|
209
|
+
if (!recent) return { hasLesson: false, text: null, link: null };
|
|
210
|
+
|
|
211
|
+
const emoji = (recent.signal === 'negative' || recent.signal === 'down') ? '👎' : '👍';
|
|
212
|
+
const truncated = recent.lesson.length > 60 ? recent.lesson.slice(0, 57) + '...' : recent.lesson;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
hasLesson: true,
|
|
216
|
+
text: `${emoji} ${truncated}`,
|
|
217
|
+
link: recent.link,
|
|
218
|
+
lessonId: recent.id,
|
|
219
|
+
confidence: recent.confidence,
|
|
220
|
+
createdAt: recent.createdAt,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// 4. Structured IF/THEN Lesson Extraction (v0.9.4)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function inferStructuredLesson(conversationWindow, signal, context) {
|
|
229
|
+
const normalizedWindow = Array.isArray(conversationWindow) ? conversationWindow : [];
|
|
230
|
+
const userMessages = normalizedWindow.filter(m => m.role === 'user');
|
|
231
|
+
const assistantMessages = normalizedWindow.filter(m => m.role === 'assistant');
|
|
232
|
+
const lastUser = userMessages[userMessages.length - 1]?.content || '';
|
|
233
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1]?.content || '';
|
|
234
|
+
const filePaths = extractFilePaths(normalizedWindow);
|
|
235
|
+
const toolCalls = extractToolCalls(normalizedWindow);
|
|
236
|
+
const errorPatterns = extractErrors(normalizedWindow);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
format: 'if-then-v1',
|
|
240
|
+
trigger: extractTrigger(lastUser),
|
|
241
|
+
action: extractAction(lastAssistant, signal),
|
|
242
|
+
signal,
|
|
243
|
+
confidence: calculateConfidence(normalizedWindow, context),
|
|
244
|
+
scope: inferScope(filePaths, toolCalls),
|
|
245
|
+
examples: [{ userIntent: lastUser.slice(0, 300), assistantAction: lastAssistant.slice(0, 300), outcome: signal === 'positive' ? 'approved' : 'rejected' }],
|
|
246
|
+
metadata: { toolsUsed: toolCalls, filesInvolved: filePaths.slice(0, 10), errorPatterns: errorPatterns.slice(0, 5), conversationLength: normalizedWindow.length, inferredAt: new Date().toISOString() },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function extractTrigger(userMsg) {
|
|
251
|
+
const text = String(userMsg || '').trim();
|
|
252
|
+
const lower = text.toLowerCase();
|
|
253
|
+
const leadingPhrases = [
|
|
254
|
+
{ phrases: ['fix ', 'debug ', 'solve ', 'investigate '], type: 'debugging' },
|
|
255
|
+
{ phrases: ['implement ', 'add ', 'create ', 'build '], type: 'implementation' },
|
|
256
|
+
{ phrases: ['why ', 'how ', 'what ', 'where '], type: 'question' },
|
|
257
|
+
{ phrases: ['don\'t ', 'do not ', 'never ', 'stop ', 'avoid '], type: 'constraint' },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
for (const entry of leadingPhrases) {
|
|
261
|
+
const match = consumePhrase(lower, text, entry.phrases);
|
|
262
|
+
if (match) return { condition: match, type: entry.type };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const errorIndex = ['error', 'fail', 'crash', 'broken', 'wrong']
|
|
266
|
+
.map((token) => lower.indexOf(token))
|
|
267
|
+
.filter((index) => index >= 0)
|
|
268
|
+
.sort((a, b) => a - b)[0];
|
|
269
|
+
if (Number.isInteger(errorIndex)) {
|
|
270
|
+
return {
|
|
271
|
+
condition: text.slice(errorIndex).replace(/^[:\-\s]+/, '').slice(0, 120).trim() || text.slice(0, 120).trim(),
|
|
272
|
+
type: 'error-report',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { condition: text.slice(0, 120).trim(), type: 'general' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function extractAction(assistantMsg, signal) {
|
|
280
|
+
return signal === 'positive'
|
|
281
|
+
? { type: 'do', description: `Repeat this approach: ${assistantMsg.slice(0, 200).trim()}` }
|
|
282
|
+
: { type: 'avoid', description: `Avoid this approach: ${assistantMsg.slice(0, 200).trim()}` };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function calculateConfidence(window, context) {
|
|
286
|
+
let s = 0.5;
|
|
287
|
+
if (window.length >= 3) s += 0.1;
|
|
288
|
+
if (window.length >= 5) s += 0.1;
|
|
289
|
+
if (context && context.length > 20) s += 0.1;
|
|
290
|
+
if (extractFilePaths(window).length > 0) s += 0.1;
|
|
291
|
+
return Math.min(s, 1.0);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function inferScope(filePaths, toolCalls) {
|
|
295
|
+
if (filePaths.length === 0 && toolCalls.length === 0) return 'global';
|
|
296
|
+
if (filePaths.length <= 2) return 'file-level';
|
|
297
|
+
return 'project-level';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function consumePhrase(lower, original, phrases) {
|
|
301
|
+
for (const phrase of phrases) {
|
|
302
|
+
if (!lower.startsWith(phrase)) continue;
|
|
303
|
+
const value = original.slice(phrase.length).replace(/^[:\-\s]+/, '').slice(0, 120).trim();
|
|
304
|
+
return value || original.slice(0, 120).trim();
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
inferFromSurroundingMessages, createLesson, getRecentLesson,
|
|
311
|
+
searchLessons, getLessonStats, getStatusbarLessonData,
|
|
312
|
+
getLessonsPath, getRecentLessonPath,
|
|
313
|
+
inferStructuredLesson, extractTrigger, extractAction, extractToolCalls,
|
|
314
|
+
extractFilePaths, extractErrors, calculateConfidence, inferScope,
|
|
315
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-action lesson retrieval.
|
|
6
|
+
* Given a tool name + context, returns the top-K most relevant lessons
|
|
7
|
+
* using keyword matching + recency decay + signal weighting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const RECENCY_DECAY_DAYS = 30; // lessons older than this get down-weighted
|
|
11
|
+
|
|
12
|
+
function retrieveRelevantLessons(toolName, actionContext, options = {}) {
|
|
13
|
+
const { maxResults = 5, feedbackDir } = options;
|
|
14
|
+
const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
|
|
15
|
+
const pathMod = require('path');
|
|
16
|
+
const paths = feedbackDir
|
|
17
|
+
? { MEMORY_LOG_PATH: pathMod.join(feedbackDir, 'memory-log.jsonl') }
|
|
18
|
+
: getFeedbackPaths();
|
|
19
|
+
|
|
20
|
+
const memories = readJSONL(paths.MEMORY_LOG_PATH, { maxLines: 200 });
|
|
21
|
+
if (memories.length === 0) return [];
|
|
22
|
+
|
|
23
|
+
// Score each memory against the current action
|
|
24
|
+
const scored = memories.map((mem) => ({
|
|
25
|
+
...mem,
|
|
26
|
+
relevanceScore: scoreRelevance(mem, toolName, actionContext),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Sort by relevance, return top-K
|
|
30
|
+
return scored
|
|
31
|
+
.filter((m) => m.relevanceScore > 0.1)
|
|
32
|
+
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
33
|
+
.slice(0, maxResults)
|
|
34
|
+
.map((m) => ({
|
|
35
|
+
id: m.id,
|
|
36
|
+
title: m.title,
|
|
37
|
+
content: m.content,
|
|
38
|
+
signal: m.tags?.includes('negative') ? 'negative' : 'positive',
|
|
39
|
+
rule: m.structuredRule || null,
|
|
40
|
+
relevanceScore: m.relevanceScore,
|
|
41
|
+
timestamp: m.timestamp,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scoreRelevance(memory, toolName, actionContext) {
|
|
46
|
+
let score = 0;
|
|
47
|
+
|
|
48
|
+
const memText = `${memory.title || ''} ${memory.content || ''} ${(memory.tags || []).join(' ')}`.toLowerCase();
|
|
49
|
+
const contextLower = (actionContext || '').toLowerCase();
|
|
50
|
+
const toolLower = (toolName || '').toLowerCase();
|
|
51
|
+
|
|
52
|
+
// 1. Tool name match (high weight)
|
|
53
|
+
if (memory.metadata?.toolsUsed?.some((t) => t.toLowerCase() === toolLower)) score += 0.4;
|
|
54
|
+
if (memText.includes(toolLower)) score += 0.2;
|
|
55
|
+
|
|
56
|
+
// 2. File path overlap
|
|
57
|
+
const contextPaths = extractPaths(actionContext);
|
|
58
|
+
const memPaths = memory.metadata?.filesInvolved || extractPaths(memText);
|
|
59
|
+
const pathOverlap = contextPaths.filter((p) =>
|
|
60
|
+
memPaths.some((mp) => mp.includes(p) || p.includes(mp)),
|
|
61
|
+
);
|
|
62
|
+
if (pathOverlap.length > 0) score += 0.3;
|
|
63
|
+
|
|
64
|
+
// 3. Keyword overlap (TF-IDF-lite)
|
|
65
|
+
const contextTokens = tokenize(contextLower);
|
|
66
|
+
const memTokens = tokenize(memText);
|
|
67
|
+
const overlap = contextTokens.filter((t) => memTokens.includes(t));
|
|
68
|
+
score += Math.min(overlap.length * 0.05, 0.3);
|
|
69
|
+
|
|
70
|
+
// 4. Signal weighting — negative lessons are more important to surface
|
|
71
|
+
if (memory.tags?.includes('negative')) score += 0.1;
|
|
72
|
+
|
|
73
|
+
// 5. Recency decay
|
|
74
|
+
if (memory.timestamp) {
|
|
75
|
+
const ageMs = Date.now() - new Date(memory.timestamp).getTime();
|
|
76
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
77
|
+
const decay = Math.max(0, 1 - ageDays / RECENCY_DECAY_DAYS);
|
|
78
|
+
score *= 0.5 + 0.5 * decay; // 50% base + 50% recency
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 6. Structured rule bonus — IF/THEN rules are more actionable
|
|
82
|
+
if (memory.structuredRule) score += 0.15;
|
|
83
|
+
|
|
84
|
+
return score;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractPaths(text) {
|
|
88
|
+
return [...new Set((text || '').match(/(?:src\/|scripts\/|tests\/)[^\s,)'"<>]+/g) || [])];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function tokenize(text) {
|
|
92
|
+
return (text || '').split(/[\s.,;:!?()\[\]{}"'`]+/).filter((t) => t.length > 3);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { retrieveRelevantLessons, scoreRelevance };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STALE_THRESHOLD_DAYS = 60;
|
|
4
|
+
const ARCHIVE_THRESHOLD_DAYS = 90;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Add last_triggered and archived columns if they don't exist.
|
|
8
|
+
* Safe to call multiple times — uses IF NOT EXISTS logic via pragma.
|
|
9
|
+
*/
|
|
10
|
+
function migrateSchema(db) {
|
|
11
|
+
const columns = db.pragma('table_info(lessons)').map((c) => c.name);
|
|
12
|
+
if (!columns.includes('last_triggered')) {
|
|
13
|
+
db.exec('ALTER TABLE lessons ADD COLUMN last_triggered TEXT');
|
|
14
|
+
}
|
|
15
|
+
if (!columns.includes('archived')) {
|
|
16
|
+
db.exec('ALTER TABLE lessons ADD COLUMN archived INTEGER DEFAULT 0');
|
|
17
|
+
}
|
|
18
|
+
if (!columns.includes('trigger_count')) {
|
|
19
|
+
db.exec('ALTER TABLE lessons ADD COLUMN trigger_count INTEGER DEFAULT 0');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Record that a lesson was triggered (matched a gate or retrieved for context).
|
|
25
|
+
*/
|
|
26
|
+
function recordTrigger(db, lessonId) {
|
|
27
|
+
migrateSchema(db);
|
|
28
|
+
db.prepare(
|
|
29
|
+
'UPDATE lessons SET last_triggered = ?, trigger_count = COALESCE(trigger_count, 0) + 1 WHERE id = ?'
|
|
30
|
+
).run(new Date().toISOString(), lessonId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Score lesson staleness: 0 = fresh, 1 = completely stale.
|
|
35
|
+
*/
|
|
36
|
+
function stalenessScore(lesson) {
|
|
37
|
+
const ref = lesson.last_triggered || lesson.timestamp;
|
|
38
|
+
if (!ref) return 1;
|
|
39
|
+
const ageDays = (Date.now() - new Date(ref).getTime()) / (1000 * 60 * 60 * 24);
|
|
40
|
+
return Math.min(1, ageDays / ARCHIVE_THRESHOLD_DAYS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get all stale lessons (not triggered in STALE_THRESHOLD_DAYS).
|
|
45
|
+
*/
|
|
46
|
+
function findStaleLessons(db) {
|
|
47
|
+
migrateSchema(db);
|
|
48
|
+
const cutoff = new Date(Date.now() - STALE_THRESHOLD_DAYS * 86400000).toISOString();
|
|
49
|
+
return db.prepare(
|
|
50
|
+
`SELECT * FROM lessons
|
|
51
|
+
WHERE archived = 0 AND pruned = 0
|
|
52
|
+
AND (last_triggered IS NULL OR last_triggered < ?)
|
|
53
|
+
AND timestamp < ?
|
|
54
|
+
ORDER BY COALESCE(last_triggered, timestamp) ASC`
|
|
55
|
+
).all(cutoff, cutoff);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Auto-archive lessons that haven't been triggered in ARCHIVE_THRESHOLD_DAYS.
|
|
60
|
+
* Returns { archived: number, reviewed: number }
|
|
61
|
+
*/
|
|
62
|
+
function autoArchive(db) {
|
|
63
|
+
migrateSchema(db);
|
|
64
|
+
const cutoff = new Date(Date.now() - ARCHIVE_THRESHOLD_DAYS * 86400000).toISOString();
|
|
65
|
+
const result = db.prepare(
|
|
66
|
+
`UPDATE lessons SET archived = 1
|
|
67
|
+
WHERE archived = 0 AND pruned = 0
|
|
68
|
+
AND (last_triggered IS NULL OR last_triggered < ?)
|
|
69
|
+
AND timestamp < ?`
|
|
70
|
+
).run(cutoff, cutoff);
|
|
71
|
+
return { archived: result.changes };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Restore a lesson from archive.
|
|
76
|
+
*/
|
|
77
|
+
function restoreLesson(db, lessonId) {
|
|
78
|
+
migrateSchema(db);
|
|
79
|
+
db.prepare('UPDATE lessons SET archived = 0 WHERE id = ?').run(lessonId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get archived lessons for review.
|
|
84
|
+
*/
|
|
85
|
+
function getArchivedLessons(db) {
|
|
86
|
+
migrateSchema(db);
|
|
87
|
+
return db.prepare(
|
|
88
|
+
'SELECT * FROM lessons WHERE archived = 1 ORDER BY timestamp DESC'
|
|
89
|
+
).all();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate a staleness report for the monthly review digest.
|
|
94
|
+
* Returns { stale: [...], archivable: [...], healthy: number }
|
|
95
|
+
*/
|
|
96
|
+
function stalenessReport(db) {
|
|
97
|
+
migrateSchema(db);
|
|
98
|
+
const staleThresholdDate = new Date(Date.now() - STALE_THRESHOLD_DAYS * 86400000).toISOString();
|
|
99
|
+
const archiveThresholdDate = new Date(Date.now() - ARCHIVE_THRESHOLD_DAYS * 86400000).toISOString();
|
|
100
|
+
|
|
101
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM lessons WHERE archived = 0 AND pruned = 0').get().count;
|
|
102
|
+
const stale = findStaleLessons(db);
|
|
103
|
+
const archivable = stale.filter((l) => {
|
|
104
|
+
const ref = l.last_triggered || l.timestamp;
|
|
105
|
+
return ref && ref < archiveThresholdDate;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
total,
|
|
110
|
+
healthy: total - stale.length,
|
|
111
|
+
stale: stale.map((l) => ({
|
|
112
|
+
id: l.id,
|
|
113
|
+
context: (l.context || '').slice(0, 80),
|
|
114
|
+
importance: l.importance,
|
|
115
|
+
daysSinceActive: Math.round((Date.now() - new Date(l.last_triggered || l.timestamp).getTime()) / 86400000),
|
|
116
|
+
triggerCount: l.trigger_count || 0,
|
|
117
|
+
})),
|
|
118
|
+
archivable: archivable.map((l) => ({
|
|
119
|
+
id: l.id,
|
|
120
|
+
context: (l.context || '').slice(0, 80),
|
|
121
|
+
importance: l.importance,
|
|
122
|
+
})),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
migrateSchema,
|
|
128
|
+
recordTrigger,
|
|
129
|
+
stalenessScore,
|
|
130
|
+
findStaleLessons,
|
|
131
|
+
autoArchive,
|
|
132
|
+
restoreLesson,
|
|
133
|
+
getArchivedLessons,
|
|
134
|
+
stalenessReport,
|
|
135
|
+
STALE_THRESHOLD_DAYS,
|
|
136
|
+
ARCHIVE_THRESHOLD_DAYS,
|
|
137
|
+
};
|