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,364 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { startServer } = require('../src/api/server');
6
+ const { handleRequest } = require('../adapters/mcp/server-stdio');
7
+ const { validateSubagentProfiles, listSubagentProfiles } = require('./subagent-profiles');
8
+ const { getAllowedTools } = require('./mcp-policy');
9
+
10
+ const ROOT = path.join(__dirname, '..');
11
+ const DEFAULT_PROOF_DIR = path.join(ROOT, 'proof', 'compatibility');
12
+
13
+ function ensureDir(dirPath) {
14
+ if (!fs.existsSync(dirPath)) {
15
+ fs.mkdirSync(dirPath, { recursive: true });
16
+ }
17
+ }
18
+
19
+ function check(condition, message) {
20
+ if (!condition) {
21
+ throw new Error(message);
22
+ }
23
+ }
24
+
25
+ function escapeRegExp(value) {
26
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
+ }
28
+
29
+ async function runProof(options = {}) {
30
+ const proofDir = options.proofDir || process.env.RLHF_PROOF_DIR || DEFAULT_PROOF_DIR;
31
+ const writeArtifacts = options.writeArtifacts !== false;
32
+ const proofPort = options.port ?? 0;
33
+
34
+ if (writeArtifacts) {
35
+ ensureDir(proofDir);
36
+ }
37
+
38
+ const tmpFeedbackDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rlhf-proof-'));
39
+ process.env.RLHF_FEEDBACK_DIR = tmpFeedbackDir;
40
+ process.env.RLHF_API_KEY = 'proof-key';
41
+ process.env.RLHF_MCP_PROFILE = 'default';
42
+
43
+ const report = {
44
+ generatedAt: new Date().toISOString(),
45
+ checks: [],
46
+ summary: { passed: 0, failed: 0 },
47
+ };
48
+
49
+ function addResult(name, passed, details) {
50
+ report.checks.push({ name, passed, details });
51
+ if (passed) report.summary.passed += 1;
52
+ else report.summary.failed += 1;
53
+ }
54
+
55
+ const { server, port } = await startServer({ port: proofPort });
56
+
57
+ try {
58
+ // API checks
59
+ {
60
+ const res = await fetch(`http://localhost:${port}/healthz`, {
61
+ headers: { Authorization: 'Bearer proof-key' },
62
+ });
63
+ check(res.status === 200, `health expected 200, got ${res.status}`);
64
+ addResult('api.healthz', true, { status: res.status });
65
+ }
66
+
67
+ {
68
+ const res = await fetch(`http://localhost:${port}/v1/feedback/stats`);
69
+ check(res.status === 401, `stats unauthorized expected 401, got ${res.status}`);
70
+ addResult('api.auth.required', true, { status: res.status });
71
+ }
72
+
73
+ {
74
+ const res = await fetch(`http://localhost:${port}/v1/intents/catalog?mcpProfile=locked`, {
75
+ headers: { Authorization: 'Bearer proof-key' },
76
+ });
77
+ check(res.status === 200, `intents catalog expected 200, got ${res.status}`);
78
+ const body = await res.json();
79
+ check(Array.isArray(body.intents), 'intents catalog should return intents array');
80
+ addResult('api.intents.catalog', true, { intents: body.intents.length, profile: body.mcpProfile });
81
+ }
82
+
83
+ {
84
+ const res = await fetch(`http://localhost:${port}/v1/intents/plan`, {
85
+ method: 'POST',
86
+ headers: {
87
+ Authorization: 'Bearer proof-key',
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ body: JSON.stringify({
91
+ intentId: 'publish_dpo_training_data',
92
+ mcpProfile: 'default',
93
+ approved: false,
94
+ }),
95
+ });
96
+ check(res.status === 200, `intent plan expected 200, got ${res.status}`);
97
+ const body = await res.json();
98
+ check(body.status === 'checkpoint_required', 'intent plan should require checkpoint when not approved');
99
+ addResult('api.intents.plan', true, { status: body.status, risk: body.intent.risk });
100
+ }
101
+
102
+ {
103
+ const res = await fetch(`http://localhost:${port}/v1/feedback/capture`, {
104
+ method: 'POST',
105
+ headers: {
106
+ Authorization: 'Bearer proof-key',
107
+ 'Content-Type': 'application/json',
108
+ },
109
+ body: JSON.stringify({
110
+ signal: 'up',
111
+ context: 'adapter proof harness',
112
+ whatWorked: 'end-to-end verification flow',
113
+ tags: ['verification', 'proof'],
114
+ }),
115
+ });
116
+ check(res.status === 200, `capture expected 200, got ${res.status}`);
117
+ const body = await res.json();
118
+ check(body.accepted === true, 'capture should be accepted');
119
+ addResult('api.capture_feedback', true, { accepted: body.accepted });
120
+ }
121
+
122
+ {
123
+ const res = await fetch(`http://localhost:${port}/v1/feedback/capture`, {
124
+ method: 'POST',
125
+ headers: {
126
+ Authorization: 'Bearer proof-key',
127
+ 'Content-Type': 'application/json',
128
+ },
129
+ body: JSON.stringify({
130
+ signal: 'up',
131
+ context: 'unsafe approval attempt',
132
+ whatWorked: 'claimed success',
133
+ rubricScores: [
134
+ { criterion: 'verification_evidence', score: 5, judge: 'judge-a' },
135
+ { criterion: 'verification_evidence', score: 2, judge: 'judge-b', evidence: 'missing logs' },
136
+ ],
137
+ guardrails: { testsPassed: false, pathSafety: true, budgetCompliant: true },
138
+ tags: ['verification'],
139
+ }),
140
+ });
141
+ check(res.status === 422, `rubric-gated capture expected 422, got ${res.status}`);
142
+ const body = await res.json();
143
+ check(body.accepted === false, 'rubric-gated capture should not be accepted');
144
+ addResult('api.capture_feedback.rubric_gate', true, { accepted: body.accepted });
145
+ }
146
+
147
+ {
148
+ const construct = await fetch(`http://localhost:${port}/v1/context/construct`, {
149
+ method: 'POST',
150
+ headers: {
151
+ Authorization: 'Bearer proof-key',
152
+ 'Content-Type': 'application/json',
153
+ },
154
+ body: JSON.stringify({ query: 'verification', maxItems: 5, maxChars: 5000 }),
155
+ });
156
+ check(construct.status === 200, `context construct expected 200, got ${construct.status}`);
157
+ const pack = await construct.json();
158
+ check(Boolean(pack.packId), 'context packId missing');
159
+ addResult('api.context.construct', true, { packId: pack.packId, items: pack.items.length });
160
+
161
+ const evaluate = await fetch(`http://localhost:${port}/v1/context/evaluate`, {
162
+ method: 'POST',
163
+ headers: {
164
+ Authorization: 'Bearer proof-key',
165
+ 'Content-Type': 'application/json',
166
+ },
167
+ body: JSON.stringify({
168
+ packId: pack.packId,
169
+ outcome: 'useful',
170
+ signal: 'positive',
171
+ rubricScores: [
172
+ { criterion: 'correctness', score: 4, evidence: 'tests pass', judge: 'judge-a' },
173
+ { criterion: 'verification_evidence', score: 4, evidence: 'logs attached', judge: 'judge-a' },
174
+ ],
175
+ guardrails: { testsPassed: true, pathSafety: true, budgetCompliant: true },
176
+ }),
177
+ });
178
+ check(evaluate.status === 200, `context evaluate expected 200, got ${evaluate.status}`);
179
+ const evalBody = await evaluate.json();
180
+ check(Boolean(evalBody.rubricEvaluation), 'context evaluate should include rubricEvaluation');
181
+ addResult('api.context.evaluate', true, { status: evaluate.status, rubric: evalBody.rubricEvaluation.rubricId });
182
+ }
183
+
184
+ // MCP checks
185
+ {
186
+ const init = await handleRequest({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
187
+ check(Boolean(init.serverInfo && init.serverInfo.name), 'mcp initialize missing serverInfo');
188
+ addResult('mcp.initialize', true, { server: init.serverInfo.name });
189
+ }
190
+
191
+ {
192
+ const list = await handleRequest({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
193
+ check(Array.isArray(list.tools) && list.tools.length > 0, 'mcp tools/list empty');
194
+ addResult('mcp.tools.list', true, { tools: list.tools.length });
195
+ }
196
+
197
+ {
198
+ const call = await handleRequest({
199
+ jsonrpc: '2.0',
200
+ id: 3,
201
+ method: 'tools/call',
202
+ params: {
203
+ name: 'feedback_summary',
204
+ arguments: { recent: 5 },
205
+ },
206
+ });
207
+ check(Array.isArray(call.content), 'mcp feedback_summary should return content[]');
208
+ addResult('mcp.tools.call.feedback_summary', true, { contentLength: call.content[0].text.length });
209
+ }
210
+
211
+ {
212
+ const call = await handleRequest({
213
+ jsonrpc: '2.0',
214
+ id: 31,
215
+ method: 'tools/call',
216
+ params: {
217
+ name: 'plan_intent',
218
+ arguments: {
219
+ intentId: 'publish_dpo_training_data',
220
+ mcpProfile: 'default',
221
+ },
222
+ },
223
+ });
224
+ const plan = JSON.parse(call.content[0].text);
225
+ check(plan.status === 'checkpoint_required', 'mcp plan_intent should return checkpoint_required by default');
226
+ addResult('mcp.tools.call.plan_intent', true, { status: plan.status });
227
+ }
228
+
229
+ {
230
+ const call = await handleRequest({
231
+ jsonrpc: '2.0',
232
+ id: 32,
233
+ method: 'tools/call',
234
+ params: {
235
+ name: 'capture_feedback',
236
+ arguments: {
237
+ signal: 'up',
238
+ context: 'unsafe approval attempt',
239
+ whatWorked: 'claimed success',
240
+ rubricScores: [
241
+ { criterion: 'verification_evidence', score: 5, judge: 'judge-a' },
242
+ { criterion: 'verification_evidence', score: 2, judge: 'judge-b', evidence: 'missing logs' },
243
+ ],
244
+ guardrails: { testsPassed: false, pathSafety: true, budgetCompliant: true },
245
+ },
246
+ },
247
+ });
248
+ const payload = JSON.parse(call.content[0].text);
249
+ check(payload.accepted === false, 'mcp capture_feedback should apply rubric gating');
250
+ addResult('mcp.tools.call.capture_feedback.rubric_gate', true, { accepted: payload.accepted });
251
+ }
252
+
253
+ {
254
+ process.env.RLHF_MCP_PROFILE = 'locked';
255
+ let denied = false;
256
+ try {
257
+ await handleRequest({
258
+ jsonrpc: '2.0',
259
+ id: 4,
260
+ method: 'tools/call',
261
+ params: {
262
+ name: 'capture_feedback',
263
+ arguments: { signal: 'up', context: 'should be denied' },
264
+ },
265
+ });
266
+ } catch (err) {
267
+ denied = /not allowed/i.test(String(err.message));
268
+ }
269
+ process.env.RLHF_MCP_PROFILE = 'default';
270
+ check(denied, 'locked profile should deny capture_feedback');
271
+ addResult('mcp.policy.locked_profile_denies_write_tool', true, { denied });
272
+ }
273
+
274
+ // Spec and adapter files checks
275
+ {
276
+ const canonical = fs.readFileSync(path.join(ROOT, 'openapi/openapi.yaml'), 'utf-8');
277
+ const chatgpt = fs.readFileSync(path.join(ROOT, 'adapters/chatgpt/openapi.yaml'), 'utf-8');
278
+ check(canonical === chatgpt, 'chatgpt openapi not in sync with canonical openapi');
279
+
280
+ ['/v1/feedback/capture', '/v1/dpo/export', '/v1/context/construct', '/v1/intents/plan'].forEach((route) => {
281
+ check(new RegExp(escapeRegExp(route)).test(canonical), `route missing from openapi: ${route}`);
282
+ });
283
+ addResult('adapter.chatgpt.openapi.parity', true, { byteEqual: true });
284
+ }
285
+
286
+ {
287
+ const gemini = JSON.parse(fs.readFileSync(path.join(ROOT, 'adapters/gemini/function-declarations.json'), 'utf-8'));
288
+ check(Array.isArray(gemini.tools), 'gemini tools missing');
289
+ check(gemini.tools.length >= 3, 'gemini tools should have at least 3 entries');
290
+ addResult('adapter.gemini.declarations', true, { tools: gemini.tools.length });
291
+ }
292
+
293
+ {
294
+ const mustExist = [
295
+ 'adapters/claude/.mcp.json',
296
+ 'adapters/codex/config.toml',
297
+ 'adapters/amp/skills/rlhf-feedback/SKILL.md',
298
+ ];
299
+ mustExist.forEach((file) => {
300
+ check(fs.existsSync(path.join(ROOT, file)), `missing adapter file: ${file}`);
301
+ });
302
+ addResult('adapter.files.present', true, { files: mustExist.length });
303
+ }
304
+
305
+ // Profiles and policy checks
306
+ {
307
+ const validation = validateSubagentProfiles();
308
+ check(validation.valid, `subagent profiles invalid: ${validation.issues.join('; ')}`);
309
+ const names = listSubagentProfiles();
310
+ check(names.length >= 2, 'expected at least 2 subagent profiles');
311
+ addResult('subagent.profiles.valid', true, { profiles: names });
312
+ }
313
+
314
+ {
315
+ const defaultTools = getAllowedTools('default');
316
+ const lockedTools = getAllowedTools('locked');
317
+ check(defaultTools.length > lockedTools.length, 'default profile should expose more tools than locked');
318
+ addResult('mcp.policy.profile_differentiation', true, {
319
+ defaultTools: defaultTools.length,
320
+ lockedTools: lockedTools.length,
321
+ });
322
+ }
323
+ } catch (err) {
324
+ addResult('fatal', false, { error: err.message });
325
+ } finally {
326
+ await new Promise((resolve) => server.close(resolve));
327
+ fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
328
+ }
329
+
330
+ if (writeArtifacts) {
331
+ const reportPath = path.join(proofDir, 'report.json');
332
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
333
+
334
+ const mdLines = [
335
+ '# Adapter Compatibility Proof',
336
+ '',
337
+ `Generated: ${report.generatedAt}`,
338
+ '',
339
+ `Passed: ${report.summary.passed}`,
340
+ `Failed: ${report.summary.failed}`,
341
+ '',
342
+ '## Checks',
343
+ ...report.checks.map((checkItem) => `- ${checkItem.passed ? 'PASS' : 'FAIL'} ${checkItem.name}`),
344
+ '',
345
+ ];
346
+ fs.writeFileSync(path.join(proofDir, 'report.md'), `${mdLines.join('\n')}\n`);
347
+ }
348
+
349
+ if (report.summary.failed > 0) {
350
+ process.exitCode = 1;
351
+ }
352
+
353
+ return report;
354
+ }
355
+
356
+ module.exports = {
357
+ runProof,
358
+ };
359
+
360
+ if (require.main === module) {
361
+ runProof().then((report) => {
362
+ console.log(JSON.stringify(report.summary, null, 2));
363
+ });
364
+ }