rlhf-feedback-loop 0.6.11 → 0.6.12
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 +10 -0
- package/README.md +116 -74
- package/adapters/README.md +3 -3
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +2 -0
- package/adapters/chatgpt/INSTALL.md +6 -3
- package/adapters/chatgpt/openapi.yaml +5 -2
- package/adapters/claude/.mcp.json +3 -3
- package/adapters/codex/config.toml +3 -3
- package/adapters/gemini/function-declarations.json +2 -2
- package/adapters/mcp/server-stdio.js +19 -5
- package/bin/cli.js +295 -25
- package/openapi/openapi.yaml +5 -2
- package/package.json +23 -10
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/adk-consolidator.js +126 -32
- package/scripts/billing.js +192 -685
- package/scripts/context-engine.js +81 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/feedback-ingest-watcher.js +290 -0
- package/scripts/feedback-loop.js +153 -8
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-schema.js +31 -5
- package/scripts/feedback-to-memory.js +13 -1
- package/scripts/hook-auto-capture.sh +6 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/install-mcp.js +168 -0
- package/scripts/jsonl-watcher.js +151 -0
- package/scripts/local-model-profile.js +207 -0
- package/scripts/pr-manager.js +112 -0
- package/scripts/prove-adapters.js +137 -15
- package/scripts/prove-automation.js +41 -8
- package/scripts/prove-lancedb.js +1 -1
- package/scripts/prove-local-intelligence.js +244 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +7 -1
- package/scripts/status-dashboard.js +155 -0
- package/scripts/test-coverage.js +1 -1
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +115 -17
- package/src/api/server.js +372 -25
package/scripts/feedback-loop.js
CHANGED
|
@@ -14,6 +14,9 @@ const {
|
|
|
14
14
|
prepareForStorage,
|
|
15
15
|
parseTimestamp,
|
|
16
16
|
} = require('./feedback-schema');
|
|
17
|
+
const {
|
|
18
|
+
buildClarificationMessage,
|
|
19
|
+
} = require('./feedback-quality');
|
|
17
20
|
const {
|
|
18
21
|
buildRubricEvaluation,
|
|
19
22
|
} = require('./rubric-engine');
|
|
@@ -30,6 +33,7 @@ const DOMAIN_CATEGORIES = [
|
|
|
30
33
|
];
|
|
31
34
|
|
|
32
35
|
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
36
|
+
const pendingBackgroundSideEffects = new Set();
|
|
33
37
|
|
|
34
38
|
function getFeedbackPaths() {
|
|
35
39
|
if (process.env.RLHF_FEEDBACK_DIR) {
|
|
@@ -85,6 +89,14 @@ function getVectorStoreModule() {
|
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
function getRiskScorerModule() {
|
|
93
|
+
try {
|
|
94
|
+
return require('./risk-scorer');
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
function getSelfAuditModule() {
|
|
89
101
|
try {
|
|
90
102
|
return require('./rlaif-self-audit');
|
|
@@ -104,6 +116,34 @@ function appendJSONL(filePath, record) {
|
|
|
104
116
|
fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
function trackBackgroundSideEffect(taskPromise) {
|
|
120
|
+
if (!taskPromise || typeof taskPromise.then !== 'function') {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let tracked;
|
|
125
|
+
tracked = Promise.resolve(taskPromise)
|
|
126
|
+
.catch(() => {
|
|
127
|
+
// Non-critical side effects should never fail the primary feedback write.
|
|
128
|
+
})
|
|
129
|
+
.finally(() => {
|
|
130
|
+
pendingBackgroundSideEffects.delete(tracked);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pendingBackgroundSideEffects.add(tracked);
|
|
134
|
+
return tracked;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function waitForBackgroundSideEffects() {
|
|
138
|
+
while (pendingBackgroundSideEffects.size > 0) {
|
|
139
|
+
await Promise.allSettled([...pendingBackgroundSideEffects]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getPendingBackgroundSideEffectCount() {
|
|
144
|
+
return pendingBackgroundSideEffects.size;
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
function readJSONL(filePath) {
|
|
108
148
|
if (!fs.existsSync(filePath)) return [];
|
|
109
149
|
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
@@ -290,15 +330,41 @@ function buildSequenceFeatures(recentEntries, currentEntry) {
|
|
|
290
330
|
};
|
|
291
331
|
}
|
|
292
332
|
|
|
293
|
-
function appendSequence(feedbackEvent, paths) {
|
|
333
|
+
function appendSequence(historyEntries, feedbackEvent, paths, outcome = {}) {
|
|
294
334
|
const sequencePath = path.join(paths.FEEDBACK_DIR, 'feedback-sequences.jsonl');
|
|
295
|
-
const recent =
|
|
335
|
+
const recent = Array.isArray(historyEntries) ? historyEntries.slice(-SEQUENCE_WINDOW) : [];
|
|
296
336
|
const features = buildSequenceFeatures(recent, feedbackEvent);
|
|
337
|
+
const rubric = feedbackEvent.rubric || null;
|
|
338
|
+
const filePaths = feedbackEvent.richContext && Array.isArray(feedbackEvent.richContext.filePaths)
|
|
339
|
+
? feedbackEvent.richContext.filePaths
|
|
340
|
+
: [];
|
|
341
|
+
const accepted = outcome.accepted === true;
|
|
342
|
+
const targetRisk = feedbackEvent.signal === 'negative' || !accepted ? 1 : 0;
|
|
297
343
|
const entry = {
|
|
298
344
|
id: `seq_${Date.now()}`,
|
|
299
345
|
timestamp: new Date().toISOString(),
|
|
300
346
|
targetReward: feedbackEvent.signal === 'positive' ? 1 : -1,
|
|
301
347
|
targetTags: feedbackEvent.tags,
|
|
348
|
+
accepted,
|
|
349
|
+
actionType: feedbackEvent.actionType || null,
|
|
350
|
+
actionReason: feedbackEvent.actionReason || null,
|
|
351
|
+
context: feedbackEvent.context || '',
|
|
352
|
+
skill: feedbackEvent.skill || null,
|
|
353
|
+
domain: feedbackEvent.richContext ? feedbackEvent.richContext.domain : 'general',
|
|
354
|
+
outcomeCategory: feedbackEvent.richContext ? feedbackEvent.richContext.outcomeCategory : 'unknown',
|
|
355
|
+
filePathCount: filePaths.length,
|
|
356
|
+
errorType: feedbackEvent.richContext ? feedbackEvent.richContext.errorType : null,
|
|
357
|
+
rubric: rubric
|
|
358
|
+
? {
|
|
359
|
+
rubricId: rubric.rubricId || null,
|
|
360
|
+
weightedScore: rubric.weightedScore,
|
|
361
|
+
failingCriteria: rubric.failingCriteria || [],
|
|
362
|
+
failingGuardrails: rubric.failingGuardrails || [],
|
|
363
|
+
judgeDisagreements: rubric.judgeDisagreements || [],
|
|
364
|
+
}
|
|
365
|
+
: null,
|
|
366
|
+
targetRisk,
|
|
367
|
+
riskLabel: targetRisk === 1 ? 'high-risk' : 'low-risk',
|
|
302
368
|
features,
|
|
303
369
|
label: feedbackEvent.signal === 'positive' ? 'positive' : 'negative',
|
|
304
370
|
};
|
|
@@ -343,7 +409,7 @@ function updateDiversityTracking(feedbackEvent, paths) {
|
|
|
343
409
|
}
|
|
344
410
|
|
|
345
411
|
function captureFeedback(params) {
|
|
346
|
-
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = getFeedbackPaths();
|
|
412
|
+
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH, FEEDBACK_DIR } = getFeedbackPaths();
|
|
347
413
|
const signal = normalizeSignal(params.signal);
|
|
348
414
|
if (!signal) {
|
|
349
415
|
return {
|
|
@@ -411,20 +477,44 @@ function captureFeedback(params) {
|
|
|
411
477
|
|
|
412
478
|
// Rich context enrichment (QUAL-02, QUAL-03) — non-blocking
|
|
413
479
|
const feedbackEvent = enrichFeedbackContext(rawFeedbackEvent, params);
|
|
480
|
+
const historyEntries = readJSONL(FEEDBACK_LOG_PATH).slice(-SEQUENCE_WINDOW);
|
|
414
481
|
|
|
415
482
|
const summary = loadSummary();
|
|
416
483
|
summary.total += 1;
|
|
417
484
|
summary[signal] += 1;
|
|
418
485
|
|
|
419
486
|
if (action.type === 'no-action') {
|
|
487
|
+
const clarification = buildClarificationMessage({
|
|
488
|
+
signal,
|
|
489
|
+
context: params.context || '',
|
|
490
|
+
whatWentWrong: params.whatWentWrong,
|
|
491
|
+
whatToChange: params.whatToChange,
|
|
492
|
+
whatWorked: params.whatWorked,
|
|
493
|
+
});
|
|
420
494
|
summary.rejected += 1;
|
|
421
495
|
summary.lastUpdated = now;
|
|
422
496
|
saveSummary(summary);
|
|
423
497
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
498
|
+
try {
|
|
499
|
+
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
500
|
+
} catch {
|
|
501
|
+
// Sequence tracking failure is non-critical
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const riskScorer = getRiskScorerModule();
|
|
505
|
+
if (riskScorer) {
|
|
506
|
+
riskScorer.trainAndPersistRiskModel(FEEDBACK_DIR);
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// Risk model refresh is non-critical
|
|
510
|
+
}
|
|
424
511
|
return {
|
|
425
512
|
accepted: false,
|
|
513
|
+
status: clarification ? 'clarification_required' : 'rejected',
|
|
426
514
|
reason: action.reason,
|
|
515
|
+
message: clarification ? clarification.message : 'Signal logged, but reusable memory was not created.',
|
|
427
516
|
feedbackEvent,
|
|
517
|
+
...(clarification || {}),
|
|
428
518
|
};
|
|
429
519
|
}
|
|
430
520
|
|
|
@@ -437,9 +527,24 @@ function captureFeedback(params) {
|
|
|
437
527
|
...feedbackEvent,
|
|
438
528
|
validationIssues: prepared.issues,
|
|
439
529
|
});
|
|
530
|
+
try {
|
|
531
|
+
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
532
|
+
} catch {
|
|
533
|
+
// Sequence tracking failure is non-critical
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const riskScorer = getRiskScorerModule();
|
|
537
|
+
if (riskScorer) {
|
|
538
|
+
riskScorer.trainAndPersistRiskModel(FEEDBACK_DIR);
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
// Risk model refresh is non-critical
|
|
542
|
+
}
|
|
440
543
|
return {
|
|
441
544
|
accepted: false,
|
|
545
|
+
status: 'rejected',
|
|
442
546
|
reason: `Schema validation failed: ${prepared.issues.join('; ')}`,
|
|
547
|
+
message: 'Signal logged, but reusable memory was not created.',
|
|
443
548
|
feedbackEvent,
|
|
444
549
|
issues: prepared.issues,
|
|
445
550
|
};
|
|
@@ -467,7 +572,7 @@ function captureFeedback(params) {
|
|
|
467
572
|
// ML side-effects: sequence tracking and diversity (non-blocking — primary write already succeeded)
|
|
468
573
|
const mlPaths = getFeedbackPaths();
|
|
469
574
|
try {
|
|
470
|
-
appendSequence(feedbackEvent, mlPaths);
|
|
575
|
+
appendSequence(historyEntries, feedbackEvent, mlPaths, { accepted: true });
|
|
471
576
|
} catch (err) {
|
|
472
577
|
// Sequence tracking failure is non-critical
|
|
473
578
|
}
|
|
@@ -479,10 +584,8 @@ function captureFeedback(params) {
|
|
|
479
584
|
|
|
480
585
|
// Vector storage side-effect (non-blocking — primary write already succeeded)
|
|
481
586
|
const vectorStore = getVectorStoreModule();
|
|
482
|
-
if (vectorStore) {
|
|
483
|
-
vectorStore.upsertFeedback(feedbackEvent)
|
|
484
|
-
// Non-critical; primary feedback log is the source of truth
|
|
485
|
-
});
|
|
587
|
+
if (vectorStore && typeof vectorStore.upsertFeedback === 'function') {
|
|
588
|
+
trackBackgroundSideEffect(vectorStore.upsertFeedback(feedbackEvent));
|
|
486
589
|
}
|
|
487
590
|
|
|
488
591
|
// RLAIF self-audit side-effect (non-blocking — 4th enrichment layer)
|
|
@@ -491,6 +594,14 @@ function captureFeedback(params) {
|
|
|
491
594
|
if (sam) sam.selfAuditAndLog(feedbackEvent, mlPaths);
|
|
492
595
|
} catch (_err) { /* non-critical */ }
|
|
493
596
|
|
|
597
|
+
// Boosted risk model refresh — local, file-based, and non-blocking
|
|
598
|
+
try {
|
|
599
|
+
const riskScorer = getRiskScorerModule();
|
|
600
|
+
if (riskScorer) {
|
|
601
|
+
riskScorer.trainAndPersistRiskModel(FEEDBACK_DIR);
|
|
602
|
+
}
|
|
603
|
+
} catch (_err) { /* non-critical */ }
|
|
604
|
+
|
|
494
605
|
// Attribution side-effects — fire-and-forget, never throw
|
|
495
606
|
try {
|
|
496
607
|
const toolName = feedbackEvent.toolName || feedbackEvent.tool_name || 'unknown';
|
|
@@ -511,6 +622,8 @@ function captureFeedback(params) {
|
|
|
511
622
|
|
|
512
623
|
return {
|
|
513
624
|
accepted: true,
|
|
625
|
+
status: 'promoted',
|
|
626
|
+
message: 'Feedback promoted to reusable memory.',
|
|
514
627
|
feedbackEvent,
|
|
515
628
|
memoryRecord,
|
|
516
629
|
};
|
|
@@ -519,6 +632,7 @@ function captureFeedback(params) {
|
|
|
519
632
|
function analyzeFeedback(logPath) {
|
|
520
633
|
const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
|
|
521
634
|
const entries = readJSONL(logPath || FEEDBACK_LOG_PATH);
|
|
635
|
+
const paths = getFeedbackPaths();
|
|
522
636
|
const skills = {};
|
|
523
637
|
const tags = {};
|
|
524
638
|
const rubricCriteria = {};
|
|
@@ -586,6 +700,24 @@ function analyzeFeedback(logPath) {
|
|
|
586
700
|
recommendations.push('DECLINING trend in last 20 signals; tighten verification before response.');
|
|
587
701
|
}
|
|
588
702
|
|
|
703
|
+
let boostedRisk = null;
|
|
704
|
+
try {
|
|
705
|
+
const riskScorer = getRiskScorerModule();
|
|
706
|
+
if (riskScorer) {
|
|
707
|
+
boostedRisk = riskScorer.getRiskSummary(paths.FEEDBACK_DIR);
|
|
708
|
+
if (boostedRisk) {
|
|
709
|
+
boostedRisk.highRiskDomains.slice(0, 2).forEach((bucket) => {
|
|
710
|
+
recommendations.push(`CHECK high-risk domain '${bucket.key}' (${bucket.highRisk}/${bucket.total} high-risk)`);
|
|
711
|
+
});
|
|
712
|
+
boostedRisk.highRiskTags.slice(0, 2).forEach((bucket) => {
|
|
713
|
+
recommendations.push(`CHECK high-risk tag '${bucket.key}' (${bucket.highRisk}/${bucket.total} high-risk)`);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
boostedRisk = null;
|
|
719
|
+
}
|
|
720
|
+
|
|
589
721
|
return {
|
|
590
722
|
total,
|
|
591
723
|
totalPositive,
|
|
@@ -599,6 +731,7 @@ function analyzeFeedback(logPath) {
|
|
|
599
731
|
blockedPromotions,
|
|
600
732
|
failingCriteria: rubricCriteria,
|
|
601
733
|
},
|
|
734
|
+
boostedRisk,
|
|
602
735
|
recommendations,
|
|
603
736
|
};
|
|
604
737
|
}
|
|
@@ -699,6 +832,15 @@ function feedbackSummary(recentN = 20) {
|
|
|
699
832
|
`- Overall approval: ${Math.round(analysis.approvalRate * 100)}%`,
|
|
700
833
|
];
|
|
701
834
|
|
|
835
|
+
if (analysis.boostedRisk) {
|
|
836
|
+
lines.push(`- Boosted risk base rate: ${Math.round((analysis.boostedRisk.baseRate || 0) * 100)}%`);
|
|
837
|
+
lines.push(`- Boosted risk mode: ${analysis.boostedRisk.mode}`);
|
|
838
|
+
if (analysis.boostedRisk.highRiskDomains.length > 0) {
|
|
839
|
+
const topDomain = analysis.boostedRisk.highRiskDomains[0];
|
|
840
|
+
lines.push(`- Highest-risk domain: ${topDomain.key} (${Math.round(topDomain.riskRate * 100)}%)`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
702
844
|
if (analysis.recommendations.length > 0) {
|
|
703
845
|
lines.push('- Recommendations:');
|
|
704
846
|
analysis.recommendations.slice(0, 5).forEach((r) => lines.push(` - ${r}`));
|
|
@@ -822,6 +964,7 @@ function runTests() {
|
|
|
822
964
|
|
|
823
965
|
const bad = captureFeedback({ signal: 'down' });
|
|
824
966
|
assert(!bad.accepted, 'captureFeedback rejects vague negative feedback');
|
|
967
|
+
assert(bad.needsClarification === true, 'captureFeedback requests clarification for vague negative feedback');
|
|
825
968
|
|
|
826
969
|
const summary = feedbackSummary(5);
|
|
827
970
|
assert(summary.includes('Feedback Summary'), 'feedbackSummary returns text output');
|
|
@@ -848,6 +991,8 @@ module.exports = {
|
|
|
848
991
|
inferDomain,
|
|
849
992
|
inferOutcome,
|
|
850
993
|
enrichFeedbackContext,
|
|
994
|
+
waitForBackgroundSideEffects,
|
|
995
|
+
getPendingBackgroundSideEffectCount,
|
|
851
996
|
get FEEDBACK_LOG_PATH() {
|
|
852
997
|
return getFeedbackPaths().FEEDBACK_LOG_PATH;
|
|
853
998
|
},
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const GENERIC_PHRASE_RULES = {
|
|
4
|
+
positive: [
|
|
5
|
+
/^up$/,
|
|
6
|
+
/^thumbs?\s*up$/,
|
|
7
|
+
/^thumbs\s+up$/,
|
|
8
|
+
/^that worked$/,
|
|
9
|
+
/^it worked$/,
|
|
10
|
+
/^worked$/,
|
|
11
|
+
/^looks good$/,
|
|
12
|
+
/^looked good$/,
|
|
13
|
+
/^good job$/,
|
|
14
|
+
/^good work$/,
|
|
15
|
+
/^nice work$/,
|
|
16
|
+
/^perfect$/,
|
|
17
|
+
/^approved$/,
|
|
18
|
+
/^lgtm$/,
|
|
19
|
+
],
|
|
20
|
+
negative: [
|
|
21
|
+
/^down$/,
|
|
22
|
+
/^thumbs?\s*down$/,
|
|
23
|
+
/^thumbs\s+down$/,
|
|
24
|
+
/^that failed$/,
|
|
25
|
+
/^it failed$/,
|
|
26
|
+
/^failed$/,
|
|
27
|
+
/^that was wrong$/,
|
|
28
|
+
/^wrong$/,
|
|
29
|
+
/^bad$/,
|
|
30
|
+
/^fix this$/,
|
|
31
|
+
/^broken$/,
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const CLARIFICATION_CONFIG = {
|
|
36
|
+
positive: {
|
|
37
|
+
prompt: 'What specifically worked that should be repeated?',
|
|
38
|
+
example: 'Example: "The agent showed test output before claiming done."',
|
|
39
|
+
missingFields: ['whatWorked'],
|
|
40
|
+
},
|
|
41
|
+
negative: {
|
|
42
|
+
prompt: 'What failed and what should change next time?',
|
|
43
|
+
example: 'Example: "It skipped tests and should run npm test before closing the task."',
|
|
44
|
+
missingFields: ['whatWentWrong', 'whatToChange'],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function normalizeFeedbackSignal(signal) {
|
|
49
|
+
const normalized = normalizeFeedbackText(signal);
|
|
50
|
+
if (['negative', 'down', 'thumbs down', 'thumbsdown', 'bad'].includes(normalized)) {
|
|
51
|
+
return 'negative';
|
|
52
|
+
}
|
|
53
|
+
return 'positive';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeFeedbackText(value) {
|
|
57
|
+
return String(value || '')
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[_-]+/g, ' ')
|
|
60
|
+
.replace(/[^\w\s]/g, ' ')
|
|
61
|
+
.replace(/\s+/g, ' ')
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isGenericFeedbackText(value, signal) {
|
|
66
|
+
const normalized = normalizeFeedbackText(value);
|
|
67
|
+
if (!normalized) return false;
|
|
68
|
+
const rules = GENERIC_PHRASE_RULES[signal] || [];
|
|
69
|
+
return rules.some((pattern) => pattern.test(normalized));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assessFeedbackActionability(params = {}) {
|
|
73
|
+
const signal = normalizeFeedbackSignal(params.signal);
|
|
74
|
+
const primaryFields = signal === 'positive'
|
|
75
|
+
? [
|
|
76
|
+
{ name: 'whatWorked', value: params.whatWorked },
|
|
77
|
+
{ name: 'context', value: params.context },
|
|
78
|
+
]
|
|
79
|
+
: [
|
|
80
|
+
{ name: 'whatWentWrong', value: params.whatWentWrong },
|
|
81
|
+
{ name: 'context', value: params.context },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const populated = primaryFields.filter((field) => normalizeFeedbackText(field.value));
|
|
85
|
+
const specific = populated.find((field) => !isGenericFeedbackText(field.value, signal));
|
|
86
|
+
|
|
87
|
+
if (specific) {
|
|
88
|
+
return {
|
|
89
|
+
promotable: true,
|
|
90
|
+
signal,
|
|
91
|
+
sourceField: specific.name,
|
|
92
|
+
prompt: null,
|
|
93
|
+
example: null,
|
|
94
|
+
missingFields: [],
|
|
95
|
+
issue: null,
|
|
96
|
+
isGenericContext: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const config = CLARIFICATION_CONFIG[signal];
|
|
101
|
+
const issue = populated.length > 0 ? 'generic' : 'missing';
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
promotable: false,
|
|
105
|
+
signal,
|
|
106
|
+
sourceField: null,
|
|
107
|
+
prompt: config.prompt,
|
|
108
|
+
example: config.example,
|
|
109
|
+
missingFields: config.missingFields,
|
|
110
|
+
issue,
|
|
111
|
+
isGenericContext: populated.some((field) => field.name === 'context'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildClarificationMessage(params = {}) {
|
|
116
|
+
const assessment = assessFeedbackActionability(params);
|
|
117
|
+
if (assessment.promotable) return null;
|
|
118
|
+
|
|
119
|
+
const intro = assessment.signal === 'positive'
|
|
120
|
+
? 'Positive signal logged, but it is not specific enough to promote to reusable memory.'
|
|
121
|
+
: 'Negative signal logged, but it is not specific enough to promote to reusable memory.';
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
needsClarification: true,
|
|
125
|
+
prompt: assessment.prompt,
|
|
126
|
+
example: assessment.example,
|
|
127
|
+
missingFields: assessment.missingFields,
|
|
128
|
+
message: `${intro} ${assessment.prompt}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
GENERIC_PHRASE_RULES,
|
|
134
|
+
normalizeFeedbackSignal,
|
|
135
|
+
normalizeFeedbackText,
|
|
136
|
+
isGenericFeedbackText,
|
|
137
|
+
assessFeedbackActionability,
|
|
138
|
+
buildClarificationMessage,
|
|
139
|
+
};
|
|
@@ -12,6 +12,9 @@ const GENERIC_TAGS = new Set(['feedback', 'positive', 'negative']);
|
|
|
12
12
|
const MIN_CONTENT_LENGTH = 20;
|
|
13
13
|
const VALID_TITLE_PREFIXES = ['SUCCESS:', 'MISTAKE:', 'LEARNING:', 'PREFERENCE:'];
|
|
14
14
|
const VALID_CATEGORIES = new Set(['error', 'learning', 'preference']);
|
|
15
|
+
const {
|
|
16
|
+
assessFeedbackActionability,
|
|
17
|
+
} = require('./feedback-quality');
|
|
15
18
|
|
|
16
19
|
function validateFeedbackMemory(memory) {
|
|
17
20
|
const issues = [];
|
|
@@ -112,8 +115,16 @@ function resolveFeedbackAction(params) {
|
|
|
112
115
|
: [];
|
|
113
116
|
|
|
114
117
|
if (signal === 'negative') {
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
const actionability = assessFeedbackActionability({
|
|
119
|
+
signal: 'negative',
|
|
120
|
+
context,
|
|
121
|
+
whatWentWrong,
|
|
122
|
+
});
|
|
123
|
+
if (!actionability.promotable) {
|
|
124
|
+
const reason = actionability.issue === 'missing'
|
|
125
|
+
? 'Negative feedback without context — cannot determine what went wrong'
|
|
126
|
+
: 'Negative feedback is too vague to promote — describe what failed in one sentence';
|
|
127
|
+
return { type: 'no-action', reason };
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
const content = [
|
|
@@ -157,8 +168,16 @@ function resolveFeedbackAction(params) {
|
|
|
157
168
|
return { type: 'no-action', reason: `Rubric gate prevented promotion: ${reasons}` };
|
|
158
169
|
}
|
|
159
170
|
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
const actionability = assessFeedbackActionability({
|
|
172
|
+
signal: 'positive',
|
|
173
|
+
context,
|
|
174
|
+
whatWorked,
|
|
175
|
+
});
|
|
176
|
+
if (!actionability.promotable) {
|
|
177
|
+
const reason = actionability.issue === 'missing'
|
|
178
|
+
? 'Positive feedback without context — cannot determine what worked'
|
|
179
|
+
: 'Positive feedback is too vague to promote — describe what worked in one sentence';
|
|
180
|
+
return { type: 'no-action', reason };
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
const content = whatWorked ? `What worked: ${whatWorked}` : `Approach: ${context}`;
|
|
@@ -246,6 +265,13 @@ function runTests() {
|
|
|
246
265
|
const bareThumbsDown = resolveFeedbackAction({ signal: 'negative' });
|
|
247
266
|
assert(bareThumbsDown.type === 'no-action', 'bare negative feedback becomes no-action');
|
|
248
267
|
|
|
268
|
+
const vagueThumbsUp = resolveFeedbackAction({
|
|
269
|
+
signal: 'positive',
|
|
270
|
+
context: 'thumbs up',
|
|
271
|
+
tags: ['verification'],
|
|
272
|
+
});
|
|
273
|
+
assert(vagueThumbsUp.type === 'no-action', 'generic positive context becomes no-action');
|
|
274
|
+
|
|
249
275
|
const fullNegative = resolveFeedbackAction({
|
|
250
276
|
signal: 'negative',
|
|
251
277
|
context: 'Pushed code with no tests',
|
|
@@ -267,7 +293,7 @@ function runTests() {
|
|
|
267
293
|
|
|
268
294
|
const blockedPositive = resolveFeedbackAction({
|
|
269
295
|
signal: 'positive',
|
|
270
|
-
whatWorked: '
|
|
296
|
+
whatWorked: 'Manual approval happened without evidence',
|
|
271
297
|
tags: ['testing'],
|
|
272
298
|
rubricEvaluation: {
|
|
273
299
|
promotionEligible: false,
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
'use strict';
|
|
26
26
|
|
|
27
27
|
const { resolveFeedbackAction, prepareForStorage } = require('./feedback-schema');
|
|
28
|
+
const { buildClarificationMessage } = require('./feedback-quality');
|
|
28
29
|
|
|
29
30
|
function convertFeedbackToMemory(params) {
|
|
30
31
|
const action = resolveFeedbackAction({
|
|
@@ -37,7 +38,18 @@ function convertFeedbackToMemory(params) {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
if (!action || action.type === 'no-action') {
|
|
40
|
-
|
|
41
|
+
const clarification = buildClarificationMessage({
|
|
42
|
+
signal: params.signal,
|
|
43
|
+
context: params.context || '',
|
|
44
|
+
whatWentWrong: params.whatWentWrong,
|
|
45
|
+
whatToChange: params.whatToChange,
|
|
46
|
+
whatWorked: params.whatWorked,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: action ? action.reason : 'Unknown action resolution failure',
|
|
51
|
+
...(clarification || {}),
|
|
52
|
+
};
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
const prep = prepareForStorage(action.memory);
|
|
@@ -17,6 +17,12 @@ capture_and_report() {
|
|
|
17
17
|
|
|
18
18
|
# Capture feedback (verbose output already shows IDs, signal, storage)
|
|
19
19
|
node "$CAPTURE" --feedback="$SIGNAL" --context="$PROMPT" --tags="auto-capture,hook"
|
|
20
|
+
local CAPTURE_STATUS=$?
|
|
21
|
+
|
|
22
|
+
if [ "$CAPTURE_STATUS" -eq 2 ]; then
|
|
23
|
+
echo "Reusable memory status: signal logged only. Add one specific sentence so the MCP can promote it."
|
|
24
|
+
echo ""
|
|
25
|
+
fi
|
|
20
26
|
|
|
21
27
|
# Show storage proof
|
|
22
28
|
echo ""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Code / Amp Stop hook — autonomous self-scoring after every agent turn
|
|
3
|
+
# Fires after the agent completes a response. Runs selfAuditAndLog to produce
|
|
4
|
+
# a RLAIF self-score entry in self-score-log.jsonl.
|
|
5
|
+
#
|
|
6
|
+
# Environment variables available in Stop hooks:
|
|
7
|
+
# CLAUDE_STOP_REASON — why the agent stopped (e.g., "end_turn", "tool_use")
|
|
8
|
+
# CLAUDE_TOOL_OUTPUT — last tool output (if any)
|
|
9
|
+
#
|
|
10
|
+
# This hook is NON-BLOCKING — it exits 0 regardless of errors.
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
RLHF_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
14
|
+
|
|
15
|
+
# Run the self-score via Node.js — sync, no API calls, ~5ms
|
|
16
|
+
node -e '
|
|
17
|
+
"use strict";
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
// Resolve modules relative to RLHF package root
|
|
21
|
+
const rlhfRoot = process.env.RLHF_ROOT;
|
|
22
|
+
const { selfAuditAndLog } = require(path.join(rlhfRoot, "scripts", "rlaif-self-audit"));
|
|
23
|
+
const { getFeedbackPaths } = require(path.join(rlhfRoot, "scripts", "feedback-loop"));
|
|
24
|
+
|
|
25
|
+
const stopReason = process.env.CLAUDE_STOP_REASON || "unknown";
|
|
26
|
+
|
|
27
|
+
// Build a minimal feedback event for self-scoring
|
|
28
|
+
const feedbackEvent = {
|
|
29
|
+
id: `stop_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
30
|
+
signal: "positive",
|
|
31
|
+
context: `Agent turn completed (stop_reason: ${stopReason}). Autonomous self-score checkpoint.`,
|
|
32
|
+
tags: ["stop-hook", "auto-score"],
|
|
33
|
+
whatWorked: null,
|
|
34
|
+
whatWentWrong: null,
|
|
35
|
+
whatToChange: null,
|
|
36
|
+
rubric: null,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const paths = getFeedbackPaths();
|
|
40
|
+
const result = selfAuditAndLog(feedbackEvent, paths);
|
|
41
|
+
|
|
42
|
+
// Output minimal JSON for hook response (non-blocking)
|
|
43
|
+
process.stdout.write(JSON.stringify({
|
|
44
|
+
hookSpecificOutput: {
|
|
45
|
+
hookEventName: "Stop",
|
|
46
|
+
additionalContext: `Self-score: ${result.score} (${result.constraints.filter(c => c.passed).length}/${result.constraints.length} constraints passed)`
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
' 2>/dev/null || true
|
|
50
|
+
|
|
51
|
+
exit 0
|