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.
- package/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/config.json +14 -4
- package/CHANGELOG.md +137 -0
- package/MINDFORGE.md +5 -5
- 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/mesh-self-healer.js +101 -28
- package/bin/autonomous/wave-executor.js +20 -1
- package/bin/browser/regression-writer.js +45 -3
- package/bin/browser/session-manager.js +21 -17
- 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/logic-drift-detector.js +14 -6
- package/bin/engine/logic-validator.js +155 -25
- package/bin/engine/orbital-guardian.js +56 -10
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/reason-source-aligner.js +19 -6
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- package/bin/engine/sre-manager.js +33 -6
- 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/policy-engine.js +17 -4
- package/bin/governance/quantum-crypto.js +63 -9
- package/bin/governance/ztai-archiver.js +74 -9
- package/bin/governance/ztai-manager.js +33 -5
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/installer-core.js +31 -2
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +45 -4
- 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 +143 -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/review/finding-synthesizer.js +35 -6
- package/bin/security/trust-boundaries.js +194 -0
- package/bin/security/trust-gate-hook.js +49 -0
- package/bin/skill-registry.js +34 -22
- 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/sre/shadow-mirror.js +90 -40
- package/bin/utils/append-queue.js +67 -0
- package/bin/utils/file-io.js +29 -80
- package/bin/utils/version-check.js +75 -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
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
package/bin/dashboard/server.js
CHANGED
|
@@ -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',
|
|
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
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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:
|
|
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;
|