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.
Files changed (42) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +116 -74
  3. package/adapters/README.md +3 -3
  4. package/adapters/amp/skills/rlhf-feedback/SKILL.md +2 -0
  5. package/adapters/chatgpt/INSTALL.md +6 -3
  6. package/adapters/chatgpt/openapi.yaml +5 -2
  7. package/adapters/claude/.mcp.json +3 -3
  8. package/adapters/codex/config.toml +3 -3
  9. package/adapters/gemini/function-declarations.json +2 -2
  10. package/adapters/mcp/server-stdio.js +19 -5
  11. package/bin/cli.js +295 -25
  12. package/openapi/openapi.yaml +5 -2
  13. package/package.json +23 -10
  14. package/scripts/a2ui-engine.js +73 -0
  15. package/scripts/adk-consolidator.js +126 -32
  16. package/scripts/billing.js +192 -685
  17. package/scripts/context-engine.js +81 -0
  18. package/scripts/export-kto-pairs.js +310 -0
  19. package/scripts/feedback-ingest-watcher.js +290 -0
  20. package/scripts/feedback-loop.js +153 -8
  21. package/scripts/feedback-quality.js +139 -0
  22. package/scripts/feedback-schema.js +31 -5
  23. package/scripts/feedback-to-memory.js +13 -1
  24. package/scripts/hook-auto-capture.sh +6 -0
  25. package/scripts/hook-stop-self-score.sh +51 -0
  26. package/scripts/install-mcp.js +168 -0
  27. package/scripts/jsonl-watcher.js +151 -0
  28. package/scripts/local-model-profile.js +207 -0
  29. package/scripts/pr-manager.js +112 -0
  30. package/scripts/prove-adapters.js +137 -15
  31. package/scripts/prove-automation.js +41 -8
  32. package/scripts/prove-lancedb.js +1 -1
  33. package/scripts/prove-local-intelligence.js +244 -0
  34. package/scripts/prove-workflow-contract.js +116 -0
  35. package/scripts/reminder-engine.js +132 -0
  36. package/scripts/risk-scorer.js +458 -0
  37. package/scripts/rlaif-self-audit.js +7 -1
  38. package/scripts/status-dashboard.js +155 -0
  39. package/scripts/test-coverage.js +1 -1
  40. package/scripts/validate-workflow-contract.js +287 -0
  41. package/scripts/vector-store.js +115 -17
  42. package/src/api/server.js +372 -25
@@ -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 = readJSONL(paths.FEEDBACK_LOG_PATH).slice(-SEQUENCE_WINDOW);
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).catch(() => {
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
- if (!whatWentWrong && !context) {
116
- return { type: 'no-action', reason: 'Negative feedback without context — cannot determine what went wrong' };
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
- if (!whatWorked && !context) {
161
- return { type: 'no-action', reason: 'Positive feedback without context — cannot determine what worked' };
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: 'Looked correct',
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
- return { ok: false, reason: action ? action.reason : 'Unknown action resolution failure' };
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