zouroboros-selfheal 2.0.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/LICENSE +21 -0
- package/dist/cli/evolve.d.ts +7 -0
- package/dist/cli/evolve.js +64 -0
- package/dist/cli/introspect.d.ts +7 -0
- package/dist/cli/introspect.js +61 -0
- package/dist/cli/prescribe.d.ts +7 -0
- package/dist/cli/prescribe.js +72 -0
- package/dist/evolve/executor.d.ts +8 -0
- package/dist/evolve/executor.js +133 -0
- package/dist/feedback.d.ts +51 -0
- package/dist/feedback.js +160 -0
- package/dist/history.d.ts +71 -0
- package/dist/history.js +179 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +94 -0
- package/dist/introspect/collector.d.ts +10 -0
- package/dist/introspect/collector.js +182 -0
- package/dist/introspect/scorecard.d.ts +6 -0
- package/dist/introspect/scorecard.js +75 -0
- package/dist/multi-metric.d.ts +53 -0
- package/dist/multi-metric.js +200 -0
- package/dist/prescribe/governor.d.ts +5 -0
- package/dist/prescribe/governor.js +44 -0
- package/dist/prescribe/playbook.d.ts +6 -0
- package/dist/prescribe/playbook.js +179 -0
- package/dist/prescribe/seed.d.ts +6 -0
- package/dist/prescribe/seed.js +115 -0
- package/dist/templates.d.ts +49 -0
- package/dist/templates.js +162 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +6 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 marlandoj
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for Zouroboros evolve — execute prescriptions with regression detection
|
|
4
|
+
*
|
|
5
|
+
* Usage: zouroboros-evolve [--prescription <path>] [--dry-run] [--skip-governor]
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'util';
|
|
8
|
+
import { evolve } from '../index.js';
|
|
9
|
+
const { values } = parseArgs({
|
|
10
|
+
args: Bun.argv.slice(2),
|
|
11
|
+
options: {
|
|
12
|
+
prescription: { type: 'string', short: 'p' },
|
|
13
|
+
'dry-run': { type: 'boolean' },
|
|
14
|
+
'skip-governor': { type: 'boolean' },
|
|
15
|
+
help: { type: 'boolean', short: 'h' },
|
|
16
|
+
},
|
|
17
|
+
strict: false,
|
|
18
|
+
});
|
|
19
|
+
if (values.help) {
|
|
20
|
+
console.log(`
|
|
21
|
+
zouroboros-evolve — execute prescriptions with regression detection
|
|
22
|
+
|
|
23
|
+
USAGE:
|
|
24
|
+
zouroboros-evolve [options]
|
|
25
|
+
|
|
26
|
+
OPTIONS:
|
|
27
|
+
--prescription, -p Path to prescription JSON (default: run prescribe first)
|
|
28
|
+
--dry-run Show what would be executed without making changes
|
|
29
|
+
--skip-governor Bypass the governor safety gate (use with caution)
|
|
30
|
+
--help, -h Show this help
|
|
31
|
+
|
|
32
|
+
SAFETY:
|
|
33
|
+
The governor gate blocks prescriptions with high regression risk.
|
|
34
|
+
Use --skip-governor only for manual overrides after review.
|
|
35
|
+
All evolutions are git-committed for rollback capability.
|
|
36
|
+
|
|
37
|
+
EXAMPLES:
|
|
38
|
+
zouroboros-evolve --prescription ./prescription.json
|
|
39
|
+
zouroboros-evolve --dry-run
|
|
40
|
+
zouroboros-evolve --prescription ./rx.json --skip-governor
|
|
41
|
+
`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const result = await evolve({
|
|
46
|
+
prescription: values.prescription,
|
|
47
|
+
dryRun: !!values['dry-run'],
|
|
48
|
+
skipGovernor: !!values['skip-governor'],
|
|
49
|
+
});
|
|
50
|
+
if (result.success) {
|
|
51
|
+
console.log(`✓ Evolution complete`);
|
|
52
|
+
console.log(` Delta: ${(result.delta * 100).toFixed(1)}%`);
|
|
53
|
+
console.log(` Reverted: ${result.reverted ? 'yes' : 'no'}`);
|
|
54
|
+
console.log(` Detail: ${result.detail}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error(`✗ Evolution failed: ${result.detail}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
main().catch((err) => {
|
|
62
|
+
console.error('Evolve failed:', err.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for Zouroboros introspection — 7-metric health scorecard
|
|
4
|
+
*
|
|
5
|
+
* Usage: zouroboros-introspect [--json] [--store] [--verbose]
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'util';
|
|
8
|
+
import { introspect } from '../index.js';
|
|
9
|
+
import { formatScorecard } from '../introspect/scorecard.js';
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
args: Bun.argv.slice(2),
|
|
12
|
+
options: {
|
|
13
|
+
json: { type: 'boolean', short: 'j' },
|
|
14
|
+
store: { type: 'boolean', short: 's' },
|
|
15
|
+
verbose: { type: 'boolean', short: 'v' },
|
|
16
|
+
help: { type: 'boolean', short: 'h' },
|
|
17
|
+
},
|
|
18
|
+
strict: false,
|
|
19
|
+
});
|
|
20
|
+
if (values.help) {
|
|
21
|
+
console.log(`
|
|
22
|
+
zouroboros-introspect — 7-metric health scorecard for Zo ecosystem
|
|
23
|
+
|
|
24
|
+
USAGE:
|
|
25
|
+
zouroboros-introspect [options]
|
|
26
|
+
|
|
27
|
+
OPTIONS:
|
|
28
|
+
--json, -j Output raw JSON scorecard
|
|
29
|
+
--store, -s Persist scorecard to ~/.zo/selfheal/
|
|
30
|
+
--verbose, -v Print formatted scorecard table
|
|
31
|
+
--help, -h Show this help
|
|
32
|
+
|
|
33
|
+
METRICS:
|
|
34
|
+
memory_health WAL size, episode count, decay distribution
|
|
35
|
+
skill_coverage Skills with SKILL.md, scripts, references
|
|
36
|
+
eval_density Evaluations per seed in last 30 days
|
|
37
|
+
swarm_success Swarm procedure pass rate
|
|
38
|
+
persona_depth Persona completeness (SOUL, rules, memory)
|
|
39
|
+
graph_connectivity Cross-entity link density in knowledge graph
|
|
40
|
+
self_heal_cadence Days since last introspect→prescribe→evolve cycle
|
|
41
|
+
|
|
42
|
+
EXAMPLES:
|
|
43
|
+
zouroboros-introspect --verbose
|
|
44
|
+
zouroboros-introspect --json --store
|
|
45
|
+
`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
const scorecard = await introspect({
|
|
50
|
+
json: !!values.json,
|
|
51
|
+
store: !!values.store,
|
|
52
|
+
verbose: !!values.verbose,
|
|
53
|
+
});
|
|
54
|
+
if (!values.json && !values.verbose) {
|
|
55
|
+
console.log(formatScorecard(scorecard));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
main().catch((err) => {
|
|
59
|
+
console.error('Introspect failed:', err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for Zouroboros prescribe — auto-generate improvement seeds
|
|
4
|
+
*
|
|
5
|
+
* Usage: zouroboros-prescribe [--scorecard <path>] [--target <metric>] [--live] [--output <dir>] [--dry-run]
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'util';
|
|
8
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { prescribe } from '../index.js';
|
|
11
|
+
const { values } = parseArgs({
|
|
12
|
+
args: Bun.argv.slice(2),
|
|
13
|
+
options: {
|
|
14
|
+
scorecard: { type: 'string', short: 's' },
|
|
15
|
+
target: { type: 'string', short: 't' },
|
|
16
|
+
live: { type: 'boolean', short: 'l' },
|
|
17
|
+
output: { type: 'string', short: 'o', default: '.' },
|
|
18
|
+
'dry-run': { type: 'boolean' },
|
|
19
|
+
help: { type: 'boolean', short: 'h' },
|
|
20
|
+
},
|
|
21
|
+
strict: false,
|
|
22
|
+
});
|
|
23
|
+
if (values.help) {
|
|
24
|
+
console.log(`
|
|
25
|
+
zouroboros-prescribe — generate improvement prescriptions from scorecard
|
|
26
|
+
|
|
27
|
+
USAGE:
|
|
28
|
+
zouroboros-prescribe [options]
|
|
29
|
+
|
|
30
|
+
OPTIONS:
|
|
31
|
+
--scorecard, -s Path to a saved scorecard JSON (default: run live introspect)
|
|
32
|
+
--target, -t Target metric name (default: weakest metric)
|
|
33
|
+
--live, -l Run live introspection instead of using cached scorecard
|
|
34
|
+
--output, -o Output directory for prescription (default: .)
|
|
35
|
+
--dry-run Show what would be prescribed without writing files
|
|
36
|
+
--help, -h Show this help
|
|
37
|
+
|
|
38
|
+
PLAYBOOKS:
|
|
39
|
+
14 playbooks map metrics to concrete improvement strategies.
|
|
40
|
+
The governor gate evaluates risk before approving execution.
|
|
41
|
+
|
|
42
|
+
EXAMPLES:
|
|
43
|
+
zouroboros-prescribe --live --target memory_health
|
|
44
|
+
zouroboros-prescribe --scorecard ./scorecard.json --output ./prescriptions
|
|
45
|
+
zouroboros-prescribe --dry-run
|
|
46
|
+
`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const prescription = await prescribe({
|
|
51
|
+
scorecard: values.scorecard,
|
|
52
|
+
live: !!values.live,
|
|
53
|
+
target: values.target,
|
|
54
|
+
});
|
|
55
|
+
if (values['dry-run']) {
|
|
56
|
+
console.log('\n[dry-run] Prescription:');
|
|
57
|
+
console.log(JSON.stringify(prescription, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const outDir = values.output || '.';
|
|
61
|
+
mkdirSync(outDir, { recursive: true });
|
|
62
|
+
const outPath = join(outDir, `prescription-${Date.now()}.json`);
|
|
63
|
+
writeFileSync(outPath, JSON.stringify(prescription, null, 2));
|
|
64
|
+
console.log(`✓ Prescription written to: ${outPath}`);
|
|
65
|
+
console.log(` Metric: ${prescription.metric.name} (${(prescription.metric.score * 100).toFixed(0)}%)`);
|
|
66
|
+
console.log(` Playbook: ${prescription.playbook.name}`);
|
|
67
|
+
console.log(` Governor: ${prescription.governor.approved ? 'APPROVED' : 'BLOCKED'}`);
|
|
68
|
+
}
|
|
69
|
+
main().catch((err) => {
|
|
70
|
+
console.error('Prescribe failed:', err.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolution executor for prescribed improvements
|
|
3
|
+
*/
|
|
4
|
+
import type { Prescription, EvolutionResult } from '../types.js';
|
|
5
|
+
export declare function executeEvolution(prescription: Prescription, options: {
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
skipGovernor?: boolean;
|
|
8
|
+
}): Promise<EvolutionResult>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolution executor for prescribed improvements
|
|
3
|
+
*/
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
const WORKSPACE = process.env.ZO_WORKSPACE || '/home/workspace';
|
|
8
|
+
const RESULTS_DIR = join(WORKSPACE, 'Seeds/zouroboros/results');
|
|
9
|
+
function run(cmd, timeout = 120000) {
|
|
10
|
+
try {
|
|
11
|
+
const stdout = execSync(cmd, {
|
|
12
|
+
cwd: WORKSPACE,
|
|
13
|
+
timeout,
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
16
|
+
});
|
|
17
|
+
return { stdout: stdout.trim(), ok: true, code: 0 };
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return { stdout: (e.stdout || '').toString().trim(), ok: false, code: e.status ?? 1 };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function measureMetric(cmd) {
|
|
24
|
+
const result = run(cmd, 60000);
|
|
25
|
+
if (!result.ok)
|
|
26
|
+
return null;
|
|
27
|
+
const num = parseFloat(result.stdout);
|
|
28
|
+
return isNaN(num) ? null : num;
|
|
29
|
+
}
|
|
30
|
+
async function executeAutoloopMode(prescription) {
|
|
31
|
+
const programPath = `/tmp/z-prescription-${prescription.id}.md`;
|
|
32
|
+
if (prescription.program) {
|
|
33
|
+
writeFileSync(programPath, prescription.program);
|
|
34
|
+
}
|
|
35
|
+
console.error(` [evolve] Starting autoloop: ${prescription.playbook.name}`);
|
|
36
|
+
const result = run(`bun Skills/autoloop/scripts/autoloop.ts --program "${programPath}" 2>&1`, 8 * 60 * 60 * 1000 // 8 hour timeout
|
|
37
|
+
);
|
|
38
|
+
const success = result.ok && result.stdout.includes('KEEP');
|
|
39
|
+
return {
|
|
40
|
+
prescriptionId: prescription.id,
|
|
41
|
+
success,
|
|
42
|
+
baseline: { composite: prescription.metric.score, metrics: [] },
|
|
43
|
+
postFlight: null, // Would need to parse autoloop results
|
|
44
|
+
delta: 0,
|
|
45
|
+
reverted: !success,
|
|
46
|
+
detail: result.stdout.slice(0, 500),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function executeScriptMode(prescription) {
|
|
50
|
+
console.error(` [evolve] Executing script mode: ${prescription.playbook.name}`);
|
|
51
|
+
// Run setup commands
|
|
52
|
+
if (prescription.playbook.setupCommands) {
|
|
53
|
+
for (const cmd of prescription.playbook.setupCommands) {
|
|
54
|
+
console.error(` [evolve] Setup: ${cmd.slice(0, 60)}...`);
|
|
55
|
+
const setupResult = run(cmd);
|
|
56
|
+
if (!setupResult.ok) {
|
|
57
|
+
return {
|
|
58
|
+
prescriptionId: prescription.id,
|
|
59
|
+
success: false,
|
|
60
|
+
baseline: { composite: prescription.metric.score, metrics: [] },
|
|
61
|
+
postFlight: null,
|
|
62
|
+
delta: 0,
|
|
63
|
+
reverted: false,
|
|
64
|
+
detail: `Setup failed: ${setupResult.stdout.slice(0, 200)}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Run main command
|
|
70
|
+
const runCmd = prescription.playbook.runCommand || 'echo "No run command"';
|
|
71
|
+
console.error(` [evolve] Executing: ${runCmd.slice(0, 60)}...`);
|
|
72
|
+
const result = run(runCmd, 300000);
|
|
73
|
+
// Measure post-flight metric
|
|
74
|
+
const postValue = measureMetric(prescription.playbook.metricCommand);
|
|
75
|
+
const baseline = prescription.metric.value;
|
|
76
|
+
const delta = postValue !== null ? postValue - baseline : 0;
|
|
77
|
+
const improved = prescription.playbook.metricDirection === 'higher_is_better'
|
|
78
|
+
? delta > 0
|
|
79
|
+
: delta < 0;
|
|
80
|
+
return {
|
|
81
|
+
prescriptionId: prescription.id,
|
|
82
|
+
success: result.ok,
|
|
83
|
+
baseline: { composite: baseline, metrics: [{ name: prescription.metric.name, value: baseline, score: prescription.metric.score, status: prescription.metric.status }] },
|
|
84
|
+
postFlight: postValue !== null ? {
|
|
85
|
+
composite: postValue,
|
|
86
|
+
metrics: [{ name: prescription.metric.name, value: postValue, score: postValue, status: improved ? 'HEALTHY' : prescription.metric.status }],
|
|
87
|
+
} : null,
|
|
88
|
+
delta,
|
|
89
|
+
reverted: !improved && result.ok,
|
|
90
|
+
detail: result.stdout.slice(0, 500),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function executeEvolution(prescription, options) {
|
|
94
|
+
const { dryRun = false, skipGovernor = false } = options;
|
|
95
|
+
// Governor check
|
|
96
|
+
if (!skipGovernor && !prescription.governor.approved) {
|
|
97
|
+
return {
|
|
98
|
+
prescriptionId: prescription.id,
|
|
99
|
+
success: false,
|
|
100
|
+
baseline: { composite: prescription.metric.score, metrics: [] },
|
|
101
|
+
postFlight: null,
|
|
102
|
+
delta: 0,
|
|
103
|
+
reverted: false,
|
|
104
|
+
detail: `Governor blocked: ${prescription.governor.reason}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
return {
|
|
109
|
+
prescriptionId: prescription.id,
|
|
110
|
+
success: true,
|
|
111
|
+
baseline: { composite: prescription.metric.score, metrics: [] },
|
|
112
|
+
postFlight: null,
|
|
113
|
+
delta: 0,
|
|
114
|
+
reverted: false,
|
|
115
|
+
detail: 'Dry run - no changes made',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Ensure results directory
|
|
119
|
+
mkdirSync(RESULTS_DIR, { recursive: true });
|
|
120
|
+
// Execute based on mode
|
|
121
|
+
const result = prescription.program
|
|
122
|
+
? await executeAutoloopMode(prescription)
|
|
123
|
+
: await executeScriptMode(prescription);
|
|
124
|
+
// Save result
|
|
125
|
+
const resultPath = join(RESULTS_DIR, `${prescription.id}-result.json`);
|
|
126
|
+
writeFileSync(resultPath, JSON.stringify(result, null, 2));
|
|
127
|
+
console.error(` [evolve] Result: ${result.success ? '✅ SUCCESS' : '❌ FAILED'}`);
|
|
128
|
+
if (result.delta !== 0) {
|
|
129
|
+
const sign = result.delta > 0 ? '+' : '';
|
|
130
|
+
console.error(` [evolve] Delta: ${sign}${(result.delta * 100).toFixed(1)}%`);
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Loop Tuning
|
|
3
|
+
*
|
|
4
|
+
* Auto-adjusts metric weights based on evolution outcomes.
|
|
5
|
+
* Learns which metrics are most predictive of successful improvements.
|
|
6
|
+
*/
|
|
7
|
+
import type { EvolutionResult, MetricResult } from './types.js';
|
|
8
|
+
export interface WeightConfig {
|
|
9
|
+
weights: Record<string, number>;
|
|
10
|
+
learningRate: number;
|
|
11
|
+
minWeight: number;
|
|
12
|
+
maxWeight: number;
|
|
13
|
+
version: number;
|
|
14
|
+
lastUpdated: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FeedbackRecord {
|
|
17
|
+
timestamp: string;
|
|
18
|
+
metricName: string;
|
|
19
|
+
prescriptionId: string;
|
|
20
|
+
baselineScore: number;
|
|
21
|
+
postFlightScore: number | null;
|
|
22
|
+
delta: number;
|
|
23
|
+
success: boolean;
|
|
24
|
+
weightBefore: number;
|
|
25
|
+
weightAfter: number;
|
|
26
|
+
}
|
|
27
|
+
export declare class FeedbackTuner {
|
|
28
|
+
private weightsFile;
|
|
29
|
+
private storeFile;
|
|
30
|
+
private config;
|
|
31
|
+
private store;
|
|
32
|
+
constructor(dataDir: string);
|
|
33
|
+
get weights(): Record<string, number>;
|
|
34
|
+
get version(): number;
|
|
35
|
+
recordOutcome(metric: MetricResult, result: EvolutionResult): FeedbackRecord;
|
|
36
|
+
getWeightForMetric(name: string): number;
|
|
37
|
+
getHistory(limit?: number): FeedbackRecord[];
|
|
38
|
+
getWeightHistory(limit?: number): Array<{
|
|
39
|
+
timestamp: string;
|
|
40
|
+
weights: Record<string, number>;
|
|
41
|
+
trigger: string;
|
|
42
|
+
}>;
|
|
43
|
+
getSuccessRate(metricName?: string): number;
|
|
44
|
+
getAvgDelta(metricName?: string): number;
|
|
45
|
+
resetWeights(): void;
|
|
46
|
+
private normalizeWeights;
|
|
47
|
+
private loadWeights;
|
|
48
|
+
private loadStore;
|
|
49
|
+
private save;
|
|
50
|
+
}
|
|
51
|
+
export declare function createFeedbackTuner(dataDir: string): FeedbackTuner;
|
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Loop Tuning
|
|
3
|
+
*
|
|
4
|
+
* Auto-adjusts metric weights based on evolution outcomes.
|
|
5
|
+
* Learns which metrics are most predictive of successful improvements.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
const DEFAULT_WEIGHTS = {
|
|
10
|
+
'Memory Recall': 0.20,
|
|
11
|
+
'Graph Connectivity': 0.15,
|
|
12
|
+
'Routing Accuracy': 0.20,
|
|
13
|
+
'Eval Calibration': 0.15,
|
|
14
|
+
'Procedure Freshness': 0.15,
|
|
15
|
+
'Episode Velocity': 0.15,
|
|
16
|
+
};
|
|
17
|
+
export class FeedbackTuner {
|
|
18
|
+
weightsFile;
|
|
19
|
+
storeFile;
|
|
20
|
+
config;
|
|
21
|
+
store;
|
|
22
|
+
constructor(dataDir) {
|
|
23
|
+
mkdirSync(dataDir, { recursive: true });
|
|
24
|
+
this.weightsFile = join(dataDir, 'metric-weights.json');
|
|
25
|
+
this.storeFile = join(dataDir, 'feedback-store.json');
|
|
26
|
+
this.config = this.loadWeights();
|
|
27
|
+
this.store = this.loadStore();
|
|
28
|
+
}
|
|
29
|
+
get weights() {
|
|
30
|
+
return { ...this.config.weights };
|
|
31
|
+
}
|
|
32
|
+
get version() {
|
|
33
|
+
return this.config.version;
|
|
34
|
+
}
|
|
35
|
+
recordOutcome(metric, result) {
|
|
36
|
+
const weightBefore = this.config.weights[metric.name] || 0.10;
|
|
37
|
+
let weightAfter = weightBefore;
|
|
38
|
+
if (result.success && result.delta !== 0) {
|
|
39
|
+
// Successful evolution — adjust weight based on impact
|
|
40
|
+
const impactMagnitude = Math.abs(result.delta);
|
|
41
|
+
if (result.delta > 0) {
|
|
42
|
+
// Positive improvement — slightly increase weight (reward productive metrics)
|
|
43
|
+
weightAfter = weightBefore + this.config.learningRate * impactMagnitude;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Regression despite "success" — decrease weight
|
|
47
|
+
weightAfter = weightBefore - this.config.learningRate * impactMagnitude;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (!result.success) {
|
|
51
|
+
// Failed evolution — slightly decrease weight (this metric led to bad outcomes)
|
|
52
|
+
weightAfter = weightBefore - this.config.learningRate * 0.5;
|
|
53
|
+
}
|
|
54
|
+
// Clamp
|
|
55
|
+
weightAfter = Math.max(this.config.minWeight, Math.min(this.config.maxWeight, weightAfter));
|
|
56
|
+
const record = {
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
metricName: metric.name,
|
|
59
|
+
prescriptionId: result.prescriptionId,
|
|
60
|
+
baselineScore: metric.score,
|
|
61
|
+
postFlightScore: result.postFlight?.composite ?? null,
|
|
62
|
+
delta: result.delta,
|
|
63
|
+
success: result.success,
|
|
64
|
+
weightBefore,
|
|
65
|
+
weightAfter,
|
|
66
|
+
};
|
|
67
|
+
// Apply weight change
|
|
68
|
+
this.config.weights[metric.name] = weightAfter;
|
|
69
|
+
this.normalizeWeights();
|
|
70
|
+
this.config.version++;
|
|
71
|
+
this.config.lastUpdated = record.timestamp;
|
|
72
|
+
// Store record
|
|
73
|
+
this.store.records.push(record);
|
|
74
|
+
if (this.store.records.length > 500) {
|
|
75
|
+
this.store.records = this.store.records.slice(-500);
|
|
76
|
+
}
|
|
77
|
+
// Store weight snapshot
|
|
78
|
+
this.store.weightHistory.push({
|
|
79
|
+
timestamp: record.timestamp,
|
|
80
|
+
weights: { ...this.config.weights },
|
|
81
|
+
trigger: `${metric.name}: ${result.success ? 'success' : 'fail'} (delta=${result.delta.toFixed(4)})`,
|
|
82
|
+
});
|
|
83
|
+
if (this.store.weightHistory.length > 100) {
|
|
84
|
+
this.store.weightHistory = this.store.weightHistory.slice(-100);
|
|
85
|
+
}
|
|
86
|
+
this.save();
|
|
87
|
+
return record;
|
|
88
|
+
}
|
|
89
|
+
getWeightForMetric(name) {
|
|
90
|
+
return this.config.weights[name] || 0.10;
|
|
91
|
+
}
|
|
92
|
+
getHistory(limit = 20) {
|
|
93
|
+
return this.store.records.slice(-limit);
|
|
94
|
+
}
|
|
95
|
+
getWeightHistory(limit = 20) {
|
|
96
|
+
return this.store.weightHistory.slice(-limit);
|
|
97
|
+
}
|
|
98
|
+
getSuccessRate(metricName) {
|
|
99
|
+
const records = metricName
|
|
100
|
+
? this.store.records.filter(r => r.metricName === metricName)
|
|
101
|
+
: this.store.records;
|
|
102
|
+
if (records.length === 0)
|
|
103
|
+
return 0;
|
|
104
|
+
return records.filter(r => r.success).length / records.length;
|
|
105
|
+
}
|
|
106
|
+
getAvgDelta(metricName) {
|
|
107
|
+
const records = metricName
|
|
108
|
+
? this.store.records.filter(r => r.metricName === metricName)
|
|
109
|
+
: this.store.records;
|
|
110
|
+
if (records.length === 0)
|
|
111
|
+
return 0;
|
|
112
|
+
return records.reduce((sum, r) => sum + r.delta, 0) / records.length;
|
|
113
|
+
}
|
|
114
|
+
resetWeights() {
|
|
115
|
+
this.config.weights = { ...DEFAULT_WEIGHTS };
|
|
116
|
+
this.config.version++;
|
|
117
|
+
this.config.lastUpdated = new Date().toISOString();
|
|
118
|
+
this.save();
|
|
119
|
+
}
|
|
120
|
+
normalizeWeights() {
|
|
121
|
+
const total = Object.values(this.config.weights).reduce((s, w) => s + w, 0);
|
|
122
|
+
if (total === 0)
|
|
123
|
+
return;
|
|
124
|
+
for (const key of Object.keys(this.config.weights)) {
|
|
125
|
+
this.config.weights[key] /= total;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
loadWeights() {
|
|
129
|
+
if (existsSync(this.weightsFile)) {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(readFileSync(this.weightsFile, 'utf-8'));
|
|
132
|
+
}
|
|
133
|
+
catch { /* fall through */ }
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
weights: { ...DEFAULT_WEIGHTS },
|
|
137
|
+
learningRate: 0.05,
|
|
138
|
+
minWeight: 0.05,
|
|
139
|
+
maxWeight: 0.40,
|
|
140
|
+
version: 0,
|
|
141
|
+
lastUpdated: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
loadStore() {
|
|
145
|
+
if (existsSync(this.storeFile)) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(readFileSync(this.storeFile, 'utf-8'));
|
|
148
|
+
}
|
|
149
|
+
catch { /* fall through */ }
|
|
150
|
+
}
|
|
151
|
+
return { records: [], weightHistory: [] };
|
|
152
|
+
}
|
|
153
|
+
save() {
|
|
154
|
+
writeFileSync(this.weightsFile, JSON.stringify(this.config, null, 2));
|
|
155
|
+
writeFileSync(this.storeFile, JSON.stringify(this.store, null, 2));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function createFeedbackTuner(dataDir) {
|
|
159
|
+
return new FeedbackTuner(dataDir);
|
|
160
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolution History
|
|
3
|
+
*
|
|
4
|
+
* Track and visualize system improvements over time.
|
|
5
|
+
*/
|
|
6
|
+
import type { EvolutionResult, ScorecardSnapshot } from './types.js';
|
|
7
|
+
export interface HistoryEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
prescriptionId: string;
|
|
11
|
+
playbookId: string;
|
|
12
|
+
playbookName: string;
|
|
13
|
+
metricName: string;
|
|
14
|
+
baseline: ScorecardSnapshot;
|
|
15
|
+
postFlight: ScorecardSnapshot | null;
|
|
16
|
+
delta: number;
|
|
17
|
+
success: boolean;
|
|
18
|
+
reverted: boolean;
|
|
19
|
+
duration?: number;
|
|
20
|
+
tags: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface HistoryStats {
|
|
23
|
+
totalEvolutions: number;
|
|
24
|
+
successCount: number;
|
|
25
|
+
failCount: number;
|
|
26
|
+
revertCount: number;
|
|
27
|
+
successRate: number;
|
|
28
|
+
avgDelta: number;
|
|
29
|
+
avgPositiveDelta: number;
|
|
30
|
+
bestEvolution: HistoryEntry | null;
|
|
31
|
+
worstEvolution: HistoryEntry | null;
|
|
32
|
+
streakCurrent: number;
|
|
33
|
+
streakBest: number;
|
|
34
|
+
byMetric: Record<string, {
|
|
35
|
+
count: number;
|
|
36
|
+
avgDelta: number;
|
|
37
|
+
successRate: number;
|
|
38
|
+
}>;
|
|
39
|
+
byPlaybook: Record<string, {
|
|
40
|
+
count: number;
|
|
41
|
+
avgDelta: number;
|
|
42
|
+
successRate: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export interface TrendPoint {
|
|
46
|
+
timestamp: string;
|
|
47
|
+
composite: number;
|
|
48
|
+
metrics: Record<string, number>;
|
|
49
|
+
}
|
|
50
|
+
export declare class EvolutionHistory {
|
|
51
|
+
private dataFile;
|
|
52
|
+
private store;
|
|
53
|
+
private maxEntries;
|
|
54
|
+
private maxTrends;
|
|
55
|
+
constructor(dataDir: string, maxEntries?: number, maxTrends?: number);
|
|
56
|
+
record(entry: Omit<HistoryEntry, 'id'>): HistoryEntry;
|
|
57
|
+
recordFromResult(result: EvolutionResult, playbookId: string, playbookName: string, metricName: string, tags?: string[]): HistoryEntry;
|
|
58
|
+
getStats(): HistoryStats;
|
|
59
|
+
getTrends(limit?: number): TrendPoint[];
|
|
60
|
+
getEntries(options?: {
|
|
61
|
+
limit?: number;
|
|
62
|
+
metricName?: string;
|
|
63
|
+
playbookId?: string;
|
|
64
|
+
successOnly?: boolean;
|
|
65
|
+
}): HistoryEntry[];
|
|
66
|
+
getRecentEntries(limit?: number): HistoryEntry[];
|
|
67
|
+
clear(): void;
|
|
68
|
+
private load;
|
|
69
|
+
private save;
|
|
70
|
+
}
|
|
71
|
+
export declare function createEvolutionHistory(dataDir: string): EvolutionHistory;
|