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,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getActiveMcpProfile, getAllowedTools } = require('./mcp-policy');
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
7
|
+
const DEFAULT_BUNDLE_DIR = path.join(PROJECT_ROOT, 'config', 'policy-bundles');
|
|
8
|
+
const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
9
|
+
|
|
10
|
+
function getDefaultBundleId() {
|
|
11
|
+
return process.env.RLHF_POLICY_BUNDLE || 'default-v1';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getBundlePath(bundleId = getDefaultBundleId()) {
|
|
15
|
+
if (process.env.RLHF_POLICY_BUNDLE_PATH) {
|
|
16
|
+
return process.env.RLHF_POLICY_BUNDLE_PATH;
|
|
17
|
+
}
|
|
18
|
+
return path.join(DEFAULT_BUNDLE_DIR, `${bundleId}.json`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateBundle(bundle) {
|
|
22
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
23
|
+
throw new Error('Invalid policy bundle: expected object');
|
|
24
|
+
}
|
|
25
|
+
if (!bundle.bundleId || typeof bundle.bundleId !== 'string') {
|
|
26
|
+
throw new Error('Invalid policy bundle: missing bundleId');
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(bundle.intents) || bundle.intents.length === 0) {
|
|
29
|
+
throw new Error('Invalid policy bundle: intents must be a non-empty array');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bundle.intents.forEach((intent) => {
|
|
33
|
+
if (!intent.id || typeof intent.id !== 'string') {
|
|
34
|
+
throw new Error('Invalid policy bundle: intent id is required');
|
|
35
|
+
}
|
|
36
|
+
if (!RISK_LEVELS.includes(intent.risk)) {
|
|
37
|
+
throw new Error(`Invalid policy bundle: unsupported risk '${intent.risk}' for intent '${intent.id}'`);
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(intent.actions) || intent.actions.length === 0) {
|
|
40
|
+
throw new Error(`Invalid policy bundle: intent '${intent.id}' must define actions`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadPolicyBundle(bundleId = getDefaultBundleId()) {
|
|
48
|
+
const raw = fs.readFileSync(getBundlePath(bundleId), 'utf-8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
validateBundle(parsed);
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRequiredApprovalRisks(bundle, mcpProfile) {
|
|
55
|
+
const approval = bundle.approval || {};
|
|
56
|
+
if (approval.profileOverrides && Array.isArray(approval.profileOverrides[mcpProfile])) {
|
|
57
|
+
return approval.profileOverrides[mcpProfile];
|
|
58
|
+
}
|
|
59
|
+
return Array.isArray(approval.requiredRisks) ? approval.requiredRisks : ['high', 'critical'];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertKnownMcpProfile(profile) {
|
|
63
|
+
getAllowedTools(profile);
|
|
64
|
+
return profile;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listIntents(options = {}) {
|
|
68
|
+
const bundle = loadPolicyBundle(options.bundleId);
|
|
69
|
+
const profile = assertKnownMcpProfile(options.mcpProfile || getActiveMcpProfile());
|
|
70
|
+
const requiredRisks = getRequiredApprovalRisks(bundle, profile);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
bundleId: bundle.bundleId,
|
|
74
|
+
mcpProfile: profile,
|
|
75
|
+
intents: bundle.intents.map((intent) => ({
|
|
76
|
+
id: intent.id,
|
|
77
|
+
description: intent.description,
|
|
78
|
+
risk: intent.risk,
|
|
79
|
+
actionCount: intent.actions.length,
|
|
80
|
+
requiresApproval: requiredRisks.includes(intent.risk),
|
|
81
|
+
})),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function planIntent(options = {}) {
|
|
86
|
+
const bundle = loadPolicyBundle(options.bundleId);
|
|
87
|
+
const profile = assertKnownMcpProfile(options.mcpProfile || getActiveMcpProfile());
|
|
88
|
+
const intentId = String(options.intentId || '').trim();
|
|
89
|
+
const context = String(options.context || '').trim();
|
|
90
|
+
const approved = options.approved === true;
|
|
91
|
+
|
|
92
|
+
if (!intentId) {
|
|
93
|
+
throw new Error('intentId is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const intent = bundle.intents.find((item) => item.id === intentId);
|
|
97
|
+
if (!intent) {
|
|
98
|
+
throw new Error(`Unknown intent: ${intentId}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const requiredRisks = getRequiredApprovalRisks(bundle, profile);
|
|
102
|
+
const requiresApproval = requiredRisks.includes(intent.risk);
|
|
103
|
+
const checkpointRequired = requiresApproval && !approved;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
bundleId: bundle.bundleId,
|
|
107
|
+
mcpProfile: profile,
|
|
108
|
+
generatedAt: new Date().toISOString(),
|
|
109
|
+
status: checkpointRequired ? 'checkpoint_required' : 'ready',
|
|
110
|
+
intent: {
|
|
111
|
+
id: intent.id,
|
|
112
|
+
description: intent.description,
|
|
113
|
+
risk: intent.risk,
|
|
114
|
+
},
|
|
115
|
+
context,
|
|
116
|
+
requiresApproval,
|
|
117
|
+
approved,
|
|
118
|
+
checkpoint: checkpointRequired
|
|
119
|
+
? {
|
|
120
|
+
type: 'human_approval',
|
|
121
|
+
reason: `Intent '${intent.id}' has risk '${intent.risk}' under profile '${profile}'.`,
|
|
122
|
+
requiredForRiskLevels: requiredRisks,
|
|
123
|
+
}
|
|
124
|
+
: null,
|
|
125
|
+
actions: intent.actions,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
DEFAULT_BUNDLE_DIR,
|
|
131
|
+
RISK_LEVELS,
|
|
132
|
+
getDefaultBundleId,
|
|
133
|
+
getBundlePath,
|
|
134
|
+
validateBundle,
|
|
135
|
+
loadPolicyBundle,
|
|
136
|
+
getRequiredApprovalRisks,
|
|
137
|
+
assertKnownMcpProfile,
|
|
138
|
+
listIntents,
|
|
139
|
+
planIntent,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (require.main === module) {
|
|
143
|
+
const args = process.argv.slice(2);
|
|
144
|
+
const intentArg = args.find((arg) => arg.startsWith('--intent='));
|
|
145
|
+
const profileArg = args.find((arg) => arg.startsWith('--profile='));
|
|
146
|
+
const bundleArg = args.find((arg) => arg.startsWith('--bundle='));
|
|
147
|
+
const approved = args.includes('--approved');
|
|
148
|
+
|
|
149
|
+
if (!intentArg) {
|
|
150
|
+
console.log(JSON.stringify(listIntents({
|
|
151
|
+
mcpProfile: profileArg ? profileArg.replace('--profile=', '') : undefined,
|
|
152
|
+
bundleId: bundleArg ? bundleArg.replace('--bundle=', '') : undefined,
|
|
153
|
+
}), null, 2));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const plan = planIntent({
|
|
158
|
+
intentId: intentArg.replace('--intent=', ''),
|
|
159
|
+
mcpProfile: profileArg ? profileArg.replace('--profile=', '') : undefined,
|
|
160
|
+
bundleId: bundleArg ? bundleArg.replace('--bundle=', '') : undefined,
|
|
161
|
+
approved,
|
|
162
|
+
});
|
|
163
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
164
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
6
|
+
const DEFAULT_POLICY_PATH = path.join(PROJECT_ROOT, 'config', 'mcp-allowlists.json');
|
|
7
|
+
const DEFAULT_SUBAGENT_PROFILE_PATH = path.join(PROJECT_ROOT, 'config', 'subagent-profiles.json');
|
|
8
|
+
|
|
9
|
+
function getPolicyPath() {
|
|
10
|
+
return process.env.RLHF_MCP_POLICY_PATH || DEFAULT_POLICY_PATH;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadMcpPolicy() {
|
|
14
|
+
const policyPath = getPolicyPath();
|
|
15
|
+
const raw = fs.readFileSync(policyPath, 'utf-8');
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
if (!parsed.profiles || typeof parsed.profiles !== 'object') {
|
|
18
|
+
throw new Error('Invalid MCP policy: missing profiles object');
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadSubagentProfiles() {
|
|
24
|
+
const profilePath = process.env.RLHF_SUBAGENT_PROFILE_PATH || DEFAULT_SUBAGENT_PROFILE_PATH;
|
|
25
|
+
const raw = fs.readFileSync(profilePath, 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (!parsed.profiles || typeof parsed.profiles !== 'object') {
|
|
28
|
+
throw new Error('Invalid subagent profile config: missing profiles object');
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getActiveMcpProfile() {
|
|
34
|
+
const explicitProfile = process.env.RLHF_MCP_PROFILE || null;
|
|
35
|
+
const runtimeSubagentProfile = process.env.RLHF_SUBAGENT_PROFILE || null;
|
|
36
|
+
|
|
37
|
+
if (!runtimeSubagentProfile) {
|
|
38
|
+
return explicitProfile || 'default';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = loadSubagentProfiles();
|
|
42
|
+
const subagent = config.profiles[runtimeSubagentProfile];
|
|
43
|
+
if (!subagent || !subagent.mcpProfile) {
|
|
44
|
+
throw new Error(`Unknown subagent profile: ${runtimeSubagentProfile}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (explicitProfile && explicitProfile !== subagent.mcpProfile) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`MCP profile conflict: RLHF_MCP_PROFILE='${explicitProfile}' does not match subagent profile '${runtimeSubagentProfile}' (${subagent.mcpProfile})`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return subagent.mcpProfile;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getAllowedTools(profileName = getActiveMcpProfile()) {
|
|
57
|
+
const policy = loadMcpPolicy();
|
|
58
|
+
const tools = policy.profiles[profileName];
|
|
59
|
+
if (!tools) {
|
|
60
|
+
throw new Error(`Unknown MCP profile: ${profileName}`);
|
|
61
|
+
}
|
|
62
|
+
return tools;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isToolAllowed(toolName, profileName = getActiveMcpProfile()) {
|
|
66
|
+
const allowed = getAllowedTools(profileName);
|
|
67
|
+
return allowed.includes(toolName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertToolAllowed(toolName, profileName = getActiveMcpProfile()) {
|
|
71
|
+
if (!isToolAllowed(toolName, profileName)) {
|
|
72
|
+
throw new Error(`Tool '${toolName}' is not allowed in MCP profile '${profileName}'`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
DEFAULT_POLICY_PATH,
|
|
78
|
+
getPolicyPath,
|
|
79
|
+
loadMcpPolicy,
|
|
80
|
+
loadSubagentProfiles,
|
|
81
|
+
getActiveMcpProfile,
|
|
82
|
+
getAllowedTools,
|
|
83
|
+
isToolAllowed,
|
|
84
|
+
assertToolAllowed,
|
|
85
|
+
DEFAULT_SUBAGENT_PROFILE_PATH,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (require.main === module) {
|
|
89
|
+
const profile = getActiveMcpProfile();
|
|
90
|
+
const tools = getAllowedTools(profile);
|
|
91
|
+
console.log(JSON.stringify({ profile, tools }, null, 2));
|
|
92
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Meta-Policy Rule Extraction (DPO-03)
|
|
5
|
+
*
|
|
6
|
+
* Reads memory-log.jsonl, groups negative memories by domain, computes
|
|
7
|
+
* recency-weighted confidence scores, detects trend direction, and writes
|
|
8
|
+
* meta-policy-rules.json.
|
|
9
|
+
*
|
|
10
|
+
* Output file: {RLHF_FEEDBACK_DIR}/meta-policy-rules.json
|
|
11
|
+
*
|
|
12
|
+
* This is a different artifact from prevention-rules.md (simpler occurrence counts).
|
|
13
|
+
* Meta-policy rules have confidence + trend + recency weighting.
|
|
14
|
+
*
|
|
15
|
+
* Min occurrences threshold: 2 (consistent with buildPreventionRules())
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const { parseTimestamp } = require('./feedback-schema');
|
|
22
|
+
const { timeDecayWeight } = require('./thompson-sampling');
|
|
23
|
+
const { inferDomain } = require('./feedback-loop');
|
|
24
|
+
|
|
25
|
+
const MIN_OCCURRENCES = 2;
|
|
26
|
+
const RECENT_DAYS = 7;
|
|
27
|
+
const RECENT_MS = RECENT_DAYS * 24 * 3600 * 1000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract meta-policy rules from memory-log.jsonl feedback trends.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
* @param {string} [opts.feedbackDir] - Override feedback directory (default: RLHF_FEEDBACK_DIR or ~/.claude/memory/feedback)
|
|
34
|
+
* @returns {Array<{category: string, confidence: number, trend: string, occurrence_count: number, last_seen: string}>}
|
|
35
|
+
*/
|
|
36
|
+
function extractMetaPolicyRules(opts = {}) {
|
|
37
|
+
const feedbackDir = opts.feedbackDir
|
|
38
|
+
|| process.env.RLHF_FEEDBACK_DIR
|
|
39
|
+
|| path.join(os.homedir(), '.claude', 'memory', 'feedback');
|
|
40
|
+
|
|
41
|
+
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(memoryLogPath)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const raw = fs.readFileSync(memoryLogPath, 'utf-8').trim();
|
|
48
|
+
if (!raw) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parse all entries, skip malformed lines
|
|
53
|
+
const allEntries = raw.split('\n').reduce((acc, line) => {
|
|
54
|
+
if (!line.trim()) return acc;
|
|
55
|
+
try {
|
|
56
|
+
acc.push(JSON.parse(line));
|
|
57
|
+
} catch {
|
|
58
|
+
process.stderr.write(`meta-policy: skipping malformed line: ${line.slice(0, 80)}\n`);
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
if (allEntries.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Filter to negative memories only
|
|
68
|
+
const negativeEntries = allEntries.filter(
|
|
69
|
+
(e) => e.signal === 'negative' || e.feedback === 'down',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (negativeEntries.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Group negative entries by domain
|
|
77
|
+
const domainMap = new Map();
|
|
78
|
+
for (const entry of negativeEntries) {
|
|
79
|
+
const domain = inferDomain(entry.tags, entry.context);
|
|
80
|
+
if (!domainMap.has(domain)) {
|
|
81
|
+
domainMap.set(domain, []);
|
|
82
|
+
}
|
|
83
|
+
domainMap.get(domain).push(entry);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build rules for domains with enough occurrences
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const rules = [];
|
|
89
|
+
|
|
90
|
+
for (const [domain, entries] of domainMap) {
|
|
91
|
+
if (entries.length < MIN_OCCURRENCES) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Compute avg time-decay weight across all negative entries
|
|
96
|
+
const weights = entries.map((e) => timeDecayWeight(e.timestamp));
|
|
97
|
+
const avg_weighted = weights.reduce((sum, w) => sum + w, 0) / weights.length;
|
|
98
|
+
|
|
99
|
+
// Count recent negative entries (last 7 days)
|
|
100
|
+
const recent_entries = entries.filter((e) => {
|
|
101
|
+
const ts = parseTimestamp(e.timestamp);
|
|
102
|
+
return ts && (now - ts.getTime()) < RECENT_MS;
|
|
103
|
+
}).length;
|
|
104
|
+
|
|
105
|
+
// Count recent positive entries for same domain (from full allEntries log)
|
|
106
|
+
const recent_positive = allEntries.filter((e) => {
|
|
107
|
+
if (e.signal !== 'positive' && e.feedback !== 'up') return false;
|
|
108
|
+
const entryDomain = inferDomain(e.tags, e.context);
|
|
109
|
+
if (entryDomain !== domain) return false;
|
|
110
|
+
const ts = parseTimestamp(e.timestamp);
|
|
111
|
+
return ts && (now - ts.getTime()) < RECENT_MS;
|
|
112
|
+
}).length;
|
|
113
|
+
|
|
114
|
+
// Compute confidence: min(0.95, 0.4 + (avg_weighted * 0.3) + (occurrence_count * 0.05))
|
|
115
|
+
const confidence = Math.min(
|
|
116
|
+
0.95,
|
|
117
|
+
0.4 + (avg_weighted * 0.3) + (entries.length * 0.05),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Determine trend
|
|
121
|
+
let trend;
|
|
122
|
+
if (recent_entries === 0 && recent_positive > 0) {
|
|
123
|
+
trend = 'improving';
|
|
124
|
+
} else if (recent_entries > 2 && recent_positive === 0) {
|
|
125
|
+
trend = 'deteriorating';
|
|
126
|
+
} else if (recent_entries > recent_positive) {
|
|
127
|
+
trend = 'needs_attention';
|
|
128
|
+
} else {
|
|
129
|
+
trend = 'stable';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Find most recent entry timestamp
|
|
133
|
+
const timestamps = entries
|
|
134
|
+
.map((e) => parseTimestamp(e.timestamp))
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.sort((a, b) => b.getTime() - a.getTime());
|
|
137
|
+
const last_seen = timestamps.length > 0
|
|
138
|
+
? timestamps[0].toISOString()
|
|
139
|
+
: new Date().toISOString();
|
|
140
|
+
|
|
141
|
+
rules.push({
|
|
142
|
+
category: domain,
|
|
143
|
+
confidence: Math.round(confidence * 1000) / 1000,
|
|
144
|
+
trend,
|
|
145
|
+
occurrence_count: entries.length,
|
|
146
|
+
last_seen,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Sort by confidence descending (most urgent first)
|
|
151
|
+
rules.sort((a, b) => b.confidence - a.confidence);
|
|
152
|
+
|
|
153
|
+
return rules;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run meta-policy rule extraction and write results to meta-policy-rules.json.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} opts - Same as extractMetaPolicyRules opts
|
|
160
|
+
* @returns {{ rules: Array, outputPath: string }}
|
|
161
|
+
*/
|
|
162
|
+
function run(opts = {}) {
|
|
163
|
+
const feedbackDir = opts.feedbackDir
|
|
164
|
+
|| process.env.RLHF_FEEDBACK_DIR
|
|
165
|
+
|| path.join(os.homedir(), '.claude', 'memory', 'feedback');
|
|
166
|
+
|
|
167
|
+
const rules = extractMetaPolicyRules({ ...opts, feedbackDir });
|
|
168
|
+
|
|
169
|
+
// Ensure output directory exists
|
|
170
|
+
if (!fs.existsSync(feedbackDir)) {
|
|
171
|
+
fs.mkdirSync(feedbackDir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Write to meta-policy-rules.json — NOT to prevention-rules.md (see RESEARCH.md Pitfall 4)
|
|
175
|
+
const outputPath = path.join(feedbackDir, 'meta-policy-rules.json');
|
|
176
|
+
fs.writeFileSync(
|
|
177
|
+
outputPath,
|
|
178
|
+
JSON.stringify({ generated: new Date().toISOString(), rules }, null, 2),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
console.log(`meta-policy: extracted ${rules.length} rules`);
|
|
182
|
+
return { rules, outputPath };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { extractMetaPolicyRules, run };
|
|
186
|
+
|
|
187
|
+
if (require.main === module && process.argv.includes('--extract')) {
|
|
188
|
+
try {
|
|
189
|
+
run();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error(e);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Gate validators
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function countTableRows(content, sectionHeading) {
|
|
12
|
+
const sectionRegex = new RegExp(
|
|
13
|
+
`#+\\s*${sectionHeading}[^\\n]*\\n([\\s\\S]*?)(?=\\n#+\\s|$)`,
|
|
14
|
+
);
|
|
15
|
+
const match = content.match(sectionRegex);
|
|
16
|
+
if (!match) return 0;
|
|
17
|
+
|
|
18
|
+
const lines = match[1].split('\n').filter((l) => l.trim().startsWith('|'));
|
|
19
|
+
// Subtract header row and separator row
|
|
20
|
+
const dataRows = lines.filter(
|
|
21
|
+
(l) => !/^\|\s*-+/.test(l.trim()) && !/^\|\s*:?-+/.test(l.trim()),
|
|
22
|
+
);
|
|
23
|
+
// First row is the header
|
|
24
|
+
return Math.max(0, dataRows.length - 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function countContracts(content) {
|
|
28
|
+
const sectionRegex = /#+\s*Contracts[^\n]*\n([\s\S]*?)(?=\n#+\s|$)/;
|
|
29
|
+
const match = content.match(sectionRegex);
|
|
30
|
+
if (!match) return 0;
|
|
31
|
+
|
|
32
|
+
const section = match[1];
|
|
33
|
+
// Find code blocks and look for interface/type keywords inside them
|
|
34
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
35
|
+
let count = 0;
|
|
36
|
+
let blockMatch;
|
|
37
|
+
while ((blockMatch = codeBlockRegex.exec(section)) !== null) {
|
|
38
|
+
const block = blockMatch[0];
|
|
39
|
+
const interfaceMatches = block.match(/\b(interface|type)\s+\w+/g);
|
|
40
|
+
if (interfaceMatches) count += interfaceMatches.length;
|
|
41
|
+
}
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function countValidationScenarios(content) {
|
|
46
|
+
const sectionRegex =
|
|
47
|
+
/#+\s*Validation\s+Checklist[^\n]*\n([\s\S]*?)(?=\n#+\s|$)/;
|
|
48
|
+
const match = content.match(sectionRegex);
|
|
49
|
+
if (!match) return 0;
|
|
50
|
+
|
|
51
|
+
const lines = match[1].split('\n');
|
|
52
|
+
return lines.filter((l) => /^\s*-\s*\[\s*\]/.test(l)).length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStatus(content) {
|
|
56
|
+
const match = content.match(/#+\s*Status[^\n]*\n\s*(\S+)/);
|
|
57
|
+
return match ? match[1].trim() : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Main
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function validatePlan(content) {
|
|
65
|
+
const questionCount = countTableRows(content, 'Clarifying Questions Resolved');
|
|
66
|
+
const contractCount = countContracts(content);
|
|
67
|
+
const scenarioCount = countValidationScenarios(content);
|
|
68
|
+
const status = getStatus(content);
|
|
69
|
+
|
|
70
|
+
const gates = [
|
|
71
|
+
{
|
|
72
|
+
name: 'Clarifying Questions',
|
|
73
|
+
pass: questionCount >= 3,
|
|
74
|
+
detail: `${questionCount} questions resolved`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Contracts Defined',
|
|
78
|
+
pass: contractCount >= 1,
|
|
79
|
+
detail: `${contractCount} interface${contractCount !== 1 ? 's' : ''} found`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Validation Checklist',
|
|
83
|
+
pass: scenarioCount >= 2,
|
|
84
|
+
detail: `${scenarioCount} scenarios defined`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Status',
|
|
88
|
+
pass: status !== 'COMPLETE',
|
|
89
|
+
detail:
|
|
90
|
+
status === 'COMPLETE'
|
|
91
|
+
? 'COMPLETE (already finished — cannot re-approve)'
|
|
92
|
+
: `${status || 'UNKNOWN'} (not COMPLETE)`,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const allPass = gates.every((g) => g.pass);
|
|
97
|
+
return { gates, allPass };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatReport(result) {
|
|
101
|
+
const lines = result.gates.map(
|
|
102
|
+
(g) => `${g.pass ? '✅' : '❌'} ${g.name}: ${g.detail}`,
|
|
103
|
+
);
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(
|
|
106
|
+
result.allPass
|
|
107
|
+
? 'RESULT: PASS — all gates satisfied'
|
|
108
|
+
: 'RESULT: BLOCKED — resolve issues above before spawning agents',
|
|
109
|
+
);
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function run() {
|
|
114
|
+
const args = process.argv.slice(2);
|
|
115
|
+
const jsonFlag = args.includes('--json');
|
|
116
|
+
const filePath = args.find((a) => a !== '--json');
|
|
117
|
+
|
|
118
|
+
if (!filePath) {
|
|
119
|
+
console.error('Usage: node scripts/plan-gate.js <plan-file.md> [--json]');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resolved = path.resolve(filePath);
|
|
124
|
+
if (!fs.existsSync(resolved)) {
|
|
125
|
+
console.error(`File not found: ${resolved}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
130
|
+
const result = validatePlan(content);
|
|
131
|
+
|
|
132
|
+
if (jsonFlag) {
|
|
133
|
+
console.log(JSON.stringify(result, null, 2));
|
|
134
|
+
} else {
|
|
135
|
+
console.log(formatReport(result));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
process.exit(result.allPass ? 0 : 1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Export for testing
|
|
142
|
+
module.exports = {
|
|
143
|
+
validatePlan,
|
|
144
|
+
formatReport,
|
|
145
|
+
countTableRows,
|
|
146
|
+
countContracts,
|
|
147
|
+
countValidationScenarios,
|
|
148
|
+
getStatus,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Run only when executed directly
|
|
152
|
+
if (require.main === module) {
|
|
153
|
+
run();
|
|
154
|
+
}
|