rlhf-feedback-loop 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +308 -0
  4. package/adapters/README.md +8 -0
  5. package/adapters/amp/skills/rlhf-feedback/SKILL.md +20 -0
  6. package/adapters/chatgpt/INSTALL.md +80 -0
  7. package/adapters/chatgpt/openapi.yaml +292 -0
  8. package/adapters/claude/.mcp.json +8 -0
  9. package/adapters/codex/config.toml +4 -0
  10. package/adapters/gemini/function-declarations.json +95 -0
  11. package/adapters/mcp/server-stdio.js +444 -0
  12. package/bin/cli.js +167 -0
  13. package/config/mcp-allowlists.json +29 -0
  14. package/config/policy-bundles/constrained-v1.json +53 -0
  15. package/config/policy-bundles/default-v1.json +80 -0
  16. package/config/rubrics/default-v1.json +52 -0
  17. package/config/subagent-profiles.json +32 -0
  18. package/openapi/openapi.yaml +292 -0
  19. package/package.json +91 -0
  20. package/plugins/amp-skill/INSTALL.md +52 -0
  21. package/plugins/amp-skill/SKILL.md +31 -0
  22. package/plugins/claude-skill/INSTALL.md +55 -0
  23. package/plugins/claude-skill/SKILL.md +46 -0
  24. package/plugins/codex-profile/AGENTS.md +20 -0
  25. package/plugins/codex-profile/INSTALL.md +57 -0
  26. package/plugins/gemini-extension/INSTALL.md +74 -0
  27. package/plugins/gemini-extension/gemini_prompt.txt +10 -0
  28. package/plugins/gemini-extension/tool_contract.json +28 -0
  29. package/scripts/billing.js +471 -0
  30. package/scripts/budget-guard.js +173 -0
  31. package/scripts/code-reasoning.js +307 -0
  32. package/scripts/context-engine.js +547 -0
  33. package/scripts/contextfs.js +513 -0
  34. package/scripts/contract-audit.js +198 -0
  35. package/scripts/dpo-optimizer.js +208 -0
  36. package/scripts/export-dpo-pairs.js +316 -0
  37. package/scripts/export-training.js +448 -0
  38. package/scripts/feedback-attribution.js +313 -0
  39. package/scripts/feedback-inbox-read.js +162 -0
  40. package/scripts/feedback-loop.js +838 -0
  41. package/scripts/feedback-schema.js +300 -0
  42. package/scripts/feedback-to-memory.js +165 -0
  43. package/scripts/feedback-to-rules.js +109 -0
  44. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  45. package/scripts/hybrid-feedback-context.js +676 -0
  46. package/scripts/intent-router.js +164 -0
  47. package/scripts/mcp-policy.js +92 -0
  48. package/scripts/meta-policy.js +194 -0
  49. package/scripts/plan-gate.js +154 -0
  50. package/scripts/prove-adapters.js +364 -0
  51. package/scripts/prove-attribution.js +364 -0
  52. package/scripts/prove-automation.js +393 -0
  53. package/scripts/prove-data-quality.js +219 -0
  54. package/scripts/prove-intelligence.js +256 -0
  55. package/scripts/prove-lancedb.js +370 -0
  56. package/scripts/prove-loop-closure.js +255 -0
  57. package/scripts/prove-rlaif.js +404 -0
  58. package/scripts/prove-subway-upgrades.js +250 -0
  59. package/scripts/prove-training-export.js +324 -0
  60. package/scripts/prove-v2-milestone.js +273 -0
  61. package/scripts/prove-v3-milestone.js +381 -0
  62. package/scripts/rlaif-self-audit.js +123 -0
  63. package/scripts/rubric-engine.js +230 -0
  64. package/scripts/self-heal.js +127 -0
  65. package/scripts/self-healing-check.js +111 -0
  66. package/scripts/skill-quality-tracker.js +284 -0
  67. package/scripts/subagent-profiles.js +79 -0
  68. package/scripts/sync-gh-secrets-from-env.sh +29 -0
  69. package/scripts/thompson-sampling.js +331 -0
  70. package/scripts/train_from_feedback.py +914 -0
  71. package/scripts/validate-feedback.js +580 -0
  72. package/scripts/vector-store.js +100 -0
  73. package/src/api/server.js +497 -0
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Training Data Exporter
4
+ *
5
+ * Exports feedback data in multiple formats for ML training pipelines:
6
+ * - PyTorch JSON (XPRT-01): prompt/chosen/rejected preference pairs
7
+ * - CSV summary (XPRT-02): one row per feedback entry with column headers
8
+ * - Action analysis report (XPRT-03): tool call patterns, success rates, top failures
9
+ * - DPO validation gate (XPRT-04): validateMemoryStructure() prevents bad training pairs
10
+ *
11
+ * Ported and adapted from Subway_RN_Demo exportTrainingData() patterns.
12
+ * PATH: PROJECT_ROOT = path.join(__dirname, '..') — 1 level from scripts/
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const PROJECT_ROOT = path.join(__dirname, '..');
21
+ const FEEDBACK_DIR = process.env.RLHF_FEEDBACK_DIR
22
+ || path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
23
+ const SEQUENCE_WINDOW = 10;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Utility helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ function readJSONL(filePath) {
30
+ if (!fs.existsSync(filePath)) return [];
31
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
32
+ if (!raw) return [];
33
+ return raw
34
+ .split('\n')
35
+ .map((line) => {
36
+ try { return JSON.parse(line); } catch { return null; }
37
+ })
38
+ .filter(Boolean);
39
+ }
40
+
41
+ function ensureDir(dirPath) {
42
+ if (!fs.existsSync(dirPath)) {
43
+ fs.mkdirSync(dirPath, { recursive: true });
44
+ }
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // XPRT-04: validateMemoryStructure
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Gate function: validates a DPO memory pair has the required fields
53
+ * before allowing it into training export.
54
+ *
55
+ * Required: title, content, category, tags (at least 1 non-generic).
56
+ * For DPO pairs specifically: must have a 'chosen' direction (prompt + chosen + rejected).
57
+ *
58
+ * @param {object} memory - DPO memory entry
59
+ * @returns {{ valid: boolean, issues: string[] }}
60
+ */
61
+ function validateMemoryStructure(memory) {
62
+ const issues = [];
63
+
64
+ if (!memory || typeof memory !== 'object') {
65
+ return { valid: false, issues: ['memory must be a non-null object'] };
66
+ }
67
+
68
+ // Required fields
69
+ if (!memory.title || typeof memory.title !== 'string') {
70
+ issues.push('title: required string');
71
+ }
72
+ if (!memory.content || typeof memory.content !== 'string') {
73
+ issues.push('content: required string');
74
+ } else if (memory.content.length < 10) {
75
+ issues.push(`content: too short (${memory.content.length} chars, min 10)`);
76
+ }
77
+ if (!memory.category) {
78
+ issues.push('category: required');
79
+ }
80
+ if (!Array.isArray(memory.tags) || memory.tags.length === 0) {
81
+ issues.push('tags: at least 1 tag required');
82
+ }
83
+
84
+ // DPO-specific: requires 'chosen' field for preference pair export
85
+ // If exporting as DPO pair, must have prompt + chosen + rejected
86
+ if (memory._dpoExport) {
87
+ if (!memory.prompt) issues.push('DPO export: prompt field required');
88
+ if (!memory.chosen) issues.push('DPO export: chosen field required');
89
+ if (!memory.rejected) issues.push('DPO export: rejected field required');
90
+ }
91
+
92
+ return { valid: issues.length === 0, issues };
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // XPRT-01: PyTorch JSON export
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Build preference pairs from feedback log.
101
+ * Matches positive entries as 'chosen' and negative entries as 'rejected'
102
+ * when they share the same context domain/tags.
103
+ *
104
+ * @param {object[]} feedbackEntries - Raw feedback log entries
105
+ * @returns {object[]} Array of { prompt, chosen, rejected } pairs
106
+ */
107
+ function buildPreferencePairs(feedbackEntries) {
108
+ const positive = feedbackEntries.filter(
109
+ (f) => f.signal === 'positive' || f.feedback === 'up'
110
+ );
111
+ const negative = feedbackEntries.filter(
112
+ (f) => f.signal === 'negative' || f.feedback === 'down'
113
+ );
114
+
115
+ const pairs = [];
116
+ const usedNeg = new Set();
117
+
118
+ for (const pos of positive) {
119
+ // Find a negative entry with overlapping tags (domain similarity)
120
+ const posTags = new Set(pos.tags || []);
121
+ let bestNegIdx = -1;
122
+ let bestOverlap = -1;
123
+
124
+ for (let i = 0; i < negative.length; i++) {
125
+ if (usedNeg.has(i)) continue;
126
+ const negTags = new Set(negative[i].tags || []);
127
+ let overlap = 0;
128
+ for (const t of posTags) {
129
+ if (negTags.has(t)) overlap++;
130
+ }
131
+ if (overlap > bestOverlap) {
132
+ bestOverlap = overlap;
133
+ bestNegIdx = i;
134
+ }
135
+ }
136
+
137
+ // If no tag overlap, still pair with any unused negative
138
+ if (bestNegIdx === -1) {
139
+ for (let i = 0; i < negative.length; i++) {
140
+ if (!usedNeg.has(i)) { bestNegIdx = i; break; }
141
+ }
142
+ }
143
+
144
+ if (bestNegIdx >= 0) {
145
+ usedNeg.add(bestNegIdx);
146
+ const neg = negative[bestNegIdx];
147
+ pairs.push({
148
+ prompt: pos.context || pos.richContext?.description || '',
149
+ chosen: (pos.richContext?.outcomeCategory || 'positive') + ': ' + (pos.context || ''),
150
+ rejected: (neg.richContext?.outcomeCategory || 'negative') + ': ' + (neg.context || ''),
151
+ metadata: {
152
+ posId: pos.id,
153
+ negId: neg.id,
154
+ posTags: pos.tags || [],
155
+ negTags: neg.tags || [],
156
+ tagOverlap: bestOverlap,
157
+ },
158
+ });
159
+ }
160
+ }
161
+
162
+ return pairs;
163
+ }
164
+
165
+ /**
166
+ * Export feedback data as PyTorch-compatible JSON.
167
+ *
168
+ * Format: { metadata: { ... }, sequences: [{ X: {...}, y, label }] }
169
+ * Each sequence contains rewardSequence, trend, timeGaps features.
170
+ *
171
+ * @param {string} [feedbackDir] - Override feedback directory
172
+ * @param {string} [outputPath] - Override output path
173
+ * @returns {{ outputPath: string, pairCount: number, sequenceCount: number }}
174
+ */
175
+ function exportPyTorchJSON(feedbackDir, outputPath) {
176
+ const fbDir = feedbackDir || FEEDBACK_DIR;
177
+ const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
178
+ const sequencePath = path.join(fbDir, 'feedback-sequences.jsonl');
179
+ const exportDir = path.join(fbDir, 'training-data');
180
+
181
+ ensureDir(exportDir);
182
+
183
+ const feedbackEntries = readJSONL(feedbackPath);
184
+ const sequences = readJSONL(sequencePath);
185
+ const pairs = buildPreferencePairs(feedbackEntries);
186
+
187
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
188
+ const outPath = outputPath || path.join(exportDir, `training-pytorch-${timestamp}.json`);
189
+
190
+ const pytorchData = {
191
+ metadata: {
192
+ exportDate: new Date().toISOString(),
193
+ format: 'pytorch-dpo',
194
+ pairCount: pairs.length,
195
+ sequenceCount: sequences.length,
196
+ windowSize: SEQUENCE_WINDOW,
197
+ features: ['rewardSequence', 'recentTrend', 'timeGaps'],
198
+ },
199
+ // DPO preference pairs (prompt/chosen/rejected)
200
+ pairs: pairs.map((p) => ({
201
+ prompt: p.prompt,
202
+ chosen: p.chosen,
203
+ rejected: p.rejected,
204
+ })),
205
+ // Raw sequences for LSTM/Transformer training
206
+ sequences: sequences.map((s) => ({
207
+ X: {
208
+ rewardSequence: (s.features && s.features.rewardSequence) || [],
209
+ trend: (s.features && s.features.recentTrend) || 0,
210
+ timeGaps: (s.features && s.features.timeGaps) || [],
211
+ },
212
+ y: s.targetReward,
213
+ label: s.label,
214
+ })),
215
+ };
216
+
217
+ fs.writeFileSync(outPath, JSON.stringify(pytorchData, null, 2));
218
+ return { outputPath: outPath, pairCount: pairs.length, sequenceCount: sequences.length };
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // XPRT-02: CSV summary export
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Escape a CSV field value (wrap in quotes if contains comma/quote/newline).
227
+ *
228
+ * @param {*} value
229
+ * @returns {string}
230
+ */
231
+ function escapeCsvField(value) {
232
+ const str = String(value == null ? '' : value);
233
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
234
+ return '"' + str.replace(/"/g, '""') + '"';
235
+ }
236
+ return str;
237
+ }
238
+
239
+ /**
240
+ * Export feedback entries as CSV with standard headers.
241
+ *
242
+ * Columns: id, timestamp, signal, reward, context, domain, tags, outcomeCategory
243
+ *
244
+ * @param {string} [feedbackDir] - Override feedback directory
245
+ * @param {string} [outputPath] - Override output path
246
+ * @returns {{ outputPath: string, rowCount: number }}
247
+ */
248
+ function exportCSV(feedbackDir, outputPath) {
249
+ const fbDir = feedbackDir || FEEDBACK_DIR;
250
+ const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
251
+ const exportDir = path.join(fbDir, 'training-data');
252
+
253
+ ensureDir(exportDir);
254
+
255
+ const entries = readJSONL(feedbackPath);
256
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
257
+ const outPath = outputPath || path.join(exportDir, `training-summary-${timestamp}.csv`);
258
+
259
+ const headers = ['id', 'timestamp', 'signal', 'reward', 'context', 'domain', 'tags', 'outcomeCategory'];
260
+ const rows = entries.map((e) => [
261
+ e.id || '',
262
+ e.timestamp || '',
263
+ e.signal || e.feedback || '',
264
+ e.reward != null ? e.reward : '',
265
+ e.context || '',
266
+ (e.richContext && e.richContext.domain) || '',
267
+ Array.isArray(e.tags) ? e.tags.join(';') : '',
268
+ (e.richContext && e.richContext.outcomeCategory) || '',
269
+ ].map(escapeCsvField).join(','));
270
+
271
+ const csv = [headers.join(','), ...rows].join('\n');
272
+ fs.writeFileSync(outPath, csv);
273
+
274
+ return { outputPath: outPath, rowCount: entries.length };
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // XPRT-03: Action analysis report
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /**
282
+ * Analyze action patterns from sequences and feedback entries.
283
+ *
284
+ * Produces: tool call frequency, success rates, top failure modes.
285
+ *
286
+ * @param {string} [feedbackDir] - Override feedback directory
287
+ * @param {string} [outputPath] - Override output path
288
+ * @returns {{ outputPath: string, report: object }}
289
+ */
290
+ function exportActionAnalysis(feedbackDir, outputPath) {
291
+ const fbDir = feedbackDir || FEEDBACK_DIR;
292
+ const feedbackPath = path.join(fbDir, 'feedback-log.jsonl');
293
+ const sequencePath = path.join(fbDir, 'feedback-sequences.jsonl');
294
+ const exportDir = path.join(fbDir, 'training-data');
295
+
296
+ ensureDir(exportDir);
297
+
298
+ const entries = readJSONL(feedbackPath);
299
+ const sequences = readJSONL(sequencePath);
300
+
301
+ // Aggregate action patterns from sequences
302
+ const allPatterns = {};
303
+ for (const s of sequences) {
304
+ const patterns = (s.features && s.features.actionPatterns) || {};
305
+ for (const [tag, counts] of Object.entries(patterns)) {
306
+ if (!allPatterns[tag]) {
307
+ allPatterns[tag] = { positive: 0, negative: 0, total: 0 };
308
+ }
309
+ allPatterns[tag].positive += counts.positive || 0;
310
+ allPatterns[tag].negative += counts.negative || 0;
311
+ allPatterns[tag].total += (counts.positive || 0) + (counts.negative || 0);
312
+ }
313
+ }
314
+
315
+ // Compute success rates
316
+ for (const data of Object.values(allPatterns)) {
317
+ data.successRate = data.total > 0
318
+ ? +(data.positive / data.total).toFixed(4)
319
+ : null;
320
+ }
321
+
322
+ // Sort by total occurrences
323
+ const sortedPatterns = Object.entries(allPatterns)
324
+ .sort((a, b) => b[1].total - a[1].total)
325
+ .reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {});
326
+
327
+ // Identify top failure modes (successRate < 0.4 with enough data)
328
+ const topFailureModes = Object.entries(allPatterns)
329
+ .filter(([, v]) => v.successRate !== null && v.successRate < 0.4 && v.total >= 2)
330
+ .sort((a, b) => a[1].successRate - b[1].successRate)
331
+ .slice(0, 5)
332
+ .map(([tag, v]) => ({
333
+ action: tag,
334
+ successRate: v.successRate,
335
+ total: v.total,
336
+ failureCount: v.negative,
337
+ }));
338
+
339
+ // Summary stats from feedback log
340
+ const posCount = entries.filter((e) => e.signal === 'positive' || e.feedback === 'up').length;
341
+ const negCount = entries.filter((e) => e.signal === 'negative' || e.feedback === 'down').length;
342
+
343
+ const report = {
344
+ generatedAt: new Date().toISOString(),
345
+ summary: {
346
+ totalFeedbackEntries: entries.length,
347
+ positiveEntries: posCount,
348
+ negativeEntries: negCount,
349
+ totalSequences: sequences.length,
350
+ uniqueActions: Object.keys(sortedPatterns).length,
351
+ },
352
+ actionPatterns: sortedPatterns,
353
+ topFailureModes,
354
+ recommendations: generateActionRecommendations(sortedPatterns),
355
+ };
356
+
357
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
358
+ const outPath = outputPath || path.join(exportDir, `action-analysis-${timestamp}.json`);
359
+ fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
360
+
361
+ return { outputPath: outPath, report };
362
+ }
363
+
364
+ /**
365
+ * Generate recommendations from action pattern data.
366
+ *
367
+ * @param {object} patterns
368
+ * @returns {string[]}
369
+ */
370
+ function generateActionRecommendations(patterns) {
371
+ const recs = [];
372
+ for (const [tag, data] of Object.entries(patterns)) {
373
+ if (data.successRate !== null && data.total >= 3) {
374
+ if (data.successRate < 0.5) {
375
+ recs.push(`Avoid "${tag}" — ${(data.successRate * 100).toFixed(0)}% success rate across ${data.total} uses.`);
376
+ } else if (data.successRate > 0.8) {
377
+ recs.push(`Expand "${tag}" — ${(data.successRate * 100).toFixed(0)}% success rate across ${data.total} uses.`);
378
+ }
379
+ }
380
+ }
381
+ if (recs.length === 0) recs.push('No actionable recommendations at this time.');
382
+ return recs;
383
+ }
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // CLI
387
+ // ---------------------------------------------------------------------------
388
+
389
+ function printUsage() {
390
+ console.log(`
391
+ Training Data Exporter — Phase 10 (XPRT-01..04)
392
+
393
+ Usage:
394
+ node export-training.js --pytorch [--output <path>]
395
+ node export-training.js --csv [--output <path>]
396
+ node export-training.js --actions [--output <path>]
397
+ node export-training.js --all
398
+
399
+ Options:
400
+ --pytorch Export PyTorch JSON format (XPRT-01)
401
+ --csv Export CSV summary (XPRT-02)
402
+ --actions Export action analysis report (XPRT-03)
403
+ --all Export all formats
404
+ --output Override output file path
405
+ --feedback-dir Override feedback directory
406
+ `);
407
+ }
408
+
409
+ if (require.main === module) {
410
+ const args = process.argv.slice(2).reduce((acc, arg) => {
411
+ if (arg.startsWith('--')) {
412
+ const [k, ...v] = arg.slice(2).split('=');
413
+ acc[k] = v.length ? v.join('=') : true;
414
+ }
415
+ return acc;
416
+ }, {});
417
+
418
+ if (args.help || Object.keys(args).length === 0) {
419
+ printUsage();
420
+ process.exit(0);
421
+ }
422
+
423
+ const fbDir = args['feedback-dir'] || undefined;
424
+ const outPath = args.output || undefined;
425
+
426
+ if (args.pytorch || args.all) {
427
+ const { outputPath, pairCount, sequenceCount } = exportPyTorchJSON(fbDir, outPath);
428
+ console.log(`PyTorch JSON: ${outputPath} (${pairCount} pairs, ${sequenceCount} sequences)`);
429
+ }
430
+ if (args.csv || args.all) {
431
+ const { outputPath, rowCount } = exportCSV(fbDir, outPath);
432
+ console.log(`CSV: ${outputPath} (${rowCount} rows)`);
433
+ }
434
+ if (args.actions || args.all) {
435
+ const { outputPath } = exportActionAnalysis(fbDir, outPath);
436
+ console.log(`Action Analysis: ${outputPath}`);
437
+ }
438
+ }
439
+
440
+ module.exports = {
441
+ exportPyTorchJSON,
442
+ exportCSV,
443
+ exportActionAnalysis,
444
+ buildPreferencePairs,
445
+ validateMemoryStructure,
446
+ escapeCsvField,
447
+ generateActionRecommendations,
448
+ };