entroly-wasm 0.9.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/index.js +67 -0
- package/js/agentskills_export.js +129 -0
- package/js/auto_index.js +262 -0
- package/js/autotune.js +627 -0
- package/js/checkpoint.js +108 -0
- package/js/cli.js +321 -0
- package/js/cogops.js +513 -0
- package/js/config.js +53 -0
- package/js/distill.js +155 -0
- package/js/evolution_daemon.js +335 -0
- package/js/federation.js +444 -0
- package/js/gateways.js +170 -0
- package/js/multimodal.js +64 -0
- package/js/repo_map.js +112 -0
- package/js/server.js +465 -0
- package/js/skills.js +124 -0
- package/js/value_tracker.js +181 -0
- package/js/vault.js +237 -0
- package/js/vault_observer.js +129 -0
- package/js/workspace.js +116 -0
- package/package.json +51 -0
- package/pkg/entroly_wasm.d.ts +156 -0
- package/pkg/entroly_wasm.js +468 -0
- package/pkg/entroly_wasm_bg.wasm +0 -0
package/js/skills.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Engine — pure-JS port of entroly/skill_engine.py
|
|
3
|
+
* Dynamic skill synthesis, benchmarking, and lifecycle management.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { randomUUID } = require('crypto');
|
|
11
|
+
const { nowISO, safeFilename } = require('./vault');
|
|
12
|
+
|
|
13
|
+
class SkillEngine {
|
|
14
|
+
constructor(vault) {
|
|
15
|
+
this._vault = vault;
|
|
16
|
+
this._skillsDir = path.join(vault._base, 'evolution', 'skills');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createSkill(entityKey, failingQueries, intent = '') {
|
|
20
|
+
this._vault.ensureStructure();
|
|
21
|
+
const skillId = `skill_${safeFilename(entityKey)}_${Date.now().toString(36)}`;
|
|
22
|
+
const skillDir = path.join(this._skillsDir, skillId);
|
|
23
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
24
|
+
fs.mkdirSync(path.join(skillDir, 'tests'), { recursive: true });
|
|
25
|
+
|
|
26
|
+
// SKILL.md
|
|
27
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), [
|
|
28
|
+
`# Skill: ${entityKey}`,
|
|
29
|
+
'',
|
|
30
|
+
`**ID:** ${skillId}`,
|
|
31
|
+
`**Entity:** ${entityKey}`,
|
|
32
|
+
`**Intent:** ${intent || 'general'}`,
|
|
33
|
+
`**Created:** ${nowISO()}`,
|
|
34
|
+
`**Status:** draft`,
|
|
35
|
+
'',
|
|
36
|
+
'## Failing Queries',
|
|
37
|
+
'',
|
|
38
|
+
...failingQueries.map(q => `- ${q}`),
|
|
39
|
+
'',
|
|
40
|
+
'## Procedure',
|
|
41
|
+
'',
|
|
42
|
+
'1. TODO: Define the skill procedure',
|
|
43
|
+
'2. TODO: Implement the tool',
|
|
44
|
+
'3. TODO: Add test cases',
|
|
45
|
+
'',
|
|
46
|
+
].join('\n'), 'utf-8');
|
|
47
|
+
|
|
48
|
+
// metrics.json
|
|
49
|
+
fs.writeFileSync(path.join(skillDir, 'metrics.json'), JSON.stringify({
|
|
50
|
+
skill_id: skillId, entity: entityKey, created: nowISO(),
|
|
51
|
+
status: 'draft', fitness: 0, runs: 0, successes: 0, failures: 0,
|
|
52
|
+
}, null, 2), 'utf-8');
|
|
53
|
+
|
|
54
|
+
// tests/test_cases.json
|
|
55
|
+
fs.writeFileSync(path.join(skillDir, 'tests', 'test_cases.json'), JSON.stringify({
|
|
56
|
+
skill_id: skillId,
|
|
57
|
+
cases: failingQueries.map((q, i) => ({
|
|
58
|
+
id: `tc_${i}`, query: q, expected_pass: true, actual: null,
|
|
59
|
+
})),
|
|
60
|
+
}, null, 2), 'utf-8');
|
|
61
|
+
|
|
62
|
+
// Update registry
|
|
63
|
+
const regPath = path.join(this._vault._base, 'evolution', 'registry.md');
|
|
64
|
+
if (fs.existsSync(regPath)) {
|
|
65
|
+
const reg = fs.readFileSync(regPath, 'utf-8');
|
|
66
|
+
fs.writeFileSync(regPath, reg + `| ${skillId} | draft | ${nowISO().slice(0, 10)} | ${entityKey} |\n`, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { status: 'created', skill_id: skillId, entity: entityKey, path: skillDir, engine: 'javascript' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
listSkills() {
|
|
73
|
+
this._vault.ensureStructure();
|
|
74
|
+
if (!fs.existsSync(this._skillsDir)) return [];
|
|
75
|
+
const skills = [];
|
|
76
|
+
for (const name of fs.readdirSync(this._skillsDir)) {
|
|
77
|
+
const metricsPath = path.join(this._skillsDir, name, 'metrics.json');
|
|
78
|
+
if (!fs.existsSync(metricsPath)) continue;
|
|
79
|
+
try {
|
|
80
|
+
const m = JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));
|
|
81
|
+
skills.push({ skill_id: m.skill_id || name, entity: m.entity || '', status: m.status || 'draft', fitness: m.fitness || 0, runs: m.runs || 0 });
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
return skills;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
benchmarkSkill(skillId) {
|
|
88
|
+
const metricsPath = path.join(this._skillsDir, skillId, 'metrics.json');
|
|
89
|
+
const testsPath = path.join(this._skillsDir, skillId, 'tests', 'test_cases.json');
|
|
90
|
+
if (!fs.existsSync(metricsPath)) return { error: `Skill '${skillId}' not found` };
|
|
91
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));
|
|
92
|
+
let cases = [];
|
|
93
|
+
if (fs.existsSync(testsPath)) {
|
|
94
|
+
try { cases = JSON.parse(fs.readFileSync(testsPath, 'utf-8')).cases || []; } catch {}
|
|
95
|
+
}
|
|
96
|
+
// Simple benchmark: count cases as "passed" if they have expected_pass
|
|
97
|
+
const passed = cases.filter(c => c.expected_pass).length;
|
|
98
|
+
const fitness = cases.length ? passed / cases.length : 0;
|
|
99
|
+
metrics.fitness = fitness;
|
|
100
|
+
metrics.runs = (metrics.runs || 0) + 1;
|
|
101
|
+
fs.writeFileSync(metricsPath, JSON.stringify(metrics, null, 2), 'utf-8');
|
|
102
|
+
return { skill_id: skillId, fitness, test_cases: cases.length, passed, status: metrics.status, engine: 'javascript' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
promoteOrPrune(skillId) {
|
|
106
|
+
const metricsPath = path.join(this._skillsDir, skillId, 'metrics.json');
|
|
107
|
+
if (!fs.existsSync(metricsPath)) return { error: `Skill '${skillId}' not found` };
|
|
108
|
+
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));
|
|
109
|
+
let action;
|
|
110
|
+
if (metrics.fitness >= 0.7) {
|
|
111
|
+
metrics.status = 'promoted';
|
|
112
|
+
action = 'promoted';
|
|
113
|
+
} else if (metrics.fitness <= 0.3) {
|
|
114
|
+
metrics.status = 'pruned';
|
|
115
|
+
action = 'pruned';
|
|
116
|
+
} else {
|
|
117
|
+
action = 'no_change';
|
|
118
|
+
}
|
|
119
|
+
fs.writeFileSync(metricsPath, JSON.stringify(metrics, null, 2), 'utf-8');
|
|
120
|
+
return { skill_id: skillId, action, fitness: metrics.fitness, new_status: metrics.status, engine: 'javascript' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { SkillEngine };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValueTracker — JS port of entroly/value_tracker.py
|
|
3
|
+
*
|
|
4
|
+
* Persistent, lifetime-savings accounting with the self-funded evolution
|
|
5
|
+
* budget invariant:
|
|
6
|
+
*
|
|
7
|
+
* C_spent(t) ≤ τ · S(t) (τ = 5%)
|
|
8
|
+
*
|
|
9
|
+
* Any token-costing evolution step MUST gate on `getEvolutionBudget().canEvolve`.
|
|
10
|
+
* The tracker is atomic-write safe and survives process restarts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const EVOLUTION_TAX_RATE = 0.05;
|
|
20
|
+
const FILE_NAME = 'value_tracker.json';
|
|
21
|
+
|
|
22
|
+
// Per-model $/1M tokens — kept in sync with entroly/value_tracker.py (×1000).
|
|
23
|
+
const COST_PER_M = {
|
|
24
|
+
default: 3.0,
|
|
25
|
+
// OpenAI
|
|
26
|
+
'gpt-4o': 2.5,
|
|
27
|
+
'gpt-4o-mini': 0.15,
|
|
28
|
+
'gpt-4-turbo': 10.0,
|
|
29
|
+
'gpt-4': 30.0,
|
|
30
|
+
'gpt-3.5-turbo': 0.5,
|
|
31
|
+
'o1': 15.0,
|
|
32
|
+
'o1-mini': 3.0,
|
|
33
|
+
'o3': 10.0,
|
|
34
|
+
'o3-mini': 1.1,
|
|
35
|
+
'o4-mini': 1.1,
|
|
36
|
+
// Anthropic
|
|
37
|
+
'claude-opus-4': 15.0,
|
|
38
|
+
'claude-sonnet-4': 3.0,
|
|
39
|
+
'claude-haiku-4': 0.8,
|
|
40
|
+
'claude-3-5-sonnet': 3.0,
|
|
41
|
+
'claude-3-5-haiku': 0.8,
|
|
42
|
+
// Google
|
|
43
|
+
'gemini-2.5-pro': 1.25,
|
|
44
|
+
'gemini-2.5-flash': 0.075,
|
|
45
|
+
'gemini-1.5-pro': 1.25,
|
|
46
|
+
'gemini-1.5-flash': 0.075,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Old→new name aliases so 'claude-3-opus' hits the same rate as Python.
|
|
50
|
+
const MODEL_ALIASES = {
|
|
51
|
+
'claude-3-opus': 'claude-opus-4',
|
|
52
|
+
'claude-3-sonnet': 'claude-sonnet-4',
|
|
53
|
+
'claude-3-haiku': 'claude-haiku-4',
|
|
54
|
+
'claude-3.5-sonnet': 'claude-3-5-sonnet',
|
|
55
|
+
'claude-3.5-haiku': 'claude-3-5-haiku',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function estimateCost(tokens, model = '') {
|
|
59
|
+
let m = (model || '').toLowerCase();
|
|
60
|
+
for (const [alias, canonical] of Object.entries(MODEL_ALIASES)) {
|
|
61
|
+
if (m.startsWith(alias)) { m = canonical + m.slice(alias.length); break; }
|
|
62
|
+
}
|
|
63
|
+
// Longest-prefix match — prevents 'gpt-4o' eating 'gpt-4o-mini'.
|
|
64
|
+
const keys = Object.keys(COST_PER_M).filter(k => k !== 'default').sort((a, b) => b.length - a.length);
|
|
65
|
+
const key = (m && keys.find(k => m.startsWith(k)));
|
|
66
|
+
if (!key && model) {
|
|
67
|
+
// One-time warning per unknown model — stderr, doesn't pollute stdout.
|
|
68
|
+
if (!estimateCost._warned) estimateCost._warned = new Set();
|
|
69
|
+
if (!estimateCost._warned.has(model)) {
|
|
70
|
+
estimateCost._warned.add(model);
|
|
71
|
+
try { console.warn(`[entroly] unknown model '${model}'; using default $${COST_PER_M.default}/M`); } catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return (tokens / 1_000_000) * COST_PER_M[key || 'default'];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function atomicWrite(filePath, content) {
|
|
78
|
+
const tmp = filePath + '.tmp-' + process.pid;
|
|
79
|
+
fs.writeFileSync(tmp, content, 'utf8');
|
|
80
|
+
fs.renameSync(tmp, filePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class ValueTracker {
|
|
84
|
+
constructor(dataDir = null) {
|
|
85
|
+
this._dir = dataDir || path.join(os.homedir(), '.entroly');
|
|
86
|
+
fs.mkdirSync(this._dir, { recursive: true });
|
|
87
|
+
this._path = path.join(this._dir, FILE_NAME);
|
|
88
|
+
this._data = this._load();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_load() {
|
|
92
|
+
if (fs.existsSync(this._path)) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = JSON.parse(fs.readFileSync(this._path, 'utf8'));
|
|
95
|
+
if (raw && typeof raw === 'object' && 'version' in raw) return raw;
|
|
96
|
+
} catch (_) { /* fall through */ }
|
|
97
|
+
}
|
|
98
|
+
return this._defaults();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_defaults() {
|
|
102
|
+
const now = Date.now() / 1000;
|
|
103
|
+
return {
|
|
104
|
+
version: 2,
|
|
105
|
+
lifetime: {
|
|
106
|
+
tokens_saved: 0,
|
|
107
|
+
cost_saved_usd: 0.0,
|
|
108
|
+
requests_optimized: 0,
|
|
109
|
+
first_seen: now,
|
|
110
|
+
last_seen: now,
|
|
111
|
+
evolution_spent_usd: 0.0,
|
|
112
|
+
evolution_attempts: 0,
|
|
113
|
+
evolution_successes: 0,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_save() {
|
|
119
|
+
try { atomicWrite(this._path, JSON.stringify(this._data, null, 2)); }
|
|
120
|
+
catch (_) { /* best-effort */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
record({ tokensSaved = 0, model = '', optimized = true } = {}) {
|
|
124
|
+
const cost = estimateCost(tokensSaved, model);
|
|
125
|
+
const lt = this._data.lifetime;
|
|
126
|
+
lt.tokens_saved += tokensSaved;
|
|
127
|
+
lt.cost_saved_usd = +(lt.cost_saved_usd + cost).toFixed(6);
|
|
128
|
+
if (optimized) lt.requests_optimized += 1;
|
|
129
|
+
lt.last_seen = Date.now() / 1000;
|
|
130
|
+
this._save();
|
|
131
|
+
return { tokensSaved, costSaved: cost };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getEvolutionBudget() {
|
|
135
|
+
const lt = this._data.lifetime;
|
|
136
|
+
const lifetimeSaved = lt.cost_saved_usd || 0;
|
|
137
|
+
const totalSpent = lt.evolution_spent_usd || 0;
|
|
138
|
+
const totalEarned = lifetimeSaved * EVOLUTION_TAX_RATE;
|
|
139
|
+
const available = Math.max(0, totalEarned - totalSpent);
|
|
140
|
+
return {
|
|
141
|
+
availableUsd: +available.toFixed(6),
|
|
142
|
+
totalEarnedUsd: +totalEarned.toFixed(6),
|
|
143
|
+
totalSpentUsd: +totalSpent.toFixed(6),
|
|
144
|
+
canEvolve: available > 0.001,
|
|
145
|
+
taxRate: EVOLUTION_TAX_RATE,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
recordEvolutionSpend(costUsd, success = false) {
|
|
150
|
+
const lt = this._data.lifetime;
|
|
151
|
+
const lifetimeSaved = lt.cost_saved_usd || 0;
|
|
152
|
+
const currentSpent = lt.evolution_spent_usd || 0;
|
|
153
|
+
const totalEarned = lifetimeSaved * EVOLUTION_TAX_RATE;
|
|
154
|
+
const available = totalEarned - currentSpent;
|
|
155
|
+
|
|
156
|
+
if (costUsd > available + 0.001) {
|
|
157
|
+
return {
|
|
158
|
+
status: 'rejected',
|
|
159
|
+
remainingUsd: +Math.max(0, available).toFixed(6),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
lt.evolution_spent_usd = +(currentSpent + costUsd).toFixed(6);
|
|
164
|
+
lt.evolution_attempts = (lt.evolution_attempts || 0) + 1;
|
|
165
|
+
if (success) lt.evolution_successes = (lt.evolution_successes || 0) + 1;
|
|
166
|
+
this._save();
|
|
167
|
+
return {
|
|
168
|
+
status: 'recorded',
|
|
169
|
+
remainingUsd: +Math.max(0, available - costUsd).toFixed(6),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
stats() {
|
|
174
|
+
return {
|
|
175
|
+
lifetime: { ...this._data.lifetime },
|
|
176
|
+
budget: this.getEvolutionBudget(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { ValueTracker, EVOLUTION_TAX_RATE, estimateCost };
|
package/js/vault.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS VaultManager — port of entroly/vault.py
|
|
3
|
+
* Manages the persistent Knowledge Vault (Obsidian-compatible markdown).
|
|
4
|
+
*
|
|
5
|
+
* Directory contract:
|
|
6
|
+
* vault/beliefs/ vault/verification/ vault/actions/
|
|
7
|
+
* vault/evolution/skills/<id>/ vault/media/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { randomUUID } = require('crypto');
|
|
15
|
+
|
|
16
|
+
const VAULT_DIRS = ['beliefs', 'verification', 'actions', 'evolution', 'media'];
|
|
17
|
+
const SOURCE_EXTS = new Set(['.py', '.rs', '.ts', '.js', '.tsx', '.jsx', '.go', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.swift', '.kt']);
|
|
18
|
+
|
|
19
|
+
function safeFilename(s) {
|
|
20
|
+
let safe = s.trim().toLowerCase().replace(/[^\w\-.]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
21
|
+
return (safe || 'untitled').slice(0, 80);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nowISO() { return new Date().toISOString(); }
|
|
25
|
+
function timestamp() { return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z/, ''); }
|
|
26
|
+
|
|
27
|
+
function parseFrontmatter(content) {
|
|
28
|
+
if (!content.startsWith('---')) return null;
|
|
29
|
+
const end = content.indexOf('---', 3);
|
|
30
|
+
if (end < 0) return null;
|
|
31
|
+
const fm = {};
|
|
32
|
+
for (const line of content.slice(3, end).trim().split('\n')) {
|
|
33
|
+
if (line.trim().startsWith('-')) continue;
|
|
34
|
+
const idx = line.indexOf(':');
|
|
35
|
+
if (idx < 0) continue;
|
|
36
|
+
const key = line.slice(0, idx).trim();
|
|
37
|
+
const val = line.slice(idx + 1).trim();
|
|
38
|
+
if (key && val) fm[key] = val;
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(fm).length ? fm : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractSources(content) {
|
|
44
|
+
if (!content.startsWith('---')) return [];
|
|
45
|
+
const end = content.indexOf('---', 3);
|
|
46
|
+
if (end < 0) return [];
|
|
47
|
+
const lines = content.slice(3, end).trim().split('\n');
|
|
48
|
+
const sources = [];
|
|
49
|
+
let inSources = false;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const t = line.trim();
|
|
52
|
+
if (t.startsWith('sources:')) { inSources = true; continue; }
|
|
53
|
+
if (inSources) {
|
|
54
|
+
if (t.startsWith('- ')) { sources.push(t.slice(2).trim()); continue; }
|
|
55
|
+
if (t && !t.startsWith('-')) break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sources;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractBody(content) {
|
|
62
|
+
if (!content.startsWith('---')) return content;
|
|
63
|
+
const end = content.indexOf('---', 3);
|
|
64
|
+
if (end < 0) return content;
|
|
65
|
+
return content.slice(end + 3).trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function beliefToMarkdown(b) {
|
|
69
|
+
const srcs = (b.sources && b.sources.length) ? b.sources.map(s => ` - ${s}`).join('\n') : ' - unknown';
|
|
70
|
+
const derived = (b.derived_from && b.derived_from.length) ? b.derived_from.map(d => ` - ${d}`).join('\n') : ' - system';
|
|
71
|
+
return `---\nclaim_id: ${b.claim_id}\nentity: ${b.entity}\nstatus: ${b.status}\nconfidence: ${b.confidence}\nsources:\n${srcs}\nlast_checked: ${b.last_checked}\nderived_from:\n${derived}\n---\n\n# ${b.title || b.entity}\n\n${b.body}\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class VaultManager {
|
|
75
|
+
constructor(basePath) {
|
|
76
|
+
this._base = basePath || path.join(process.cwd(), '.entroly', 'vault');
|
|
77
|
+
this._initialized = false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ensureStructure() {
|
|
81
|
+
if (this._initialized) return { status: 'already_initialized', path: this._base };
|
|
82
|
+
const created = [];
|
|
83
|
+
for (const d of VAULT_DIRS) {
|
|
84
|
+
const p = path.join(this._base, d);
|
|
85
|
+
if (!fs.existsSync(p)) { fs.mkdirSync(p, { recursive: true }); created.push(d); }
|
|
86
|
+
}
|
|
87
|
+
const skillsDir = path.join(this._base, 'evolution', 'skills');
|
|
88
|
+
if (!fs.existsSync(skillsDir)) { fs.mkdirSync(skillsDir, { recursive: true }); created.push('evolution/skills'); }
|
|
89
|
+
const reg = path.join(this._base, 'evolution', 'registry.md');
|
|
90
|
+
if (!fs.existsSync(reg)) {
|
|
91
|
+
fs.writeFileSync(reg, '# Skill Registry\n\nIndex of all dynamically generated skills.\n\n| Skill ID | Status | Created | Description |\n|---|---|---|---|\n', 'utf-8');
|
|
92
|
+
created.push('evolution/registry.md');
|
|
93
|
+
}
|
|
94
|
+
this._initialized = true;
|
|
95
|
+
return { status: 'initialized', path: this._base, created };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
writeBelief(artifact) {
|
|
99
|
+
this.ensureStructure();
|
|
100
|
+
const safe = safeFilename(artifact.entity || artifact.claim_id);
|
|
101
|
+
const fp = path.join(this._base, 'beliefs', `${safe}.md`);
|
|
102
|
+
fs.writeFileSync(fp, beliefToMarkdown(artifact), 'utf-8');
|
|
103
|
+
return { status: 'written', directory: 'beliefs', path: fp, claim_id: artifact.claim_id, entity: artifact.entity };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
readBelief(entity) {
|
|
107
|
+
this.ensureStructure();
|
|
108
|
+
const beliefsDir = path.join(this._base, 'beliefs');
|
|
109
|
+
const safe = safeFilename(entity);
|
|
110
|
+
let fp = path.join(beliefsDir, `${safe}.md`);
|
|
111
|
+
if (!fs.existsSync(fp)) {
|
|
112
|
+
const files = this._globBeliefs();
|
|
113
|
+
const match = files.find(f => path.basename(f, '.md').toLowerCase().includes(entity.toLowerCase()));
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
fp = match;
|
|
116
|
+
}
|
|
117
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
118
|
+
return { path: fp, frontmatter: parseFrontmatter(content) || {}, body: extractBody(content) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
listBeliefs() {
|
|
122
|
+
this.ensureStructure();
|
|
123
|
+
const results = [];
|
|
124
|
+
for (const fp of this._globBeliefs()) {
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
127
|
+
const fm = parseFrontmatter(content);
|
|
128
|
+
const beliefsDir = path.join(this._base, 'beliefs');
|
|
129
|
+
results.push({
|
|
130
|
+
file: path.relative(beliefsDir, fp),
|
|
131
|
+
entity: fm ? (fm.entity || path.basename(fp, '.md')) : path.basename(fp, '.md'),
|
|
132
|
+
status: fm ? (fm.status || 'unknown') : 'unknown',
|
|
133
|
+
confidence: fm ? parseFloat(fm.confidence || '0') : 0,
|
|
134
|
+
last_checked: fm ? (fm.last_checked || '') : '',
|
|
135
|
+
});
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
coverageIndex() {
|
|
142
|
+
const beliefs = this.listBeliefs();
|
|
143
|
+
const total = beliefs.length;
|
|
144
|
+
const verified = beliefs.filter(b => b.status === 'verified').length;
|
|
145
|
+
const stale = beliefs.filter(b => b.status === 'stale').length;
|
|
146
|
+
const avg = total ? beliefs.reduce((a, b) => a + b.confidence, 0) / total : 0;
|
|
147
|
+
return { total_beliefs: total, verified, stale, inferred: total - verified - stale, average_confidence: Math.round(avg * 1000) / 1000, entities: beliefs.map(b => b.entity) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
writeVerification(artifact) {
|
|
151
|
+
this.ensureStructure();
|
|
152
|
+
const ts = timestamp();
|
|
153
|
+
const safe = safeFilename(artifact.title || artifact.challenges || 'check');
|
|
154
|
+
const fp = path.join(this._base, 'verification', `${ts}_${safe}.md`);
|
|
155
|
+
const md = `---\nchallenges: ${artifact.challenges}\nresult: ${artifact.result}\nconfidence_delta: ${artifact.confidence_delta >= 0 ? '+' : ''}${artifact.confidence_delta.toFixed(2)}\nchecked_at: ${artifact.checked_at || nowISO()}\nmethod: ${artifact.method || ''}\n---\n\n# ${artifact.title || ''}\n\n${artifact.body || ''}\n`;
|
|
156
|
+
fs.writeFileSync(fp, md, 'utf-8');
|
|
157
|
+
if (artifact.result === 'confirmed' && artifact.challenges) {
|
|
158
|
+
this._updateBeliefConfidence(artifact.challenges, artifact.confidence_delta);
|
|
159
|
+
}
|
|
160
|
+
return { status: 'written', directory: 'verification', path: fp, challenges: artifact.challenges, result: artifact.result };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
writeAction(title, content, actionType = 'report') {
|
|
164
|
+
this.ensureStructure();
|
|
165
|
+
const ts = timestamp();
|
|
166
|
+
const safe = safeFilename(title);
|
|
167
|
+
const fp = path.join(this._base, 'actions', `${ts}_${safe}.md`);
|
|
168
|
+
fs.writeFileSync(fp, `---\ntype: ${actionType}\ntimestamp: ${ts}\n---\n\n# ${title}\n\n${content}\n`, 'utf-8');
|
|
169
|
+
return { status: 'written', directory: 'actions', path: fp, type: actionType };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
markBeliefsStaleForFiles(changedFiles) {
|
|
173
|
+
this.ensureStructure();
|
|
174
|
+
const changedPaths = new Set(changedFiles.map(f => f.replace(/\\/g, '/').toLowerCase()));
|
|
175
|
+
const changedStems = new Set(changedFiles.map(f => path.basename(f).replace(/\.[^.]+$/, '').toLowerCase()));
|
|
176
|
+
const updatedEntities = [], updatedFiles = [], alreadyStale = [];
|
|
177
|
+
for (const fp of this._globBeliefs()) {
|
|
178
|
+
try {
|
|
179
|
+
let content = fs.readFileSync(fp, 'utf-8');
|
|
180
|
+
const fm = parseFrontmatter(content);
|
|
181
|
+
if (!fm) continue;
|
|
182
|
+
const entity = fm.entity || path.basename(fp, '.md');
|
|
183
|
+
const sources = extractSources(content);
|
|
184
|
+
let matched = sources.some(src => {
|
|
185
|
+
const sp = src.split(':')[0].replace(/\\/g, '/').toLowerCase();
|
|
186
|
+
return changedPaths.has(sp) || changedStems.has(path.basename(sp).replace(/\.[^.]+$/, ''));
|
|
187
|
+
});
|
|
188
|
+
if (!matched) matched = [...changedStems].some(stem => entity.toLowerCase().includes(stem));
|
|
189
|
+
if (!matched) continue;
|
|
190
|
+
if (fm.status === 'stale') { alreadyStale.push(entity); continue; }
|
|
191
|
+
content = content.replace(/^status:\s+.+$/m, 'status: stale');
|
|
192
|
+
fs.writeFileSync(fp, content, 'utf-8');
|
|
193
|
+
updatedEntities.push(entity);
|
|
194
|
+
updatedFiles.push(fp);
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
return { status: 'updated', changed_files: changedFiles.length, updated_entities: updatedEntities, updated_files: updatedFiles, already_stale: alreadyStale };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_updateBeliefConfidence(claimId, delta) {
|
|
201
|
+
for (const fp of this._globBeliefs()) {
|
|
202
|
+
try {
|
|
203
|
+
let content = fs.readFileSync(fp, 'utf-8');
|
|
204
|
+
const fm = parseFrontmatter(content);
|
|
205
|
+
if (!fm || fm.claim_id !== claimId) continue;
|
|
206
|
+
const oldConf = parseFloat(fm.confidence || '0.5');
|
|
207
|
+
const newConf = Math.max(0, Math.min(1, oldConf + delta));
|
|
208
|
+
content = content.replace(`confidence: ${fm.confidence}`, `confidence: ${newConf}`);
|
|
209
|
+
if (delta > 0 && content.includes('status: inferred')) content = content.replace('status: inferred', 'status: verified');
|
|
210
|
+
content = content.replace(/last_checked: .+/, `last_checked: ${nowISO()}`);
|
|
211
|
+
fs.writeFileSync(fp, content, 'utf-8');
|
|
212
|
+
break;
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_globBeliefs() {
|
|
218
|
+
const dir = path.join(this._base, 'beliefs');
|
|
219
|
+
if (!fs.existsSync(dir)) return [];
|
|
220
|
+
return this._rglob(dir, '.md');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_rglob(dir, ext) {
|
|
224
|
+
const results = [];
|
|
225
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
226
|
+
const full = path.join(dir, entry.name);
|
|
227
|
+
if (entry.isDirectory()) results.push(...this._rglob(full, ext));
|
|
228
|
+
else if (entry.name.endsWith(ext)) results.push(full);
|
|
229
|
+
}
|
|
230
|
+
return results.sort();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
VaultManager, safeFilename, parseFrontmatter, extractSources, extractBody,
|
|
236
|
+
beliefToMarkdown, nowISO, timestamp, SOURCE_EXTS,
|
|
237
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VaultObserver — derive self-evolution stats from the on-disk vault.
|
|
3
|
+
*
|
|
4
|
+
* The vault IS the state machine. Whichever runtime (pip or npm) is
|
|
5
|
+
* actually running the daemon, the effects land here:
|
|
6
|
+
*
|
|
7
|
+
* evolution/skills/<id>/SKILL.md (status: draft|promoted|pruned)
|
|
8
|
+
* evolution/skills/<id>/metrics.json (fitness, runs, successes)
|
|
9
|
+
* evolution/gap_*.md (pending gaps)
|
|
10
|
+
* value_tracker.json (in data dir) (budget + savings)
|
|
11
|
+
*
|
|
12
|
+
* This observer reads them. It exposes the same `.stats()` shape the
|
|
13
|
+
* gateways expect, so you can do:
|
|
14
|
+
*
|
|
15
|
+
* const obs = new VaultObserver('.entroly/vault');
|
|
16
|
+
* new TelegramGateway({ token, chatId }).attach(obs).start();
|
|
17
|
+
*
|
|
18
|
+
* No daemon required. No duplicated orchestration logic. Works
|
|
19
|
+
* whether the Python or Node runtime owns the vault.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
|
|
28
|
+
function parseFrontmatter(text) {
|
|
29
|
+
if (!text.startsWith('---')) return {};
|
|
30
|
+
const end = text.indexOf('\n---', 3);
|
|
31
|
+
if (end < 0) return {};
|
|
32
|
+
const meta = {};
|
|
33
|
+
for (const line of text.slice(3, end).split('\n')) {
|
|
34
|
+
const i = line.indexOf(':');
|
|
35
|
+
if (i > 0) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
|
|
36
|
+
}
|
|
37
|
+
return meta;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class VaultObserver {
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} vaultPath - Path to the vault root (default: .entroly/vault)
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {string} opts.dataDir - Where value_tracker.json lives (default: ~/.entroly)
|
|
45
|
+
*/
|
|
46
|
+
constructor(vaultPath = '.entroly/vault', opts = {}) {
|
|
47
|
+
this._vault = vaultPath;
|
|
48
|
+
this._dataDir = opts.dataDir || path.join(os.homedir(), '.entroly');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Derived state ──────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
_listSkills() {
|
|
54
|
+
const dir = path.join(this._vault, 'evolution', 'skills');
|
|
55
|
+
if (!fs.existsSync(dir)) return [];
|
|
56
|
+
return fs.readdirSync(dir)
|
|
57
|
+
.map(name => {
|
|
58
|
+
const sdir = path.join(dir, name);
|
|
59
|
+
const skillMd = path.join(sdir, 'SKILL.md');
|
|
60
|
+
if (!fs.existsSync(skillMd)) return null;
|
|
61
|
+
const meta = parseFrontmatter(fs.readFileSync(skillMd, 'utf8'));
|
|
62
|
+
let metrics = {};
|
|
63
|
+
const mp = path.join(sdir, 'metrics.json');
|
|
64
|
+
if (fs.existsSync(mp)) {
|
|
65
|
+
try { metrics = JSON.parse(fs.readFileSync(mp, 'utf8')); } catch (_) {}
|
|
66
|
+
}
|
|
67
|
+
return { id: name, status: meta.status || 'draft', entity: meta.entity || '', metrics };
|
|
68
|
+
})
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_listGaps() {
|
|
73
|
+
const dir = path.join(this._vault, 'evolution');
|
|
74
|
+
if (!fs.existsSync(dir)) return [];
|
|
75
|
+
return fs.readdirSync(dir).filter(f => f.startsWith('gap_') && f.endsWith('.md'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_readValueTracker() {
|
|
79
|
+
const p = path.join(this._dataDir, 'value_tracker.json');
|
|
80
|
+
if (!fs.existsSync(p)) return null;
|
|
81
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
82
|
+
catch (_) { return null; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_readRegistry() {
|
|
86
|
+
const p = path.join(this._vault, 'evolution', 'registry.md');
|
|
87
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Public API (matches EvolutionDaemon.stats() shape) ─────
|
|
91
|
+
|
|
92
|
+
stats() {
|
|
93
|
+
const skills = this._listSkills();
|
|
94
|
+
const promoted = skills.filter(s => s.status === 'promoted').length;
|
|
95
|
+
const pruned = skills.filter(s => s.status === 'pruned').length;
|
|
96
|
+
const structuralSuccesses = skills.filter(s =>
|
|
97
|
+
(s.metrics && s.metrics.fitness_score >= 0.5)
|
|
98
|
+
).length;
|
|
99
|
+
|
|
100
|
+
const vt = this._readValueTracker();
|
|
101
|
+
const lifetimeSaved = vt ? (vt.lifetime?.cost_saved_usd || 0) : 0;
|
|
102
|
+
const evolutionSpent = vt ? (vt.lifetime?.evolution_spent_usd || 0) : 0;
|
|
103
|
+
const earned = lifetimeSaved * 0.05;
|
|
104
|
+
const available = Math.max(0, earned - evolutionSpent);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
running: true,
|
|
108
|
+
skills_promoted: promoted,
|
|
109
|
+
skills_pruned: pruned,
|
|
110
|
+
structural_successes: structuralSuccesses,
|
|
111
|
+
dream_cycles: 0, // not observable from vault alone
|
|
112
|
+
gaps_pending: this._listGaps().length,
|
|
113
|
+
budget: {
|
|
114
|
+
available_usd: +available.toFixed(6),
|
|
115
|
+
total_earned_usd: +earned.toFixed(6),
|
|
116
|
+
total_spent_usd: +evolutionSpent.toFixed(6),
|
|
117
|
+
can_evolve: available > 0.001,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Raw accessors for gateway command handlers (/skills, /gaps, /status). */
|
|
123
|
+
skills() { return this._listSkills(); }
|
|
124
|
+
gaps() { return this._listGaps(); }
|
|
125
|
+
registry() { return this._readRegistry(); }
|
|
126
|
+
budget() { return this.stats().budget; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { VaultObserver };
|