mindforge-cc 11.0.0 → 11.2.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/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/config.json +13 -4
- package/CHANGELOG.md +101 -0
- package/MINDFORGE.md +3 -3
- package/RELEASENOTES.md +1 -1
- package/bin/autonomous/audit-writer.js +108 -86
- package/bin/autonomous/auto-runner.js +304 -19
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/wave-executor.js +20 -1
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/server.js +1 -1
- package/bin/dashboard/sse-bridge.js +9 -12
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/verification-runner.js +131 -0
- package/bin/engine/verify-cli.js +34 -0
- package/bin/eval/eval-harness.js +82 -0
- package/bin/eval/golden-set-retrieval.json +46 -0
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/quantum-crypto.js +63 -9
- package/bin/governance/ztai-manager.js +30 -2
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-store.js +30 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +111 -6
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +13 -4
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +6 -2
- package/bin/models/model-router.js +31 -18
- package/bin/models/openai-provider.js +6 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/security/trust-boundaries.js +102 -0
- package/bin/security/trust-gate-hook.js +39 -0
- package/bin/skill-registry.js +3 -2
- package/bin/skills-builder/marketplace-cli.js +5 -3
- package/bin/skills-builder/skill-registrar.js +4 -6
- package/bin/sre/sentinel.js +7 -5
- package/bin/utils/append-queue.js +55 -0
- package/bin/utils/file-io.js +27 -37
- package/bin/utils/version-check.js +59 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/package.json +1 -1
- package/bin/dashboard/team-tracker.js +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* MindForge — Audit chain verifier (UC-04). Fail-closed: any break => valid:false.
|
|
4
|
+
*
|
|
5
|
+
* Walks the JSONL audit log line-by-line and re-derives each entry's hash from its
|
|
6
|
+
* content plus the prior entry's hash. The chain is valid only if EVERY link holds:
|
|
7
|
+
* 1. entry.previous_hash matches the running previousHash (null for the first entry)
|
|
8
|
+
* 2. entry._hash equals sha256({...entry-without-_hash, previous_hash})
|
|
9
|
+
*
|
|
10
|
+
* CRITICAL INVARIANT: hashEntry() here MUST produce byte-identical material to the
|
|
11
|
+
* writer (bin/autonomous/audit-writer.js). The writer hashes {...e, previous_hash}
|
|
12
|
+
* where `e` has no `_hash`. The stored entry is {...e, previous_hash, _hash}; stripping
|
|
13
|
+
* `_hash` via destructuring yields {...e, previous_hash} with the SAME key insertion
|
|
14
|
+
* order, so {...rest, previous_hash} reproduces the writer's material exactly.
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { hashAuditEntry } = require('./audit-hash');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Re-derives an entry's chained hash. Excludes `_hash` from the material by
|
|
21
|
+
* stripping it, then delegating to the canonical {@link hashAuditEntry} so the
|
|
22
|
+
* writer and verifier share ONE hasher (no material drift possible).
|
|
23
|
+
* @param {object} entry — stored entry (may include `_hash`, which is stripped)
|
|
24
|
+
* @param {string|null} previousHash — prior entry's `_hash` (null for the first link)
|
|
25
|
+
* @returns {string} hex-encoded SHA-256 digest
|
|
26
|
+
*/
|
|
27
|
+
function hashEntry(entry, previousHash) {
|
|
28
|
+
const { _hash, ...rest } = entry; // exclude _hash from material
|
|
29
|
+
return hashAuditEntry(rest, previousHash);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Verifies the integrity of a hash-chained audit log. Fail-closed.
|
|
34
|
+
* @param {string} auditPath — path to the AUDIT.jsonl file
|
|
35
|
+
* @returns {{ valid: boolean, count: number, brokenAt?: number, reason?: string }}
|
|
36
|
+
*/
|
|
37
|
+
function verifyAuditChain(auditPath) {
|
|
38
|
+
let lines;
|
|
39
|
+
try { lines = fs.readFileSync(auditPath, 'utf8').split('\n').filter(Boolean); }
|
|
40
|
+
catch (e) { return { valid: false, count: 0, brokenAt: 0, reason: `unreadable: ${e.message}` }; }
|
|
41
|
+
|
|
42
|
+
let previousHash = null;
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
let entry;
|
|
45
|
+
try { entry = JSON.parse(lines[i]); }
|
|
46
|
+
catch { return { valid: false, count: lines.length, brokenAt: i, reason: 'unparseable line' }; }
|
|
47
|
+
|
|
48
|
+
if (entry.previous_hash !== previousHash) {
|
|
49
|
+
return { valid: false, count: lines.length, brokenAt: i, reason: 'previous_hash mismatch' };
|
|
50
|
+
}
|
|
51
|
+
const expected = hashEntry(entry, previousHash);
|
|
52
|
+
if (entry._hash !== expected) {
|
|
53
|
+
return { valid: false, count: lines.length, brokenAt: i, reason: 'hash mismatch (entry mutated)' };
|
|
54
|
+
}
|
|
55
|
+
previousHash = entry._hash;
|
|
56
|
+
}
|
|
57
|
+
return { valid: true, count: lines.length };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { verifyAuditChain };
|
|
@@ -10,20 +10,59 @@
|
|
|
10
10
|
const crypto = require('node:crypto');
|
|
11
11
|
const configManager = require('./config-manager');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Honest-disclosure guard message. The signatures produced by this module are
|
|
15
|
+
* SIMULATED Dilithium-5 (base64(SHA3 + random) — NOT real ML-DSA/FIPS-204
|
|
16
|
+
* lattice crypto). They must NEVER sit on the live trust path silently. The
|
|
17
|
+
* simulated implementation is preserved for demonstration only and is gated
|
|
18
|
+
* behind an explicit, opt-in flag.
|
|
19
|
+
*/
|
|
20
|
+
const PQC_DEMO_DISABLED_MSG =
|
|
21
|
+
'PQC demo disabled — set experimental.pqc_demo=true to use simulated lattice crypto (NOT for production trust)';
|
|
22
|
+
|
|
13
23
|
class QuantumCrypto {
|
|
14
24
|
constructor() {
|
|
15
25
|
this.providerId = configManager.get('security.provider', 'simulated-lattice');
|
|
16
|
-
|
|
26
|
+
// UC-24: simulated PQC is OFF the live trust path by default. Real PQC has
|
|
27
|
+
// not shipped. The simulated path is gated SOLELY behind the explicit
|
|
28
|
+
// experimental.pqc_demo opt-in (read fresh via isPqcDemoEnabled), so there
|
|
29
|
+
// is exactly one switch controlling the simulated minter. The legacy
|
|
30
|
+
// security.pqas_enabled flag is retained for provider metadata only and
|
|
31
|
+
// MUST NOT independently gate minting (see getProvider).
|
|
32
|
+
this.pqasEnabled = configManager.get('security.pqas_enabled', false);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns true only when the operator has explicitly opted into the SIMULATED
|
|
37
|
+
* post-quantum demo. Read fresh from config so a runtime toggle is honored.
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
isPqcDemoEnabled() {
|
|
41
|
+
return configManager.get('experimental.pqc_demo', false) === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hard gate: throws unless the operator has explicitly enabled the simulated
|
|
46
|
+
* PQC demo. Prevents simulated (false-assurance) signatures from silently
|
|
47
|
+
* landing on the live trust path.
|
|
48
|
+
*/
|
|
49
|
+
_assertPqcDemoEnabled() {
|
|
50
|
+
if (!this.isPqcDemoEnabled()) {
|
|
51
|
+
throw new Error(PQC_DEMO_DISABLED_MSG);
|
|
52
|
+
}
|
|
17
53
|
}
|
|
18
54
|
|
|
19
55
|
/**
|
|
20
56
|
* Returns the current active crypto provider.
|
|
21
57
|
*/
|
|
22
58
|
getProvider() {
|
|
23
|
-
// In v7, this would resolve to a real provider like 'oqs-provider.js'
|
|
59
|
+
// In v7, this would resolve to a real provider like 'oqs-provider.js'.
|
|
60
|
+
// Read pqas_enabled FRESH from config (not the constructor cache) for
|
|
61
|
+
// symmetry with the demo gate. This is metadata only — it does NOT gate
|
|
62
|
+
// minting; experimental.pqc_demo is the single source of truth for that.
|
|
24
63
|
return {
|
|
25
64
|
id: this.providerId,
|
|
26
|
-
pqas_enabled:
|
|
65
|
+
pqas_enabled: configManager.get('security.pqas_enabled', false),
|
|
27
66
|
algorithm: 'Dilithium-5'
|
|
28
67
|
};
|
|
29
68
|
}
|
|
@@ -32,7 +71,10 @@ class QuantumCrypto {
|
|
|
32
71
|
* Generates a key pair using the configured PQ provider.
|
|
33
72
|
*/
|
|
34
73
|
async generateLatticeKeyPair() {
|
|
35
|
-
|
|
74
|
+
// UC-24: simulated keys are demo-only. The SINGLE gate is the fresh-read
|
|
75
|
+
// experimental.pqc_demo flag — enabling it is sufficient to mint, and
|
|
76
|
+
// disabling it fails closed. security.pqas_enabled does NOT gate this.
|
|
77
|
+
this._assertPqcDemoEnabled();
|
|
36
78
|
|
|
37
79
|
// Simulate high-entropy lattice seeds
|
|
38
80
|
const seed = crypto.randomBytes(64).toString('hex');
|
|
@@ -53,7 +95,11 @@ class QuantumCrypto {
|
|
|
53
95
|
* @returns {{ signature: string, simulated: true, algorithm: string }}
|
|
54
96
|
*/
|
|
55
97
|
async signPQ(data, privateKey) {
|
|
56
|
-
|
|
98
|
+
// UC-24: never produce a simulated (false-assurance) signature on the live
|
|
99
|
+
// trust path. The SINGLE gate is the fresh-read experimental.pqc_demo flag
|
|
100
|
+
// (same flag _selectProvider routes on) — security.pqas_enabled does NOT
|
|
101
|
+
// independently block signing.
|
|
102
|
+
this._assertPqcDemoEnabled();
|
|
57
103
|
if (!privateKey.startsWith('mfq7_dilithium5_priv_')) {
|
|
58
104
|
throw new Error('Invalid Post-Quantum private key format.');
|
|
59
105
|
}
|
|
@@ -92,19 +138,27 @@ class QuantumCrypto {
|
|
|
92
138
|
}
|
|
93
139
|
|
|
94
140
|
/**
|
|
95
|
-
* Generates a
|
|
96
|
-
* This mimics a SNARK where the agent proves it ran the PolicyEngine rules
|
|
141
|
+
* Generates a SIMULATED ZK-Proof of policy adherence.
|
|
142
|
+
* This mimics a SNARK where the agent proves it ran the PolicyEngine rules,
|
|
143
|
+
* but it is NOT a real zero-knowledge proof. The returned token is explicitly
|
|
144
|
+
* stamped with a `sim` marker (zkp_v1_sim_...) so downstream code and audit
|
|
145
|
+
* logs can never mistake it for a real SNARK. It remains inert by default —
|
|
146
|
+
* verifyZKProof DENYs unless an external verifier module is configured.
|
|
147
|
+
* @returns {string} A simulated ZK-proof token prefixed `zkp_v1_sim_`.
|
|
97
148
|
*/
|
|
98
149
|
generateZKProof(intent, result) {
|
|
99
150
|
const proofPayload = JSON.stringify({
|
|
100
151
|
intent: intent.id,
|
|
101
152
|
verdict: result.verdict,
|
|
102
153
|
timestamp: Date.now(),
|
|
103
|
-
entropy: crypto.randomBytes(16).toString('hex')
|
|
154
|
+
entropy: crypto.randomBytes(16).toString('hex'),
|
|
155
|
+
simulated: true
|
|
104
156
|
});
|
|
105
157
|
|
|
106
158
|
const hash = crypto.createHash('sha256').update(proofPayload).digest('hex');
|
|
107
|
-
|
|
159
|
+
// `sim` marker is embedded AFTER the `zkp_v1_` prefix so the existing
|
|
160
|
+
// format check (startsWith 'zkp_v1_') and any external verifier still work.
|
|
161
|
+
return `zkp_v1_sim_${hash}_${crypto.randomBytes(32).toString('base64')}`;
|
|
108
162
|
}
|
|
109
163
|
|
|
110
164
|
verifyZKProof(proof, intentId) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const crypto = require('node:crypto');
|
|
7
7
|
const { promisify } = require('node:util');
|
|
8
|
+
const configManager = require('./config-manager');
|
|
8
9
|
|
|
9
10
|
const generateKeyPair = promisify(crypto.generateKeyPair);
|
|
10
11
|
|
|
@@ -147,6 +148,34 @@ class ZTAIManager {
|
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Selects a key provider for a given trust tier.
|
|
153
|
+
*
|
|
154
|
+
* UC-24: All tiers route to REAL crypto by default — Tier 1-2 use the
|
|
155
|
+
* in-memory Ed25519 provider, Tier 3+ use the (simulated-HSM) enclave
|
|
156
|
+
* provider which also signs with REAL Ed25519. The SIMULATED post-quantum
|
|
157
|
+
* lattice provider ('quantum') is NEVER selected on the live trust path
|
|
158
|
+
* unless the operator has explicitly opted into the demo via
|
|
159
|
+
* experimental.pqc_demo. This keeps false-assurance signatures off the
|
|
160
|
+
* default trust path while preserving the demo for explicit exploration.
|
|
161
|
+
*
|
|
162
|
+
* @param {number} tier - Trust tier (1-4)
|
|
163
|
+
* @returns {'local'|'enclave'|'quantum'}
|
|
164
|
+
*/
|
|
165
|
+
_selectProvider(tier) {
|
|
166
|
+
const pqcDemo = configManager.get('experimental.pqc_demo', false) === true;
|
|
167
|
+
|
|
168
|
+
// Tier 4+ MAY use the simulated lattice provider, but only when the
|
|
169
|
+
// operator has explicitly enabled the PQC demo. Otherwise it falls back to
|
|
170
|
+
// the real-Ed25519 enclave provider so the trust path stays verifiable.
|
|
171
|
+
if (tier >= 4) {
|
|
172
|
+
return pqcDemo ? 'quantum' : 'enclave';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Tier 3 agents use the SecureEnclaveProvider (real Ed25519).
|
|
176
|
+
return tier >= 3 ? 'enclave' : 'local';
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
/**
|
|
151
180
|
* Registers a new agent and assigns a provider based on Trust Tier.
|
|
152
181
|
* @param {string} persona - Agent persona identifier
|
|
@@ -157,8 +186,7 @@ class ZTAIManager {
|
|
|
157
186
|
const uuid = crypto.randomUUID();
|
|
158
187
|
const did = `did:mindforge:${uuid}`;
|
|
159
188
|
|
|
160
|
-
|
|
161
|
-
const providerType = tier >= 3 ? 'enclave' : 'local';
|
|
189
|
+
const providerType = this._selectProvider(tier);
|
|
162
190
|
const provider = this.providers[providerType];
|
|
163
191
|
|
|
164
192
|
const publicKeyPEM = await provider.generate(did);
|
|
@@ -21,17 +21,16 @@ class HindsightInjector {
|
|
|
21
21
|
// 1. Rollback .planning directory
|
|
22
22
|
TemporalHub.rollbackTo(auditId);
|
|
23
23
|
|
|
24
|
-
// 2. Append the "Hindsight" event to AUDIT.jsonl
|
|
24
|
+
// 2. Append the "Hindsight" event to AUDIT.jsonl via the unified, hash-chained,
|
|
25
|
+
// durable append (UC-04b) so this entry links into the single verifiable chain.
|
|
26
|
+
const { appendAuditEntrySync } = require('./autonomous/audit-writer');
|
|
25
27
|
const auditPath = path.join(process.cwd(), '.planning', 'AUDIT.jsonl');
|
|
26
|
-
const hindsightEvent = {
|
|
27
|
-
id: require('crypto').randomBytes(8).toString('hex'),
|
|
28
|
-
timestamp: new Date().toISOString(),
|
|
28
|
+
const hindsightEvent = appendAuditEntrySync(auditPath, {
|
|
29
29
|
event: 'hindsight_injected',
|
|
30
30
|
target_id: auditId,
|
|
31
31
|
description: fixDescription,
|
|
32
32
|
agent: 'temporal-hub'
|
|
33
|
-
};
|
|
34
|
-
fs.appendFileSync(auditPath, JSON.stringify(hindsightEvent) + '\n');
|
|
33
|
+
});
|
|
35
34
|
|
|
36
35
|
// 3. Mark the state as "ready_for_regeneration"
|
|
37
36
|
const statePath = path.join(process.cwd(), '.planning', 'auto-state.json');
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* MindForge — Instinct Auto-Capture Hook (UC-11)
|
|
5
|
+
* Invoked as a PostToolUse hook. Reads hook event JSON from stdin,
|
|
6
|
+
* detects successful task completions, and appends lightweight instinct
|
|
7
|
+
* entries to the configured store path.
|
|
8
|
+
*
|
|
9
|
+
* Session capture limit is enforced via a temp counter file to avoid
|
|
10
|
+
* flooding the store with low-signal entries.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const CONFIG_PATH = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
21
|
+
const SESSION_ID = process.env.MINDFORGE_SESSION_ID || process.ppid || 'default';
|
|
22
|
+
const SESSION_COUNTER_PATH = path.join(
|
|
23
|
+
os.tmpdir(),
|
|
24
|
+
`mindforge-instinct-session-${SESSION_ID}.count`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getSessionCount() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(SESSION_COUNTER_PATH, 'utf8');
|
|
39
|
+
return parseInt(raw, 10) || 0;
|
|
40
|
+
} catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function incrementSessionCount() {
|
|
46
|
+
const current = getSessionCount();
|
|
47
|
+
fs.writeFileSync(SESSION_COUNTER_PATH, String(current + 1));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Success Detection ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function isSuccessfulCompletion(payload) {
|
|
53
|
+
const tool = (payload.tool_name || payload.tool || '').toLowerCase();
|
|
54
|
+
|
|
55
|
+
// Bash tool with exit code 0
|
|
56
|
+
if (tool === 'bash') {
|
|
57
|
+
const exitCode = payload.exit_code ?? payload.result?.exit_code ?? null;
|
|
58
|
+
if (exitCode === 0) return true;
|
|
59
|
+
// If no explicit exit code but has output and no error marker
|
|
60
|
+
if (exitCode === null && payload.output && !payload.error) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Task tool with completed status
|
|
65
|
+
if (tool === 'task') {
|
|
66
|
+
const status = (payload.status || payload.result?.status || '').toLowerCase();
|
|
67
|
+
return status === 'completed' || status === 'done';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Pattern Extraction ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function extractPattern(payload) {
|
|
76
|
+
const tool = (payload.tool_name || payload.tool || '').toLowerCase();
|
|
77
|
+
|
|
78
|
+
if (tool === 'bash') {
|
|
79
|
+
const command = payload.command || payload.input?.command || payload.tool_input?.command || '';
|
|
80
|
+
if (!command || command.length < 5) return null;
|
|
81
|
+
// Skip trivial commands
|
|
82
|
+
if (/^(ls|pwd|echo|cat|cd)\b/.test(command.trim())) return null;
|
|
83
|
+
return {
|
|
84
|
+
observation: `Bash command succeeded: ${command.slice(0, 200)}`,
|
|
85
|
+
behavior: `Use pattern: ${command.slice(0, 200)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (tool === 'task') {
|
|
90
|
+
const description = payload.description || payload.task_description || payload.name || '';
|
|
91
|
+
if (!description) return null;
|
|
92
|
+
return {
|
|
93
|
+
observation: `Task completed successfully: ${description.slice(0, 200)}`,
|
|
94
|
+
behavior: `Reuse approach for similar tasks: ${description.slice(0, 200)}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function main() {
|
|
104
|
+
const config = loadConfig();
|
|
105
|
+
if (!config || !config.instincts) {
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { mode, max_capture_per_session, store_path } = config.instincts;
|
|
110
|
+
if (mode !== 'auto-capture') {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check session limit
|
|
115
|
+
const sessionCount = getSessionCount();
|
|
116
|
+
if (sessionCount >= (max_capture_per_session || 5)) {
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Read stdin (hook payload)
|
|
121
|
+
let input = '';
|
|
122
|
+
try {
|
|
123
|
+
input = fs.readFileSync(0, 'utf8');
|
|
124
|
+
} catch {
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!input.trim()) {
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let payload;
|
|
133
|
+
try {
|
|
134
|
+
payload = JSON.parse(input);
|
|
135
|
+
} catch {
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if this is a successful completion
|
|
140
|
+
if (!isSuccessfulCompletion(payload)) {
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract pattern
|
|
145
|
+
const pattern = extractPattern(payload);
|
|
146
|
+
if (!pattern) {
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Build instinct entry
|
|
151
|
+
const entry = {
|
|
152
|
+
id: `inst-${crypto.randomUUID()}`,
|
|
153
|
+
created_at: new Date().toISOString(),
|
|
154
|
+
updated_at: new Date().toISOString(),
|
|
155
|
+
observation: pattern.observation,
|
|
156
|
+
behavior: pattern.behavior,
|
|
157
|
+
confidence: 0.3,
|
|
158
|
+
times_applied: 0,
|
|
159
|
+
times_succeeded: 0,
|
|
160
|
+
times_failed: 0,
|
|
161
|
+
project: 'mindforge',
|
|
162
|
+
tags: [],
|
|
163
|
+
status: 'active',
|
|
164
|
+
promoted_to_skill: null,
|
|
165
|
+
last_applied_at: null,
|
|
166
|
+
source: 'auto-capture',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Write to store
|
|
170
|
+
const storePath = path.resolve(process.cwd(), store_path);
|
|
171
|
+
const storeDir = path.dirname(storePath);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.existsSync(storeDir)) {
|
|
175
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
fs.appendFileSync(storePath, JSON.stringify(entry) + '\n');
|
|
178
|
+
incrementSessionCount();
|
|
179
|
+
} catch {
|
|
180
|
+
// Non-fatal — hooks must not block
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main();
|
|
@@ -18,6 +18,8 @@ const path = require('path');
|
|
|
18
18
|
const Store = require('./knowledge-store');
|
|
19
19
|
const Graph = require('./knowledge-graph');
|
|
20
20
|
const Embedder = require('./embedding-engine');
|
|
21
|
+
const Indexer = require('./knowledge-indexer');
|
|
22
|
+
const { fuseResults } = require('./retrieval-fusion');
|
|
21
23
|
|
|
22
24
|
// ── Configuration ─────────────────────────────────────────────────────────────
|
|
23
25
|
const MAX_SHADOW_CHARS = 8000; // ~2KB tokens
|
|
@@ -63,13 +65,40 @@ function generateShadowContext(opts = {}) {
|
|
|
63
65
|
|
|
64
66
|
const { vectors, df, N } = Embedder.buildEmbeddings(activeEntries);
|
|
65
67
|
|
|
66
|
-
// 2.
|
|
68
|
+
// 2. Multi-path retrieval with RRF fusion (UC-20)
|
|
69
|
+
// Path 1: Knowledge Graph (embedding + graph traversal)
|
|
70
|
+
// Path 2: Knowledge Indexer (BM25 + confidence)
|
|
71
|
+
// Results are fused via Reciprocal Rank Fusion for scale-free merging.
|
|
67
72
|
const queryText = `${taskDescription} ${techStack.join(' ')}`;
|
|
68
|
-
const
|
|
73
|
+
const fetchK = maxItems * 3; // Over-fetch for filtering headroom
|
|
74
|
+
|
|
75
|
+
const graphResults = Graph.findRelated(queryText, vectors, df, N, {
|
|
69
76
|
maxHops: 2,
|
|
70
|
-
topK:
|
|
77
|
+
topK: fetchK,
|
|
71
78
|
});
|
|
72
79
|
|
|
80
|
+
let indexerResults = [];
|
|
81
|
+
try {
|
|
82
|
+
const rawIndexer = Indexer.search(queryText, { includeGlobal: true }, fetchK);
|
|
83
|
+
indexerResults = rawIndexer.map((entry, rank) => ({
|
|
84
|
+
id: entry.id,
|
|
85
|
+
score: entry.confidence || 0,
|
|
86
|
+
source: 'indexer',
|
|
87
|
+
}));
|
|
88
|
+
} catch {
|
|
89
|
+
// Indexer may fail on empty store — non-fatal
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// RRF fusion: merge both ranked lists by ordinal position
|
|
93
|
+
const fusedResults = fuseResults([graphResults, indexerResults]);
|
|
94
|
+
|
|
95
|
+
// Map fused results back to the legacy shape expected downstream
|
|
96
|
+
const related = fusedResults.map(item => ({
|
|
97
|
+
id: item.id,
|
|
98
|
+
score: item.rrfScore, // RRF score replaces incomparable linear blends
|
|
99
|
+
source: item.source || 'fused',
|
|
100
|
+
}));
|
|
101
|
+
|
|
73
102
|
// 3. Filter and enrich results
|
|
74
103
|
const excludeSet = new Set(excludeIds);
|
|
75
104
|
const enriched = [];
|
|
@@ -25,7 +25,7 @@ class IdentitySynthesizer {
|
|
|
25
25
|
.replace(/{PROJECT_OBJECTIVE}/g, answers.goal || 'Maximizing engineering leverage');
|
|
26
26
|
|
|
27
27
|
await fs.writeFile(this.soulPath, soulContent);
|
|
28
|
-
console.log(
|
|
28
|
+
console.log('[IDENTITY] SOUL.md bootstrapped successfully from the Grand Blueprint.');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -41,7 +41,7 @@ class IdentitySynthesizer {
|
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
if (traces.length === 0) {
|
|
44
|
-
console.log(
|
|
44
|
+
console.log('[IDENTITY] No execution traces found in celestial.db. Evolution skipped.');
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -15,6 +15,30 @@ const path = require('path');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
|
|
18
|
+
// ── Durable append (UC-09) ──────────────────────────────────────────────────
|
|
19
|
+
// The knowledge-store public API (add/deprecate/reinforce) is SYNCHRONOUS and
|
|
20
|
+
// callers read-after-write synchronously (e.g. `const id = Store.add(...)`
|
|
21
|
+
// immediately followed by `Store.readAll()`). Routing through the async
|
|
22
|
+
// append-queue would make those reads observe stale data and would require an
|
|
23
|
+
// API change across 9+ consumers — out of scope for UC-09.
|
|
24
|
+
//
|
|
25
|
+
// Instead we centralize every append through one durable, fsync'd, synchronous
|
|
26
|
+
// writer. This delivers UC-09's durability guarantee (acknowledged writes are on
|
|
27
|
+
// disk before the call returns) and a single serialized append path per file,
|
|
28
|
+
// while preserving the synchronous read-after-write contract. appendFileSync's
|
|
29
|
+
// per-call append is atomic on POSIX, so concurrent in-process appends do not
|
|
30
|
+
// interleave at the byte level.
|
|
31
|
+
function appendDurableSync(filePath, line) {
|
|
32
|
+
const record = line.endsWith('\n') ? line : line + '\n';
|
|
33
|
+
const fd = fs.openSync(filePath, 'a');
|
|
34
|
+
try {
|
|
35
|
+
fs.writeSync(fd, record);
|
|
36
|
+
fs.fsyncSync(fd);
|
|
37
|
+
} finally {
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
// ── ID Index for fast lookups (built lazily, invalidated on writes) ───────────
|
|
19
43
|
let _idIndex = null; // Map<id, entry> — latest version per ID
|
|
20
44
|
let _indexDirty = true; // Invalidated whenever entries are appended
|
|
@@ -201,12 +225,12 @@ function add(entry) {
|
|
|
201
225
|
|
|
202
226
|
const filePath = getFilePath(entry.type);
|
|
203
227
|
verifyFileIntegrity(filePath);
|
|
204
|
-
|
|
228
|
+
appendDurableSync(filePath, JSON.stringify(full));
|
|
205
229
|
|
|
206
230
|
// Also append to unified knowledge-base.jsonl for cross-type queries
|
|
207
231
|
if (filePath !== paths.KB_PATH) {
|
|
208
232
|
verifyFileIntegrity(paths.KB_PATH);
|
|
209
|
-
|
|
233
|
+
appendDurableSync(paths.KB_PATH, JSON.stringify(full));
|
|
210
234
|
}
|
|
211
235
|
|
|
212
236
|
_invalidateIndex();
|
|
@@ -236,10 +260,10 @@ function deprecate(id, reason, supersededBy = null) {
|
|
|
236
260
|
};
|
|
237
261
|
|
|
238
262
|
verifyFileIntegrity(filePath);
|
|
239
|
-
|
|
263
|
+
appendDurableSync(filePath, JSON.stringify(deprecated));
|
|
240
264
|
if (filePath !== paths.KB_PATH) {
|
|
241
265
|
verifyFileIntegrity(paths.KB_PATH);
|
|
242
|
-
|
|
266
|
+
appendDurableSync(paths.KB_PATH, JSON.stringify(deprecated));
|
|
243
267
|
}
|
|
244
268
|
|
|
245
269
|
_invalidateIndex();
|
|
@@ -267,10 +291,10 @@ function reinforce(id) {
|
|
|
267
291
|
|
|
268
292
|
const filePath = getFilePath(entry.type);
|
|
269
293
|
verifyFileIntegrity(filePath);
|
|
270
|
-
|
|
294
|
+
appendDurableSync(filePath, JSON.stringify(reinforced));
|
|
271
295
|
if (filePath !== paths.KB_PATH) {
|
|
272
296
|
verifyFileIntegrity(paths.KB_PATH);
|
|
273
|
-
|
|
297
|
+
appendDurableSync(paths.KB_PATH, JSON.stringify(reinforced));
|
|
274
298
|
}
|
|
275
299
|
|
|
276
300
|
_invalidateIndex();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* MindForge — Reciprocal Rank Fusion (UC-20).
|
|
4
|
+
* Merges multiple ranked lists using scale-free RRF scoring.
|
|
5
|
+
*
|
|
6
|
+
* RRF eliminates the need for score normalization across retrieval paths
|
|
7
|
+
* with incomparable scoring functions (embedding similarity, BM25, graph
|
|
8
|
+
* traversal, FTS rank). Only ordinal rank matters, not score magnitude.
|
|
9
|
+
*
|
|
10
|
+
* Formula:
|
|
11
|
+
* rrfScore(item) = SUM( 1 / (K + rank_i) ) for all lists containing the item
|
|
12
|
+
*
|
|
13
|
+
* Where K=60 is the standard constant from the original RRF paper
|
|
14
|
+
* (Cormack, Clarke, Butt — 2009).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const K = 60; // Standard RRF constant — dampens the influence of high ranks
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fuse multiple ranked result lists using Reciprocal Rank Fusion.
|
|
21
|
+
*
|
|
22
|
+
* Each list is an array of objects with at least an `id` field.
|
|
23
|
+
* Items appearing in multiple lists accumulate RRF score and rank higher.
|
|
24
|
+
*
|
|
25
|
+
* @param {Array<Array<{id: string, [key: string]: any}>>} rankedLists
|
|
26
|
+
* Array of ranked lists. Each list is ordered by relevance (index 0 = most relevant).
|
|
27
|
+
* @returns {Array<{id: string, rrfScore: number, [key: string]: any}>}
|
|
28
|
+
* Fused results sorted by RRF score descending. Each item retains its
|
|
29
|
+
* original properties from the first list it appeared in.
|
|
30
|
+
*/
|
|
31
|
+
function fuseResults(rankedLists) {
|
|
32
|
+
if (!rankedLists || rankedLists.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const scores = new Map(); // id -> merged item with rrfScore
|
|
35
|
+
|
|
36
|
+
for (const list of rankedLists) {
|
|
37
|
+
if (!Array.isArray(list)) continue;
|
|
38
|
+
|
|
39
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
40
|
+
const item = list[rank];
|
|
41
|
+
if (!item || !item.id) continue;
|
|
42
|
+
|
|
43
|
+
const id = item.id;
|
|
44
|
+
const rrfContribution = 1 / (K + rank + 1); // rank is 0-based, +1 makes it 1-based
|
|
45
|
+
|
|
46
|
+
if (scores.has(id)) {
|
|
47
|
+
const existing = scores.get(id);
|
|
48
|
+
existing.rrfScore += rrfContribution;
|
|
49
|
+
} else {
|
|
50
|
+
scores.set(id, { ...item, rrfScore: rrfContribution });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [...scores.values()].sort((a, b) => b.rrfScore - a.rrfScore);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { fuseResults, K };
|
|
@@ -83,7 +83,7 @@ class SemanticHub {
|
|
|
83
83
|
try {
|
|
84
84
|
const data = await fs.readFile(this.syncManifest, 'utf8');
|
|
85
85
|
manifest = JSON.parse(data);
|
|
86
|
-
} catch (e) {}
|
|
86
|
+
} catch (e) { /* intentionally empty */ }
|
|
87
87
|
|
|
88
88
|
manifest[libraryName] = {
|
|
89
89
|
lastSync: new Date().toISOString(),
|
|
@@ -106,7 +106,7 @@ class SemanticHub {
|
|
|
106
106
|
sqliteTraces = await vectorHub.searchTraces(skillFilter);
|
|
107
107
|
} else {
|
|
108
108
|
sqliteTraces = vectorHub.query(
|
|
109
|
-
|
|
109
|
+
'SELECT * FROM traces WHERE event = ? LIMIT 20',
|
|
110
110
|
['reasoning_trace']
|
|
111
111
|
);
|
|
112
112
|
}
|