mindforge-cc 11.0.0 → 11.2.1

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 (70) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/config.json +14 -4
  3. package/CHANGELOG.md +137 -0
  4. package/MINDFORGE.md +5 -5
  5. package/RELEASENOTES.md +1 -1
  6. package/bin/autonomous/audit-writer.js +108 -86
  7. package/bin/autonomous/auto-runner.js +304 -19
  8. package/bin/autonomous/dependency-dag.js +59 -0
  9. package/bin/autonomous/mesh-self-healer.js +101 -28
  10. package/bin/autonomous/wave-executor.js +20 -1
  11. package/bin/browser/regression-writer.js +45 -3
  12. package/bin/browser/session-manager.js +21 -17
  13. package/bin/council-cli.js +161 -0
  14. package/bin/dashboard/approval-handler.js +3 -1
  15. package/bin/dashboard/server.js +1 -1
  16. package/bin/dashboard/sse-bridge.js +9 -12
  17. package/bin/engine/council-runtime.js +124 -0
  18. package/bin/engine/logic-drift-detector.js +14 -6
  19. package/bin/engine/logic-validator.js +155 -25
  20. package/bin/engine/orbital-guardian.js +56 -10
  21. package/bin/engine/otel-exporter.js +123 -0
  22. package/bin/engine/reason-source-aligner.js +19 -6
  23. package/bin/engine/remediation-engine.js +1 -1
  24. package/bin/engine/self-corrective-synthesizer.js +1 -1
  25. package/bin/engine/sre-manager.js +33 -6
  26. package/bin/engine/temporal-cli.js +4 -2
  27. package/bin/engine/verification-runner.js +131 -0
  28. package/bin/engine/verify-cli.js +34 -0
  29. package/bin/eval/eval-harness.js +82 -0
  30. package/bin/eval/golden-set-retrieval.json +46 -0
  31. package/bin/governance/audit-hash.js +12 -0
  32. package/bin/governance/audit-verifier.js +60 -0
  33. package/bin/governance/policy-engine.js +17 -4
  34. package/bin/governance/quantum-crypto.js +63 -9
  35. package/bin/governance/ztai-archiver.js +74 -9
  36. package/bin/governance/ztai-manager.js +33 -5
  37. package/bin/hindsight-injector.js +5 -6
  38. package/bin/hooks/instinct-capture-hook.js +186 -0
  39. package/bin/installer-core.js +31 -2
  40. package/bin/memory/auto-shadow.js +32 -3
  41. package/bin/memory/eis-client.js +45 -4
  42. package/bin/memory/identity-synthesizer.js +2 -2
  43. package/bin/memory/knowledge-store.js +30 -6
  44. package/bin/memory/retrieval-fusion.js +58 -0
  45. package/bin/memory/semantic-hub.js +2 -2
  46. package/bin/memory/vector-hub.js +143 -6
  47. package/bin/mindforge-cli.js +4 -5
  48. package/bin/models/anthropic-provider.js +13 -4
  49. package/bin/models/cost-tracker.js +3 -1
  50. package/bin/models/difficulty-scorer.js +54 -0
  51. package/bin/models/gemini-provider.js +6 -2
  52. package/bin/models/model-router.js +31 -18
  53. package/bin/models/openai-provider.js +6 -3
  54. package/bin/models/pricing-registry.js +128 -0
  55. package/bin/review/ads-engine.js +1 -1
  56. package/bin/review/finding-synthesizer.js +35 -6
  57. package/bin/security/trust-boundaries.js +194 -0
  58. package/bin/security/trust-gate-hook.js +49 -0
  59. package/bin/skill-registry.js +34 -22
  60. package/bin/skills-builder/marketplace-cli.js +5 -3
  61. package/bin/skills-builder/skill-registrar.js +4 -6
  62. package/bin/sre/sentinel.js +7 -5
  63. package/bin/sre/shadow-mirror.js +90 -40
  64. package/bin/utils/append-queue.js +67 -0
  65. package/bin/utils/file-io.js +29 -80
  66. package/bin/utils/version-check.js +75 -0
  67. package/bin/verify-audit.js +12 -0
  68. package/bin/wizard/theme.js +1 -2
  69. package/package.json +1 -1
  70. package/bin/dashboard/team-tracker.js +0 -0
@@ -10,12 +10,54 @@ function write(bug, phaseNum) {
10
10
  const dir = path.join(process.cwd(), 'tests', 'regression');
11
11
  fs.mkdirSync(dir, { recursive: true });
12
12
  const name = `phase${phaseNum}-${bug.surface.replace(/\//g, '-').slice(1) || 'home'}.test.ts`;
13
+
14
+ // Embed the bug's surface and failure signal as safely-escaped JS string
15
+ // literals. JSON.stringify escapes quotes, backticks and ${...} so a
16
+ // freeform bug.error cannot break out of the generated source.
17
+ const surfaceLit = JSON.stringify(bug.surface);
18
+ const errorLit = JSON.stringify(bug.error);
19
+
20
+ // The generated test reproduces the original failure conditions and asserts
21
+ // the page no longer exhibits THIS bug's signal — it is NOT a body-visibility
22
+ // tautology that passes for any page.
13
23
  const content = `
14
24
  import { test, expect } from '@playwright/test';
15
25
 
16
- test('Regression: ${bug.surface} [${bug.error}]', async ({ page }) => {
17
- await page.goto('${bug.surface}');
18
- // TODO: Add more specific assertions based on the bug
26
+ // Regression guard for the bug originally observed on ${bug.surface}:
27
+ // ${String(bug.error).replace(/[\r\n]+/g, ' ')}
28
+ // This test fails again if that failure signal re-appears (console error,
29
+ // page text, or a >=400 HTTP status on the affected surface).
30
+ const SURFACE = ${surfaceLit};
31
+ const BUG_SIGNAL = ${errorLit};
32
+
33
+ test('Regression: ' + SURFACE + ' [' + BUG_SIGNAL + ']', async ({ page }) => {
34
+ const consoleErrors: string[] = [];
35
+ page.on('console', (msg) => {
36
+ if (msg.type() === 'error') consoleErrors.push(msg.text());
37
+ });
38
+ page.on('pageerror', (err) => consoleErrors.push(String(err)));
39
+
40
+ const response = await page.goto(SURFACE);
41
+
42
+ // 1. The affected surface must load without the original HTTP failure.
43
+ if (response) {
44
+ expect(response.status(), 'surface re-returned a failing HTTP status').toBeLessThan(400);
45
+ }
46
+
47
+ // 2. The specific failure signal must not re-appear in the console.
48
+ expect(
49
+ consoleErrors.some((line) => line.includes(BUG_SIGNAL)),
50
+ 'console re-emitted the original error: ' + BUG_SIGNAL
51
+ ).toBeFalsy();
52
+
53
+ // 3. ...nor be surfaced in the rendered page text.
54
+ const bodyText = await page.textContent('body');
55
+ expect(
56
+ (bodyText || '').includes(BUG_SIGNAL),
57
+ 'page re-rendered the original error: ' + BUG_SIGNAL
58
+ ).toBeFalsy();
59
+
60
+ // 4. Smoke check: the page actually rendered something.
19
61
  expect(await page.isVisible('body')).toBeTruthy();
20
62
  });
21
63
  `;
@@ -7,7 +7,6 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const os = require('os');
11
10
 
12
11
  const SESSIONS_DIR = path.join(process.cwd(), '.mindforge', 'browser', 'sessions');
13
12
  const ensureDir = () => fs.mkdirSync(SESSIONS_DIR, { recursive: true });
@@ -71,23 +70,28 @@ async function loadSession(name, context) {
71
70
  return { cookiesLoaded };
72
71
  }
73
72
 
73
+ /**
74
+ * Import cookies/sessions directly from a native browser profile.
75
+ *
76
+ * NOT IMPLEMENTED: native browser cookie DB import was removed together with
77
+ * the `better-sqlite3` dependency (the project now uses sql.js / WASM). Browser
78
+ * cookie stores are SQLite databases, and decoding them required that native
79
+ * backend. Rather than silently returning an empty array — which would lie about
80
+ * capability and let callers mistake "no cookies imported" for success — this
81
+ * method throws so the missing capability is explicit.
82
+ *
83
+ * To populate a session, capture cookies live via a browser context and use
84
+ * `saveSession` / `loadSession` instead.
85
+ *
86
+ * @param {string} source - Browser identifier (chrome, arc, brave, edge).
87
+ * @throws {Error} Always — native browser cookie import is not implemented.
88
+ */
74
89
  function importFromBrowser(source) {
75
- const home = os.homedir();
76
- const paths = {
77
- chrome: `${home}/Library/Application Support/Google/Chrome/Default/Cookies`,
78
- arc: `${home}/Library/Application Support/Arc/User Data/Default/Cookies`,
79
- brave: `${home}/Library/Application Support/BraveSoftware/Brave-Browser/Default/Cookies`,
80
- edge: `${home}/Library/Application Support/Microsoft Edge/Default/Cookies`,
81
- };
82
-
83
- const p = paths[source.toLowerCase()];
84
- if (!p || !fs.existsSync(p)) {
85
- throw new Error(`Cookie file for ${source} not found at ${p}`);
86
- }
87
-
88
- // Real SQLite parsing would happen here via better-sqlite3 if installed.
89
- // This is a placeholder for the logic specified in the roadmap.
90
- return [];
90
+ throw new Error(
91
+ `importFromBrowser not implemented for "${source}": the native browser ` +
92
+ 'cookie-DB backend (better-sqlite3) was removed project-wide. ' +
93
+ 'Capture cookies live via a browser context and use saveSession/loadSession instead.'
94
+ );
91
95
  }
92
96
 
93
97
  module.exports = { saveSession, loadSession, importFromBrowser };
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * MindForge — Council CLI (UC-22)
5
+ *
6
+ * Thin CLI wrapper around council-runtime.runCouncil. Provides the injectable
7
+ * model function via ModelClient and formats structured output for the
8
+ * /mindforge:council command.
9
+ *
10
+ * Usage:
11
+ * node bin/council-cli.js "Should we adopt event sourcing for the payments domain?"
12
+ * node bin/council-cli.js --id payment-es "Should we adopt event sourcing?"
13
+ *
14
+ * Exit codes:
15
+ * 0 — PROCEED
16
+ * 1 — REVISE
17
+ * 2 — NO_CONSENSUS
18
+ * 3 — Runtime error
19
+ */
20
+ const { runCouncil } = require('./engine/council-runtime');
21
+ const ModelClient = require('./models/model-client');
22
+
23
+ const VOICE_SYSTEM_PROMPTS = {
24
+ architect: 'You are the Architect voice in a decision council. You focus on system design, scalability, maintainability, and long-term architectural integrity. Evaluate the decision from a structural perspective.',
25
+ skeptic: 'You are the Skeptic voice in a decision council. You challenge assumptions, identify risks, hidden costs, and failure modes. Your job is to stress-test the proposal.',
26
+ pragmatist: 'You are the Pragmatist voice in a decision council. You focus on delivery timelines, team capacity, incremental value, and practical trade-offs. Favor what ships reliably.',
27
+ critic: 'You are the Critic voice in a decision council. You evaluate quality, correctness, edge cases, and whether the solution meets its stated goals without over-engineering.',
28
+ };
29
+
30
+ const POSITION_INSTRUCTION = `
31
+ Respond with ONLY a JSON object (no markdown fences, no prose) in this exact shape:
32
+ {
33
+ "recommendation": "PROCEED" or "REVISE",
34
+ "confidence": <number between 0 and 1>,
35
+ "rationale": "<1-3 sentence explanation>"
36
+ }
37
+ Do NOT include any text outside the JSON object.`;
38
+
39
+ /**
40
+ * Injectable model function for runCouncil — calls ModelClient.complete per voice.
41
+ */
42
+ async function councilModel({ voice, question }) {
43
+ const systemPrompt = (VOICE_SYSTEM_PROMPTS[voice] || VOICE_SYSTEM_PROMPTS.architect) +
44
+ '\n' + POSITION_INSTRUCTION;
45
+
46
+ const result = await ModelClient.complete({
47
+ persona: 'council',
48
+ tier: 2,
49
+ systemPrompt,
50
+ userMessage: `Decision under review:\n${question}`,
51
+ maxTokens: 300,
52
+ temperature: 0.4,
53
+ taskName: `council-${voice}`,
54
+ });
55
+
56
+ // Parse the JSON response from the model
57
+ const content = (result.content || '').trim();
58
+ let parsed;
59
+ try {
60
+ // Strip markdown fences if model adds them despite instructions
61
+ const cleaned = content.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
62
+ parsed = JSON.parse(cleaned);
63
+ } catch {
64
+ throw new Error(`Council voice "${voice}" returned unparseable response: ${content.slice(0, 200)}`);
65
+ }
66
+
67
+ return {
68
+ recommendation: parsed.recommendation,
69
+ confidence: parsed.confidence,
70
+ rationale: parsed.rationale,
71
+ };
72
+ }
73
+
74
+ // --- CLI argument parsing ---
75
+ function parseArgs(argv) {
76
+ const args = argv.slice(2);
77
+ const opts = { question: null, decisionId: null };
78
+
79
+ for (let i = 0; i < args.length; i++) {
80
+ if (args[i] === '--id' && args[i + 1]) {
81
+ opts.decisionId = args[++i];
82
+ } else if (!args[i].startsWith('-')) {
83
+ opts.question = opts.question ? `${opts.question} ${args[i]}` : args[i];
84
+ }
85
+ }
86
+ return opts;
87
+ }
88
+
89
+ // --- Formatted output ---
90
+ function formatOutput(result) {
91
+ const lines = [];
92
+ lines.push('');
93
+ lines.push('=== COUNCIL VERDICT ===');
94
+ lines.push('');
95
+ lines.push(`Question: ${result.question}`);
96
+ lines.push('');
97
+ lines.push('--- Positions ---');
98
+ for (const pos of result.positions) {
99
+ const icon = pos.recommendation === 'PROCEED' ? '[+]' : '[-]';
100
+ lines.push(` ${icon} ${pos.voice.toUpperCase()} (${pos.recommendation}, confidence: ${pos.confidence.toFixed(2)})`);
101
+ lines.push(` ${pos.rationale}`);
102
+ }
103
+ lines.push('');
104
+ lines.push(`--- Consensus: ${(result.consensus * 100).toFixed(1)}% ---`);
105
+ lines.push('');
106
+ lines.push(`VERDICT: ${result.verdict}`);
107
+
108
+ if (result.verdict === 'NO_CONSENSUS' && result.dissent.length > 0) {
109
+ lines.push('');
110
+ lines.push('--- Dissent (full split) ---');
111
+ for (const d of result.dissent) {
112
+ lines.push(` * ${d.voice.toUpperCase()} (${d.recommendation}): ${d.rationale}`);
113
+ }
114
+ } else if (result.dissent.length > 0) {
115
+ lines.push('');
116
+ lines.push('--- Dissent ---');
117
+ for (const d of result.dissent) {
118
+ lines.push(` * ${d.voice.toUpperCase()}: ${d.rationale}`);
119
+ }
120
+ }
121
+
122
+ lines.push('');
123
+ lines.push('Council is advisory -- you have final say.');
124
+ lines.push('');
125
+ return lines.join('\n');
126
+ }
127
+
128
+ // --- Main ---
129
+ async function main() {
130
+ const { question, decisionId } = parseArgs(process.argv);
131
+
132
+ if (!question) {
133
+ process.stderr.write('Usage: node bin/council-cli.js [--id <decision-id>] "<question>"\n');
134
+ process.exit(3);
135
+ }
136
+
137
+ try {
138
+ const result = await runCouncil(question, {
139
+ model: councilModel,
140
+ writeDecision: true,
141
+ decisionId: decisionId || undefined,
142
+ });
143
+
144
+ // Output structured JSON to stdout for programmatic consumption
145
+ console.log(JSON.stringify(result, null, 2));
146
+
147
+ // Output formatted human-readable summary to stderr
148
+ process.stderr.write(formatOutput(result));
149
+
150
+ // Exit code reflects verdict
151
+ const exitCode = result.verdict === 'PROCEED' ? 0
152
+ : result.verdict === 'REVISE' ? 1
153
+ : 2;
154
+ process.exit(exitCode);
155
+ } catch (err) {
156
+ process.stderr.write(`[council-cli] ERROR: ${err.message}\n`);
157
+ process.exit(3);
158
+ }
159
+ }
160
+
161
+ main();
@@ -115,7 +115,9 @@ function writeAuditEntry(entry) {
115
115
  try {
116
116
  const paths = getPaths();
117
117
  if (!fs.existsSync(path.dirname(paths.audit))) return;
118
- fs.appendFileSync(paths.audit, JSON.stringify(entry) + '\n');
118
+ // UC-04b: unified, hash-chained, durable append into the single verifiable chain.
119
+ const { appendAuditEntrySync } = require('../autonomous/audit-writer');
120
+ appendAuditEntrySync(paths.audit, entry);
119
121
  } catch { /* ignore AUDIT write failures */ }
120
122
  }
121
123
 
@@ -161,7 +161,7 @@ app.use((req, res, next) => {
161
161
  res.setHeader('X-Content-Type-Options', 'nosniff');
162
162
  res.setHeader('X-Frame-Options', 'DENY');
163
163
  res.setHeader('Cache-Control', 'no-store'); // Never cache dashboard responses
164
- res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'");
164
+ res.setHeader('Content-Security-Policy', 'default-src \'self\'; script-src \'self\'; style-src \'self\' \'unsafe-inline\'; connect-src \'self\'');
165
165
  res.setHeader('X-XSS-Protection', '1; mode=block');
166
166
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
167
167
  next();
@@ -21,7 +21,6 @@ const APPROVAL_DIR = path.join(process.cwd(), '.planning', 'approvals');
21
21
  const clients = new Set(); // Connected SSE response objects
22
22
 
23
23
  let _lastAuditSize = 0;
24
- let _auditInode = 0; // Track file inode for rotation detection
25
24
  let _lastAutoState = '';
26
25
  let _lastApprovals = '';
27
26
 
@@ -78,14 +77,16 @@ function pollAuditLog() {
78
77
  try {
79
78
  const stat = fs.statSync(AUDIT_PATH);
80
79
  const newSize = stat.size;
81
- const newIno = stat.ino;
82
80
 
83
- // File rotation detected: inode changed or file shrunk (truncated after archival)
84
- if ((newIno !== _auditInode && _auditInode !== 0) || (newSize < _lastAuditSize)) {
85
- process.stderr.write(`[sse-bridge] AUDIT.jsonl rotation detected (size: ${_lastAuditSize} -> ${newSize}, ino: ${_auditInode} -> ${newIno})\n`);
81
+ // Truncation / recreation recovery (NOT rotation audit rotation was retired in
82
+ // UC-04b because it broke the hash chain; AUDIT.jsonl grows unbounded). If the
83
+ // file ever SHRINKS (manually truncated, .planning wiped, or replaced), reset the
84
+ // read offset to 0 so the live tail keeps working instead of stalling forever on
85
+ // the `newSize <= _lastAuditSize` early-return below or reading at a stale offset.
86
+ if (newSize < _lastAuditSize) {
87
+ process.stderr.write(`[sse-bridge] AUDIT.jsonl shrank (size: ${_lastAuditSize} -> ${newSize}) — re-tailing from start\n`);
86
88
  _lastAuditSize = 0;
87
89
  }
88
- _auditInode = newIno;
89
90
 
90
91
  if (newSize <= _lastAuditSize) return;
91
92
 
@@ -169,9 +170,7 @@ function startPolling() {
169
170
 
170
171
  // Initialize AUDIT position on first start
171
172
  if (!_initialized && fs.existsSync(AUDIT_PATH)) {
172
- const stat = fs.statSync(AUDIT_PATH);
173
- _lastAuditSize = stat.size;
174
- _auditInode = stat.ino;
173
+ _lastAuditSize = fs.statSync(AUDIT_PATH).size;
175
174
  _initialized = true;
176
175
  }
177
176
 
@@ -207,9 +206,7 @@ function stopPolling() {
207
206
  function start() {
208
207
  // Pre-initialize AUDIT position so first client gets instant data
209
208
  if (!_initialized && fs.existsSync(AUDIT_PATH)) {
210
- const stat = fs.statSync(AUDIT_PATH);
211
- _lastAuditSize = stat.size;
212
- _auditInode = stat.ino;
209
+ _lastAuditSize = fs.statSync(AUDIT_PATH).size;
213
210
  _initialized = true;
214
211
  }
215
212
  // Polling starts lazily when addClient() is called
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+ /**
3
+ * MindForge — Council Runtime (UC-10). Thin multi-voice decision harness (ADS).
4
+ *
5
+ * Activates the Adversarial Decision Loop (ADS) mandated by CLAUDE.md: instead of
6
+ * a multi-round debate simulator, this is a THIN runtime — parallel position
7
+ * collection (one per voice) + consensus scoring + dissent capture.
8
+ *
9
+ * The model is INJECTABLE (no hard LLM dependency) so callers/tests can supply a
10
+ * mock. The Semaphore from wave-executor is reused to bound concurrent voice calls.
11
+ *
12
+ * Design decision — NO challenge round (kept thin per adversarial review + YAGNI):
13
+ * A second model call per dissenter that folds a revised confidence back into the
14
+ * consensus adds ordering/weighting complexity and extra failure modes without
15
+ * changing the activation goal. Position-collection + consensus fully satisfies the
16
+ * ADS loop and the verdict contract. Add a challenge round only if a concrete need
17
+ * arises (an explicit `opts.challengeRound` flag would be the clean extension point).
18
+ *
19
+ * Filenames use opts.decisionId (NOT Date.now(), which is unavailable in some
20
+ * MindForge execution contexts); falls back to "council-latest.json".
21
+ */
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { Semaphore } = require('../autonomous/wave-executor');
25
+
26
+ const DEFAULT_VOICES = ['architect', 'skeptic', 'pragmatist', 'critic'];
27
+ const VALID_RECOMMENDATIONS = ['PROCEED', 'REVISE'];
28
+
29
+ /**
30
+ * Validates a single voice's position payload. Throws a clear, voice-named error
31
+ * on a malformed payload rather than letting it degrade into NaN consensus.
32
+ * @param {string} voice — The voice that produced the position (for the error message).
33
+ * @param {object} position — The raw position returned by the model.
34
+ */
35
+ function validatePosition(voice, position) {
36
+ if (!position || typeof position !== 'object') {
37
+ throw new Error(`Council voice "${voice}" returned an invalid position: expected an object, got ${position === null ? 'null' : typeof position}`);
38
+ }
39
+ if (!VALID_RECOMMENDATIONS.includes(position.recommendation)) {
40
+ throw new Error(`Council voice "${voice}" returned an invalid position: recommendation must be one of ${VALID_RECOMMENDATIONS.join('/')}, got ${JSON.stringify(position.recommendation)}`);
41
+ }
42
+ if (typeof position.confidence !== 'number' || !Number.isFinite(position.confidence) || position.confidence < 0 || position.confidence > 1) {
43
+ throw new Error(`Council voice "${voice}" returned an invalid position: confidence must be a number in [0,1], got ${position.confidence}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Runs a thin adversarial council over a question.
49
+ * @param {string} question — The decision/question put to the council.
50
+ * @param {object} [opts]
51
+ * @param {string[]} [opts.voices] — Voice personas to consult (default: 4 ADS voices).
52
+ * @param {number} [opts.consensusThreshold=0.75] — Threshold for PROCEED/REVISE.
53
+ * @param {function} opts.model — REQUIRED. async ({voice, question}) =>
54
+ * { recommendation: 'PROCEED'|'REVISE', confidence: number(0..1), rationale: string }
55
+ * @param {number} [opts.maxConcurrency] — Bound on parallel voice calls (default: #voices).
56
+ * @param {boolean} [opts.writeDecision=true] — Persist a decision record to disk.
57
+ * @param {string} [opts.outputPath] — Directory for the record (default: .planning/decisions).
58
+ * @param {string} [opts.decisionId] — Stable id used in the filename (no Date.now()).
59
+ * @returns {Promise<{question,positions,consensus,verdict,dissent}>}
60
+ */
61
+ async function runCouncil(question, opts = {}) {
62
+ const voices = Array.isArray(opts.voices) && opts.voices.length > 0
63
+ ? opts.voices
64
+ : DEFAULT_VOICES;
65
+ const consensusThreshold = opts.consensusThreshold ?? 0.75;
66
+ const model = opts.model;
67
+ if (typeof model !== 'function') {
68
+ throw new Error('runCouncil requires an injectable model function (opts.model)');
69
+ }
70
+ const maxConcurrency = opts.maxConcurrency || voices.length;
71
+
72
+ // Parallel position collection — bounded by the reused Semaphore.
73
+ const sem = new Semaphore(maxConcurrency);
74
+ const positions = await Promise.all(voices.map(async (voice) => {
75
+ await sem.acquire();
76
+ try {
77
+ const position = await model({ voice, question });
78
+ // Validate the payload immediately — never silently swallow a malformed
79
+ // position into NaN consensus (which would collapse to NO_CONSENSUS and
80
+ // write NaN to the decision record).
81
+ validatePosition(voice, position);
82
+ return { voice, ...position };
83
+ } finally {
84
+ sem.release();
85
+ }
86
+ }));
87
+
88
+ // Consensus = mean approval signal across voices.
89
+ // A PROCEED contributes its confidence; a REVISE contributes its inverse
90
+ // (so a high-confidence REVISE pulls consensus down hard).
91
+ const consensus = positions.reduce((sum, p) => {
92
+ const approval = p.recommendation === 'PROCEED' ? p.confidence : (1 - p.confidence);
93
+ return sum + approval;
94
+ }, 0) / positions.length;
95
+
96
+ const verdict = consensus >= consensusThreshold ? 'PROCEED'
97
+ : consensus <= (1 - consensusThreshold) ? 'REVISE'
98
+ : 'NO_CONSENSUS';
99
+
100
+ // Dissent capture:
101
+ // - For a decisive verdict (PROCEED/REVISE): the voices opposing that direction.
102
+ // - For NO_CONSENSUS (the deadlock ADS most needs documented): the FULL split —
103
+ // every voice's {voice, recommendation, rationale} — so the decision record
104
+ // preserves both camps rather than recording an empty dissent list.
105
+ const dissent = verdict === 'NO_CONSENSUS'
106
+ ? positions.map((p) => ({ voice: p.voice, recommendation: p.recommendation, rationale: p.rationale }))
107
+ : positions.filter((p) =>
108
+ (verdict === 'PROCEED' && p.recommendation !== 'PROCEED') ||
109
+ (verdict === 'REVISE' && p.recommendation === 'PROCEED'))
110
+ .map((d) => ({ voice: d.voice, rationale: d.rationale }));
111
+
112
+ const result = { question, positions, consensus, verdict, dissent };
113
+
114
+ if (opts.writeDecision !== false) {
115
+ const dir = opts.outputPath || path.join(process.cwd(), '.planning', 'decisions');
116
+ fs.mkdirSync(dir, { recursive: true });
117
+ const name = opts.decisionId ? `council-${opts.decisionId}.json` : 'council-latest.json';
118
+ fs.writeFileSync(path.join(dir, name), JSON.stringify(result, null, 2));
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ module.exports = { runCouncil };
@@ -1,9 +1,15 @@
1
1
  /**
2
- * MindForge v6.1.0-alpha — Neural Drift Remediation (NDR)
3
- * Component: Logic Drift Detector (Pillar X)
4
- *
5
- * Analyzes reasoning traces for "Semantic Decay" (repeated failure patterns,
6
- * hallucination-like markers, or mission drift).
2
+ * MindForge v6.1.0-alpha — Logic Drift Detector (Pillar X)
3
+ *
4
+ * HEURISTIC drift detector. Despite the "Pillar X" product naming, this
5
+ * component does NOT use a neural network, embeddings, or any learned model.
6
+ * It scores reasoning traces using pure keyword/ratio heuristics:
7
+ * - unique-word-to-total ratio (proxy for "rambling")
8
+ * - max word-repetition count (proxy for circular reasoning)
9
+ * - presence of a small hardcoded list of contradiction phrases
10
+ *
11
+ * Flags "Semantic Decay" (repeated failure patterns, contradiction markers,
12
+ * or mission drift) heuristically. No model inference is performed.
7
13
  */
8
14
  'use strict';
9
15
 
@@ -48,7 +54,9 @@ class LogicDriftDetector {
48
54
  }
49
55
 
50
56
  /**
51
- * Internal Heuristic: Detects low semantic density (rambling).
57
+ * Internal Heuristic: approximates "rambling" via a unique-keyword-to-word
58
+ * ratio. NOTE: this is NOT a semantic/embedding measure — "density" here is
59
+ * a plain lexical ratio, not model-derived semantic similarity.
52
60
  */
53
61
  _calculateSemanticDensity(thought) {
54
62
  const words = thought.split(/\s+/).length;