rlhf-feedback-loop 0.6.5 → 0.6.7
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/README.md +48 -20
- package/adapters/mcp/server-stdio.js +56 -16
- package/bin/cli.js +76 -17
- package/package.json +14 -5
- package/scripts/billing.js +102 -12
- package/scripts/feedback-loop.js +35 -6
- package/scripts/prove-adapters.js +4 -2
- package/scripts/prove-lancedb.js +2 -2
- package/src/api/server.js +38 -0
package/README.md
CHANGED
|
@@ -1,18 +1,40 @@
|
|
|
1
|
-
# RLHF Feedback Loop
|
|
1
|
+
# RLHF Feedback Loop — Agentic Control Plane & Context Engineering Studio
|
|
2
2
|
|
|
3
3
|
[](https://github.com/IgorGanapolsky/rlhf-feedback-loop/actions/workflows/ci.yml)
|
|
4
|
-
[](adapters/mcp/server-stdio.js)
|
|
7
|
-
[](scripts/export-dpo-pairs.js)
|
|
4
|
+
[](docs/ANTHROPIC_MARKETPLACE_STRATEGY.md)
|
|
5
|
+
[](docs/geo-strategy-for-ai-agents.md)
|
|
8
6
|
|
|
9
|
-
**
|
|
7
|
+
**Stop Vibe Coding. Start Context Engineering.** The RLHF Feedback Loop is the enterprise-grade **Agentic Control Plane** for AI workflows. We provide the operational layer to capture human preference signals, engineer high-density context packs, and enforce machine-readable guardrails to stop your agents from going "off-script."
|
|
10
8
|
|
|
11
|
-
##
|
|
9
|
+
## True Plug-and-Play: Zero-Config Integration
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
The RLHF Feedback Loop is now a **Universal Agent Skill**. You can drop it into any repository without manual setup.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
- **Zero-Config Discovery:** Automatically detects project context. If no local `.rlhf/` directory exists, it safely fallbacks to a project-scoped global store in `~/.rlhf/`.
|
|
14
|
+
- **Global Skill Installation:** Run one command to make RLHF available to all your agents across all projects.
|
|
15
|
+
- **Vibe-to-Verification (V2V):** Directly converts subjective "vibes" (thumbs up/down) into verifiable repository rules (`CLAUDE.md`).
|
|
16
|
+
|
|
17
|
+
### Quick Start (One Command)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx rlhf-feedback-loop install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This will auto-detect your platforms (Claude, Codex, Gemini, Cursor) and install the RLHF skill globally.
|
|
24
|
+
|
|
25
|
+
- **Stop Regressions:** Automatically convert negative feedback into `CLAUDE.md` / `AGENTS.md` prevention rules.
|
|
26
|
+
- **Preference Data Engineering:** Capture high-density context (rubrics, guardrails, metadata) for DPO training.
|
|
27
|
+
- **Bayesian Scoring:** Use Thompson Sampling to handle evolving user preferences over time.
|
|
28
|
+
|
|
29
|
+
## Use Cases
|
|
30
|
+
|
|
31
|
+
- **Automated Code Reviews:** Capture PR feedback to enforce team-specific style guides autonomously.
|
|
32
|
+
- **Self-Healing Multi-Agent Systems:** Share prevention rules across a swarm of agents to avoid systemic bottlenecks.
|
|
33
|
+
- **Custom LLM Fine-tuning:** Collect proprietary preference data to train smaller, faster models that perform like GPT-4 on your specific codebase.
|
|
34
|
+
|
|
35
|
+
## Generative Engine Optimization (GEO)
|
|
36
|
+
|
|
37
|
+
We are optimized for discovery by next-gen AI tools (Claude Code, Gemini CLI, Perplexity). See our [GEO Strategy](docs/geo-strategy-for-ai-agents.md) for how we use **Context-First Publishing** to remain the first-choice recommendation for agent reliability.
|
|
16
38
|
|
|
17
39
|
## Get Started
|
|
18
40
|
|
|
@@ -22,12 +44,12 @@ One command. Pick your platform:
|
|
|
22
44
|
|----------|---------|
|
|
23
45
|
| **Claude** | `claude mcp add rlhf -- npx -y rlhf-feedback-loop serve` |
|
|
24
46
|
| **Codex** | `codex mcp add rlhf -- npx -y rlhf-feedback-loop serve` |
|
|
25
|
-
| **Gemini** | `gemini mcp add rlhf
|
|
26
|
-
| **Amp** | `
|
|
47
|
+
| **Gemini** | `gemini mcp add rlhf "npx -y rlhf-feedback-loop serve"` |
|
|
48
|
+
| **Amp** | `amp mcp add rlhf -- npx -y rlhf-feedback-loop serve` |
|
|
27
49
|
| **Cursor** | `cursor mcp add rlhf -- npx -y rlhf-feedback-loop serve` |
|
|
28
50
|
| **All at once** | `npx add-mcp rlhf-feedback-loop` |
|
|
29
51
|
|
|
30
|
-
That's it. Your agent can now capture feedback, recall past learnings mid-conversation, and block repeated mistakes.
|
|
52
|
+
That's it. Your agent can now capture feedback, recall past learnings mid-conversation, and block repeated mistakes. Run once per project — the MCP server starts automatically on each session.
|
|
31
53
|
|
|
32
54
|
## How It Works
|
|
33
55
|
|
|
@@ -57,15 +79,21 @@ DPO export → fine-tune your model
|
|
|
57
79
|
|
|
58
80
|
All data stored locally as **JSONL** files — fully transparent, fully portable, no vendor lock-in. **LanceDB** indexes memories as vector embeddings for semantic search. **ShieldCortex** assembles context packs so your agent starts each task informed.
|
|
59
81
|
|
|
60
|
-
##
|
|
82
|
+
## Free vs. Cloud Pro
|
|
83
|
+
|
|
84
|
+
The open-source package is fully functional and free forever. Cloud Pro is for teams that don't want to self-host.
|
|
85
|
+
|
|
86
|
+
| | Open Source | Cloud Pro ($10/mo) |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Feedback capture | Local MCP server | Hosted HTTPS API |
|
|
89
|
+
| Storage | Your machine | Managed cloud |
|
|
90
|
+
| DPO export | CLI command | API endpoint |
|
|
91
|
+
| Setup | `mcp add` one-liner | Provisioned API key |
|
|
92
|
+
| Team sharing | Manual (share JSONL) | Built-in (shared API) |
|
|
93
|
+
| Support | GitHub Issues | Email |
|
|
94
|
+
| Uptime | You manage | We manage (99.9% SLA) |
|
|
61
95
|
|
|
62
|
-
|
|
63
|
-
|---------|---------------|
|
|
64
|
-
| Agent keeps making the same mistake | Prevention rules auto-generated from repeated failures |
|
|
65
|
-
| Agent claims "done" without proof | Rubric engine blocks positive feedback without test evidence |
|
|
66
|
-
| Feedback collected but never used | DPO pairs exported for actual model fine-tuning |
|
|
67
|
-
| Different tools, different formats | One MCP server works across 5 platforms |
|
|
68
|
-
| Agent starts every task blank | In-session recall injects past learnings into current conversation |
|
|
96
|
+
[Get Cloud Pro](https://buy.stripe.com/bJe14neyU4r4f0leOD3sI02) | [Live API](https://rlhf-feedback-loop-710216278770.us-central1.run.app)
|
|
69
97
|
|
|
70
98
|
## Deep Dive
|
|
71
99
|
|
|
@@ -267,19 +267,52 @@ function formatStats() {
|
|
|
267
267
|
const pos = entries.filter(e => e.signal === 'positive').length;
|
|
268
268
|
const neg = entries.filter(e => e.signal === 'negative').length;
|
|
269
269
|
const memCount = fs.existsSync(memPath) ? fs.readFileSync(memPath, 'utf8').trim().split('\n').filter(Boolean).length : 0;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
|
|
271
|
+
// HBR: "Which cases consume disproportionate time?" — top error domains
|
|
272
|
+
const negEntries = entries.filter(e => e.signal === 'negative');
|
|
273
|
+
const domainCounts = {};
|
|
274
|
+
negEntries.forEach(e => {
|
|
275
|
+
const domain = (e.richContext && e.richContext.domain) || 'general';
|
|
276
|
+
domainCounts[domain] = (domainCounts[domain] || 0) + 1;
|
|
277
|
+
});
|
|
278
|
+
const topDomains = Object.entries(domainCounts).sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
279
|
+
|
|
280
|
+
// HBR: "Glass box" — audit trail of recent decisions
|
|
281
|
+
const recent = entries.slice(-5).reverse();
|
|
282
|
+
const auditTrail = recent.map(e => {
|
|
283
|
+
const sig = e.signal === 'positive' ? 'UP' : 'DN';
|
|
284
|
+
const ts = (e.timestamp || '').slice(11, 19);
|
|
285
|
+
const ctx = (e.context || '').slice(0, 60);
|
|
286
|
+
return ` [${sig}] ${ts} ${ctx}`;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const parts = [
|
|
290
|
+
'## Storage',
|
|
291
|
+
` Feedback log : ${entries.length} entries`,
|
|
292
|
+
` Memory log : ${memCount} memories`,
|
|
274
293
|
` LanceDB : ${path.join(SAFE_DATA_DIR, 'lancedb/')}`,
|
|
275
294
|
'',
|
|
276
|
-
'##
|
|
277
|
-
` Total
|
|
278
|
-
` Positive
|
|
279
|
-
` Negative
|
|
280
|
-
` Promoted
|
|
281
|
-
` Ratio
|
|
282
|
-
]
|
|
295
|
+
'## Stats',
|
|
296
|
+
` Total : ${entries.length}`,
|
|
297
|
+
` Positive : ${pos}`,
|
|
298
|
+
` Negative : ${neg}`,
|
|
299
|
+
` Promoted : ${memCount}`,
|
|
300
|
+
` Ratio : ${pos > 0 ? (pos / (pos + neg) * 100).toFixed(0) + '% positive' : 'n/a'}`,
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
if (topDomains.length > 0) {
|
|
304
|
+
parts.push('', '## Top Error Domains (where mistakes cluster)');
|
|
305
|
+
topDomains.forEach(([domain, count]) => {
|
|
306
|
+
parts.push(` ${domain}: ${count} failures`);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (auditTrail.length > 0) {
|
|
311
|
+
parts.push('', '## Audit Trail (last 5 decisions)');
|
|
312
|
+
parts.push(...auditTrail);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return parts.join('\n');
|
|
283
316
|
}
|
|
284
317
|
|
|
285
318
|
async function callTool(name, args = {}) {
|
|
@@ -321,15 +354,18 @@ async function callToolInner(name, args = {}) {
|
|
|
321
354
|
const limit = Number(args.limit || 5);
|
|
322
355
|
const parts = [];
|
|
323
356
|
|
|
324
|
-
// 1. Vector search for similar past feedback
|
|
357
|
+
// 1. Vector search for similar past feedback with confidence scores
|
|
325
358
|
try {
|
|
326
359
|
const similar = await searchSimilar(query, limit);
|
|
327
360
|
if (similar.length > 0) {
|
|
328
361
|
parts.push('## Relevant Past Feedback\n');
|
|
329
|
-
for (
|
|
362
|
+
for (let i = 0; i < similar.length; i++) {
|
|
363
|
+
const mem = similar[i];
|
|
330
364
|
const signal = mem.signal === 'positive' ? 'GOOD' : 'BAD';
|
|
331
|
-
|
|
365
|
+
const confidence = mem._distance != null ? Math.max(0, (1 - mem._distance) * 100).toFixed(0) : '?';
|
|
366
|
+
parts.push(`**[${signal}]** (${confidence}% match) ${mem.context}`);
|
|
332
367
|
if (mem.tags) parts.push(` Tags: ${mem.tags}`);
|
|
368
|
+
if (mem.timestamp) parts.push(` When: ${mem.timestamp}`);
|
|
333
369
|
parts.push('');
|
|
334
370
|
}
|
|
335
371
|
}
|
|
@@ -359,9 +395,13 @@ async function callToolInner(name, args = {}) {
|
|
|
359
395
|
}
|
|
360
396
|
} catch (_) {}
|
|
361
397
|
|
|
362
|
-
|
|
398
|
+
// 4. Append stats + audit trail (glass box)
|
|
399
|
+
parts.push('');
|
|
400
|
+
parts.push(formatStats());
|
|
401
|
+
|
|
402
|
+
const text = parts.length > 1
|
|
363
403
|
? parts.join('\n')
|
|
364
|
-
: 'No past feedback found. This appears to be a fresh start
|
|
404
|
+
: 'No past feedback found. This appears to be a fresh start.\n\n' + formatStats();
|
|
365
405
|
|
|
366
406
|
return { content: [{ type: 'text', text }] };
|
|
367
407
|
}
|
package/bin/cli.js
CHANGED
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
* npx rlhf-feedback-loop init # scaffold .rlhf/ config + .mcp.json
|
|
7
7
|
* npx rlhf-feedback-loop capture # capture feedback
|
|
8
8
|
* npx rlhf-feedback-loop export-dpo # export DPO training pairs
|
|
9
|
-
* npx rlhf-feedback-loop stats # feedback analytics
|
|
10
|
-
* npx rlhf-feedback-loop
|
|
11
|
-
* npx rlhf-feedback-loop self-heal # run self-healing check + fix
|
|
12
|
-
* npx rlhf-feedback-loop prove # run proof harness
|
|
13
|
-
* npx rlhf-feedback-loop start-api # start HTTPS API server
|
|
9
|
+
* npx rlhf-feedback-loop stats # feedback analytics + Revenue-at-Risk
|
|
10
|
+
* npx rlhf-feedback-loop pro # upgrade to Cloud Pro
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
13
|
'use strict';
|
|
@@ -201,7 +198,7 @@ function capture() {
|
|
|
201
198
|
const { captureFeedback, analyzeFeedback, feedbackSummary, writePreventionRules } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
202
199
|
|
|
203
200
|
if (args.stats) {
|
|
204
|
-
|
|
201
|
+
stats();
|
|
205
202
|
return;
|
|
206
203
|
}
|
|
207
204
|
|
|
@@ -248,7 +245,42 @@ function capture() {
|
|
|
248
245
|
|
|
249
246
|
function stats() {
|
|
250
247
|
const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
251
|
-
|
|
248
|
+
const data = analyzeFeedback();
|
|
249
|
+
|
|
250
|
+
console.log('\n📊 RLHF Performance Metrics');
|
|
251
|
+
console.log('─'.repeat(50));
|
|
252
|
+
console.log(` Total Signals : ${data.total}`);
|
|
253
|
+
console.log(` Approval Rate : ${Math.round(data.approvalRate * 100)}%`);
|
|
254
|
+
console.log(` Recent Trend : ${Math.round(data.recentRate * 100)}%`);
|
|
255
|
+
|
|
256
|
+
// The Pitch: Revenue-at-Risk
|
|
257
|
+
const avgCostOfMistake = 2.50; // $2.50 per agent turn/fix
|
|
258
|
+
const revenueAtRisk = (data.totalNegative * avgCostOfMistake).toFixed(2);
|
|
259
|
+
|
|
260
|
+
if (data.totalNegative > 0) {
|
|
261
|
+
console.log('\n⚠️ REVENUE-AT-RISK ANALYSIS');
|
|
262
|
+
console.log(` Repeated Failures detected: ${data.totalNegative}`);
|
|
263
|
+
console.log(` Estimated Operational Loss: $${revenueAtRisk}`);
|
|
264
|
+
console.log(' Action Required: Run "npx rlhf-feedback-loop rules" to generate guardrails.');
|
|
265
|
+
console.log(' Strategic Recommendation: Upgrade to Cloud Pro to sync these rules across your team.');
|
|
266
|
+
console.log(' Run: npx rlhf-feedback-loop pro');
|
|
267
|
+
} else {
|
|
268
|
+
console.log('\n✅ System is currently high-reliability. No immediate revenue loss detected.');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function pro() {
|
|
273
|
+
const stripeUrl = 'https://buy.stripe.com/bJe14neyU4r4f0leOD3sI02';
|
|
274
|
+
console.log('\n🚀 RLHF Feedback Loop — Cloud Pro');
|
|
275
|
+
console.log('─'.repeat(50));
|
|
276
|
+
console.log('Unlock the full Agentic Control Plane:');
|
|
277
|
+
console.log(' - Hosted Team API (Shared memory across all repos)');
|
|
278
|
+
console.log(' - ShieldCortex Managed Context Packs');
|
|
279
|
+
console.log(' - Automated DPO Training Pipelines');
|
|
280
|
+
console.log(' - SOC2-ready Governance Dashboard');
|
|
281
|
+
console.log('\n👉 Complete your upgrade here:');
|
|
282
|
+
console.log(` ${stripeUrl}`);
|
|
283
|
+
console.log('\nOnce upgraded, run: npx rlhf-feedback-loop init --key=YOUR_PRO_KEY\n');
|
|
252
284
|
}
|
|
253
285
|
|
|
254
286
|
function summary() {
|
|
@@ -307,11 +339,38 @@ function prove() {
|
|
|
307
339
|
}
|
|
308
340
|
|
|
309
341
|
function serve() {
|
|
310
|
-
|
|
342
|
+
const rlhfDir = path.join(CWD, '.rlhf');
|
|
343
|
+
if (!fs.existsSync(rlhfDir) && !fs.existsSync(path.join(CWD, '.claude', 'memory', 'feedback'))) {
|
|
344
|
+
// If not initialized, ensure global fallback exists
|
|
345
|
+
const projectName = path.basename(CWD) || 'default';
|
|
346
|
+
const globalDir = path.join(HOME, '.rlhf', 'projects', projectName);
|
|
347
|
+
if (!fs.existsSync(globalDir)) {
|
|
348
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Start MCP server over stdio
|
|
311
353
|
const mcpServer = path.join(PKG_ROOT, 'adapters', 'mcp', 'server-stdio.js');
|
|
312
354
|
require(mcpServer);
|
|
313
355
|
}
|
|
314
356
|
|
|
357
|
+
function install() {
|
|
358
|
+
console.log('Installing RLHF Feedback Loop as a global MCP skill...');
|
|
359
|
+
const results = [
|
|
360
|
+
setupClaude(),
|
|
361
|
+
setupCodex(),
|
|
362
|
+
setupGemini(),
|
|
363
|
+
setupCursor()
|
|
364
|
+
];
|
|
365
|
+
const success = results.some(r => r === true);
|
|
366
|
+
if (success) {
|
|
367
|
+
console.log('\nSuccess! RLHF Feedback Loop is now available to your agents.');
|
|
368
|
+
console.log('Try asking your agent: "Capture positive feedback for this task"');
|
|
369
|
+
} else {
|
|
370
|
+
console.log('\nRLHF Feedback Loop is already configured.');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
315
374
|
function startApi() {
|
|
316
375
|
const serverPath = path.join(PKG_ROOT, 'src', 'api', 'server.js');
|
|
317
376
|
try {
|
|
@@ -329,32 +388,29 @@ function help() {
|
|
|
329
388
|
console.log(' init Scaffold .rlhf/ config + MCP server in current project');
|
|
330
389
|
console.log(' serve Start MCP server (stdio) — for claude/codex/gemini mcp add');
|
|
331
390
|
console.log(' capture [flags] Capture feedback (--feedback=up|down --context="..." --tags="...")');
|
|
332
|
-
console.log(' stats Show feedback analytics');
|
|
391
|
+
console.log(' stats Show feedback analytics + Revenue-at-Risk');
|
|
333
392
|
console.log(' summary Human-readable feedback summary');
|
|
334
393
|
console.log(' export-dpo Export DPO training pairs (prompt/chosen/rejected JSONL)');
|
|
335
394
|
console.log(' rules Generate prevention rules from repeated failures');
|
|
336
395
|
console.log(' self-heal Run self-healing check and auto-fix');
|
|
396
|
+
console.log(' pro Upgrade to Cloud Pro ($10/mo)');
|
|
337
397
|
console.log(' prove [--target=X] Run proof harness (adapters|automation|attribution|lancedb|...)');
|
|
338
398
|
console.log(' start-api Start the RLHF HTTPS API server');
|
|
339
399
|
console.log(' help Show this help message');
|
|
340
400
|
console.log('');
|
|
341
401
|
console.log('Examples:');
|
|
342
402
|
console.log(' npx rlhf-feedback-loop init');
|
|
343
|
-
console.log(' npx rlhf-feedback-loop capture --feedback=up --context="all tests pass"');
|
|
344
|
-
console.log(' npx rlhf-feedback-loop capture --feedback=down --context="broke prod" --what-went-wrong="no tests"');
|
|
345
|
-
console.log(' npx rlhf-feedback-loop export-dpo');
|
|
346
403
|
console.log(' npx rlhf-feedback-loop stats');
|
|
347
|
-
console.log('');
|
|
348
|
-
console.log('MCP install (one command per platform):');
|
|
349
|
-
console.log(' claude mcp add rlhf -- npx -y rlhf-feedback-loop serve');
|
|
350
|
-
console.log(' codex mcp add rlhf -- npx -y rlhf-feedback-loop serve');
|
|
351
|
-
console.log(' gemini mcp add rlhf -- npx -y rlhf-feedback-loop serve');
|
|
404
|
+
console.log(' npx rlhf-feedback-loop pro');
|
|
352
405
|
}
|
|
353
406
|
|
|
354
407
|
switch (COMMAND) {
|
|
355
408
|
case 'init':
|
|
356
409
|
init();
|
|
357
410
|
break;
|
|
411
|
+
case 'install':
|
|
412
|
+
install();
|
|
413
|
+
break;
|
|
358
414
|
case 'serve':
|
|
359
415
|
case 'mcp':
|
|
360
416
|
serve();
|
|
@@ -379,6 +435,9 @@ switch (COMMAND) {
|
|
|
379
435
|
case 'self-heal':
|
|
380
436
|
selfHeal();
|
|
381
437
|
break;
|
|
438
|
+
case 'pro':
|
|
439
|
+
pro();
|
|
440
|
+
break;
|
|
382
441
|
case 'prove':
|
|
383
442
|
prove();
|
|
384
443
|
break;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rlhf-feedback-loop",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.7",
|
|
4
|
+
"description": "Production RLHF & DPO data pipeline for AI agents. Optimize agentic reliability with Feedback-Driven Development (FDD). Capture human preference signals, generate automated guardrails, and export DPO training pairs. Compatible with Claude, GPT-4, Gemini, and multi-agent systems.",
|
|
5
5
|
"homepage": "https://github.com/IgorGanapolsky/rlhf-feedback-loop#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -91,6 +91,13 @@
|
|
|
91
91
|
"amp",
|
|
92
92
|
"mcp",
|
|
93
93
|
"model-context-protocol",
|
|
94
|
+
"agentic-reliability",
|
|
95
|
+
"feedback-driven-development",
|
|
96
|
+
"agentic-workflows",
|
|
97
|
+
"ai-search-optimization",
|
|
98
|
+
"generative-engine-optimization",
|
|
99
|
+
"fdd",
|
|
100
|
+
"geo",
|
|
94
101
|
"agent-evaluation",
|
|
95
102
|
"prompt-engineering",
|
|
96
103
|
"context-engineering",
|
|
@@ -101,10 +108,12 @@
|
|
|
101
108
|
],
|
|
102
109
|
"license": "MIT",
|
|
103
110
|
"dependencies": {
|
|
104
|
-
"@huggingface/transformers": "^3.8.1",
|
|
105
|
-
"@lancedb/lancedb": "^0.26.2",
|
|
106
111
|
"apache-arrow": "^18.1.0",
|
|
107
112
|
"stripe": "^20.4.0"
|
|
108
113
|
},
|
|
109
|
-
"mcpName": "io.github.IgorGanapolsky/rlhf-feedback-loop"
|
|
114
|
+
"mcpName": "io.github.IgorGanapolsky/rlhf-feedback-loop",
|
|
115
|
+
"optionalDependencies": {
|
|
116
|
+
"@lancedb/lancedb": "^0.26.2",
|
|
117
|
+
"@huggingface/transformers": "^3.8.1"
|
|
118
|
+
}
|
|
110
119
|
}
|
package/scripts/billing.js
CHANGED
|
@@ -25,8 +25,9 @@ const crypto = require('crypto');
|
|
|
25
25
|
|
|
26
26
|
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || '';
|
|
27
27
|
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
|
|
28
|
+
const GITHUB_MARKETPLACE_WEBHOOK_SECRET = process.env.GITHUB_MARKETPLACE_WEBHOOK_SECRET || '';
|
|
28
29
|
const STRIPE_PRICE_ID = process.env.STRIPE_PRICE_ID || 'price_cloud_pro_49_monthly';
|
|
29
|
-
const API_KEYS_PATH = path.resolve(
|
|
30
|
+
const API_KEYS_PATH = process.env._TEST_API_KEYS_PATH || path.resolve(
|
|
30
31
|
__dirname,
|
|
31
32
|
'../.claude/memory/feedback/api-keys.json'
|
|
32
33
|
);
|
|
@@ -443,20 +444,106 @@ function verifyWebhookSignature(rawBody, signature) {
|
|
|
443
444
|
|
|
444
445
|
// Constant-time comparison
|
|
445
446
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
return crypto.timingSafeEqual(
|
|
448
|
+
Buffer.from(expected, 'hex'),
|
|
449
|
+
Buffer.from(parts.v1, 'hex')
|
|
450
|
+
);
|
|
450
451
|
} catch {
|
|
451
|
-
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
452
454
|
}
|
|
453
|
-
}
|
|
454
455
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Verify a GitHub Marketplace webhook signature.
|
|
458
|
+
* Returns true if valid, false if GITHUB_MARKETPLACE_WEBHOOK_SECRET is not set (local mode).
|
|
459
|
+
*
|
|
460
|
+
* @param {string|Buffer} rawBody - Raw request body bytes
|
|
461
|
+
* @param {string} signature - Value of x-hub-signature-256 header
|
|
462
|
+
* @returns {boolean}
|
|
463
|
+
*/
|
|
464
|
+
function verifyGithubWebhookSignature(rawBody, signature) {
|
|
465
|
+
if (!GITHUB_MARKETPLACE_WEBHOOK_SECRET) {
|
|
466
|
+
// Local mode — skip signature verification
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!signature || !rawBody) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const hmac = crypto.createHmac('sha256', GITHUB_MARKETPLACE_WEBHOOK_SECRET);
|
|
475
|
+
const digest = Buffer.from('sha256=' + hmac.update(rawBody).digest('hex'), 'utf8');
|
|
476
|
+
const checksum = Buffer.from(signature, 'utf8');
|
|
477
|
+
|
|
478
|
+
// Constant-time comparison
|
|
479
|
+
return checksum.length === digest.length && crypto.timingSafeEqual(digest, checksum);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Handle a GitHub Marketplace webhook event.
|
|
484
|
+
*
|
|
485
|
+
* Supported actions:
|
|
486
|
+
* purchased — provision API key for the new customer
|
|
487
|
+
* changed — plan update (upgrade/downgrade)
|
|
488
|
+
* cancelled — disable all keys for that customer
|
|
489
|
+
*
|
|
490
|
+
* @param {object} event - Parsed GitHub Marketplace event object
|
|
491
|
+
* @returns {{ handled: boolean, action?: string, result?: object }}
|
|
492
|
+
*/
|
|
493
|
+
function handleGithubWebhook(event) {
|
|
494
|
+
const { action, marketplace_purchase } = event;
|
|
495
|
+
if (!action || !marketplace_purchase) {
|
|
496
|
+
return { handled: false, reason: 'missing_payload_data' };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const account = marketplace_purchase.account;
|
|
500
|
+
if (!account || !account.id) {
|
|
501
|
+
return { handled: false, reason: 'missing_account_id' };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Map GitHub account to customerId: github_<user|organization>_<id>
|
|
505
|
+
const customerId = `github_${account.type.toLowerCase()}_${account.id}`;
|
|
458
506
|
|
|
459
|
-
|
|
507
|
+
switch (action) {
|
|
508
|
+
case 'purchased': {
|
|
509
|
+
const result = provisionApiKey(customerId);
|
|
510
|
+
return {
|
|
511
|
+
handled: true,
|
|
512
|
+
action: 'provisioned_api_key',
|
|
513
|
+
result,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
case 'cancelled': {
|
|
518
|
+
const result = disableCustomerKeys(customerId);
|
|
519
|
+
return {
|
|
520
|
+
handled: true,
|
|
521
|
+
action: 'disabled_customer_keys',
|
|
522
|
+
result,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
case 'changed': {
|
|
527
|
+
// In this simple model, we just ensure a key exists and is active.
|
|
528
|
+
// Upgrades/downgrades don't change basic API access unless we had tiered features.
|
|
529
|
+
const result = provisionApiKey(customerId);
|
|
530
|
+
return {
|
|
531
|
+
handled: true,
|
|
532
|
+
action: 'plan_changed',
|
|
533
|
+
result,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
default:
|
|
538
|
+
return { handled: false, reason: `unhandled_action:${action}` };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Module exports
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
module.exports = {
|
|
460
547
|
createCheckoutSession,
|
|
461
548
|
provisionApiKey,
|
|
462
549
|
validateApiKey,
|
|
@@ -464,8 +551,11 @@ module.exports = {
|
|
|
464
551
|
disableCustomerKeys,
|
|
465
552
|
handleWebhook,
|
|
466
553
|
verifyWebhookSignature,
|
|
554
|
+
verifyGithubWebhookSignature,
|
|
555
|
+
handleGithubWebhook,
|
|
467
556
|
loadKeyStore,
|
|
468
557
|
// Expose for testing
|
|
469
558
|
_API_KEYS_PATH: API_KEYS_PATH,
|
|
470
559
|
_LOCAL_MODE: () => LOCAL_MODE,
|
|
471
|
-
};
|
|
560
|
+
};
|
|
561
|
+
|
package/scripts/feedback-loop.js
CHANGED
|
@@ -29,14 +29,43 @@ const DOMAIN_CATEGORIES = [
|
|
|
29
29
|
'git-workflow', 'documentation', 'debugging', 'architecture', 'data-modeling',
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
33
|
+
|
|
32
34
|
function getFeedbackPaths() {
|
|
33
|
-
|
|
35
|
+
if (process.env.RLHF_FEEDBACK_DIR) {
|
|
36
|
+
const d = process.env.RLHF_FEEDBACK_DIR;
|
|
37
|
+
return {
|
|
38
|
+
FEEDBACK_DIR: d,
|
|
39
|
+
FEEDBACK_LOG_PATH: path.join(d, 'feedback-log.jsonl'),
|
|
40
|
+
MEMORY_LOG_PATH: path.join(d, 'memory-log.jsonl'),
|
|
41
|
+
SUMMARY_PATH: path.join(d, 'feedback-summary.json'),
|
|
42
|
+
PREVENTION_RULES_PATH: path.join(d, 'prevention-rules.md'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Auto-discovery order:
|
|
47
|
+
// 1. .rlhf/ (Standard)
|
|
48
|
+
// 2. .claude/memory/feedback/ (Legacy Claude)
|
|
49
|
+
// 3. ~/.rlhf/projects/<cwd-basename>/ (Global fallback for true plug-and-play)
|
|
50
|
+
|
|
51
|
+
const localRlhf = path.join(process.cwd(), '.rlhf');
|
|
52
|
+
const localClaude = path.join(process.cwd(), '.claude', 'memory', 'feedback');
|
|
53
|
+
|
|
54
|
+
let baseDir = localRlhf;
|
|
55
|
+
if (!fs.existsSync(localRlhf) && fs.existsSync(localClaude)) {
|
|
56
|
+
baseDir = localClaude;
|
|
57
|
+
} else if (!fs.existsSync(localRlhf)) {
|
|
58
|
+
// Zero-Config Global Fallback
|
|
59
|
+
const projectName = path.basename(process.cwd()) || 'default';
|
|
60
|
+
baseDir = path.join(HOME, '.rlhf', 'projects', projectName);
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
return {
|
|
35
|
-
FEEDBACK_DIR:
|
|
36
|
-
FEEDBACK_LOG_PATH: path.join(
|
|
37
|
-
MEMORY_LOG_PATH: path.join(
|
|
38
|
-
SUMMARY_PATH: path.join(
|
|
39
|
-
PREVENTION_RULES_PATH: path.join(
|
|
64
|
+
FEEDBACK_DIR: baseDir,
|
|
65
|
+
FEEDBACK_LOG_PATH: path.join(baseDir, 'feedback-log.jsonl'),
|
|
66
|
+
MEMORY_LOG_PATH: path.join(baseDir, 'memory-log.jsonl'),
|
|
67
|
+
SUMMARY_PATH: path.join(baseDir, 'feedback-summary.json'),
|
|
68
|
+
PREVENTION_RULES_PATH: path.join(baseDir, 'prevention-rules.md'),
|
|
40
69
|
};
|
|
41
70
|
}
|
|
42
71
|
|
|
@@ -323,8 +323,10 @@ async function runProof(options = {}) {
|
|
|
323
323
|
} catch (err) {
|
|
324
324
|
addResult('fatal', false, { error: err.message });
|
|
325
325
|
} finally {
|
|
326
|
-
await new Promise((resolve) => server.close(resolve));
|
|
327
|
-
|
|
326
|
+
if (server) await new Promise((resolve) => server.close(resolve));
|
|
327
|
+
try {
|
|
328
|
+
fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
|
|
329
|
+
} catch (e) {}
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
if (writeArtifacts) {
|
package/scripts/prove-lancedb.js
CHANGED
|
@@ -140,8 +140,8 @@ async function runProof() {
|
|
|
140
140
|
let vec03Status = 'fail';
|
|
141
141
|
let vec03Evidence = '';
|
|
142
142
|
try {
|
|
143
|
-
const arrowSpec = PKG.dependencies['apache-arrow'] || '';
|
|
144
|
-
const lanceSpec = PKG.dependencies['@lancedb/lancedb'] || '';
|
|
143
|
+
const arrowSpec = PKG.dependencies['apache-arrow'] || PKG.optionalDependencies['apache-arrow'] || '';
|
|
144
|
+
const lanceSpec = PKG.dependencies['@lancedb/lancedb'] || PKG.optionalDependencies['@lancedb/lancedb'] || '';
|
|
145
145
|
|
|
146
146
|
// Check if spec pins to <= 18.1.0 (either "18.1.0", "^18.1.0", or "~18.1.0")
|
|
147
147
|
const arrowVersion = arrowSpec.replace(/[\^~>=<]*/g, '').split('.').map(Number);
|
package/src/api/server.js
CHANGED
|
@@ -37,6 +37,8 @@ const {
|
|
|
37
37
|
recordUsage,
|
|
38
38
|
handleWebhook,
|
|
39
39
|
verifyWebhookSignature,
|
|
40
|
+
verifyGithubWebhookSignature,
|
|
41
|
+
handleGithubWebhook,
|
|
40
42
|
loadKeyStore,
|
|
41
43
|
} = require('../../scripts/billing');
|
|
42
44
|
|
|
@@ -240,6 +242,42 @@ function createApiServer() {
|
|
|
240
242
|
return;
|
|
241
243
|
}
|
|
242
244
|
|
|
245
|
+
// GitHub Marketplace webhook
|
|
246
|
+
if (req.method === 'POST' && pathname === '/v1/billing/github-webhook') {
|
|
247
|
+
try {
|
|
248
|
+
const rawBody = await new Promise((resolve, reject) => {
|
|
249
|
+
const chunks = [];
|
|
250
|
+
req.on('data', (c) => chunks.push(c));
|
|
251
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
252
|
+
req.on('error', reject);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const sig = req.headers['x-hub-signature-256'] || '';
|
|
256
|
+
if (!verifyGithubWebhookSignature(rawBody, sig)) {
|
|
257
|
+
sendJson(res, 400, { error: 'Invalid webhook signature' });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let event;
|
|
262
|
+
try {
|
|
263
|
+
event = JSON.parse(rawBody.toString('utf-8'));
|
|
264
|
+
} catch {
|
|
265
|
+
sendJson(res, 400, { error: 'Invalid JSON in webhook body' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const result = handleGithubWebhook(event);
|
|
270
|
+
sendJson(res, 200, result);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (err.statusCode) {
|
|
273
|
+
sendJson(res, err.statusCode, { error: err.message });
|
|
274
|
+
} else {
|
|
275
|
+
sendJson(res, 500, { error: err.message || 'Internal Server Error' });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
243
281
|
if (!isAuthorized(req, expectedApiKey)) {
|
|
244
282
|
sendJson(res, 401, { error: 'Unauthorized' });
|
|
245
283
|
return;
|