thumbgate 0.9.9
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/rlhf-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 +1483 -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 +286 -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 +1195 -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-314.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 +293 -0
- package/scripts/auto-wire-hooks.js +316 -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 +93 -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 +231 -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 +206 -0
- package/scripts/ensure-repo-bootstrap.js +129 -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 +344 -0
- package/scripts/export-kto-pairs.js +309 -0
- package/scripts/export-training.js +450 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +110 -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 +164 -0
- package/scripts/filesystem-search.js +405 -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 +100 -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 +156 -0
- package/scripts/hybrid-feedback-context.js +675 -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 +310 -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 +384 -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 +190 -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 +713 -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 +459 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rlhf_session_start.sh +32 -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 +282 -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/db/social-analytics.db +0 -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 +107 -0
- package/scripts/social-analytics/load-env.js +46 -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 +92 -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 +183 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
- package/scripts/social-analytics/publish-thumbgate-launch.js +316 -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 +378 -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 +447 -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 +933 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +581 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +197 -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/rlhf-feedback/SKILL.md +49 -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/src/api/server.js +4206 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Feedback Sessions
|
|
6
|
+
*
|
|
7
|
+
* When a user gives thumbs up/down, a session opens. Follow-up messages
|
|
8
|
+
* within a time window enrich the session. When the window closes,
|
|
9
|
+
* the full context is used to infer the lesson.
|
|
10
|
+
*
|
|
11
|
+
* Flow:
|
|
12
|
+
* 1. User gives 👎 → openSession() → returns sessionId
|
|
13
|
+
* 2. User types "you lied about X" → appendToSession(sessionId, message)
|
|
14
|
+
* 3. User types "you forgot Y" → appendToSession(sessionId, message)
|
|
15
|
+
* 4. 60s passes OR next assistant response → finalizeSession(sessionId)
|
|
16
|
+
* 5. Lesson is re-inferred with ALL follow-up context
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { buildStableId } = require('./conversation-context');
|
|
22
|
+
|
|
23
|
+
const SESSION_TIMEOUT_MS = 60000; // 60 seconds
|
|
24
|
+
const MAX_FOLLOWUP_MESSAGES = 20;
|
|
25
|
+
|
|
26
|
+
// In-memory store for active sessions (keyed by sessionId)
|
|
27
|
+
const activeSessions = new Map();
|
|
28
|
+
|
|
29
|
+
function scheduleTimer(callback, timeoutMs) {
|
|
30
|
+
const handle = setTimeout(callback, timeoutMs);
|
|
31
|
+
if (typeof handle.unref === 'function') {
|
|
32
|
+
handle.unref();
|
|
33
|
+
}
|
|
34
|
+
return handle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resetSessionTimer(sessionId, session) {
|
|
38
|
+
if (session.timeoutHandle) {
|
|
39
|
+
clearTimeout(session.timeoutHandle);
|
|
40
|
+
}
|
|
41
|
+
session.timeoutHandle = scheduleTimer(() => {
|
|
42
|
+
if (session.status === 'open') {
|
|
43
|
+
finalizeSession(sessionId);
|
|
44
|
+
}
|
|
45
|
+
}, SESSION_TIMEOUT_MS);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Open a new feedback session after a thumbs up/down signal.
|
|
50
|
+
* The session stays open for follow-up messages.
|
|
51
|
+
*/
|
|
52
|
+
function openSession(feedbackEventId, signal, initialContext) {
|
|
53
|
+
const sessionId = buildStableId('fbs');
|
|
54
|
+
|
|
55
|
+
const session = {
|
|
56
|
+
sessionId,
|
|
57
|
+
feedbackEventId,
|
|
58
|
+
signal,
|
|
59
|
+
initialContext: initialContext || '',
|
|
60
|
+
followUpMessages: [],
|
|
61
|
+
openedAt: new Date().toISOString(),
|
|
62
|
+
status: 'open',
|
|
63
|
+
timeoutHandle: null,
|
|
64
|
+
finalizedAt: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
resetSessionTimer(sessionId, session);
|
|
68
|
+
|
|
69
|
+
activeSessions.set(sessionId, session);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
sessionId,
|
|
73
|
+
status: 'open',
|
|
74
|
+
message: `Feedback session opened. Follow-up messages will be captured for ${SESSION_TIMEOUT_MS / 1000}s.`,
|
|
75
|
+
expiresAt: new Date(Date.now() + SESSION_TIMEOUT_MS).toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Append a follow-up message to an open session.
|
|
81
|
+
* Called when user types additional context after thumbs up/down.
|
|
82
|
+
*/
|
|
83
|
+
function appendToSession(sessionId, message, role = 'user') {
|
|
84
|
+
const session = activeSessions.get(sessionId);
|
|
85
|
+
if (!session) {
|
|
86
|
+
return { status: 'not_found', message: `No active session: ${sessionId}` };
|
|
87
|
+
}
|
|
88
|
+
if (session.status !== 'open') {
|
|
89
|
+
return { status: 'closed', message: `Session already finalized at ${session.finalizedAt}` };
|
|
90
|
+
}
|
|
91
|
+
if (session.followUpMessages.length >= MAX_FOLLOWUP_MESSAGES) {
|
|
92
|
+
return { status: 'full', message: `Session has reached max ${MAX_FOLLOWUP_MESSAGES} messages` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
session.followUpMessages.push({
|
|
96
|
+
role,
|
|
97
|
+
content: (message || '').slice(0, 1000), // Cap per-message
|
|
98
|
+
timestamp: new Date().toISOString(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
resetSessionTimer(sessionId, session);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
status: 'appended',
|
|
105
|
+
messageCount: session.followUpMessages.length,
|
|
106
|
+
sessionId,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Finalize a session — collect all follow-up messages and re-infer the lesson.
|
|
112
|
+
* Called automatically after timeout, or manually when assistant responds.
|
|
113
|
+
*/
|
|
114
|
+
function finalizeSession(sessionId) {
|
|
115
|
+
const session = activeSessions.get(sessionId);
|
|
116
|
+
if (!session) {
|
|
117
|
+
return { status: 'not_found' };
|
|
118
|
+
}
|
|
119
|
+
if (session.status !== 'open') {
|
|
120
|
+
return { status: 'already_finalized', finalizedAt: session.finalizedAt };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Clear timeout
|
|
124
|
+
if (session.timeoutHandle) {
|
|
125
|
+
clearTimeout(session.timeoutHandle);
|
|
126
|
+
session.timeoutHandle = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
session.status = 'finalized';
|
|
130
|
+
session.finalizedAt = new Date().toISOString();
|
|
131
|
+
|
|
132
|
+
// Build the enriched context from follow-up messages
|
|
133
|
+
const followUpText = session.followUpMessages
|
|
134
|
+
.filter(m => m.role === 'user')
|
|
135
|
+
.map(m => m.content)
|
|
136
|
+
.join('\n');
|
|
137
|
+
|
|
138
|
+
const enrichedContext = session.initialContext
|
|
139
|
+
? `${session.initialContext}\n\nFollow-up:\n${followUpText}`
|
|
140
|
+
: followUpText;
|
|
141
|
+
|
|
142
|
+
// Extract specific complaints/corrections from follow-ups
|
|
143
|
+
const complaints = extractComplaints(session.followUpMessages);
|
|
144
|
+
|
|
145
|
+
const result = {
|
|
146
|
+
status: 'finalized',
|
|
147
|
+
sessionId,
|
|
148
|
+
feedbackEventId: session.feedbackEventId,
|
|
149
|
+
signal: session.signal,
|
|
150
|
+
enrichedContext,
|
|
151
|
+
followUpMessages: session.followUpMessages,
|
|
152
|
+
followUpCount: session.followUpMessages.length,
|
|
153
|
+
complaints,
|
|
154
|
+
duration: new Date(session.finalizedAt) - new Date(session.openedAt),
|
|
155
|
+
openedAt: session.openedAt,
|
|
156
|
+
finalizedAt: session.finalizedAt,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Persist to disk for durability
|
|
160
|
+
try {
|
|
161
|
+
persistSession(result);
|
|
162
|
+
} catch (_err) { /* non-critical */ }
|
|
163
|
+
|
|
164
|
+
// Clean up from active sessions after a delay (allow reads)
|
|
165
|
+
scheduleTimer(() => activeSessions.delete(sessionId), 5000);
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract specific complaints/corrections from follow-up messages.
|
|
172
|
+
* These are the actual lesson content.
|
|
173
|
+
*/
|
|
174
|
+
function extractComplaints(messages) {
|
|
175
|
+
const complaints = [];
|
|
176
|
+
|
|
177
|
+
for (const msg of messages) {
|
|
178
|
+
if (msg.role !== 'user') continue;
|
|
179
|
+
const content = msg.content || '';
|
|
180
|
+
|
|
181
|
+
const matches = extractComplaintMatches(content);
|
|
182
|
+
complaints.push(...matches.map((match) => ({
|
|
183
|
+
type: match.type,
|
|
184
|
+
detail: match.detail,
|
|
185
|
+
source: content.slice(0, 100),
|
|
186
|
+
timestamp: msg.timestamp,
|
|
187
|
+
})));
|
|
188
|
+
|
|
189
|
+
if (matches.length === 0 && isGeneralFrustration(content)) {
|
|
190
|
+
complaints.push({
|
|
191
|
+
type: 'general-frustration',
|
|
192
|
+
detail: content.slice(0, 200),
|
|
193
|
+
source: content.slice(0, 100),
|
|
194
|
+
timestamp: msg.timestamp,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return complaints;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function extractComplaintMatches(content) {
|
|
203
|
+
const normalized = String(content || '').trim();
|
|
204
|
+
const lower = normalized.toLowerCase();
|
|
205
|
+
if (!lower) return [];
|
|
206
|
+
|
|
207
|
+
const phraseSets = [
|
|
208
|
+
{ type: 'dishonesty', phrases: ['you lied about ', 'you lying about ', 'you lied to me about '] },
|
|
209
|
+
{ type: 'omission', phrases: ['you didn\'t ', 'you did not ', 'you forgot to ', 'you failed to ', 'you never '] },
|
|
210
|
+
{ type: 'damage', phrases: ['you broke ', 'you ruined ', 'you messed up ', 'you screwed up '] },
|
|
211
|
+
{ type: 'quality', phrases: ['wrong ', 'incorrect ', 'bad ', 'terrible ', 'stupid '] },
|
|
212
|
+
{ type: 'ignored-instruction', phrases: ['i said ', 'i told you ', 'i asked you to '] },
|
|
213
|
+
{ type: 'constraint', phrases: ['don\'t ', 'do not ', 'never ', 'stop '] },
|
|
214
|
+
{ type: 'missed-expectation', phrases: ['should have ', 'should\'ve ', 'why didn\'t you '] },
|
|
215
|
+
{ type: 'performance', phrases: ['too slow', 'took too long', 'waste of time', '5 minutes'] },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const matches = [];
|
|
219
|
+
for (const entry of phraseSets) {
|
|
220
|
+
for (const phrase of entry.phrases) {
|
|
221
|
+
const index = lower.indexOf(phrase);
|
|
222
|
+
if (index === -1) continue;
|
|
223
|
+
const start = index + phrase.length;
|
|
224
|
+
const detail = normalized.slice(start).trim().slice(0, 200);
|
|
225
|
+
matches.push({
|
|
226
|
+
type: entry.type,
|
|
227
|
+
detail: detail || phrase.trim(),
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return matches;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isGeneralFrustration(content) {
|
|
236
|
+
const lower = String(content || '').toLowerCase();
|
|
237
|
+
return lower.includes('!!')
|
|
238
|
+
|| lower.includes('fuck')
|
|
239
|
+
|| lower.includes('shit')
|
|
240
|
+
|| lower.includes('stupid')
|
|
241
|
+
|| lower.includes('terrible')
|
|
242
|
+
|| lower.includes('wrong')
|
|
243
|
+
|| lower.includes('bad')
|
|
244
|
+
|| lower.includes('lied');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get an active session by ID
|
|
249
|
+
*/
|
|
250
|
+
function getSession(sessionId) {
|
|
251
|
+
return activeSessions.get(sessionId) || null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the most recent open session (if any)
|
|
256
|
+
*/
|
|
257
|
+
function getActiveSession() {
|
|
258
|
+
for (const [, session] of activeSessions) {
|
|
259
|
+
if (session.status === 'open') return session;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Persist finalized session to disk
|
|
266
|
+
*/
|
|
267
|
+
function persistSession(result) {
|
|
268
|
+
const { getFeedbackPaths, appendJSONL } = require('./feedback-loop');
|
|
269
|
+
const paths = getFeedbackPaths();
|
|
270
|
+
const sessionLogPath = path.join(path.dirname(paths.FEEDBACK_LOG_PATH), 'feedback-sessions.jsonl');
|
|
271
|
+
appendJSONL(sessionLogPath, result);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
openSession,
|
|
276
|
+
appendToSession,
|
|
277
|
+
finalizeSession,
|
|
278
|
+
getSession,
|
|
279
|
+
getActiveSession,
|
|
280
|
+
extractComplaints,
|
|
281
|
+
SESSION_TIMEOUT_MS,
|
|
282
|
+
MAX_FOLLOWUP_MESSAGES,
|
|
283
|
+
scheduleTimer,
|
|
284
|
+
// For testing
|
|
285
|
+
_activeSessions: activeSessions,
|
|
286
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Feedback → Memory Bridge
|
|
4
|
+
*
|
|
5
|
+
* Converts raw feedback params into a schema-validated memory object
|
|
6
|
+
* suitable for mcp__memory__remember. This is the validation boundary
|
|
7
|
+
* between external feedback signals and MCP memory storage.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* echo '{"signal":"negative","context":"...","whatWentWrong":"...","tags":["testing"]}' | node scripts/feedback-to-memory.js
|
|
11
|
+
* node scripts/feedback-to-memory.js --test
|
|
12
|
+
*
|
|
13
|
+
* Input (stdin JSON):
|
|
14
|
+
* signal: "positive" | "negative"
|
|
15
|
+
* context: string — what the agent was doing
|
|
16
|
+
* whatWentWrong: string — for negative: what failed
|
|
17
|
+
* whatToChange: string — for negative: how to avoid
|
|
18
|
+
* whatWorked: string — for positive: the pattern to repeat
|
|
19
|
+
* tags: string[] — optional domain tags; a fallback domain tag is inferred when omitted
|
|
20
|
+
*
|
|
21
|
+
* Output (stdout JSON):
|
|
22
|
+
* { ok: true, memory: { title, content, category, importance, tags } }
|
|
23
|
+
* { ok: false, reason: string, issues?: string[] }
|
|
24
|
+
*/
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const { resolveFeedbackAction, prepareForStorage } = require('./feedback-schema');
|
|
28
|
+
const { buildClarificationMessage } = require('./feedback-quality');
|
|
29
|
+
|
|
30
|
+
function convertFeedbackToMemory(params) {
|
|
31
|
+
const action = resolveFeedbackAction({
|
|
32
|
+
signal: params.signal,
|
|
33
|
+
context: params.context || '',
|
|
34
|
+
whatWentWrong: params.whatWentWrong,
|
|
35
|
+
whatToChange: params.whatToChange,
|
|
36
|
+
whatWorked: params.whatWorked,
|
|
37
|
+
tags: params.tags || [],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!action || action.type === 'no-action') {
|
|
41
|
+
const clarification = buildClarificationMessage({
|
|
42
|
+
signal: params.signal,
|
|
43
|
+
context: params.context || '',
|
|
44
|
+
whatWentWrong: params.whatWentWrong,
|
|
45
|
+
whatToChange: params.whatToChange,
|
|
46
|
+
whatWorked: params.whatWorked,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: action ? action.reason : 'Unknown action resolution failure',
|
|
51
|
+
...(clarification || {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const prep = prepareForStorage(action.memory);
|
|
56
|
+
if (!prep.ok) {
|
|
57
|
+
return { ok: false, reason: `Schema validation failed: ${prep.issues.join('; ')}`, issues: prep.issues };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ok: true, actionType: action.type, memory: prep.memory };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// CLI: stdin mode
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function runStdin() {
|
|
68
|
+
let input = '';
|
|
69
|
+
process.stdin.setEncoding('utf-8');
|
|
70
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
71
|
+
process.stdin.on('end', () => {
|
|
72
|
+
try {
|
|
73
|
+
const params = JSON.parse(input.trim());
|
|
74
|
+
const result = convertFeedbackToMemory(params);
|
|
75
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
76
|
+
process.exit(result.ok ? 0 : 2);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
process.stdout.write(JSON.stringify({ ok: false, reason: `Parse error: ${err.message}` }, null, 2) + '\n');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Built-in Tests
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function runTests() {
|
|
89
|
+
let passed = 0;
|
|
90
|
+
let failed = 0;
|
|
91
|
+
|
|
92
|
+
function assert(condition, name) {
|
|
93
|
+
if (condition) {
|
|
94
|
+
passed++;
|
|
95
|
+
console.log(` ✅ ${name}`);
|
|
96
|
+
} else {
|
|
97
|
+
failed++;
|
|
98
|
+
console.log(` ❌ ${name}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('\n🧪 feedback-to-memory.js — Tests\n');
|
|
103
|
+
|
|
104
|
+
// Valid negative feedback → memory
|
|
105
|
+
const neg = convertFeedbackToMemory({
|
|
106
|
+
signal: 'negative',
|
|
107
|
+
context: 'Agent claimed fix without test evidence',
|
|
108
|
+
whatWentWrong: 'No tests were run before claiming the bug was fixed',
|
|
109
|
+
whatToChange: 'Always run tests and show output before claiming done',
|
|
110
|
+
tags: ['verification', 'testing'],
|
|
111
|
+
});
|
|
112
|
+
assert(neg.ok === true, 'valid negative → ok');
|
|
113
|
+
assert(neg.actionType === 'store-mistake', 'negative → store-mistake');
|
|
114
|
+
assert(neg.memory.title.startsWith('MISTAKE:'), 'negative → MISTAKE: prefix');
|
|
115
|
+
assert(neg.memory.category === 'error', 'negative → error category');
|
|
116
|
+
assert(neg.memory.tags.includes('verification'), 'preserves domain tags');
|
|
117
|
+
|
|
118
|
+
// Valid positive feedback → memory
|
|
119
|
+
const pos = convertFeedbackToMemory({
|
|
120
|
+
signal: 'positive',
|
|
121
|
+
whatWorked: 'Built schema-validated feedback system with prevention rules',
|
|
122
|
+
tags: ['architecture', 'rlhf'],
|
|
123
|
+
});
|
|
124
|
+
assert(pos.ok === true, 'valid positive → ok');
|
|
125
|
+
assert(pos.actionType === 'store-learning', 'positive → store-learning');
|
|
126
|
+
assert(pos.memory.title.startsWith('SUCCESS:'), 'positive → SUCCESS: prefix');
|
|
127
|
+
assert(pos.memory.category === 'learning', 'positive → learning category');
|
|
128
|
+
|
|
129
|
+
const inferredPos = convertFeedbackToMemory({
|
|
130
|
+
signal: 'positive',
|
|
131
|
+
context: 'ThumbGate automation and Claude statusline repair',
|
|
132
|
+
whatWorked: 'Verified the live ThumbGate version and fixed the stale Claude statusline wiring',
|
|
133
|
+
});
|
|
134
|
+
assert(inferredPos.ok === true, 'specific positive without tags → ok');
|
|
135
|
+
assert(inferredPos.memory.tags.includes('thumbgate'), 'infers a non-generic domain tag');
|
|
136
|
+
|
|
137
|
+
// Bare thumbs down → rejected
|
|
138
|
+
const bare = convertFeedbackToMemory({ signal: 'negative' });
|
|
139
|
+
assert(bare.ok === false, 'bare negative → rejected');
|
|
140
|
+
assert(bare.reason.includes('No context') || bare.reason.includes('cannot'), 'reports missing context');
|
|
141
|
+
|
|
142
|
+
// Bare thumbs up → rejected
|
|
143
|
+
const bareUp = convertFeedbackToMemory({ signal: 'positive' });
|
|
144
|
+
assert(bareUp.ok === false, 'bare positive → rejected');
|
|
145
|
+
|
|
146
|
+
// Unknown signal → rejected
|
|
147
|
+
const unknown = convertFeedbackToMemory({ signal: 'maybe', context: 'test' });
|
|
148
|
+
assert(unknown.ok === false, 'unknown signal → rejected');
|
|
149
|
+
|
|
150
|
+
// Context-only negative → ok
|
|
151
|
+
const ctxNeg = convertFeedbackToMemory({
|
|
152
|
+
signal: 'negative',
|
|
153
|
+
context: 'Showed fake ThumbGate statistics panel to user',
|
|
154
|
+
tags: ['rlhf'],
|
|
155
|
+
});
|
|
156
|
+
assert(ctxNeg.ok === true, 'context-only negative → ok');
|
|
157
|
+
|
|
158
|
+
// Context-only positive → ok
|
|
159
|
+
const ctxPos = convertFeedbackToMemory({
|
|
160
|
+
signal: 'positive',
|
|
161
|
+
context: 'Ran full test suite and showed green output before responding',
|
|
162
|
+
tags: ['verification'],
|
|
163
|
+
});
|
|
164
|
+
assert(ctxPos.ok === true, 'context-only positive → ok');
|
|
165
|
+
|
|
166
|
+
console.log(`\n${'═'.repeat(50)}`);
|
|
167
|
+
console.log(`Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
|
|
168
|
+
console.log(`${'═'.repeat(50)}\n`);
|
|
169
|
+
|
|
170
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Exports & main
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
module.exports = { convertFeedbackToMemory };
|
|
178
|
+
|
|
179
|
+
if (require.main === module) {
|
|
180
|
+
if (process.argv.includes('--test')) {
|
|
181
|
+
runTests();
|
|
182
|
+
} else {
|
|
183
|
+
runStdin();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getAutoGatesPath } = require('./auto-promote-gates');
|
|
6
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LOG = path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
|
|
9
|
+
const NEG = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
|
|
10
|
+
const POS = new Set(['positive', 'positive_strong', 'up', 'thumbs_up']);
|
|
11
|
+
|
|
12
|
+
function parseFeedbackFile(filePath) {
|
|
13
|
+
if (!fs.existsSync(filePath)) return [];
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed) continue;
|
|
18
|
+
try { entries.push(JSON.parse(trimmed)); } catch { /* skip malformed */ }
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function classifySignal(entry) {
|
|
24
|
+
const sig = (entry.signal || entry.feedback || '').toLowerCase();
|
|
25
|
+
if (NEG.has(sig)) return 'negative';
|
|
26
|
+
if (POS.has(sig)) return 'positive';
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalize(ctx) {
|
|
31
|
+
return (ctx || '').replace(/\/Users\/[^\s/]+/g, '~').replace(/:[0-9]+/g, '').toLowerCase().trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const HIGH_RISK_TAGS = new Set(['git-workflow', 'scope-control', 'trust-breach', 'execution-gap', 'regression', 'security']);
|
|
35
|
+
function analyze(entries) {
|
|
36
|
+
let positiveCount = 0, negativeCount = 0;
|
|
37
|
+
const categories = {};
|
|
38
|
+
const toolBuckets = {};
|
|
39
|
+
const contextCounts = {};
|
|
40
|
+
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
const cls = classifySignal(e);
|
|
43
|
+
if (!cls) continue;
|
|
44
|
+
cls === 'positive' ? positiveCount++ : negativeCount++;
|
|
45
|
+
|
|
46
|
+
const cat = e.task_category || e.category || 'uncategorized';
|
|
47
|
+
categories[cat] = categories[cat] || { positive: 0, negative: 0, total: 0 };
|
|
48
|
+
categories[cat][cls]++;
|
|
49
|
+
categories[cat].total++;
|
|
50
|
+
|
|
51
|
+
if (cls === 'negative') {
|
|
52
|
+
const tool = e.tool_name || 'unknown';
|
|
53
|
+
toolBuckets[tool] = (toolBuckets[tool] || 0) + 1;
|
|
54
|
+
const key = normalize(e.context);
|
|
55
|
+
if (key.length > 10) {
|
|
56
|
+
if (!contextCounts[key]) {
|
|
57
|
+
contextCounts[key] = {
|
|
58
|
+
raw: e.context,
|
|
59
|
+
count: 0,
|
|
60
|
+
tool,
|
|
61
|
+
tags: e.tags || [],
|
|
62
|
+
hasHighRisk: (e.tags || []).some(t => HIGH_RISK_TAGS.has(t))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
contextCounts[key].count++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const total = positiveCount + negativeCount;
|
|
71
|
+
const recurringIssues = Object.values(contextCounts)
|
|
72
|
+
.filter(v => v.count >= 2 || (v.count >= 1 && v.hasHighRisk)) // Lower threshold for high-risk
|
|
73
|
+
.sort((a, b) => b.count - a.count)
|
|
74
|
+
.map(v => {
|
|
75
|
+
// Threshold hardening: promote high-risk to block after 2 failures
|
|
76
|
+
const threshold = v.hasHighRisk ? 2 : 4;
|
|
77
|
+
const severity = v.count >= threshold ? 'critical' : v.count >= (threshold - 1) ? 'high' : 'medium';
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
pattern: v.raw.slice(0, 120),
|
|
81
|
+
count: v.count,
|
|
82
|
+
severity,
|
|
83
|
+
hasHighRisk: v.hasHighRisk,
|
|
84
|
+
suggestedRule: `NEVER ${v.raw.slice(0, 80).replace(/CRITICAL ERROR - User frustrated: /i, '')}`,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Auto-Gate Promotion logic
|
|
89
|
+
promoteToGates(recurringIssues);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
generatedAt: new Date().toISOString(),
|
|
93
|
+
totalFeedback: total,
|
|
94
|
+
negativeCount,
|
|
95
|
+
positiveCount,
|
|
96
|
+
negativeRate: total ? `${((negativeCount / total) * 100).toFixed(1)}%` : '0%',
|
|
97
|
+
recurringIssues,
|
|
98
|
+
categoryBreakdown: categories,
|
|
99
|
+
topTools: toolBuckets,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function promoteToGates(recurringIssues) {
|
|
104
|
+
const autoGatePath = getAutoGatesPath();
|
|
105
|
+
const autoGates = { version: 1, gates: [] };
|
|
106
|
+
|
|
107
|
+
for (const issue of recurringIssues) {
|
|
108
|
+
if (issue.severity === 'critical') {
|
|
109
|
+
// Extract key nouns/verbs for pattern matching
|
|
110
|
+
const keywords = issue.pattern
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
113
|
+
.split(/\s+/)
|
|
114
|
+
.filter(w => w.length > 4)
|
|
115
|
+
.slice(0, 3);
|
|
116
|
+
|
|
117
|
+
if (keywords.length >= 2) {
|
|
118
|
+
const pattern = keywords.join('.*');
|
|
119
|
+
autoGates.gates.push({
|
|
120
|
+
id: `auto-${issue.hasHighRisk ? 'hardened' : 'promoted'}-${Date.now().toString(36)}`,
|
|
121
|
+
pattern,
|
|
122
|
+
action: 'block',
|
|
123
|
+
message: `Automatically blocked due to repeated failures: ${issue.suggestedRule}`,
|
|
124
|
+
severity: 'critical',
|
|
125
|
+
source: 'feedback-auto-promotion'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (autoGates.gates.length > 0) {
|
|
132
|
+
fs.mkdirSync(path.dirname(autoGatePath), { recursive: true });
|
|
133
|
+
fs.writeFileSync(autoGatePath, JSON.stringify(autoGates, null, 2));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toRules(report) {
|
|
138
|
+
const lines = ['# Suggested Rules from Feedback Analysis', `# Generated: ${report.generatedAt}`, ''];
|
|
139
|
+
lines.push(`# Negative rate: ${report.negativeRate} (${report.negativeCount}/${report.totalFeedback})`);
|
|
140
|
+
lines.push('');
|
|
141
|
+
for (const issue of report.recurringIssues) {
|
|
142
|
+
lines.push(`- [${issue.severity.toUpperCase()}] (${issue.count}x) ${issue.suggestedRule}`);
|
|
143
|
+
}
|
|
144
|
+
if (!report.recurringIssues.length) lines.push('- No recurring issues detected.');
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (require.main === module) {
|
|
149
|
+
try {
|
|
150
|
+
const logPath = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : DEFAULT_LOG;
|
|
151
|
+
const entries = parseFeedbackFile(logPath);
|
|
152
|
+
const report = analyze(entries);
|
|
153
|
+
if (process.argv.includes('--rules')) {
|
|
154
|
+
console.log(toRules(report));
|
|
155
|
+
} else {
|
|
156
|
+
console.log(JSON.stringify(report, null, 2));
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('Warning:', err.message);
|
|
160
|
+
}
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { parseFeedbackFile, classifySignal, analyze, toRules, normalize };
|