Files
Affaan Mustafa c53bba9e02 feat: self-improving skills loop — observe, inspect, amend, evaluate
- Add skill health observation layer (execution logging, success/failure tracking)
- Add skill health inspector (trace recurring failures across runs)
- Add amendify mechanism (propose SKILL.md patches from failure evidence)
- Add evaluation scaffolding (compare amended vs original performance)
- Wire into session-inspect CLI: skills:health, skills:amendify, skills:evaluate
- 1145/1145 tests passing (+3 new)
2026-03-14 23:21:18 -07:00

109 lines
3.1 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const OBSERVATION_SCHEMA_VERSION = 'ecc.skill-observation.v1';
function resolveProjectRoot(options = {}) {
return path.resolve(options.projectRoot || options.cwd || process.cwd());
}
function getSkillTelemetryRoot(options = {}) {
return path.join(resolveProjectRoot(options), '.claude', 'ecc', 'skills');
}
function getSkillObservationsPath(options = {}) {
return path.join(getSkillTelemetryRoot(options), 'observations.jsonl');
}
function ensureString(value, label) {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`${label} must be a non-empty string`);
}
return value.trim();
}
function createObservationId() {
return `obs-${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2, 8)}`;
}
function createSkillObservation(input) {
const task = ensureString(input.task, 'task');
const skillId = ensureString(input.skill && input.skill.id, 'skill.id');
const skillPath = typeof input.skill.path === 'string' && input.skill.path.trim().length > 0
? input.skill.path.trim()
: null;
const success = Boolean(input.success);
const error = input.error == null ? null : String(input.error);
const feedback = input.feedback == null ? null : String(input.feedback);
const variant = typeof input.variant === 'string' && input.variant.trim().length > 0
? input.variant.trim()
: 'baseline';
return {
schemaVersion: OBSERVATION_SCHEMA_VERSION,
observationId: typeof input.observationId === 'string' && input.observationId.length > 0
? input.observationId
: createObservationId(),
timestamp: typeof input.timestamp === 'string' && input.timestamp.length > 0
? input.timestamp
: new Date().toISOString(),
task,
skill: {
id: skillId,
path: skillPath
},
outcome: {
success,
status: success ? 'success' : 'failure',
error,
feedback
},
run: {
variant,
amendmentId: input.amendmentId || null,
sessionId: input.sessionId || null,
source: input.source || 'manual'
}
};
}
function appendSkillObservation(observation, options = {}) {
const outputPath = getSkillObservationsPath(options);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.appendFileSync(outputPath, `${JSON.stringify(observation)}${os.EOL}`, 'utf8');
return outputPath;
}
function readSkillObservations(options = {}) {
const observationPath = path.resolve(options.observationsPath || getSkillObservationsPath(options));
if (!fs.existsSync(observationPath)) {
return [];
}
return fs.readFileSync(observationPath, 'utf8')
.split(/\r?\n/)
.filter(Boolean)
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(record => record && record.schemaVersion === OBSERVATION_SCHEMA_VERSION);
}
module.exports = {
OBSERVATION_SCHEMA_VERSION,
appendSkillObservation,
createSkillObservation,
getSkillObservationsPath,
getSkillTelemetryRoot,
readSkillObservations,
resolveProjectRoot
};