UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

500 lines 18.8 kB
/** * Evolution Pipeline * * Every change to prompts, policies, tools, and code becomes a signed change * proposal that goes through simulation, replay comparison, and staged rollout. * * Pipeline stages: * 1. Propose - Create a signed ChangeProposal * 2. Simulate - Replay golden traces with baseline vs candidate config * 3. Compare - Approve or reject based on divergence threshold * 4. Stage - Create a staged rollout plan (canary -> partial -> full) * 5. Advance - Progress through stages with metric gates * 6. Promote / Rollback - Apply permanently or revert * * @module @claude-flow/guidance/evolution */ import { createHash, createHmac, randomUUID } from 'node:crypto'; const DEFAULT_MAX_DIVERGENCE = 0.3; const DEFAULT_STAGES = [ { name: 'canary', percentage: 5, durationMs: 60_000, metrics: {}, divergenceThreshold: 0.2, passed: null, startedAt: null, completedAt: null, }, { name: 'partial', percentage: 50, durationMs: 300_000, metrics: {}, divergenceThreshold: 0.25, passed: null, startedAt: null, completedAt: null, }, { name: 'full', percentage: 100, durationMs: 600_000, metrics: {}, divergenceThreshold: 0.3, passed: null, startedAt: null, completedAt: null, }, ]; // ============================================================================ // EvolutionPipeline // ============================================================================ /** * The Evolution Pipeline manages the lifecycle of change proposals through * signing, simulation, comparison, staged rollout, and promotion or rollback. */ export class EvolutionPipeline { signingKey; maxDivergence; defaultStages; proposals = new Map(); simulations = new Map(); rollouts = new Map(); constructor(config = {}) { if (!config.signingKey) { throw new Error('EvolutionPipeline requires an explicit signingKey — hardcoded defaults are not secure'); } this.signingKey = config.signingKey; this.maxDivergence = config.maxDivergence ?? DEFAULT_MAX_DIVERGENCE; this.defaultStages = config.stages ?? DEFAULT_STAGES; } // ========================================================================== // Propose // ========================================================================== /** * Create and sign a new change proposal. */ propose(params) { const proposalId = randomUUID(); const createdAt = Date.now(); const proposal = { proposalId, kind: params.kind, title: params.title, description: params.description, author: params.author, targetPath: params.targetPath, diff: params.diff, rationale: params.rationale, riskAssessment: params.riskAssessment, signature: '', // placeholder, signed below createdAt, status: 'draft', }; proposal.signature = this.signProposal(proposal); proposal.status = 'signed'; this.proposals.set(proposalId, proposal); return proposal; } // ========================================================================== // Simulate // ========================================================================== /** * Run golden traces through both baseline and candidate configs to measure * divergence. The evaluator is called once per golden trace per config. */ simulate(proposalId, goldenTraces, evaluator) { const proposal = this.proposals.get(proposalId); if (!proposal) { throw new Error(`Proposal not found: ${proposalId}`); } proposal.status = 'simulating'; // Evaluate each trace against both configs const baselineResults = goldenTraces.map(t => evaluator(t, 'baseline')); const candidateResults = goldenTraces.map(t => evaluator(t, 'candidate')); // Compute composite trace hashes const baselineTraceHash = this.hashTraceResults(baselineResults.map(r => r.traceHash)); const candidateTraceHash = this.hashTraceResults(candidateResults.map(r => r.traceHash)); // Compute decision diffs const decisionDiffs = []; for (let i = 0; i < goldenTraces.length; i++) { const bDecisions = baselineResults[i].decisions; const cDecisions = candidateResults[i].decisions; const maxLen = Math.max(bDecisions.length, cDecisions.length); for (let seq = 0; seq < maxLen; seq++) { const bVal = seq < bDecisions.length ? bDecisions[seq] : undefined; const cVal = seq < cDecisions.length ? cDecisions[seq] : undefined; if (JSON.stringify(bVal) !== JSON.stringify(cVal)) { decisionDiffs.push({ seq, baseline: bVal, candidate: cVal, severity: this.classifyDiffSeverity(bVal, cVal), }); } } } // Aggregate metrics const baselineMetrics = this.aggregateMetrics(baselineResults.map(r => r.metrics)); const candidateMetrics = this.aggregateMetrics(candidateResults.map(r => r.metrics)); // Compute divergence score (0-1) const divergenceScore = this.computeDivergenceScore(baselineTraceHash, candidateTraceHash, decisionDiffs, goldenTraces.length); const passed = divergenceScore <= this.maxDivergence; const reason = passed ? `Divergence ${divergenceScore.toFixed(3)} is within threshold ${this.maxDivergence}` : `Divergence ${divergenceScore.toFixed(3)} exceeds threshold ${this.maxDivergence}`; const result = { proposalId, baselineTraceHash, candidateTraceHash, divergenceScore, decisionDiffs, metricsComparison: { baseline: baselineMetrics, candidate: candidateMetrics, }, passed, reason, }; this.simulations.set(proposalId, result); return result; } // ========================================================================== // Compare // ========================================================================== /** * Compare a simulation result against acceptance criteria. * * Checks: * 1. Divergence is below threshold * 2. No regression in key metrics (candidate >= baseline) */ compare(proposalId, simulationResult) { const proposal = this.proposals.get(proposalId); if (!proposal) { throw new Error(`Proposal not found: ${proposalId}`); } // Check divergence threshold if (simulationResult.divergenceScore > this.maxDivergence) { proposal.status = 'rejected'; return { approved: false, reason: `Divergence ${simulationResult.divergenceScore.toFixed(3)} exceeds threshold ${this.maxDivergence}`, }; } // Check for metric regressions const { baseline, candidate } = simulationResult.metricsComparison; const regressions = []; for (const key of Object.keys(baseline)) { if (candidate[key] !== undefined && candidate[key] < baseline[key]) { const pctDrop = ((baseline[key] - candidate[key]) / Math.max(baseline[key], 1)) * 100; // Only flag significant regressions (> 5%) if (pctDrop > 5) { regressions.push(`${key} regressed by ${pctDrop.toFixed(1)}%`); } } } if (regressions.length > 0) { proposal.status = 'rejected'; return { approved: false, reason: `Metric regressions detected: ${regressions.join('; ')}`, }; } proposal.status = 'compared'; return { approved: true, reason: `Divergence ${simulationResult.divergenceScore.toFixed(3)} within threshold, no metric regressions`, }; } // ========================================================================== // Stage // ========================================================================== /** * Create a staged rollout plan for a proposal. */ stage(proposalId) { const proposal = this.proposals.get(proposalId); if (!proposal) { throw new Error(`Proposal not found: ${proposalId}`); } const now = Date.now(); // Deep-clone default stages so each rollout has independent state const stages = this.defaultStages.map(s => ({ ...s, metrics: { ...s.metrics }, passed: null, startedAt: null, completedAt: null, })); // Start the first stage immediately stages[0].startedAt = now; const rollout = { rolloutId: randomUUID(), proposalId, stages, currentStage: 0, status: 'in-progress', startedAt: now, completedAt: null, }; proposal.status = 'staged'; this.rollouts.set(rollout.rolloutId, rollout); return rollout; } // ========================================================================== // Advance Stage // ========================================================================== /** * Advance to the next rollout stage or auto-rollback. * * If `stageMetrics.divergence` exceeds the current stage's threshold, * the rollout is automatically rolled back. */ advanceStage(rolloutId, stageMetrics) { const rollout = this.rollouts.get(rolloutId); if (!rollout) { throw new Error(`Rollout not found: ${rolloutId}`); } if (rollout.status !== 'in-progress') { return { advanced: false, rolledBack: false, reason: `Rollout is ${rollout.status}, not in-progress`, }; } const current = rollout.stages[rollout.currentStage]; const now = Date.now(); // Record metrics on the current stage current.metrics = { ...stageMetrics }; // Check divergence against threshold const divergence = stageMetrics.divergence ?? 0; if (divergence > current.divergenceThreshold) { // Auto-rollback current.passed = false; current.completedAt = now; this.rollback(rolloutId, `Stage "${current.name}" divergence ${divergence.toFixed(3)} exceeded threshold ${current.divergenceThreshold}`); return { advanced: false, rolledBack: true, reason: `Auto-rollback: divergence ${divergence.toFixed(3)} exceeded threshold ${current.divergenceThreshold} at stage "${current.name}"`, }; } // Current stage passed current.passed = true; current.completedAt = now; // Check if there are more stages if (rollout.currentStage < rollout.stages.length - 1) { rollout.currentStage += 1; rollout.stages[rollout.currentStage].startedAt = now; return { advanced: true, rolledBack: false, reason: `Advanced to stage "${rollout.stages[rollout.currentStage].name}"`, }; } // All stages complete - auto-promote rollout.status = 'completed'; rollout.completedAt = now; const proposal = this.proposals.get(rollout.proposalId); if (proposal) { proposal.status = 'promoted'; } return { advanced: true, rolledBack: false, reason: 'All stages completed successfully; proposal promoted', }; } // ========================================================================== // Rollback // ========================================================================== /** * Roll back a staged rollout. */ rollback(rolloutId, reason) { const rollout = this.rollouts.get(rolloutId); if (!rollout) { throw new Error(`Rollout not found: ${rolloutId}`); } rollout.status = 'rolled-back'; rollout.completedAt = Date.now(); const proposal = this.proposals.get(rollout.proposalId); if (proposal) { proposal.status = 'rolled-back'; } } // ========================================================================== // Promote // ========================================================================== /** * Promote a rollout, permanently applying the change. */ promote(rolloutId) { const rollout = this.rollouts.get(rolloutId); if (!rollout) { throw new Error(`Rollout not found: ${rolloutId}`); } rollout.status = 'completed'; rollout.completedAt = Date.now(); const proposal = this.proposals.get(rollout.proposalId); if (proposal) { proposal.status = 'promoted'; } } // ========================================================================== // Queries // ========================================================================== /** * Get a proposal by ID. */ getProposal(id) { return this.proposals.get(id); } /** * Get all proposals, optionally filtered by status. */ getProposals(status) { const all = Array.from(this.proposals.values()); if (status === undefined) { return all; } return all.filter(p => p.status === status); } /** * Get a rollout by ID. */ getRollout(id) { return this.rollouts.get(id); } /** * Get the full evolution history across all proposals. */ getHistory() { return Array.from(this.proposals.values()).map(proposal => ({ proposal, simulation: this.simulations.get(proposal.proposalId), rollout: this.findRolloutByProposal(proposal.proposalId), outcome: proposal.status, })); } // ========================================================================== // Private Helpers // ========================================================================== /** * Produce an HMAC-SHA256 signature for a proposal. * * The signature covers every field except `signature` and `status`. */ signProposal(proposal) { const body = { proposalId: proposal.proposalId, kind: proposal.kind, title: proposal.title, description: proposal.description, author: proposal.author, targetPath: proposal.targetPath, diff: proposal.diff, rationale: proposal.rationale, riskAssessment: proposal.riskAssessment, createdAt: proposal.createdAt, }; const payload = JSON.stringify(body); return createHmac('sha256', this.signingKey).update(payload).digest('hex'); } /** * Compute a composite hash from an array of trace hashes. */ hashTraceResults(traceHashes) { const payload = traceHashes.join(':'); return createHash('sha256').update(payload).digest('hex'); } /** * Classify how severe a single decision diff is. */ classifyDiffSeverity(baseline, candidate) { // If one is undefined (extra/missing decision), it is high if (baseline === undefined || candidate === undefined) { return 'high'; } const bStr = JSON.stringify(baseline); const cStr = JSON.stringify(candidate); // Very different lengths suggest structural changes if (Math.abs(bStr.length - cStr.length) > bStr.length * 0.5) { return 'high'; } // Moderate difference if (bStr.length > 0 && Math.abs(bStr.length - cStr.length) > bStr.length * 0.2) { return 'medium'; } return 'low'; } /** * Compute an overall divergence score (0-1). */ computeDivergenceScore(baselineHash, candidateHash, diffs, traceCount) { // If hashes are identical, divergence = 0 if (baselineHash === candidateHash) { return 0; } // If there are no golden traces, treat as fully divergent if (traceCount === 0) { return 1; } // Weight diffs by severity const severityWeights = { low: 0.1, medium: 0.4, high: 1.0, }; const totalWeight = diffs.reduce((sum, d) => sum + (severityWeights[d.severity] ?? 0.5), 0); // Normalize: max possible weight is traceCount * maxDecisionsPerTrace // Use a heuristic cap to keep score in [0, 1] const maxExpected = traceCount * 5; // assume ~5 decisions per trace at max weight const raw = totalWeight / Math.max(maxExpected, 1); return Math.min(1, Math.max(0, raw)); } /** * Aggregate an array of metric records into averages. */ aggregateMetrics(records) { if (records.length === 0) return {}; const sums = {}; const counts = {}; for (const record of records) { for (const [key, value] of Object.entries(record)) { sums[key] = (sums[key] ?? 0) + value; counts[key] = (counts[key] ?? 0) + 1; } } const result = {}; for (const key of Object.keys(sums)) { result[key] = sums[key] / counts[key]; } return result; } /** * Find the rollout associated with a proposal. */ findRolloutByProposal(proposalId) { for (const rollout of this.rollouts.values()) { if (rollout.proposalId === proposalId) { return rollout; } } return undefined; } } // ============================================================================ // Factory // ============================================================================ /** * Create an EvolutionPipeline instance. */ export function createEvolutionPipeline(config) { return new EvolutionPipeline(config); } //# sourceMappingURL=evolution.js.map