mindforge-cc 10.7.0 → 11.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/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
- package/.mindforge/config.json +6 -1
- package/CHANGELOG.md +64 -0
- package/MINDFORGE.md +3 -3
- package/README.md +49 -4
- package/RELEASENOTES.md +80 -0
- package/SECURITY.md +20 -8
- package/bin/autonomous/audit-writer.js +13 -0
- package/bin/autonomous/auto-runner.js +74 -16
- package/bin/autonomous/context-refactorer.js +26 -11
- package/bin/autonomous/state-manager.js +62 -6
- package/bin/autonomous/stuck-monitor.js +46 -7
- package/bin/autonomous/wave-executor.js +66 -25
- package/bin/dashboard/api-router.js +43 -0
- package/bin/dashboard/metrics-aggregator.js +28 -1
- package/bin/dashboard/server.js +67 -4
- package/bin/dashboard/sse-bridge.js +4 -4
- package/bin/engine/feedback-loop.js +8 -0
- package/bin/engine/intelligence-interlock.js +32 -15
- package/bin/engine/logic-drift-detector.js +2 -1
- package/bin/engine/nexus-tracer.js +3 -2
- package/bin/engine/remediation-engine.js +155 -32
- package/bin/engine/self-corrective-synthesizer.js +84 -10
- package/bin/engine/sre-manager.js +12 -4
- package/bin/engine/temporal-hub.js +131 -34
- package/bin/governance/approve.js +41 -5
- package/bin/governance/impact-analyzer.js +28 -0
- package/bin/governance/policy-engine.js +10 -3
- package/bin/governance/quantum-crypto.js +32 -19
- package/bin/governance/rbac-manager.js +74 -2
- package/bin/governance/ztai-manager.js +49 -7
- package/bin/hindsight-injector.js +3 -3
- package/bin/memory/eis-client.js +71 -34
- package/bin/memory/embedding-engine.js +61 -0
- package/bin/memory/knowledge-graph.js +58 -5
- package/bin/memory/knowledge-indexer.js +53 -6
- package/bin/memory/knowledge-store.js +22 -0
- package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
- package/bin/migrations/schema-versions.js +13 -0
- package/bin/models/anthropic-provider.js +45 -0
- package/bin/models/cloud-broker.js +68 -20
- package/bin/models/gemini-provider.js +51 -0
- package/bin/models/model-client.js +20 -0
- package/bin/models/model-router.js +28 -8
- package/bin/models/openai-provider.js +44 -0
- package/bin/utils/file-io.js +63 -1
- package/bin/utils/index.js +58 -0
- package/docs/getting-started.md +1 -1
- package/docs/user-guide.md +2 -2
- package/package.json +2 -2
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MindForge
|
|
2
|
+
* MindForge v11.0.0 — Neural Drift Remediation (NDR)
|
|
3
3
|
* Component: Remediation Engine (Pillar X)
|
|
4
|
-
*
|
|
5
|
-
* Triggers corrective actions when logic drift or reasoning
|
|
6
|
-
* stagnation is detected.
|
|
4
|
+
*
|
|
5
|
+
* Triggers corrective actions when logic drift or reasoning
|
|
6
|
+
* stagnation is detected. v11: Full strategy implementations
|
|
7
|
+
* for CONTEXT_COMPRESSION, GOLDEN_TRACE_INJECTION, and REASONING_RESTART.
|
|
7
8
|
*/
|
|
8
9
|
'use strict';
|
|
9
10
|
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
10
13
|
const remediationQueue = require('../revops/remediation-queue');
|
|
11
14
|
const logicValidator = require('./logic-validator');
|
|
12
|
-
|
|
15
|
+
|
|
16
|
+
const MAX_PENDING_REMEDIATIONS = 50;
|
|
13
17
|
|
|
14
18
|
class RemediationEngine {
|
|
15
19
|
constructor() {
|
|
16
|
-
this.activeRemediations = new
|
|
20
|
+
this.activeRemediations = new Map();
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Triggers a specific remediation workflow.
|
|
21
|
-
* @param {string} spanId
|
|
25
|
+
* @param {string} spanId
|
|
22
26
|
* @param {Object} report - From LogicDriftDetector
|
|
23
27
|
*/
|
|
24
28
|
async trigger(spanId, report) {
|
|
25
29
|
const { drift_score, markers } = report;
|
|
26
30
|
let strategy = 'NOT_REQUIRED';
|
|
27
31
|
|
|
28
|
-
// Tiered Remediation Logic
|
|
29
32
|
if (drift_score > 0.9) strategy = 'REASONING_RESTART';
|
|
30
33
|
else if (drift_score > 0.8 || report.invalid_logic) strategy = 'GOLDEN_TRACE_INJECTION';
|
|
31
34
|
else if (drift_score > 0.75) strategy = 'CONTEXT_COMPRESSION';
|
|
@@ -41,40 +44,160 @@ class RemediationEngine {
|
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
console.log(`[Remediation] Triggered ${strategy} for ${spanId} (Score: ${drift_score})`);
|
|
44
|
-
|
|
45
|
-
// v7: Finalize with Stateful Queueing
|
|
47
|
+
|
|
46
48
|
await remediationQueue.enqueue(action);
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const result = await this._executeStrategy(strategy, spanId);
|
|
51
|
+
|
|
52
|
+
this.activeRemediations.set(action.remediation_id, {
|
|
53
|
+
spanId,
|
|
54
|
+
strategy: action.strategy,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
preScore: drift_score
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Evict oldest entries if map exceeds bound
|
|
60
|
+
if (this.activeRemediations.size > MAX_PENDING_REMEDIATIONS) {
|
|
61
|
+
const firstKey = this.activeRemediations.keys().next().value;
|
|
62
|
+
this.activeRemediations.delete(firstKey);
|
|
63
|
+
}
|
|
50
64
|
|
|
51
|
-
return action;
|
|
65
|
+
return { ...action, execution: result };
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
/**
|
|
55
|
-
* functional implementation of remediation strategies.
|
|
56
|
-
*/
|
|
57
68
|
async _executeStrategy(strategy, spanId) {
|
|
58
|
-
switch(strategy) {
|
|
59
|
-
case '
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
switch (strategy) {
|
|
70
|
+
case 'CONTEXT_COMPRESSION': return this._executeContextCompression(spanId);
|
|
71
|
+
case 'GOLDEN_TRACE_INJECTION': return this._executeGoldenTraceInjection(spanId);
|
|
72
|
+
case 'REASONING_RESTART': return this._executeReasoningRestart(spanId);
|
|
73
|
+
default: return { strategy, result: 'unknown_strategy' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _executeContextCompression(spanId) {
|
|
78
|
+
try {
|
|
79
|
+
const { ContextEntropyGuard } = require('./context-entropy-guard');
|
|
80
|
+
const guard = typeof ContextEntropyGuard === 'function'
|
|
81
|
+
? new ContextEntropyGuard()
|
|
82
|
+
: ContextEntropyGuard;
|
|
83
|
+
|
|
84
|
+
const traces = this._getRecentTraces(spanId, 20);
|
|
85
|
+
const compressed = guard.compress(traces);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
strategy: 'CONTEXT_COMPRESSION',
|
|
89
|
+
result: 'applied',
|
|
90
|
+
tracesCompressed: traces.length,
|
|
91
|
+
outputSize: compressed.length
|
|
92
|
+
};
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return {
|
|
95
|
+
strategy: 'CONTEXT_COMPRESSION',
|
|
96
|
+
result: 'error',
|
|
97
|
+
message: err.message
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _executeGoldenTraceInjection(spanId) {
|
|
103
|
+
try {
|
|
104
|
+
let SemanticHub;
|
|
105
|
+
try {
|
|
106
|
+
SemanticHub = require('../memory/semantic-hub');
|
|
107
|
+
} catch {
|
|
108
|
+
return { strategy: 'GOLDEN_TRACE_INJECTION', result: 'unavailable' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await SemanticHub.ensureInit();
|
|
112
|
+
const goldenTraces = await SemanticHub.getGoldenTraces({ limit: 3 });
|
|
113
|
+
|
|
114
|
+
if (!goldenTraces || goldenTraces.length === 0) {
|
|
115
|
+
return { strategy: 'GOLDEN_TRACE_INJECTION', result: 'no_traces_found' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
strategy: 'GOLDEN_TRACE_INJECTION',
|
|
120
|
+
result: 'injected',
|
|
121
|
+
tracesInjected: goldenTraces.length,
|
|
122
|
+
traceIds: goldenTraces.map(t => t.id || t.trace_id).filter(Boolean)
|
|
123
|
+
};
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
strategy: 'GOLDEN_TRACE_INJECTION',
|
|
127
|
+
result: 'error',
|
|
128
|
+
message: err.message
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async _executeReasoningRestart(spanId) {
|
|
134
|
+
try {
|
|
135
|
+
return {
|
|
136
|
+
strategy: 'REASONING_RESTART',
|
|
137
|
+
result: 'signalled',
|
|
138
|
+
instruction: 'Clear current reasoning context and re-read project constitution',
|
|
139
|
+
spanId
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return {
|
|
143
|
+
strategy: 'REASONING_RESTART',
|
|
144
|
+
result: 'error',
|
|
145
|
+
message: err.message
|
|
146
|
+
};
|
|
73
147
|
}
|
|
74
148
|
}
|
|
75
149
|
|
|
150
|
+
_getRecentTraces(spanId, limit) {
|
|
151
|
+
try {
|
|
152
|
+
const NexusTracer = require('./nexus-tracer');
|
|
153
|
+
const spans = NexusTracer.activeSpans || new Map();
|
|
154
|
+
return Array.from(spans.values()).slice(-limit);
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
evaluateOutcome(spanId, currentDriftScore) {
|
|
161
|
+
const results = [];
|
|
162
|
+
for (const [remId, rem] of this.activeRemediations) {
|
|
163
|
+
if (rem.spanId === spanId) {
|
|
164
|
+
const improved = currentDriftScore < rem.preScore;
|
|
165
|
+
const effectiveness = improved ? (rem.preScore - currentDriftScore) / rem.preScore : 0;
|
|
166
|
+
results.push({
|
|
167
|
+
remediation_id: remId,
|
|
168
|
+
strategy: rem.strategy,
|
|
169
|
+
effective: improved,
|
|
170
|
+
effectiveness_score: Math.round(effectiveness * 100) / 100,
|
|
171
|
+
pre_score: rem.preScore,
|
|
172
|
+
post_score: currentDriftScore
|
|
173
|
+
});
|
|
174
|
+
this.activeRemediations.delete(remId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (results.length > 0) {
|
|
178
|
+
this._persistEffectivenessStats(results);
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_persistEffectivenessStats(results) {
|
|
184
|
+
try {
|
|
185
|
+
const statsPath = path.join(process.cwd(), 'bin', 'models', 'performance-stats.json');
|
|
186
|
+
let stats = {};
|
|
187
|
+
if (fs.existsSync(statsPath)) {
|
|
188
|
+
stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
|
189
|
+
}
|
|
190
|
+
if (!stats.remediation_effectiveness) stats.remediation_effectiveness = [];
|
|
191
|
+
stats.remediation_effectiveness.push(...results);
|
|
192
|
+
if (stats.remediation_effectiveness.length > 100) {
|
|
193
|
+
stats.remediation_effectiveness = stats.remediation_effectiveness.slice(-100);
|
|
194
|
+
}
|
|
195
|
+
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2));
|
|
196
|
+
} catch { /* non-critical */ }
|
|
197
|
+
}
|
|
198
|
+
|
|
76
199
|
getActiveRemediations() {
|
|
77
|
-
return Array.from(this.activeRemediations);
|
|
200
|
+
return Array.from(this.activeRemediations.entries()).map(([id, data]) => ({ id, ...data }));
|
|
78
201
|
}
|
|
79
202
|
}
|
|
80
203
|
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MindForge
|
|
2
|
+
* MindForge v11.0.0 — Self-Corrective Synthesis (SCS)
|
|
3
3
|
* Component: Self-Corrective Synthesizer (Pillar XII)
|
|
4
|
-
*
|
|
5
|
-
* Analyzes mission drift and logic stagnation to synthesize
|
|
4
|
+
*
|
|
5
|
+
* Analyzes mission drift and logic stagnation to synthesize
|
|
6
6
|
* corrective steering signals (Homing Instructions).
|
|
7
|
+
*
|
|
8
|
+
* v11: Expanded analysis window (50 events), exponential decay weighting,
|
|
9
|
+
* and correction effectiveness tracking.
|
|
7
10
|
*/
|
|
8
11
|
'use strict';
|
|
9
12
|
|
|
10
13
|
const rsa = require('./reason-source-aligner.js');
|
|
11
14
|
|
|
15
|
+
const HISTORY_LIMIT = 50;
|
|
16
|
+
const DECAY_FACTOR = 0.95;
|
|
17
|
+
const MAX_CORRECTION_HISTORY = 20;
|
|
18
|
+
|
|
12
19
|
class SelfCorrectiveSynthesizer {
|
|
13
20
|
constructor() {
|
|
14
|
-
this.historyLimit =
|
|
21
|
+
this.historyLimit = HISTORY_LIMIT;
|
|
15
22
|
this.synthesisCount = 0;
|
|
23
|
+
this.correctionHistory = [];
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/**
|
|
@@ -22,9 +30,18 @@ class SelfCorrectiveSynthesizer {
|
|
|
22
30
|
*/
|
|
23
31
|
async synthesizeCorrection(auditTrail, context) {
|
|
24
32
|
console.log('[SCS] Critical drift detected. Initiating internal alignment pass...');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
|
|
34
|
+
this._evaluatePreviousCorrection(auditTrail);
|
|
35
|
+
|
|
36
|
+
const recentEvents = auditTrail.slice(-this.historyLimit);
|
|
37
|
+
|
|
38
|
+
// Weight events by recency: newest = 1.0, decays by 0.95^position
|
|
39
|
+
const weightedEvents = recentEvents.map((event, i) => ({
|
|
40
|
+
...event,
|
|
41
|
+
weight: Math.pow(DECAY_FACTOR, recentEvents.length - 1 - i)
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const failureEvents = weightedEvents.filter(e =>
|
|
28
45
|
e.type === 'mission_fidelity' && e.alignment.confidence < 0.50
|
|
29
46
|
);
|
|
30
47
|
|
|
@@ -32,27 +49,84 @@ class SelfCorrectiveSynthesizer {
|
|
|
32
49
|
return this._generateGenericRefocus(context);
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
52
|
+
// Weighted sort: higher weight (more recent) failures surface first
|
|
53
|
+
const sortedFailures = [...failureEvents].sort((a, b) => b.weight - a.weight);
|
|
54
|
+
|
|
55
|
+
const targetId = sortedFailures[0].alignment.best_match_id;
|
|
37
56
|
const requirement = rsa.getRequirementDetails(targetId);
|
|
38
57
|
|
|
39
58
|
if (!requirement) {
|
|
40
59
|
return this._generateGenericRefocus(context);
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
// 3. Synthesize the "Homing Signal"
|
|
44
62
|
this.synthesisCount++;
|
|
63
|
+
|
|
64
|
+
const currentConfidence = sortedFailures[0].alignment.confidence;
|
|
65
|
+
const correctionId = `scs_${Date.now()}_${this.synthesisCount}`;
|
|
66
|
+
|
|
45
67
|
const correction = {
|
|
46
68
|
type: 'scs_refocus',
|
|
69
|
+
correctionId,
|
|
47
70
|
req_id: targetId,
|
|
48
71
|
instruction: `[SCS-REFOCUS] Targeting [${targetId}]: ${requirement.summary}. Action: Resuming strict alignment with core requirement: ${requirement.description.split('\n')[0]}`,
|
|
49
72
|
confidence: 0.98
|
|
50
73
|
};
|
|
51
74
|
|
|
75
|
+
this._recordCorrection(correctionId, currentConfidence);
|
|
76
|
+
|
|
52
77
|
console.log(`[SCS] Synthesis complete. Correction targeted at ${targetId}.`);
|
|
53
78
|
return correction;
|
|
54
79
|
}
|
|
55
80
|
|
|
81
|
+
_evaluatePreviousCorrection(auditTrail) {
|
|
82
|
+
if (this.correctionHistory.length === 0) return;
|
|
83
|
+
|
|
84
|
+
const lastCorrection = this.correctionHistory[this.correctionHistory.length - 1];
|
|
85
|
+
if (lastCorrection.effective !== undefined) return;
|
|
86
|
+
|
|
87
|
+
const recentEvents = auditTrail.slice(-this.historyLimit);
|
|
88
|
+
const fidelityEvents = recentEvents.filter(e =>
|
|
89
|
+
e.type === 'mission_fidelity' && e.alignment
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (fidelityEvents.length === 0) return;
|
|
93
|
+
|
|
94
|
+
const latestConfidence = fidelityEvents[fidelityEvents.length - 1].alignment.confidence;
|
|
95
|
+
const improved = latestConfidence > lastCorrection.preConfidence;
|
|
96
|
+
|
|
97
|
+
// Immutable update: replace last entry with effectiveness result
|
|
98
|
+
const updatedEntry = {
|
|
99
|
+
...lastCorrection,
|
|
100
|
+
postConfidence: latestConfidence,
|
|
101
|
+
effective: improved
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
this.correctionHistory = [
|
|
105
|
+
...this.correctionHistory.slice(0, -1),
|
|
106
|
+
updatedEntry
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_recordCorrection(correctionId, preConfidence) {
|
|
111
|
+
const entry = {
|
|
112
|
+
correctionId,
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
preConfidence
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (this.correctionHistory.length >= MAX_CORRECTION_HISTORY) {
|
|
118
|
+
this.correctionHistory = [...this.correctionHistory.slice(1), entry];
|
|
119
|
+
} else {
|
|
120
|
+
this.correctionHistory = [...this.correctionHistory, entry];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getEffectivenessRate() {
|
|
125
|
+
if (this.correctionHistory.length === 0) return null;
|
|
126
|
+
const effective = this.correctionHistory.filter(c => c.effective).length;
|
|
127
|
+
return effective / this.correctionHistory.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
56
130
|
_generateGenericRefocus(context) {
|
|
57
131
|
return {
|
|
58
132
|
type: 'scs_refocus',
|
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const ENCLAVE_PRIVATE_KEY = 'tier3-enclave-secret-key-sim'; // In production, this would be a TEE-bound private key
|
|
9
|
+
const EPHEMERAL_ENCLAVE_KEY = crypto.randomBytes(32).toString('hex');
|
|
11
10
|
const SYSTEM_DID = 'did:mindforge:enclave:0xenterprise';
|
|
12
11
|
|
|
12
|
+
let _enclaveWarningShown = false;
|
|
13
|
+
function warnNonTEE() {
|
|
14
|
+
if (!_enclaveWarningShown) {
|
|
15
|
+
console.warn('[SRE] Running in simulated enclave mode — not backed by hardware TEE');
|
|
16
|
+
_enclaveWarningShown = true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
class SREManager {
|
|
14
21
|
constructor() {
|
|
15
22
|
this.activeEnclaves = new Map();
|
|
@@ -25,6 +32,7 @@ class SREManager {
|
|
|
25
32
|
throw new Error(`[SRE-DENY] Tier ${context.tier} principal is not authorized for Sovereign Reason Enclaves.`);
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
warnNonTEE();
|
|
28
36
|
const enclaveId = crypto.randomBytes(12).toString('hex');
|
|
29
37
|
this.activeEnclaves.set(enclaveId, {
|
|
30
38
|
startedAt: new Date().toISOString(),
|
|
@@ -67,7 +75,7 @@ class SREManager {
|
|
|
67
75
|
};
|
|
68
76
|
|
|
69
77
|
// Sign the proof with the Enclave Private Key
|
|
70
|
-
const signature = crypto.createHmac('sha256',
|
|
78
|
+
const signature = crypto.createHmac('sha256', EPHEMERAL_ENCLAVE_KEY)
|
|
71
79
|
.update(JSON.stringify(proofPayload))
|
|
72
80
|
.digest('hex');
|
|
73
81
|
|
|
@@ -93,7 +101,7 @@ class SREManager {
|
|
|
93
101
|
verifyZKProof(certificate) {
|
|
94
102
|
if (certificate.status !== 'SRE-ISOLATED') return false;
|
|
95
103
|
|
|
96
|
-
const expectedSignature = crypto.createHmac('sha256',
|
|
104
|
+
const expectedSignature = crypto.createHmac('sha256', EPHEMERAL_ENCLAVE_KEY)
|
|
97
105
|
.update(JSON.stringify(certificate.proof))
|
|
98
106
|
.digest('hex');
|
|
99
107
|
|
|
@@ -1,67 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MindForge v3 — Temporal Hub (State Versioner)
|
|
3
3
|
* Managed high-fidelity snapshots of the .planning directory.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Design:
|
|
6
6
|
* - Each snapshot is identified by an audit_id.
|
|
7
7
|
* - Snapshots are stored in .planning/history/[audit_id]/
|
|
8
8
|
* - Atomic snapshots ensure time-travel debugging consistency.
|
|
9
|
+
* - HMAC integrity signatures on metadata for tamper detection.
|
|
9
10
|
*/
|
|
10
11
|
'use strict';
|
|
11
12
|
|
|
12
13
|
const fs = require('fs');
|
|
14
|
+
const fsPromises = require('fs/promises');
|
|
13
15
|
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
14
17
|
const { execSync } = require('child_process');
|
|
15
18
|
|
|
16
19
|
const PLANNING_DIR = path.join(process.cwd(), '.planning');
|
|
17
20
|
const HISTORY_DIR = path.join(PLANNING_DIR, 'history');
|
|
18
21
|
|
|
22
|
+
const HMAC_KEY = 'mindforge-temporal-v3';
|
|
23
|
+
|
|
19
24
|
class TemporalHub {
|
|
25
|
+
|
|
26
|
+
static _signMetadata(metadata) {
|
|
27
|
+
const content = JSON.stringify(metadata);
|
|
28
|
+
const hmac = crypto.createHmac('sha256', HMAC_KEY)
|
|
29
|
+
.update(content)
|
|
30
|
+
.digest('hex');
|
|
31
|
+
return { ...metadata, integrity: hmac };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static _verifyMetadata(metadata) {
|
|
35
|
+
if (!metadata.integrity) return false;
|
|
36
|
+
const { integrity, ...rest } = metadata;
|
|
37
|
+
const expected = crypto.createHmac('sha256', HMAC_KEY)
|
|
38
|
+
.update(JSON.stringify(rest))
|
|
39
|
+
.digest('hex');
|
|
40
|
+
return crypto.timingSafeEqual(Buffer.from(integrity), Buffer.from(expected));
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
/**
|
|
21
44
|
* Capture the current state of the .planning directory.
|
|
22
45
|
* @param {string} auditId - Unique identifier from AUDIT.jsonl
|
|
23
46
|
* @param {object} metadata - Optional context (task_name, session_id)
|
|
47
|
+
* @returns {Promise<string|null>} Path to snapshot dir, or null on failure
|
|
24
48
|
*/
|
|
25
|
-
static captureState(auditId, metadata = {}) {
|
|
49
|
+
static async captureState(auditId, metadata = {}) {
|
|
26
50
|
if (!/^[a-f0-9-]{8,40}$/.test(auditId)) {
|
|
27
51
|
throw new Error('Invalid audit ID format');
|
|
28
52
|
}
|
|
29
|
-
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await fsPromises.access(PLANNING_DIR);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
30
59
|
|
|
31
60
|
const snapshotDir = path.join(HISTORY_DIR, auditId);
|
|
32
61
|
if (!path.resolve(snapshotDir).startsWith(path.resolve(HISTORY_DIR))) {
|
|
33
62
|
throw new Error('Path traversal detected in audit ID');
|
|
34
63
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
64
|
+
|
|
65
|
+
await fsPromises.mkdir(snapshotDir, { recursive: true });
|
|
38
66
|
|
|
39
67
|
try {
|
|
40
|
-
|
|
41
|
-
const files =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const ext = path.extname(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
const allEntries = await fsPromises.readdir(PLANNING_DIR, { withFileTypes: true });
|
|
69
|
+
const files = [];
|
|
70
|
+
|
|
71
|
+
for (const entry of allEntries) {
|
|
72
|
+
if (entry.isDirectory()) continue;
|
|
73
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
74
|
+
if (['.md', '.json', '.yml', '.yaml', '.log'].includes(ext)) {
|
|
75
|
+
files.push(entry.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await Promise.all(files.map(file =>
|
|
80
|
+
fsPromises.copyFile(
|
|
52
81
|
path.join(PLANNING_DIR, file),
|
|
53
82
|
path.join(snapshotDir, file)
|
|
54
|
-
)
|
|
55
|
-
|
|
83
|
+
)
|
|
84
|
+
));
|
|
56
85
|
|
|
57
|
-
// 3. Save snapshot metadata
|
|
58
86
|
const meta = {
|
|
59
87
|
id: auditId,
|
|
60
88
|
timestamp: new Date().toISOString(),
|
|
61
89
|
...metadata,
|
|
62
90
|
files: files
|
|
63
91
|
};
|
|
64
|
-
|
|
92
|
+
const signedMeta = TemporalHub._signMetadata(meta);
|
|
93
|
+
await fsPromises.writeFile(
|
|
94
|
+
path.join(snapshotDir, 'SNAPSHOT-META.json'),
|
|
95
|
+
JSON.stringify(signedMeta, null, 2)
|
|
96
|
+
);
|
|
65
97
|
|
|
66
98
|
return snapshotDir;
|
|
67
99
|
} catch (err) {
|
|
@@ -72,9 +104,11 @@ class TemporalHub {
|
|
|
72
104
|
|
|
73
105
|
/**
|
|
74
106
|
* Restore the .planning directory to a specific snapshot.
|
|
75
|
-
*
|
|
107
|
+
* Verifies HMAC integrity before restoring.
|
|
108
|
+
* @param {string} auditId
|
|
109
|
+
* @returns {Promise<boolean>}
|
|
76
110
|
*/
|
|
77
|
-
static rollbackTo(auditId) {
|
|
111
|
+
static async rollbackTo(auditId) {
|
|
78
112
|
if (!/^[a-f0-9-]{8,40}$/.test(auditId)) {
|
|
79
113
|
throw new Error('Invalid audit ID format');
|
|
80
114
|
}
|
|
@@ -82,20 +116,39 @@ class TemporalHub {
|
|
|
82
116
|
if (!path.resolve(snapshotDir).startsWith(path.resolve(HISTORY_DIR))) {
|
|
83
117
|
throw new Error('Path traversal detected in audit ID');
|
|
84
118
|
}
|
|
85
|
-
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await fsPromises.access(snapshotDir);
|
|
122
|
+
} catch {
|
|
86
123
|
throw new Error(`Snapshot ${auditId} not found in history.`);
|
|
87
124
|
}
|
|
88
125
|
|
|
126
|
+
const metaPath = path.join(snapshotDir, 'SNAPSHOT-META.json');
|
|
89
127
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
const metaRaw = await fsPromises.readFile(metaPath, 'utf8');
|
|
129
|
+
const metaData = JSON.parse(metaRaw);
|
|
130
|
+
if (!TemporalHub._verifyMetadata(metaData)) {
|
|
131
|
+
throw new Error(`Snapshot ${auditId} failed integrity verification — metadata may be tampered.`);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err.message.includes('integrity verification') || err.message.includes('tampered')) {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
// Missing metadata file on legacy snapshots — allow rollback with warning
|
|
138
|
+
console.warn(`[temporal-hub] No verifiable metadata for ${auditId}, proceeding without integrity check.`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const allEntries = await fsPromises.readdir(snapshotDir);
|
|
143
|
+
const files = allEntries.filter(f => f !== 'SNAPSHOT-META.json');
|
|
144
|
+
|
|
145
|
+
await Promise.all(files.map(file =>
|
|
146
|
+
fsPromises.copyFile(
|
|
94
147
|
path.join(snapshotDir, file),
|
|
95
148
|
path.join(PLANNING_DIR, file)
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
)
|
|
150
|
+
));
|
|
151
|
+
|
|
99
152
|
return true;
|
|
100
153
|
} catch (err) {
|
|
101
154
|
console.error(`[temporal-hub] Rollback failed for ${auditId}:`, err.message);
|
|
@@ -108,7 +161,7 @@ class TemporalHub {
|
|
|
108
161
|
*/
|
|
109
162
|
static getHistory() {
|
|
110
163
|
if (!fs.existsSync(HISTORY_DIR)) return [];
|
|
111
|
-
|
|
164
|
+
|
|
112
165
|
try {
|
|
113
166
|
return fs.readdirSync(HISTORY_DIR)
|
|
114
167
|
.map(id => {
|
|
@@ -141,6 +194,50 @@ class TemporalHub {
|
|
|
141
194
|
return null;
|
|
142
195
|
}
|
|
143
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Garbage-collect old snapshots to prevent unbounded disk growth.
|
|
199
|
+
* Keeps the most recent `maxSnapshots` and deletes anything older than `maxAgeDays`.
|
|
200
|
+
*/
|
|
201
|
+
static async gc(options = {}) {
|
|
202
|
+
try {
|
|
203
|
+
const maxSnapshots = options.maxSnapshots || 50;
|
|
204
|
+
const maxAgeDays = options.maxAgeDays || 7;
|
|
205
|
+
const historyDir = path.join(process.cwd(), '.planning', 'history');
|
|
206
|
+
|
|
207
|
+
if (!fs.existsSync(historyDir)) return { deleted: 0, remaining: 0 };
|
|
208
|
+
|
|
209
|
+
const entries = fs.readdirSync(historyDir)
|
|
210
|
+
.filter(name => {
|
|
211
|
+
const fullPath = path.join(historyDir, name);
|
|
212
|
+
try { return fs.statSync(fullPath).isDirectory(); } catch { return false; }
|
|
213
|
+
})
|
|
214
|
+
.map(name => {
|
|
215
|
+
const fullPath = path.join(historyDir, name);
|
|
216
|
+
return { name, path: fullPath, mtime: fs.statSync(fullPath).mtime };
|
|
217
|
+
})
|
|
218
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
219
|
+
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
222
|
+
let deleted = 0;
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < entries.length; i++) {
|
|
225
|
+
const entry = entries[i];
|
|
226
|
+
const isOverLimit = i >= maxSnapshots;
|
|
227
|
+
const isExpired = (now - entry.mtime.getTime()) > maxAgeMs;
|
|
228
|
+
|
|
229
|
+
if (isOverLimit || isExpired) {
|
|
230
|
+
fs.rmSync(entry.path, { recursive: true, force: true });
|
|
231
|
+
deleted++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { deleted, remaining: entries.length - deleted };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return { deleted: 0, remaining: 0, error: err.message };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
144
241
|
/**
|
|
145
242
|
* Capture terminal output for a command and associate with audit point.
|
|
146
243
|
*/
|
|
@@ -153,7 +250,7 @@ class TemporalHub {
|
|
|
153
250
|
throw new Error('Path traversal detected in audit ID');
|
|
154
251
|
}
|
|
155
252
|
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
156
|
-
|
|
253
|
+
|
|
157
254
|
if (stdout) fs.writeFileSync(path.join(logDir, 'stdout.log'), stdout);
|
|
158
255
|
if (stderr) fs.writeFileSync(path.join(logDir, 'stderr.log'), stderr);
|
|
159
256
|
}
|