mindforge-cc 10.7.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.
Files changed (85) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
  3. package/.mindforge/config.json +18 -4
  4. package/CHANGELOG.md +165 -0
  5. package/MINDFORGE.md +3 -3
  6. package/README.md +49 -4
  7. package/RELEASENOTES.md +81 -1
  8. package/SECURITY.md +20 -8
  9. package/bin/autonomous/audit-writer.js +105 -70
  10. package/bin/autonomous/auto-runner.js +377 -34
  11. package/bin/autonomous/context-refactorer.js +26 -11
  12. package/bin/autonomous/dependency-dag.js +59 -0
  13. package/bin/autonomous/state-manager.js +62 -6
  14. package/bin/autonomous/stuck-monitor.js +46 -7
  15. package/bin/autonomous/wave-executor.js +86 -26
  16. package/bin/council-cli.js +161 -0
  17. package/bin/dashboard/api-router.js +43 -0
  18. package/bin/dashboard/approval-handler.js +3 -1
  19. package/bin/dashboard/metrics-aggregator.js +28 -1
  20. package/bin/dashboard/server.js +68 -5
  21. package/bin/dashboard/sse-bridge.js +10 -13
  22. package/bin/engine/council-runtime.js +124 -0
  23. package/bin/engine/feedback-loop.js +8 -0
  24. package/bin/engine/intelligence-interlock.js +32 -15
  25. package/bin/engine/logic-drift-detector.js +2 -1
  26. package/bin/engine/nexus-tracer.js +3 -2
  27. package/bin/engine/otel-exporter.js +123 -0
  28. package/bin/engine/remediation-engine.js +155 -32
  29. package/bin/engine/self-corrective-synthesizer.js +84 -10
  30. package/bin/engine/sre-manager.js +12 -4
  31. package/bin/engine/temporal-cli.js +4 -2
  32. package/bin/engine/temporal-hub.js +131 -34
  33. package/bin/engine/verification-runner.js +131 -0
  34. package/bin/engine/verify-cli.js +34 -0
  35. package/bin/eval/eval-harness.js +82 -0
  36. package/bin/eval/golden-set-retrieval.json +46 -0
  37. package/bin/governance/approve.js +41 -5
  38. package/bin/governance/audit-hash.js +12 -0
  39. package/bin/governance/audit-verifier.js +60 -0
  40. package/bin/governance/impact-analyzer.js +28 -0
  41. package/bin/governance/policy-engine.js +10 -3
  42. package/bin/governance/quantum-crypto.js +95 -28
  43. package/bin/governance/rbac-manager.js +74 -2
  44. package/bin/governance/ztai-manager.js +79 -9
  45. package/bin/hindsight-injector.js +8 -9
  46. package/bin/hooks/instinct-capture-hook.js +186 -0
  47. package/bin/memory/auto-shadow.js +32 -3
  48. package/bin/memory/eis-client.js +71 -34
  49. package/bin/memory/embedding-engine.js +61 -0
  50. package/bin/memory/identity-synthesizer.js +2 -2
  51. package/bin/memory/knowledge-graph.js +58 -5
  52. package/bin/memory/knowledge-indexer.js +53 -6
  53. package/bin/memory/knowledge-store.js +52 -6
  54. package/bin/memory/retrieval-fusion.js +58 -0
  55. package/bin/memory/semantic-hub.js +2 -2
  56. package/bin/memory/vector-hub.js +111 -6
  57. package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
  58. package/bin/migrations/schema-versions.js +13 -0
  59. package/bin/mindforge-cli.js +4 -5
  60. package/bin/models/anthropic-provider.js +58 -4
  61. package/bin/models/cloud-broker.js +68 -20
  62. package/bin/models/cost-tracker.js +3 -1
  63. package/bin/models/difficulty-scorer.js +54 -0
  64. package/bin/models/gemini-provider.js +57 -2
  65. package/bin/models/model-client.js +20 -0
  66. package/bin/models/model-router.js +59 -26
  67. package/bin/models/openai-provider.js +50 -3
  68. package/bin/models/pricing-registry.js +128 -0
  69. package/bin/review/ads-engine.js +1 -1
  70. package/bin/security/trust-boundaries.js +102 -0
  71. package/bin/security/trust-gate-hook.js +39 -0
  72. package/bin/skill-registry.js +3 -2
  73. package/bin/skills-builder/marketplace-cli.js +5 -3
  74. package/bin/skills-builder/skill-registrar.js +4 -6
  75. package/bin/sre/sentinel.js +7 -5
  76. package/bin/utils/append-queue.js +55 -0
  77. package/bin/utils/file-io.js +90 -38
  78. package/bin/utils/index.js +58 -0
  79. package/bin/utils/version-check.js +59 -0
  80. package/bin/verify-audit.js +12 -0
  81. package/bin/wizard/theme.js +1 -2
  82. package/docs/getting-started.md +1 -1
  83. package/docs/user-guide.md +2 -2
  84. package/package.json +2 -2
  85. package/bin/dashboard/team-tracker.js +0 -0
@@ -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
- if (!fs.existsSync(PLANNING_DIR)) return null;
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
- if (!fs.existsSync(snapshotDir)) {
36
- fs.mkdirSync(snapshotDir, { recursive: true });
37
- }
64
+
65
+ await fsPromises.mkdir(snapshotDir, { recursive: true });
38
66
 
39
67
  try {
40
- // 1. Identify files to snapshot (exclude history itself and archive)
41
- const files = fs.readdirSync(PLANNING_DIR).filter(f => {
42
- const stats = fs.statSync(path.join(PLANNING_DIR, f));
43
- if (stats.isDirectory()) return false;
44
-
45
- const ext = path.extname(f).toLowerCase();
46
- return ['.md', '.json', '.yml', '.yaml', '.log'].includes(ext);
47
- });
48
-
49
- // 2. Snapshot files
50
- for (const file of files) {
51
- fs.copyFileSync(
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
- fs.writeFileSync(path.join(snapshotDir, 'SNAPSHOT-META.json'), JSON.stringify(meta, null, 2));
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
- * @param {string} auditId
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
- if (!fs.existsSync(snapshotDir)) {
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 files = fs.readdirSync(snapshotDir).filter(f => f !== 'SNAPSHOT-META.json');
91
-
92
- for (const file of files) {
93
- fs.copyFileSync(
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
  }
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const MAX_OUTPUT_LENGTH = 2000;
8
+
9
+ /**
10
+ * Stage definitions — each maps a stage name to its command and optional skip condition.
11
+ * The tests stage guards against recursion: if NODE_ENV=test (set by run-all.js) or
12
+ * MINDFORGE_VERIFICATION_ACTIVE=1 (set by this runner), we skip to prevent infinite nesting.
13
+ */
14
+ const STAGE_DEFS = {
15
+ tests: {
16
+ command: 'node tests/run-all.js',
17
+ skipIf: () =>
18
+ process.env.MINDFORGE_VERIFICATION_ACTIVE === '1' ||
19
+ process.env.NODE_ENV === 'test',
20
+ },
21
+ lint: {
22
+ command: 'npx eslint . --max-warnings=0',
23
+ skipIf: null,
24
+ },
25
+ audit: {
26
+ command: 'node bin/verify-audit.js',
27
+ skipIf: null,
28
+ },
29
+ typecheck: {
30
+ command: 'npx tsc --noEmit',
31
+ skipIf: (cwd) => !fs.existsSync(path.join(cwd, 'tsconfig.json')),
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Run a single stage, returning a structured result object.
37
+ */
38
+ function executeStage(name, cwd) {
39
+ const def = STAGE_DEFS[name];
40
+ if (!def) {
41
+ return { name, status: 'skip', durationMs: 0, output: `Unknown stage: ${name}` };
42
+ }
43
+
44
+ // Check skip condition
45
+ if (def.skipIf && def.skipIf(cwd)) {
46
+ return { name, status: 'skip', durationMs: 0, output: '' };
47
+ }
48
+
49
+ const start = Date.now();
50
+ let output = '';
51
+ let status = 'pass';
52
+
53
+ try {
54
+ const env = Object.assign({}, process.env, {
55
+ MINDFORGE_VERIFICATION_ACTIVE: '1',
56
+ });
57
+ const result = execSync(def.command, {
58
+ cwd,
59
+ encoding: 'utf8',
60
+ stdio: ['pipe', 'pipe', 'pipe'],
61
+ timeout: 120000,
62
+ env,
63
+ });
64
+ output = (result || '').slice(0, MAX_OUTPUT_LENGTH);
65
+ } catch (err) {
66
+ status = 'fail';
67
+ const stdout = err.stdout || '';
68
+ const stderr = err.stderr || '';
69
+ output = (stdout + '\n' + stderr).trim().slice(0, MAX_OUTPUT_LENGTH);
70
+ }
71
+
72
+ const durationMs = Date.now() - start;
73
+ return { name, status, durationMs, output };
74
+ }
75
+
76
+ /**
77
+ * Run verification across multiple stages.
78
+ * @param {{ cwd: string, stages: string[] }} opts
79
+ * @returns {Promise<object>} Structured verification result
80
+ */
81
+ async function runVerification({ cwd, stages }) {
82
+ const resolvedCwd = path.resolve(cwd);
83
+ const results = [];
84
+
85
+ for (const stageName of stages) {
86
+ const result = executeStage(stageName, resolvedCwd);
87
+ results.push(result);
88
+ }
89
+
90
+ const passed = results.filter(s => s.status === 'pass').length;
91
+ const failed = results.filter(s => s.status === 'fail').length;
92
+ const skipped = results.filter(s => s.status === 'skip').length;
93
+ const totalDurationMs = results.reduce((sum, s) => sum + s.durationMs, 0);
94
+
95
+ return {
96
+ stages: results,
97
+ summary: { passed, failed, skipped, totalDurationMs },
98
+ timestamp: new Date().toISOString(),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Format a verification result as a markdown report.
104
+ * @param {object} result — output from runVerification
105
+ * @returns {string} Markdown report
106
+ */
107
+ function formatReport(result) {
108
+ const statusEmoji = { pass: '✅', fail: '❌', skip: '⏭️' };
109
+ const lines = [];
110
+
111
+ lines.push('# Verification Report');
112
+ lines.push('');
113
+ lines.push(`**Timestamp:** ${result.timestamp}`);
114
+ lines.push('');
115
+ lines.push('| Stage | Status | Duration |');
116
+ lines.push('|-------|--------|----------|');
117
+
118
+ for (const stage of result.stages) {
119
+ const emoji = statusEmoji[stage.status] || '?';
120
+ const duration = stage.durationMs > 0 ? `${stage.durationMs}ms` : '-';
121
+ lines.push(`| ${stage.name} | ${emoji} ${stage.status} | ${duration} |`);
122
+ }
123
+
124
+ lines.push('');
125
+ lines.push(`**Summary:** ${result.summary.passed} passed, ${result.summary.failed} failed, ${result.summary.skipped} skipped (${result.summary.totalDurationMs}ms total)`);
126
+ lines.push('');
127
+
128
+ return lines.join('\n');
129
+ }
130
+
131
+ module.exports = { runVerification, formatReport };
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * verify-cli.js — Entrypoint for the `verify` CLI command.
6
+ * Calls the unified verification runner across all stages and writes
7
+ * the formatted report to .planning/VERIFICATION.md.
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { runVerification, formatReport } = require('./verification-runner');
13
+
14
+ const STAGES = ['tests', 'lint', 'audit', 'typecheck'];
15
+ const CWD = process.env.MINDFORGE_ROOT || path.resolve(__dirname, '../..');
16
+
17
+ async function main() {
18
+ const planningDir = path.join(CWD, '.planning');
19
+ if (!fs.existsSync(planningDir)) {
20
+ fs.mkdirSync(planningDir, { recursive: true });
21
+ }
22
+
23
+ const result = await runVerification({ cwd: CWD, stages: STAGES });
24
+ const report = formatReport(result);
25
+
26
+ fs.writeFileSync(path.join(planningDir, 'VERIFICATION.md'), report);
27
+ process.stdout.write(report + '\n');
28
+ process.exit(result.summary.failed > 0 ? 1 : 0);
29
+ }
30
+
31
+ main().catch(err => {
32
+ console.error('Verification runner failed:', err.message);
33
+ process.exit(1);
34
+ });
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Recall@K — fraction of relevant items found in the top-k retrieved results.
5
+ * @param {string[]} retrieved - IDs in ranked order
6
+ * @param {string[]} relevant - ground-truth relevant IDs
7
+ * @param {number} k - cutoff
8
+ * @returns {number} recall in [0, 1]
9
+ */
10
+ function recallAtK(retrieved, relevant, k) {
11
+ if (relevant.length === 0) return 0;
12
+ const topK = retrieved.slice(0, k);
13
+ const relevantSet = new Set(relevant);
14
+ const found = topK.filter(id => relevantSet.has(id)).length;
15
+ return found / relevant.length;
16
+ }
17
+
18
+ /**
19
+ * nDCG (Normalized Discounted Cumulative Gain) with graded relevance.
20
+ * @param {string[]} retrieved - IDs in ranked order
21
+ * @param {Object.<string, number>} relevanceMap - {id: grade} where grade is 0-3
22
+ * @param {number} k - cutoff
23
+ * @returns {number} nDCG in [0, 1]
24
+ */
25
+ function ndcg(retrieved, relevanceMap, k) {
26
+ const topK = retrieved.slice(0, k);
27
+
28
+ // DCG = Σ (2^rel_i - 1) / log2(i + 2) for i = 0..k-1
29
+ const dcg = topK.reduce((sum, id, i) => {
30
+ const rel = relevanceMap[id] || 0;
31
+ return sum + (Math.pow(2, rel) - 1) / Math.log2(i + 2);
32
+ }, 0);
33
+
34
+ // IDCG — ideal ordering: sort all relevance grades descending, take top-k
35
+ const idealGrades = Object.values(relevanceMap)
36
+ .filter(g => g > 0)
37
+ .sort((a, b) => b - a)
38
+ .slice(0, k);
39
+
40
+ const idcg = idealGrades.reduce((sum, rel, i) => {
41
+ return sum + (Math.pow(2, rel) - 1) / Math.log2(i + 2);
42
+ }, 0);
43
+
44
+ if (idcg === 0) return 0;
45
+ return dcg / idcg;
46
+ }
47
+
48
+ /**
49
+ * Run a full evaluation over a golden set of queries.
50
+ * @param {Object} opts
51
+ * @param {Array<{query: string, relevant: string[]}>} opts.goldenSet
52
+ * @param {function(string): string[]} opts.retriever
53
+ * @param {number} opts.k
54
+ * @returns {Promise<{meanRecallAtK: number, meanNDCG: number, perQuery: Array}>}
55
+ */
56
+ async function runEval({ goldenSet, retriever, k }) {
57
+ const perQuery = [];
58
+
59
+ for (const { query, relevant } of goldenSet) {
60
+ const retrieved = await Promise.resolve(retriever(query));
61
+
62
+ // Binary relevance map: relevant items get grade 1, others 0
63
+ const relevanceMap = {};
64
+ for (const id of relevant) {
65
+ relevanceMap[id] = 1;
66
+ }
67
+
68
+ const recall = recallAtK(retrieved, relevant, k);
69
+ const ndcgScore = ndcg(retrieved, relevanceMap, k);
70
+
71
+ perQuery.push({ query, recall, ndcg: ndcgScore, retrieved });
72
+ }
73
+
74
+ if (perQuery.length === 0) return { meanRecallAtK: 0, meanNDCG: 0, perQuery: [] };
75
+
76
+ const meanRecallAtK = perQuery.reduce((s, q) => s + q.recall, 0) / perQuery.length;
77
+ const meanNDCG = perQuery.reduce((s, q) => s + q.ndcg, 0) / perQuery.length;
78
+
79
+ return { meanRecallAtK, meanNDCG, perQuery };
80
+ }
81
+
82
+ module.exports = { recallAtK, ndcg, runEval };
@@ -0,0 +1,46 @@
1
+ {
2
+ "description": "Golden set for retrieval quality evaluation. Each entry has a natural-language query and the IDs of documents that SHOULD be retrieved.",
3
+ "version": "1.0.0",
4
+ "queries": [
5
+ {
6
+ "query": "how does the audit hash chain work",
7
+ "relevant": ["audit-hash", "audit-verifier", "verify-audit"]
8
+ },
9
+ {
10
+ "query": "what model should I use for a security-sensitive task",
11
+ "relevant": ["difficulty-scorer", "model-router", "pricing-registry", "trust-boundaries"]
12
+ },
13
+ {
14
+ "query": "how does wave execution and parallel orchestration work",
15
+ "relevant": ["wave-executor", "swarm-controller", "auto-executor"]
16
+ },
17
+ {
18
+ "query": "how do I track token costs and budget enforcement",
19
+ "relevant": ["cost-tracker", "token-ledger", "budget-enforcer", "finops-hub"]
20
+ },
21
+ {
22
+ "query": "how does the knowledge store persist and retrieve entries",
23
+ "relevant": ["knowledge-store", "knowledge-graph-protocol", "shard-controller"]
24
+ },
25
+ {
26
+ "query": "what happens during council consensus and synthesis",
27
+ "relevant": ["council-protocol", "synthesis-engine", "council-templates"]
28
+ },
29
+ {
30
+ "query": "how do instincts get captured and promoted to skills",
31
+ "relevant": ["capture-engine", "promotion-engine", "instinct-schema", "skill-registry"]
32
+ },
33
+ {
34
+ "query": "what verification checks run before marking a task complete",
35
+ "relevant": ["verification-pipeline", "trust-verifier", "policy-engine"]
36
+ },
37
+ {
38
+ "query": "how does the autonomous stuck detector recover failed tasks",
39
+ "relevant": ["stuck-detector", "node-repair", "steering-manager", "progress-reporter"]
40
+ },
41
+ {
42
+ "query": "how are hooks triggered and what security gates exist pre-commit",
43
+ "relevant": ["trust-gate-hook", "instinct-capture-hook", "policy-gate-hardened", "impact-analyzer"]
44
+ }
45
+ ]
46
+ }
@@ -10,6 +10,7 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const os = require('os');
12
12
  const crypto = require('crypto');
13
+ const { execFileSync } = require('child_process');
13
14
 
14
15
  const REASON = process.argv[2] || 'Manual approval for sensitive changes.';
15
16
  const ROOT = path.resolve(__dirname, '../../');
@@ -19,14 +20,47 @@ if (!fs.existsSync(APPROVALS_DIR)) {
19
20
  fs.mkdirSync(APPROVALS_DIR, { recursive: true });
20
21
  }
21
22
 
23
+ /**
24
+ * Attempts to retrieve the GPG signing key configured in git.
25
+ * Returns null if no key is configured or git is unavailable.
26
+ */
27
+ function getGPGSigningKey() {
28
+ try {
29
+ const key = execFileSync('git', ['config', 'user.signingkey'], { encoding: 'utf8' }).trim();
30
+ return key || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Verifies the identity of the approver using GPG if available.
38
+ * Falls back to git identity only (with warning) if no GPG key is configured.
39
+ * @param {string} approver - The approver identity string
40
+ */
41
+ function verifyApproverIdentity(approver) {
42
+ const gpgKey = getGPGSigningKey();
43
+
44
+ if (!gpgKey) {
45
+ console.warn('[GOVERNANCE] No GPG signing key configured — approval accepted with git identity only');
46
+ return { verified: false, method: 'git_identity', identity: approver };
47
+ }
48
+
49
+ return { verified: true, method: 'gpg_key', identity: approver, keyId: gpgKey };
50
+ }
51
+
22
52
  async function approve() {
23
53
  const pkgPath = path.join(ROOT, 'package.json');
24
54
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
25
55
 
26
56
  const id = `MF-AUTH-${Date.now().toString(36).toUpperCase()}`;
27
57
  const timestamp = new Date().toISOString();
28
-
29
- // Calculate a mock signature based on current state (can be hardened with real crypto sign later)
58
+ const approver = process.env.USER || 'MindForge User';
59
+
60
+ // Verify approver identity (GPG if available, git identity fallback)
61
+ const identityVerification = verifyApproverIdentity(approver);
62
+
63
+ // Calculate a signature based on current state
30
64
  const signature = crypto.createHash('sha256')
31
65
  .update(`${id}:${REASON}:${timestamp}:${os.hostname()}`)
32
66
  .digest('hex');
@@ -36,20 +70,22 @@ async function approve() {
36
70
  project: pkg.name,
37
71
  version: pkg.version,
38
72
  tier: 3,
39
- approved_by: process.env.USER || 'MindForge User',
73
+ approved_by: approver,
40
74
  timestamp,
41
75
  reason: REASON,
42
- signature: `sha256:${signature}`
76
+ signature: `sha256:${signature}`,
77
+ identity_verification: identityVerification
43
78
  };
44
79
 
45
80
  const filename = `approval-${id.toLowerCase()}.json`;
46
81
  const filePath = path.join(APPROVALS_DIR, filename);
47
82
 
48
83
  fs.writeFileSync(filePath, JSON.stringify(record, null, 2));
49
-
84
+
50
85
  console.log('\n✅ Governance approval generated!\n');
51
86
  console.log(`ID: ${id}`);
52
87
  console.log(`Reason: ${REASON}`);
88
+ console.log(`Verified: ${identityVerification.verified ? 'GPG (' + identityVerification.keyId + ')' : 'git identity only (no GPG key)'}`);
53
89
  console.log(`File: .planning/approvals/${filename}`);
54
90
  console.log('\nCommit this file to unblock Tier 3 gates in CI.\n');
55
91
  }
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+ const crypto = require('crypto');
3
+ /**
4
+ * Canonical audit hash material. MUST be the single source of truth for both
5
+ * the writer (pre-_hash entry) and the verifier (entry with _hash stripped).
6
+ * Hashes {...entry, previous_hash} — entry must NOT contain _hash.
7
+ */
8
+ function hashAuditEntry(entryWithoutHash, previousHash) {
9
+ const material = JSON.stringify({ ...entryWithoutHash, previous_hash: previousHash });
10
+ return crypto.createHash('sha256').update(material).digest('hex');
11
+ }
12
+ module.exports = { hashAuditEntry };