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.
- package/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
- package/.mindforge/config.json +18 -4
- package/CHANGELOG.md +165 -0
- package/MINDFORGE.md +3 -3
- package/README.md +49 -4
- package/RELEASENOTES.md +81 -1
- package/SECURITY.md +20 -8
- package/bin/autonomous/audit-writer.js +105 -70
- package/bin/autonomous/auto-runner.js +377 -34
- package/bin/autonomous/context-refactorer.js +26 -11
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/state-manager.js +62 -6
- package/bin/autonomous/stuck-monitor.js +46 -7
- package/bin/autonomous/wave-executor.js +86 -26
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/api-router.js +43 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/metrics-aggregator.js +28 -1
- package/bin/dashboard/server.js +68 -5
- package/bin/dashboard/sse-bridge.js +10 -13
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/feedback-loop.js +8 -0
- package/bin/engine/intelligence-interlock.js +32 -15
- package/bin/engine/logic-drift-detector.js +2 -1
- package/bin/engine/nexus-tracer.js +3 -2
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/remediation-engine.js +155 -32
- package/bin/engine/self-corrective-synthesizer.js +84 -10
- package/bin/engine/sre-manager.js +12 -4
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/temporal-hub.js +131 -34
- 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/approve.js +41 -5
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/impact-analyzer.js +28 -0
- package/bin/governance/policy-engine.js +10 -3
- package/bin/governance/quantum-crypto.js +95 -28
- package/bin/governance/rbac-manager.js +74 -2
- package/bin/governance/ztai-manager.js +79 -9
- package/bin/hindsight-injector.js +8 -9
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +71 -34
- package/bin/memory/embedding-engine.js +61 -0
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-graph.js +58 -5
- package/bin/memory/knowledge-indexer.js +53 -6
- package/bin/memory/knowledge-store.js +52 -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/migrations/10.7.0-to-11.0.0.js +110 -0
- package/bin/migrations/schema-versions.js +13 -0
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +58 -4
- package/bin/models/cloud-broker.js +68 -20
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +57 -2
- package/bin/models/model-client.js +20 -0
- package/bin/models/model-router.js +59 -26
- package/bin/models/openai-provider.js +50 -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 +90 -38
- package/bin/utils/index.js +58 -0
- package/bin/utils/version-check.js +59 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/docs/getting-started.md +1 -1
- package/docs/user-guide.md +2 -2
- package/package.json +2 -2
- package/bin/dashboard/team-tracker.js +0 -0
|
@@ -7,8 +7,59 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { atomicWriteJSON } = require('../utils/file-io');
|
|
10
11
|
|
|
11
12
|
const VALID_PHASES = ['idle', 'planning', 'executing', 'verifying', 'complete', 'running', 'paused', 'completed'];
|
|
13
|
+
const VALID_STATUSES = ['idle', 'running', 'paused', 'completed', 'escalated', 'timeout'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validates HANDOFF.json structure without blocking on failure (fail-open).
|
|
17
|
+
* Warns on malformed fields but always returns the data for processing.
|
|
18
|
+
* @param {*} data — Parsed HANDOFF.json content
|
|
19
|
+
* @returns {{ valid: boolean, warnings: string[] }}
|
|
20
|
+
*/
|
|
21
|
+
function validateHandoff(data) {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
|
|
24
|
+
if (!data || typeof data !== 'object') {
|
|
25
|
+
warnings.push('HANDOFF.json is not a valid object');
|
|
26
|
+
return { valid: false, warnings };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!data.schema_version) {
|
|
30
|
+
warnings.push('Missing schema_version field');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data.handoffs && !Array.isArray(data.handoffs)) {
|
|
34
|
+
warnings.push('handoffs field must be an array');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (data.handoffs && Array.isArray(data.handoffs)) {
|
|
38
|
+
for (let i = 0; i < data.handoffs.length; i++) {
|
|
39
|
+
const task = data.handoffs[i];
|
|
40
|
+
if (!task.id) warnings.push(`handoffs[${i}] missing required field: id`);
|
|
41
|
+
if (!task.name) warnings.push(`handoffs[${i}] missing required field: name`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (data.status && !VALID_STATUSES.includes(data.status)) {
|
|
46
|
+
warnings.push(`Invalid status: "${data.status}". Expected one of: ${VALID_STATUSES.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (data.wave_current !== undefined && typeof data.wave_current !== 'number') {
|
|
50
|
+
warnings.push('wave_current must be a number');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (data.tasks_completed !== undefined && typeof data.tasks_completed !== 'number') {
|
|
54
|
+
warnings.push('tasks_completed must be a number');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (data.timestamps && typeof data.timestamps !== 'object') {
|
|
58
|
+
warnings.push('timestamps must be an object');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { valid: warnings.length === 0, warnings };
|
|
62
|
+
}
|
|
12
63
|
|
|
13
64
|
/**
|
|
14
65
|
* Creates a state manager for the given planning directory.
|
|
@@ -45,7 +96,7 @@ function createStateManager(planningDir) {
|
|
|
45
96
|
function updateState(patch) {
|
|
46
97
|
const current = getState();
|
|
47
98
|
const merged = Object.assign(Object.create(null), current, patch);
|
|
48
|
-
|
|
99
|
+
atomicWriteJSON(statePath, merged);
|
|
49
100
|
return merged;
|
|
50
101
|
}
|
|
51
102
|
|
|
@@ -64,7 +115,8 @@ function createStateManager(planningDir) {
|
|
|
64
115
|
}
|
|
65
116
|
|
|
66
117
|
/**
|
|
67
|
-
* Reads and parses HANDOFF.json
|
|
118
|
+
* Reads and parses HANDOFF.json with schema validation (fail-open).
|
|
119
|
+
* Logs warnings for structural issues but always returns data if parseable.
|
|
68
120
|
* @returns {object} Parsed handoff data (fresh object)
|
|
69
121
|
*/
|
|
70
122
|
function readHandoff() {
|
|
@@ -79,8 +131,12 @@ function createStateManager(planningDir) {
|
|
|
79
131
|
throw new Error(`HANDOFF.json is malformed: ${e.message}`);
|
|
80
132
|
}
|
|
81
133
|
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
// Fail-open schema validation — warn but never block
|
|
135
|
+
const { valid, warnings } = validateHandoff(handoff);
|
|
136
|
+
if (!valid) {
|
|
137
|
+
for (const warning of warnings) {
|
|
138
|
+
console.warn('[STATE] HANDOFF validation:', warning);
|
|
139
|
+
}
|
|
84
140
|
}
|
|
85
141
|
|
|
86
142
|
return handoff;
|
|
@@ -94,7 +150,7 @@ function createStateManager(planningDir) {
|
|
|
94
150
|
const timestamped = Object.assign(Object.create(null), data, {
|
|
95
151
|
last_updated: new Date().toISOString(),
|
|
96
152
|
});
|
|
97
|
-
|
|
153
|
+
atomicWriteJSON(handoffPath, timestamped);
|
|
98
154
|
return timestamped;
|
|
99
155
|
}
|
|
100
156
|
|
|
@@ -113,4 +169,4 @@ function sanitizeState(parsed) {
|
|
|
113
169
|
return clean;
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
module.exports = { createStateManager };
|
|
172
|
+
module.exports = { createStateManager, validateHandoff };
|
|
@@ -96,12 +96,49 @@ class StuckMonitor {
|
|
|
96
96
|
return identical.length >= 3;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
isContentSimilar(a, b) {
|
|
99
|
+
isContentSimilar(a, b, threshold = 10) {
|
|
100
100
|
if (!a || !b) return false;
|
|
101
101
|
if (a === b) return true;
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
102
|
+
|
|
103
|
+
const hashA = this._quickHash(a);
|
|
104
|
+
const hashB = this._quickHash(b);
|
|
105
|
+
if (hashA === hashB) return true;
|
|
106
|
+
|
|
107
|
+
const lenDiff = Math.abs(a.length - b.length);
|
|
108
|
+
if (lenDiff > Math.max(a.length, b.length) * 0.2) return false;
|
|
109
|
+
|
|
110
|
+
const cached = this._getCachedSimilarity(hashA, hashB);
|
|
111
|
+
if (cached !== undefined) return cached;
|
|
112
|
+
|
|
113
|
+
const truncA = a.substring(0, 100);
|
|
114
|
+
const truncB = b.substring(0, 100);
|
|
115
|
+
const result = this.levenshtein(truncA, truncB) <= threshold;
|
|
116
|
+
|
|
117
|
+
this._setCachedSimilarity(hashA, hashB, result);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_quickHash(str) {
|
|
122
|
+
let hash = 0;
|
|
123
|
+
for (let i = 0; i < str.length; i++) {
|
|
124
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
125
|
+
hash = hash & hash;
|
|
126
|
+
}
|
|
127
|
+
return hash;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_getCachedSimilarity(keyA, keyB) {
|
|
131
|
+
const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
|
|
132
|
+
return StuckMonitor._similarityCache.get(key);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_setCachedSimilarity(keyA, keyB, result) {
|
|
136
|
+
const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
|
|
137
|
+
if (StuckMonitor._similarityCache.size >= 200) {
|
|
138
|
+
const firstKey = StuckMonitor._similarityCache.keys().next().value;
|
|
139
|
+
StuckMonitor._similarityCache.delete(firstKey);
|
|
140
|
+
}
|
|
141
|
+
StuckMonitor._similarityCache.set(key, result);
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
levenshtein(a, b) {
|
|
@@ -109,12 +146,14 @@ class StuckMonitor {
|
|
|
109
146
|
for (let i = 0; i <= a.length; i++) { tmp[i] = [i]; }
|
|
110
147
|
for (let j = 0; j <= b.length; j++) { tmp[0][j] = j; }
|
|
111
148
|
for (let i = 1; i <= a.length; i++) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
for (let j = 1; j <= b.length; j++) {
|
|
150
|
+
tmp[i][j] = Math.min(tmp[i - 1][j] + 1, tmp[i][j - 1] + 1, tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
151
|
+
}
|
|
115
152
|
}
|
|
116
153
|
return tmp[a.length][b.length];
|
|
117
154
|
}
|
|
118
155
|
}
|
|
119
156
|
|
|
157
|
+
StuckMonitor._similarityCache = new Map();
|
|
158
|
+
|
|
120
159
|
module.exports = StuckMonitor;
|
|
@@ -6,6 +6,36 @@
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
|
+
const { buildGraph, groupIntoWaves } = require('./dependency-dag');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Semaphore for bounding concurrency within a wave.
|
|
13
|
+
* Tasks within a wave are independent — this limits how many run simultaneously.
|
|
14
|
+
*/
|
|
15
|
+
class Semaphore {
|
|
16
|
+
constructor(max) {
|
|
17
|
+
this.max = max;
|
|
18
|
+
this.current = 0;
|
|
19
|
+
this.queue = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async acquire() {
|
|
23
|
+
if (this.current < this.max) {
|
|
24
|
+
this.current++;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await new Promise(resolve => this.queue.push(resolve));
|
|
28
|
+
this.current++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
release() {
|
|
32
|
+
this.current--;
|
|
33
|
+
if (this.queue.length > 0) {
|
|
34
|
+
const next = this.queue.shift();
|
|
35
|
+
next();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
9
39
|
|
|
10
40
|
/**
|
|
11
41
|
* Creates a wave executor with the given configuration.
|
|
@@ -35,10 +65,19 @@ function createWaveExecutor(config = {}) {
|
|
|
35
65
|
/**
|
|
36
66
|
* Groups handoff tasks into sequential waves based on wave field or dependency topology.
|
|
37
67
|
* Returns a new array of wave objects — does not mutate input.
|
|
68
|
+
*
|
|
69
|
+
* Resolution order (UC-03):
|
|
70
|
+
* 1. Explicit numeric `.wave` field ALWAYS wins (legacy behavior, unchanged).
|
|
71
|
+
* 2. Else if `options.useDag === true` (OPT-IN), order by `depends_on` via Kahn
|
|
72
|
+
* topological sort. Halts loud (throws) on cycles or unknown dependencies.
|
|
73
|
+
* 3. Else (legacy default), all tasks in a single parallel wave (unchanged).
|
|
74
|
+
*
|
|
75
|
+
* DAG ordering is OPT-IN to avoid silently reordering existing PLAN files.
|
|
38
76
|
* @param {Array} handoffs — Raw handoffs array from HANDOFF.json
|
|
77
|
+
* @param {object} [options] — { useDag?: boolean }
|
|
39
78
|
* @returns {Array<{ wave: number, tasks: Array }>}
|
|
40
79
|
*/
|
|
41
|
-
function planWaves(handoffs) {
|
|
80
|
+
function planWaves(handoffs, options = {}) {
|
|
42
81
|
if (!Array.isArray(handoffs) || handoffs.length === 0) {
|
|
43
82
|
waves = [];
|
|
44
83
|
return [];
|
|
@@ -57,6 +96,15 @@ function createWaveExecutor(config = {}) {
|
|
|
57
96
|
waves = Array.from(byWave.entries())
|
|
58
97
|
.sort((a, b) => a[0] - b[0])
|
|
59
98
|
.map(([waveNum, tasks]) => Object.freeze({ wave: waveNum, tasks: Object.freeze(tasks) }));
|
|
99
|
+
} else if (options.useDag === true) {
|
|
100
|
+
const normalized = handoffs.map(normalizeTask);
|
|
101
|
+
const graph = buildGraph(normalized);
|
|
102
|
+
const waveIds = groupIntoWaves(graph);
|
|
103
|
+
const byId = new Map(normalized.map(t => [t.id, t]));
|
|
104
|
+
waves = waveIds.map((ids, i) => Object.freeze({
|
|
105
|
+
wave: i,
|
|
106
|
+
tasks: Object.freeze(ids.map(id => byId.get(id))),
|
|
107
|
+
}));
|
|
60
108
|
} else {
|
|
61
109
|
// Single wave with all tasks
|
|
62
110
|
waves = [Object.freeze({
|
|
@@ -73,42 +121,54 @@ function createWaveExecutor(config = {}) {
|
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
/**
|
|
76
|
-
* Executes a single wave — runs tasks
|
|
124
|
+
* Executes a single wave — runs tasks in parallel (bounded by maxConcurrency),
|
|
125
|
+
* skipping already-completed ones.
|
|
77
126
|
* @param {object} wave — A wave object from planWaves
|
|
78
127
|
* @param {object} context — Execution context passed to callbacks
|
|
79
|
-
* @param {
|
|
128
|
+
* @param {function} context.executor — async function(task) => result (performs actual work)
|
|
129
|
+
* @param {number} [context.maxConcurrency=3] — Max parallel tasks within this wave
|
|
80
130
|
* @returns {Promise<{ completed: string[], failed: string[], skipped: string[] }>}
|
|
81
131
|
*/
|
|
82
132
|
async function executeWave(wave, context = {}) {
|
|
83
|
-
const { executor = async () => {} } = context;
|
|
133
|
+
const { executor = async () => {}, maxConcurrency = 3 } = context;
|
|
84
134
|
status = 'running';
|
|
85
135
|
|
|
86
136
|
const pending = wave.tasks.filter(t => !completedTasks.has(t.id));
|
|
87
137
|
const result = { completed: [], failed: [], skipped: [] };
|
|
138
|
+
const semaphore = new Semaphore(maxConcurrency);
|
|
88
139
|
|
|
89
140
|
onWaveStart({ wave: wave.wave, taskCount: pending.length });
|
|
90
141
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
const settled = await Promise.allSettled(
|
|
143
|
+
pending.map(async (task) => {
|
|
144
|
+
await semaphore.acquire();
|
|
145
|
+
const taskStart = Date.now();
|
|
146
|
+
try {
|
|
147
|
+
onTaskStart({ task, wave: wave.wave });
|
|
148
|
+
await executor(task);
|
|
149
|
+
|
|
150
|
+
const duration = Date.now() - taskStart;
|
|
151
|
+
completedTasks = new Set([...completedTasks, task.id]);
|
|
152
|
+
result.completed.push(task.id);
|
|
153
|
+
|
|
154
|
+
onTaskComplete({ task, wave: wave.wave, duration_ms: duration });
|
|
155
|
+
return { task, status: 'fulfilled' };
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const duration = Date.now() - taskStart;
|
|
158
|
+
result.failed.push(task.id);
|
|
159
|
+
|
|
160
|
+
onTaskFail({ task, wave: wave.wave, error: err, duration_ms: duration });
|
|
161
|
+
throw err;
|
|
162
|
+
} finally {
|
|
163
|
+
semaphore.release();
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const failures = settled.filter(r => r.status === 'rejected');
|
|
169
|
+
if (failures.length > 0) {
|
|
170
|
+
const failMsg = failures.map(f => f.reason?.message || 'unknown').join(', ');
|
|
171
|
+
throw new Error(`${failures.length} task(s) failed in wave: ${failMsg}`);
|
|
112
172
|
}
|
|
113
173
|
|
|
114
174
|
currentWaveIndex++;
|
|
@@ -166,4 +226,4 @@ function normalizeTask(h) {
|
|
|
166
226
|
});
|
|
167
227
|
}
|
|
168
228
|
|
|
169
|
-
module.exports = { createWaveExecutor };
|
|
229
|
+
module.exports = { createWaveExecutor, Semaphore };
|
|
@@ -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();
|
|
@@ -192,6 +192,49 @@ function register(app) {
|
|
|
192
192
|
app.get('/api/connections', (req, res) => {
|
|
193
193
|
res.json({ clients: SSE.getClientCount() });
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
// ── System observability ────────────────────────────────────────────────────
|
|
197
|
+
app.get('/api/v1/system', (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
200
|
+
const heapTotal = process.memoryUsage().heapTotal;
|
|
201
|
+
const uptime = process.uptime();
|
|
202
|
+
|
|
203
|
+
let auditLines = 0;
|
|
204
|
+
try {
|
|
205
|
+
const auditPath = path.join(process.cwd(), '.planning', 'AUDIT.jsonl');
|
|
206
|
+
if (fs.existsSync(auditPath)) {
|
|
207
|
+
const content = fs.readFileSync(auditPath, 'utf8');
|
|
208
|
+
auditLines = content.split('\n').filter(l => l.trim()).length;
|
|
209
|
+
}
|
|
210
|
+
} catch { /* non-critical */ }
|
|
211
|
+
|
|
212
|
+
let snapshotCount = 0;
|
|
213
|
+
try {
|
|
214
|
+
const historyDir = path.join(process.cwd(), '.planning', 'history');
|
|
215
|
+
if (fs.existsSync(historyDir)) {
|
|
216
|
+
snapshotCount = fs.readdirSync(historyDir).length;
|
|
217
|
+
}
|
|
218
|
+
} catch { /* non-critical */ }
|
|
219
|
+
|
|
220
|
+
const heapHealth = Metrics.checkHeapHealth();
|
|
221
|
+
|
|
222
|
+
res.json({
|
|
223
|
+
heap_used_mb: Math.round(heapUsed / 1024 / 1024 * 100) / 100,
|
|
224
|
+
heap_total_mb: Math.round(heapTotal / 1024 / 1024 * 100) / 100,
|
|
225
|
+
heap_usage_pct: Math.round(heapUsed / heapTotal * 100),
|
|
226
|
+
heap_alert: heapHealth,
|
|
227
|
+
uptime_seconds: Math.round(uptime),
|
|
228
|
+
audit_lines: auditLines,
|
|
229
|
+
snapshot_count: snapshotCount,
|
|
230
|
+
sse_clients: SSE.getClientCount(),
|
|
231
|
+
node_version: process.version,
|
|
232
|
+
timestamp: new Date().toISOString()
|
|
233
|
+
});
|
|
234
|
+
} catch (err) {
|
|
235
|
+
res.status(500).json({ error: err.message });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
195
238
|
}
|
|
196
239
|
|
|
197
240
|
module.exports = { register };
|
|
@@ -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
|
|
|
@@ -326,6 +326,32 @@ function getCosts(windowDays = 7) {
|
|
|
326
326
|
return stats;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
// ── Heap Health ──────────────────────────────────────────────────────────────
|
|
330
|
+
function checkHeapHealth() {
|
|
331
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
332
|
+
const maxHeap = getMaxOldSpaceSize();
|
|
333
|
+
const usagePct = Math.round(heapUsed / maxHeap * 100);
|
|
334
|
+
|
|
335
|
+
let status = 'healthy';
|
|
336
|
+
if (usagePct > 85) {
|
|
337
|
+
status = 'critical';
|
|
338
|
+
} else if (usagePct > 70) {
|
|
339
|
+
status = 'warning';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { status, usage_pct: usagePct };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getMaxOldSpaceSize() {
|
|
346
|
+
// Parse --max-old-space-size from process args, default 1.4GB
|
|
347
|
+
const flag = process.execArgv.find(a => a.startsWith('--max-old-space-size'));
|
|
348
|
+
if (flag) {
|
|
349
|
+
const mb = parseInt(flag.split('=')[1], 10);
|
|
350
|
+
if (mb > 0) return mb * 1024 * 1024;
|
|
351
|
+
}
|
|
352
|
+
return 1.4 * 1024 * 1024 * 1024;
|
|
353
|
+
}
|
|
354
|
+
|
|
329
355
|
module.exports = {
|
|
330
356
|
getStatus,
|
|
331
357
|
getAuditEntries,
|
|
@@ -333,5 +359,6 @@ module.exports = {
|
|
|
333
359
|
getApprovals,
|
|
334
360
|
getTeamActivity,
|
|
335
361
|
getMemory,
|
|
336
|
-
getCosts
|
|
362
|
+
getCosts,
|
|
363
|
+
checkHeapHealth
|
|
337
364
|
};
|