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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/adapters/README.md +8 -0
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +20 -0
- package/adapters/chatgpt/INSTALL.md +80 -0
- package/adapters/chatgpt/openapi.yaml +292 -0
- package/adapters/claude/.mcp.json +8 -0
- package/adapters/codex/config.toml +4 -0
- package/adapters/gemini/function-declarations.json +95 -0
- package/adapters/mcp/server-stdio.js +444 -0
- package/bin/cli.js +167 -0
- package/config/mcp-allowlists.json +29 -0
- package/config/policy-bundles/constrained-v1.json +53 -0
- package/config/policy-bundles/default-v1.json +80 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/subagent-profiles.json +32 -0
- package/openapi/openapi.yaml +292 -0
- package/package.json +91 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +31 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +57 -0
- package/plugins/gemini-extension/INSTALL.md +74 -0
- package/plugins/gemini-extension/gemini_prompt.txt +10 -0
- package/plugins/gemini-extension/tool_contract.json +28 -0
- package/scripts/billing.js +471 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/code-reasoning.js +307 -0
- package/scripts/context-engine.js +547 -0
- package/scripts/contextfs.js +513 -0
- package/scripts/contract-audit.js +198 -0
- package/scripts/dpo-optimizer.js +208 -0
- package/scripts/export-dpo-pairs.js +316 -0
- package/scripts/export-training.js +448 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +838 -0
- package/scripts/feedback-schema.js +300 -0
- package/scripts/feedback-to-memory.js +165 -0
- package/scripts/feedback-to-rules.js +109 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/intent-router.js +164 -0
- package/scripts/mcp-policy.js +92 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/prove-adapters.js +364 -0
- package/scripts/prove-attribution.js +364 -0
- package/scripts/prove-automation.js +393 -0
- package/scripts/prove-data-quality.js +219 -0
- package/scripts/prove-intelligence.js +256 -0
- package/scripts/prove-lancedb.js +370 -0
- package/scripts/prove-loop-closure.js +255 -0
- package/scripts/prove-rlaif.js +404 -0
- package/scripts/prove-subway-upgrades.js +250 -0
- package/scripts/prove-training-export.js +324 -0
- package/scripts/prove-v2-milestone.js +273 -0
- package/scripts/prove-v3-milestone.js +381 -0
- package/scripts/rlaif-self-audit.js +123 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/self-heal.js +127 -0
- package/scripts/self-healing-check.js +111 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +29 -0
- package/scripts/thompson-sampling.js +331 -0
- package/scripts/train_from_feedback.py +914 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/vector-store.js +100 -0
- package/src/api/server.js +497 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const {
|
|
5
|
+
captureFeedback,
|
|
6
|
+
feedbackSummary,
|
|
7
|
+
analyzeFeedback,
|
|
8
|
+
writePreventionRules,
|
|
9
|
+
FEEDBACK_LOG_PATH,
|
|
10
|
+
} = require('../../scripts/feedback-loop');
|
|
11
|
+
const {
|
|
12
|
+
exportDpoFromMemories,
|
|
13
|
+
readJSONL,
|
|
14
|
+
DEFAULT_LOCAL_MEMORY_LOG,
|
|
15
|
+
} = require('../../scripts/export-dpo-pairs');
|
|
16
|
+
const {
|
|
17
|
+
ensureContextFs,
|
|
18
|
+
constructContextPack,
|
|
19
|
+
evaluateContextPack,
|
|
20
|
+
getProvenance,
|
|
21
|
+
} = require('../../scripts/contextfs');
|
|
22
|
+
const {
|
|
23
|
+
buildRubricEvaluation,
|
|
24
|
+
} = require('../../scripts/rubric-engine');
|
|
25
|
+
const {
|
|
26
|
+
listIntents,
|
|
27
|
+
planIntent,
|
|
28
|
+
} = require('../../scripts/intent-router');
|
|
29
|
+
const {
|
|
30
|
+
getActiveMcpProfile,
|
|
31
|
+
getAllowedTools,
|
|
32
|
+
assertToolAllowed,
|
|
33
|
+
} = require('../../scripts/mcp-policy');
|
|
34
|
+
|
|
35
|
+
const SERVER_INFO = {
|
|
36
|
+
name: 'rlhf-feedback-loop-mcp',
|
|
37
|
+
version: '1.1.0',
|
|
38
|
+
};
|
|
39
|
+
const SAFE_DATA_DIR = path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
40
|
+
|
|
41
|
+
function resolveSafePath(inputPath, { mustExist = false } = {}) {
|
|
42
|
+
const allowExternal = process.env.RLHF_ALLOW_EXTERNAL_PATHS === 'true';
|
|
43
|
+
const resolved = path.resolve(String(inputPath || ''));
|
|
44
|
+
const inSafeRoot = resolved === SAFE_DATA_DIR || resolved.startsWith(`${SAFE_DATA_DIR}${path.sep}`);
|
|
45
|
+
|
|
46
|
+
if (!allowExternal && !inSafeRoot) {
|
|
47
|
+
throw new Error(`Path must stay within ${SAFE_DATA_DIR}`);
|
|
48
|
+
}
|
|
49
|
+
if (mustExist && !fs.existsSync(resolved)) {
|
|
50
|
+
throw new Error(`Path does not exist: ${resolved}`);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const TOOLS = [
|
|
56
|
+
{
|
|
57
|
+
name: 'capture_feedback',
|
|
58
|
+
description: 'Capture thumbs up/down feedback and promote actionable memory',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
required: ['signal', 'context'],
|
|
62
|
+
properties: {
|
|
63
|
+
signal: { type: 'string', enum: ['up', 'down'] },
|
|
64
|
+
context: { type: 'string' },
|
|
65
|
+
whatWentWrong: { type: 'string' },
|
|
66
|
+
whatToChange: { type: 'string' },
|
|
67
|
+
whatWorked: { type: 'string' },
|
|
68
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
69
|
+
skill: { type: 'string' },
|
|
70
|
+
rubricScores: {
|
|
71
|
+
type: 'array',
|
|
72
|
+
items: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
criterion: { type: 'string' },
|
|
76
|
+
score: { type: 'number' },
|
|
77
|
+
evidence: { type: 'string' },
|
|
78
|
+
judge: { type: 'string' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
guardrails: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
testsPassed: { type: 'boolean' },
|
|
86
|
+
pathSafety: { type: 'boolean' },
|
|
87
|
+
budgetCompliant: { type: 'boolean' },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'feedback_summary',
|
|
95
|
+
description: 'Get summary of recent feedback',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
recent: { type: 'number' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'feedback_stats',
|
|
105
|
+
description: 'Get feedback stats and recommendations',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'list_intents',
|
|
113
|
+
description: 'List available intent plans and whether each requires human approval in the active profile',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
mcpProfile: { type: 'string' },
|
|
118
|
+
bundleId: { type: 'string' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'plan_intent',
|
|
124
|
+
description: 'Generate an intent execution plan with policy checkpoints',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
required: ['intentId'],
|
|
128
|
+
properties: {
|
|
129
|
+
intentId: { type: 'string' },
|
|
130
|
+
context: { type: 'string' },
|
|
131
|
+
mcpProfile: { type: 'string' },
|
|
132
|
+
bundleId: { type: 'string' },
|
|
133
|
+
approved: { type: 'boolean' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'prevention_rules',
|
|
139
|
+
description: 'Generate prevention rules from repeated mistake patterns',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
minOccurrences: { type: 'number' },
|
|
144
|
+
outputPath: { type: 'string' },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'export_dpo_pairs',
|
|
150
|
+
description: 'Export DPO preference pairs from local memory log',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
memoryLogPath: { type: 'string' },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'construct_context_pack',
|
|
160
|
+
description: 'Construct a bounded context pack from contextfs',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
query: { type: 'string' },
|
|
165
|
+
maxItems: { type: 'number' },
|
|
166
|
+
maxChars: { type: 'number' },
|
|
167
|
+
namespaces: { type: 'array', items: { type: 'string' } },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'evaluate_context_pack',
|
|
173
|
+
description: 'Record evaluation outcome for a context pack',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
required: ['packId', 'outcome'],
|
|
177
|
+
properties: {
|
|
178
|
+
packId: { type: 'string' },
|
|
179
|
+
outcome: { type: 'string' },
|
|
180
|
+
signal: { type: 'string' },
|
|
181
|
+
notes: { type: 'string' },
|
|
182
|
+
rubricScores: {
|
|
183
|
+
type: 'array',
|
|
184
|
+
items: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
criterion: { type: 'string' },
|
|
188
|
+
score: { type: 'number' },
|
|
189
|
+
evidence: { type: 'string' },
|
|
190
|
+
judge: { type: 'string' },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
guardrails: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
testsPassed: { type: 'boolean' },
|
|
198
|
+
pathSafety: { type: 'boolean' },
|
|
199
|
+
budgetCompliant: { type: 'boolean' },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'context_provenance',
|
|
207
|
+
description: 'Get recent context/provenance events',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
limit: { type: 'number' },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
function toText(result) {
|
|
218
|
+
if (typeof result === 'string') return result;
|
|
219
|
+
return JSON.stringify(result, null, 2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseOptionalObject(input, name) {
|
|
223
|
+
if (input == null) return {};
|
|
224
|
+
if (typeof input === 'object' && !Array.isArray(input)) return input;
|
|
225
|
+
if (typeof input === 'string') {
|
|
226
|
+
const trimmed = input.trim();
|
|
227
|
+
if (!trimmed) return {};
|
|
228
|
+
const parsed = JSON.parse(trimmed);
|
|
229
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
230
|
+
throw new Error(`${name} must be an object`);
|
|
231
|
+
}
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`${name} must be an object`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function callTool(name, args = {}) {
|
|
238
|
+
assertToolAllowed(name, getActiveMcpProfile());
|
|
239
|
+
|
|
240
|
+
if (name === 'capture_feedback') {
|
|
241
|
+
const result = captureFeedback({
|
|
242
|
+
signal: args.signal,
|
|
243
|
+
context: args.context,
|
|
244
|
+
whatWentWrong: args.whatWentWrong,
|
|
245
|
+
whatToChange: args.whatToChange,
|
|
246
|
+
whatWorked: args.whatWorked,
|
|
247
|
+
rubricScores: args.rubricScores,
|
|
248
|
+
guardrails: parseOptionalObject(args.guardrails, 'guardrails'),
|
|
249
|
+
tags: args.tags || [],
|
|
250
|
+
skill: args.skill,
|
|
251
|
+
});
|
|
252
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (name === 'feedback_summary') {
|
|
256
|
+
const recent = Number(args.recent || 20);
|
|
257
|
+
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20);
|
|
258
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (name === 'feedback_stats') {
|
|
262
|
+
return { content: [{ type: 'text', text: toText(analyzeFeedback()) }] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (name === 'list_intents') {
|
|
266
|
+
const result = listIntents({
|
|
267
|
+
mcpProfile: args.mcpProfile,
|
|
268
|
+
bundleId: args.bundleId,
|
|
269
|
+
});
|
|
270
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (name === 'plan_intent') {
|
|
274
|
+
const result = planIntent({
|
|
275
|
+
intentId: args.intentId,
|
|
276
|
+
context: args.context || '',
|
|
277
|
+
mcpProfile: args.mcpProfile,
|
|
278
|
+
bundleId: args.bundleId,
|
|
279
|
+
approved: args.approved === true,
|
|
280
|
+
});
|
|
281
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (name === 'prevention_rules') {
|
|
285
|
+
const minOccurrences = Number(args.minOccurrences || 2);
|
|
286
|
+
const outputPath = args.outputPath ? resolveSafePath(args.outputPath) : undefined;
|
|
287
|
+
const result = writePreventionRules(outputPath, Number.isFinite(minOccurrences) ? minOccurrences : 2);
|
|
288
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (name === 'export_dpo_pairs') {
|
|
292
|
+
const memoryLogPath = args.memoryLogPath
|
|
293
|
+
? resolveSafePath(args.memoryLogPath, { mustExist: true })
|
|
294
|
+
: DEFAULT_LOCAL_MEMORY_LOG;
|
|
295
|
+
const memories = readJSONL(memoryLogPath);
|
|
296
|
+
const result = exportDpoFromMemories(memories);
|
|
297
|
+
return {
|
|
298
|
+
content: [{
|
|
299
|
+
type: 'text',
|
|
300
|
+
text: toText({
|
|
301
|
+
pairs: result.pairs.length,
|
|
302
|
+
errors: result.errors.length,
|
|
303
|
+
learnings: result.learnings.length,
|
|
304
|
+
}),
|
|
305
|
+
}],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (name === 'construct_context_pack') {
|
|
310
|
+
ensureContextFs();
|
|
311
|
+
const result = constructContextPack({
|
|
312
|
+
query: args.query || '',
|
|
313
|
+
maxItems: Number(args.maxItems || 8),
|
|
314
|
+
maxChars: Number(args.maxChars || 6000),
|
|
315
|
+
namespaces: Array.isArray(args.namespaces) ? args.namespaces : [],
|
|
316
|
+
});
|
|
317
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (name === 'evaluate_context_pack') {
|
|
321
|
+
if (!args.packId || !args.outcome) {
|
|
322
|
+
throw new Error('packId and outcome are required');
|
|
323
|
+
}
|
|
324
|
+
const result = evaluateContextPack({
|
|
325
|
+
packId: args.packId,
|
|
326
|
+
outcome: args.outcome,
|
|
327
|
+
signal: args.signal || null,
|
|
328
|
+
notes: args.notes || '',
|
|
329
|
+
rubricEvaluation: args.rubricScores || args.guardrails
|
|
330
|
+
? buildRubricEvaluation({
|
|
331
|
+
rubricScores: args.rubricScores,
|
|
332
|
+
guardrails: parseOptionalObject(args.guardrails, 'guardrails'),
|
|
333
|
+
})
|
|
334
|
+
: null,
|
|
335
|
+
});
|
|
336
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (name === 'context_provenance') {
|
|
340
|
+
const limit = Number(args.limit || 50);
|
|
341
|
+
const result = getProvenance(Number.isFinite(limit) ? limit : 50);
|
|
342
|
+
return { content: [{ type: 'text', text: toText(result) }] };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function handleRequest(message) {
|
|
349
|
+
if (message.method === 'initialize') {
|
|
350
|
+
return {
|
|
351
|
+
protocolVersion: '2024-11-05',
|
|
352
|
+
capabilities: {
|
|
353
|
+
tools: {},
|
|
354
|
+
},
|
|
355
|
+
serverInfo: SERVER_INFO,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (message.method === 'tools/list') {
|
|
360
|
+
const profile = getActiveMcpProfile();
|
|
361
|
+
const allowed = new Set(getAllowedTools(profile));
|
|
362
|
+
const tools = TOOLS.filter((tool) => allowed.has(tool.name));
|
|
363
|
+
return { tools };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (message.method === 'tools/call') {
|
|
367
|
+
const name = message.params && message.params.name;
|
|
368
|
+
const args = (message.params && message.params.arguments) || {};
|
|
369
|
+
return callTool(name, args);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
throw new Error(`Unsupported method: ${message.method}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function writeMessage(payload) {
|
|
376
|
+
const json = JSON.stringify(payload);
|
|
377
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let buffer = Buffer.alloc(0);
|
|
381
|
+
|
|
382
|
+
function tryReadMessage() {
|
|
383
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
384
|
+
if (headerEnd === -1) return null;
|
|
385
|
+
|
|
386
|
+
const headerRaw = buffer.slice(0, headerEnd).toString('utf8');
|
|
387
|
+
const match = headerRaw.match(/Content-Length:\s*(\d+)/i);
|
|
388
|
+
if (!match) {
|
|
389
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const length = Number(match[1]);
|
|
394
|
+
const totalSize = headerEnd + 4 + length;
|
|
395
|
+
if (buffer.length < totalSize) return null;
|
|
396
|
+
|
|
397
|
+
const body = buffer.slice(headerEnd + 4, totalSize).toString('utf8');
|
|
398
|
+
buffer = buffer.slice(totalSize);
|
|
399
|
+
|
|
400
|
+
return JSON.parse(body);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function onData(chunk) {
|
|
404
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
405
|
+
|
|
406
|
+
while (true) {
|
|
407
|
+
const message = tryReadMessage();
|
|
408
|
+
if (!message) return;
|
|
409
|
+
|
|
410
|
+
if (!Object.prototype.hasOwnProperty.call(message, 'id')) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const result = await handleRequest(message);
|
|
416
|
+
writeMessage({ jsonrpc: '2.0', id: message.id, result });
|
|
417
|
+
} catch (err) {
|
|
418
|
+
writeMessage({
|
|
419
|
+
jsonrpc: '2.0',
|
|
420
|
+
id: message.id,
|
|
421
|
+
error: {
|
|
422
|
+
code: -32603,
|
|
423
|
+
message: err.message || 'Internal error',
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = {
|
|
431
|
+
TOOLS,
|
|
432
|
+
handleRequest,
|
|
433
|
+
callTool,
|
|
434
|
+
resolveSafePath,
|
|
435
|
+
SAFE_DATA_DIR,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (require.main === module) {
|
|
439
|
+
process.stdin.on('data', (chunk) => {
|
|
440
|
+
onData(chunk).catch((err) => {
|
|
441
|
+
writeMessage({ jsonrpc: '2.0', id: null, error: { code: -32603, message: err.message } });
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* rlhf-feedback-loop CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx rlhf-feedback-loop init
|
|
7
|
+
*
|
|
8
|
+
* Creates a .rlhf/ directory with config and capture script for local use.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const COMMAND = process.argv[2];
|
|
17
|
+
const CWD = process.cwd();
|
|
18
|
+
|
|
19
|
+
function init() {
|
|
20
|
+
const rlhfDir = path.join(CWD, '.rlhf');
|
|
21
|
+
|
|
22
|
+
// Create directory
|
|
23
|
+
if (!fs.existsSync(rlhfDir)) {
|
|
24
|
+
fs.mkdirSync(rlhfDir, { recursive: true });
|
|
25
|
+
console.log('Created .rlhf/');
|
|
26
|
+
} else {
|
|
27
|
+
console.log('.rlhf/ already exists — updating config');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Write config.json
|
|
31
|
+
const config = {
|
|
32
|
+
version: '0.5.0',
|
|
33
|
+
apiUrl: process.env.RLHF_API_URL || 'http://localhost:3000',
|
|
34
|
+
logPath: '.rlhf/feedback-log.jsonl',
|
|
35
|
+
memoryPath: '.rlhf/memory-log.jsonl',
|
|
36
|
+
createdAt: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const configPath = path.join(rlhfDir, 'config.json');
|
|
40
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
41
|
+
console.log('Wrote .rlhf/config.json');
|
|
42
|
+
|
|
43
|
+
// Copy capture-feedback script (inline minimal version for standalone use)
|
|
44
|
+
const captureScript = `#!/usr/bin/env node
|
|
45
|
+
/**
|
|
46
|
+
* Standalone feedback capture script — created by npx rlhf-feedback-loop init
|
|
47
|
+
* Full version: https://github.com/IgorGanapolsky/rlhf-feedback-loop
|
|
48
|
+
*
|
|
49
|
+
* Usage:
|
|
50
|
+
* node .rlhf/capture-feedback.js --feedback=up --context="that worked great" --tags="testing"
|
|
51
|
+
* node .rlhf/capture-feedback.js --feedback=down --context="missed edge case" --what-went-wrong="..." --what-to-change="..."
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
'use strict';
|
|
55
|
+
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const path = require('path');
|
|
58
|
+
const os = require('os');
|
|
59
|
+
|
|
60
|
+
const CONFIG_PATH = path.join(__dirname, 'config.json');
|
|
61
|
+
const config = fs.existsSync(CONFIG_PATH) ? JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) : {};
|
|
62
|
+
const LOG_PATH = path.join(process.cwd(), config.logPath || '.rlhf/feedback-log.jsonl');
|
|
63
|
+
|
|
64
|
+
function parseArgs(argv) {
|
|
65
|
+
const args = {};
|
|
66
|
+
argv.forEach((arg) => {
|
|
67
|
+
if (!arg.startsWith('--')) return;
|
|
68
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
69
|
+
args[key] = rest.length ? rest.join('=') : true;
|
|
70
|
+
});
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const args = parseArgs(process.argv.slice(2));
|
|
75
|
+
const signal = args.feedback || args.signal;
|
|
76
|
+
|
|
77
|
+
if (!signal) {
|
|
78
|
+
console.error('Error: --feedback=up or --feedback=down required');
|
|
79
|
+
console.error('Usage: node .rlhf/capture-feedback.js --feedback=up --context="..."');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const normalized = ['up', 'thumbs_up', 'positive'].includes(signal) ? 'up' : 'down';
|
|
84
|
+
|
|
85
|
+
const entry = {
|
|
86
|
+
id: \`fb-\${Date.now()}-\${Math.random().toString(36).slice(2, 7)}\`,
|
|
87
|
+
signal: normalized,
|
|
88
|
+
context: args.context || '',
|
|
89
|
+
whatWentWrong: args['what-went-wrong'] || undefined,
|
|
90
|
+
whatToChange: args['what-to-change'] || undefined,
|
|
91
|
+
whatWorked: args['what-worked'] || undefined,
|
|
92
|
+
tags: args.tags ? args.tags.split(',').map((t) => t.trim()) : [],
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
hostname: os.hostname(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Remove undefined fields
|
|
98
|
+
Object.keys(entry).forEach((k) => entry[k] === undefined && delete entry[k]);
|
|
99
|
+
|
|
100
|
+
// Ensure log directory exists
|
|
101
|
+
const logDir = path.dirname(LOG_PATH);
|
|
102
|
+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + '\\n');
|
|
105
|
+
console.log(\`Feedback captured [\${normalized}]: \${entry.id}\`);
|
|
106
|
+
console.log(\`Logged to: \${LOG_PATH}\`);
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const scriptPath = path.join(rlhfDir, 'capture-feedback.js');
|
|
110
|
+
fs.writeFileSync(scriptPath, captureScript);
|
|
111
|
+
// Make executable
|
|
112
|
+
try {
|
|
113
|
+
fs.chmodSync(scriptPath, '755');
|
|
114
|
+
} catch (_) {
|
|
115
|
+
// chmod may not be available on all platforms — not fatal
|
|
116
|
+
}
|
|
117
|
+
console.log('Wrote .rlhf/capture-feedback.js');
|
|
118
|
+
|
|
119
|
+
// Add .rlhf/feedback-log.jsonl to .gitignore if it exists
|
|
120
|
+
const gitignorePath = path.join(CWD, '.gitignore');
|
|
121
|
+
if (fs.existsSync(gitignorePath)) {
|
|
122
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
123
|
+
const entries = ['.rlhf/feedback-log.jsonl', '.rlhf/memory-log.jsonl'];
|
|
124
|
+
const missing = entries.filter((e) => !gitignore.includes(e));
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
fs.appendFileSync(gitignorePath, '\n# RLHF local feedback data\n' + missing.join('\n') + '\n');
|
|
127
|
+
console.log('Updated .gitignore with RLHF data paths');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log('Setup complete! Run:');
|
|
133
|
+
console.log(" node .rlhf/capture-feedback.js --feedback=up --context='test'");
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log('Full docs: https://github.com/IgorGanapolsky/rlhf-feedback-loop');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function help() {
|
|
139
|
+
console.log('rlhf-feedback-loop CLI');
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('Commands:');
|
|
142
|
+
console.log(' init Scaffold .rlhf/ config and capture script in current directory');
|
|
143
|
+
console.log(' help Show this help message');
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log('Examples:');
|
|
146
|
+
console.log(' npx rlhf-feedback-loop init');
|
|
147
|
+
console.log(' node .rlhf/capture-feedback.js --feedback=up --context="great result"');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
switch (COMMAND) {
|
|
151
|
+
case 'init':
|
|
152
|
+
init();
|
|
153
|
+
break;
|
|
154
|
+
case 'help':
|
|
155
|
+
case '--help':
|
|
156
|
+
case '-h':
|
|
157
|
+
help();
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
if (COMMAND) {
|
|
161
|
+
console.error(`Unknown command: ${COMMAND}`);
|
|
162
|
+
console.error('Run: npx rlhf-feedback-loop help');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
} else {
|
|
165
|
+
help();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"profiles": {
|
|
4
|
+
"default": [
|
|
5
|
+
"capture_feedback",
|
|
6
|
+
"feedback_summary",
|
|
7
|
+
"feedback_stats",
|
|
8
|
+
"list_intents",
|
|
9
|
+
"plan_intent",
|
|
10
|
+
"prevention_rules",
|
|
11
|
+
"export_dpo_pairs",
|
|
12
|
+
"construct_context_pack",
|
|
13
|
+
"evaluate_context_pack",
|
|
14
|
+
"context_provenance"
|
|
15
|
+
],
|
|
16
|
+
"readonly": [
|
|
17
|
+
"feedback_summary",
|
|
18
|
+
"feedback_stats",
|
|
19
|
+
"list_intents",
|
|
20
|
+
"plan_intent",
|
|
21
|
+
"context_provenance"
|
|
22
|
+
],
|
|
23
|
+
"locked": [
|
|
24
|
+
"feedback_summary",
|
|
25
|
+
"list_intents",
|
|
26
|
+
"plan_intent"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bundleId": "constrained-v1",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "Conservative execution bundle for locked and review-heavy environments.",
|
|
5
|
+
"defaultMcpProfile": "locked",
|
|
6
|
+
"approval": {
|
|
7
|
+
"requiredRisks": ["medium", "high", "critical"],
|
|
8
|
+
"profileOverrides": {
|
|
9
|
+
"default": ["medium", "high", "critical"],
|
|
10
|
+
"readonly": ["medium", "high", "critical"],
|
|
11
|
+
"locked": ["low", "medium", "high", "critical"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"intents": [
|
|
15
|
+
{
|
|
16
|
+
"id": "capture_feedback_loop",
|
|
17
|
+
"description": "Capture user outcome and update memory artifacts.",
|
|
18
|
+
"risk": "low",
|
|
19
|
+
"actions": [
|
|
20
|
+
{
|
|
21
|
+
"kind": "mcp_tool",
|
|
22
|
+
"name": "capture_feedback"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "improve_response_quality",
|
|
28
|
+
"description": "Summarize recent failures and regenerate prevention rules.",
|
|
29
|
+
"risk": "medium",
|
|
30
|
+
"actions": [
|
|
31
|
+
{
|
|
32
|
+
"kind": "mcp_tool",
|
|
33
|
+
"name": "feedback_summary"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"kind": "mcp_tool",
|
|
37
|
+
"name": "prevention_rules"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "publish_dpo_training_data",
|
|
43
|
+
"description": "Export DPO preference pairs for model improvement pipelines.",
|
|
44
|
+
"risk": "high",
|
|
45
|
+
"actions": [
|
|
46
|
+
{
|
|
47
|
+
"kind": "mcp_tool",
|
|
48
|
+
"name": "export_dpo_pairs"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|