mindforge-cc 11.9.1 → 11.9.3
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/CLAUDE.md +37 -13
- package/.agent/hooks/mindforge-block-no-verify.js +61 -13
- package/.agent/hooks/mindforge-config-protection.js +82 -3
- package/.agent/hooks/mindforge-context-monitor.js +1 -1
- package/.agent/hooks/mindforge-workflow-guard.js +2 -2
- package/.agent/hooks/run-with-flags.js +190 -20
- package/.agent/mindforge/browse.md +2 -2
- package/.agent/mindforge/checkpoint.md +1 -1
- package/.agent/mindforge/consult.md +1 -1
- package/.agent/mindforge/cost-report.md +1 -1
- package/.agent/mindforge/harness-audit.md +1 -1
- package/.agent/mindforge/orch-add-feature.md +1 -1
- package/.agent/mindforge/orch-build-mvp.md +1 -1
- package/.agent/mindforge/orch-change-feature.md +1 -1
- package/.agent/mindforge/orch-fix-defect.md +1 -1
- package/.agent/mindforge/orch-refine-code.md +1 -1
- package/.agent/mindforge/qa.md +2 -2
- package/.claude/CLAUDE.md +37 -13
- package/.claude/commands/mindforge/browse.md +2 -2
- package/.claude/commands/mindforge/checkpoint.md +1 -1
- package/.claude/commands/mindforge/consult.md +1 -1
- package/.claude/commands/mindforge/cost-report.md +1 -1
- package/.claude/commands/mindforge/harness-audit.md +1 -1
- package/.claude/commands/mindforge/orch-add-feature.md +1 -1
- package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
- package/.claude/commands/mindforge/orch-change-feature.md +1 -1
- package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
- package/.claude/commands/mindforge/orch-refine-code.md +1 -1
- package/.claude/commands/mindforge/qa.md +2 -2
- package/.mindforge/MINDFORGE-SCHEMA.json +126 -13
- package/.mindforge/config.json +4 -4
- package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
- package/.mindforge/engine/cost-tracking/router.md +1 -1
- package/.mindforge/engine/cost-tracking/token-ledger.md +21 -24
- package/.mindforge/engine/temporal-protocol.md +2 -2
- package/.mindforge/governance/change-classifier.md +20 -4
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/metrics/METRICS-SCHEMA.md +13 -4
- package/.mindforge/personas/cost-optimizer.md +2 -2
- package/.mindforge/personas/multi-model-bridge.md +1 -1
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
- package/.mindforge/skills/cost-aware-routing/SKILL.md +3 -3
- package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
- package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
- package/CHANGELOG.md +402 -0
- package/MINDFORGE.md +13 -6
- package/README.md +51 -2
- package/RELEASENOTES.md +55 -2
- package/SECURITY.md +22 -3
- package/bin/autonomous/audit-writer.js +48 -33
- package/bin/autonomous/auto-runner.js +65 -2
- package/bin/change-classifier.js +151 -16
- package/bin/dashboard/api-router.js +28 -47
- package/bin/dashboard/error-response.js +44 -0
- package/bin/dashboard/frontend/app.js +429 -0
- package/bin/dashboard/frontend/index.html +14 -390
- package/bin/dashboard/metrics-aggregator.js +75 -30
- package/bin/dashboard/revops-api.js +12 -2
- package/bin/dashboard/server.js +245 -6
- package/bin/dashboard/sse-bridge.js +11 -8
- package/bin/dashboard/temporal-api.js +11 -5
- package/bin/engine/remediation-engine.js +12 -1
- package/bin/engine/sre-manager.js +1 -1
- package/bin/engine/temporal-cli.js +56 -6
- package/bin/engine/temporal-hub.js +41 -9
- package/bin/engine/verification-runner.js +134 -17
- package/bin/engine/verify-cli.js +25 -7
- package/bin/eval/eval-harness.js +212 -1
- package/bin/eval/golden-set-retrieval.json +9 -0
- package/bin/governance/approval-record.js +147 -0
- package/bin/governance/approve.js +12 -7
- package/bin/governance/policy-engine.js +41 -3
- package/bin/governance/policy-gate-hardened.js +36 -1
- package/bin/governance/verify-approvals.js +163 -0
- package/bin/harness-audit.js +224 -10
- package/bin/hindsight-injector.js +8 -2
- package/bin/hooks/instinct-capture-hook.js +19 -5
- package/bin/install.js +63 -3
- package/bin/installer/harness-adapter-compliance.js +339 -28
- package/bin/installer/hook-registration.js +504 -0
- package/bin/installer-core.js +451 -63
- package/bin/learning/instinct-cli.js +14 -24
- package/bin/memory/knowledge-capture.js +23 -3
- package/bin/memory/knowledge-graph.js +70 -31
- package/bin/memory/vector-hub.js +500 -44
- package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
- package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
- package/bin/mindforge-cli.js +110 -17
- package/bin/models/cost-tracker.js +126 -29
- package/bin/models/model-client.js +6 -1
- package/bin/models/model-router.js +28 -7
- package/bin/models/usage-record.js +71 -0
- package/bin/revops/debt-monitor.js +57 -13
- package/bin/security/trust-gate-hook.js +50 -6
- package/bin/skill-validator.js +6 -1
- package/bin/skills-builder/skill-scorer.js +46 -6
- package/bin/updater/self-update.js +6 -1
- package/bin/updater/version-comparator.js +21 -1
- package/bin/utils/file-lock.js +106 -0
- package/bin/utils/mindforge-params.js +124 -0
- package/bin/utils/mindforge-version.js +99 -0
- package/bin/utils/redact-secrets.js +106 -0
- package/bin/validate-config.js +75 -17
- package/bin/wizard/setup-wizard.js +4 -1
- package/bin/wizard/theme.js +9 -1
- package/changelogs/index.json +11 -9
- package/changelogs/v11.9.2.md +209 -0
- package/changelogs/v11.9.3.md +195 -0
- package/docs/References/config-reference.md +76 -14
- package/docs/References/sdk-api.md +1 -1
- package/docs/Templates/Codebase/architecture.md +1 -1
- package/docs/commands-reference.md +4 -5
- package/docs/faq.md +25 -5
- package/docs/getting-started.md +3 -3
- package/docs/sdk-reference.md +15 -7
- package/docs/troubleshooting.md +10 -6
- package/docs/user-guide.md +14 -14
- package/examples/sdk-integration/README.md +1 -1
- package/package.json +10 -4
- package/subagents/.claude-plugin/marketplace.json +1 -1
- package/bin/dashboard/approval-handler.js +0 -136
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const Metrics = require('./metrics-aggregator');
|
|
10
|
-
const Approval = require('./approval-handler');
|
|
11
10
|
const SSE = require('./sse-bridge');
|
|
11
|
+
const { sendServerError } = require('./error-response');
|
|
12
12
|
|
|
13
13
|
// Steering queue path (from Day 8 auto-executor)
|
|
14
14
|
const STEERING_QUEUE = path.join(process.cwd(), '.planning', 'steering-queue.jsonl');
|
|
@@ -38,7 +38,7 @@ function register(app) {
|
|
|
38
38
|
try {
|
|
39
39
|
res.json(Metrics.getStatus());
|
|
40
40
|
} catch (err) {
|
|
41
|
-
res
|
|
41
|
+
sendServerError(res, 'GET /api/status', err, 'Failed to read project status');
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -50,7 +50,7 @@ function register(app) {
|
|
|
50
50
|
const event = typeof req.query.event === 'string' ? req.query.event : null;
|
|
51
51
|
res.json(Metrics.getAuditEntries(limit, offset, event));
|
|
52
52
|
} catch (err) {
|
|
53
|
-
res
|
|
53
|
+
sendServerError(res, 'GET /api/audit', err, 'Failed to read audit entries');
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -59,52 +59,33 @@ function register(app) {
|
|
|
59
59
|
try {
|
|
60
60
|
res.json(Metrics.getMetrics());
|
|
61
61
|
} catch (err) {
|
|
62
|
-
res
|
|
62
|
+
sendServerError(res, 'GET /api/metrics', err, 'Failed to read quality metrics');
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
// ── Approvals
|
|
66
|
+
// ── Approvals (read-only) ───────────────────────────────────────────────────
|
|
67
|
+
// Returns the approval records that exist, each with its verified integrity state.
|
|
68
|
+
//
|
|
69
|
+
// `POST /api/approve/:id` was REMOVED. It implemented a pending-request -> decide workflow
|
|
70
|
+
// that nothing in MindForge has ever produced: bin/governance/approve.js records an
|
|
71
|
+
// already-made decision, and no code path anywhere writes a request awaiting one. The endpoint
|
|
72
|
+
// was unreachable six independent ways — it required a 36-char UUID id while the writer emits
|
|
73
|
+
// `MF-AUTH-<base36>`, required `status: 'pending'` which no producer writes, and looked for
|
|
74
|
+
// `APPROVAL-*.json` while the writer emits `approval-*.json`; the reader returned an object
|
|
75
|
+
// where the frontend expected an array; and the UI rendered `phase`, `plan` and `summary`,
|
|
76
|
+
// none of which any producer has ever written. It was speculative UI against a schema that
|
|
77
|
+
// never existed, not a flow with a bug.
|
|
78
|
+
//
|
|
79
|
+
// Disclosure note: requireAuth exempts GET, so this body is unauthenticated. That is
|
|
80
|
+
// proportionate here — the server binds to 127.0.0.1 only (ADR-017) as a single-operator
|
|
81
|
+
// tool, and the fields returned are the operator's own git-committed records. It is a
|
|
82
|
+
// different case from LEAK-01 (revops-api.js), which concerned error messages leaking
|
|
83
|
+
// filesystem paths the operator never chose to expose.
|
|
67
84
|
app.get('/api/approvals', (req, res) => {
|
|
68
85
|
try {
|
|
69
86
|
res.json(Metrics.getApprovals());
|
|
70
87
|
} catch (err) {
|
|
71
|
-
res
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
app.post('/api/approve/:id', (req, res) => {
|
|
76
|
-
try {
|
|
77
|
-
const { id } = req.params;
|
|
78
|
-
const { decision, comment } = req.body || {};
|
|
79
|
-
|
|
80
|
-
if (!decision) {
|
|
81
|
-
return res.status(400).json({ error: 'Missing "decision" field (approve|reject)' });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// SECURITY (v11.5.1): do NOT trust a client-supplied `approver` for the
|
|
85
|
-
// recorded identity — it is forgeable and would let any caller write a
|
|
86
|
-
// false approval audit trail (e.g. resolved_by: 'admin'). requireAuth
|
|
87
|
-
// (server.js) proves the caller holds the owner-only dashboard token but
|
|
88
|
-
// exposes no named principal, so we attribute the action to a FIXED
|
|
89
|
-
// trusted actor. (A future RBAC pass can map a Bearer token -> DID and
|
|
90
|
-
// record the real principal; until then, never echo req.body.approver.)
|
|
91
|
-
const approver = 'dashboard-authenticated';
|
|
92
|
-
|
|
93
|
-
const result = Approval.processDecision(id, decision, comment, approver);
|
|
94
|
-
|
|
95
|
-
if (!result.success) {
|
|
96
|
-
return res.status(400).json(result);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Broadcast approval event to all SSE clients
|
|
100
|
-
SSE.broadcast(
|
|
101
|
-
decision === 'approve' ? 'approval:resolved' : 'approval:resolved',
|
|
102
|
-
{ approval_id: id, decision, message: result.message }
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
res.json(result);
|
|
106
|
-
} catch (err) {
|
|
107
|
-
res.status(500).json({ error: err.message });
|
|
88
|
+
sendServerError(res, 'GET /api/approvals', err, 'Failed to read approvals');
|
|
108
89
|
}
|
|
109
90
|
});
|
|
110
91
|
|
|
@@ -113,7 +94,7 @@ function register(app) {
|
|
|
113
94
|
try {
|
|
114
95
|
res.json(Metrics.getTeamActivity());
|
|
115
96
|
} catch (err) {
|
|
116
|
-
res
|
|
97
|
+
sendServerError(res, 'GET /api/team', err, 'Failed to read team activity');
|
|
117
98
|
}
|
|
118
99
|
});
|
|
119
100
|
|
|
@@ -124,7 +105,7 @@ function register(app) {
|
|
|
124
105
|
const limit = Math.min(parseInt(req.query.limit || '20', 10), 100);
|
|
125
106
|
res.json(Metrics.getMemory(q, limit));
|
|
126
107
|
} catch (err) {
|
|
127
|
-
res
|
|
108
|
+
sendServerError(res, 'GET /api/memory', err, 'Failed to query knowledge base');
|
|
128
109
|
}
|
|
129
110
|
});
|
|
130
111
|
|
|
@@ -134,7 +115,7 @@ function register(app) {
|
|
|
134
115
|
const window = Math.min(parseInt(req.query.window || '7', 10), 90);
|
|
135
116
|
res.json(Metrics.getCosts(window));
|
|
136
117
|
} catch (err) {
|
|
137
|
-
res
|
|
118
|
+
sendServerError(res, 'GET /api/costs', err, 'Failed to read cost metrics');
|
|
138
119
|
}
|
|
139
120
|
});
|
|
140
121
|
|
|
@@ -193,7 +174,7 @@ function register(app) {
|
|
|
193
174
|
|
|
194
175
|
res.json({ success: true, queued: true, id: entry.id, action });
|
|
195
176
|
} catch (err) {
|
|
196
|
-
res
|
|
177
|
+
sendServerError(res, 'POST /api/steer', err, 'Failed to queue steering directive');
|
|
197
178
|
}
|
|
198
179
|
});
|
|
199
180
|
|
|
@@ -241,7 +222,7 @@ function register(app) {
|
|
|
241
222
|
timestamp: new Date().toISOString()
|
|
242
223
|
});
|
|
243
224
|
} catch (err) {
|
|
244
|
-
res
|
|
225
|
+
sendServerError(res, 'GET /api/v1/system', err, 'Failed to read system observability data');
|
|
245
226
|
}
|
|
246
227
|
});
|
|
247
228
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge — Dashboard error responses (LEAK-01)
|
|
3
|
+
*
|
|
4
|
+
* Every dashboard route used to return `err.message` — and express's default
|
|
5
|
+
* handler returned `err.stack` — straight to the client. fs-sourced errors embed
|
|
6
|
+
* ABSOLUTE paths (EACCES/ENOENT/EISDIR), so a 500 disclosed the operator's home
|
|
7
|
+
* directory and username. This matters more than a normal 500 body leak because
|
|
8
|
+
* `requireAuth` (server.js) lets GET and OPTIONS through unguarded: every read
|
|
9
|
+
* route was an *unauthenticated* filesystem-layout oracle.
|
|
10
|
+
*
|
|
11
|
+
* Contract: the full error (stack included) is logged server-side against a
|
|
12
|
+
* correlation id; the client receives only a generic message plus that id.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
/** Short, non-guessable id used to tie a client response to a server log line. */
|
|
19
|
+
function newCorrelationId() {
|
|
20
|
+
return crypto.randomBytes(8).toString('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Log `err` server-side and answer with a generic 500 carrying a correlation id.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} res Express response.
|
|
27
|
+
* @param {string} context Route identifier for the log, e.g. 'GET /api/metrics'.
|
|
28
|
+
* @param {unknown} err The caught error (or any value).
|
|
29
|
+
* @param {string} clientMessage Safe, static message for the client.
|
|
30
|
+
* @param {object} [extra] Extra fields merged into the body (e.g. { success: false }).
|
|
31
|
+
* @returns {object} the Express response.
|
|
32
|
+
*/
|
|
33
|
+
function sendServerError(res, context, err, clientMessage, extra = {}) {
|
|
34
|
+
const correlationId = newCorrelationId();
|
|
35
|
+
const detail = err && err.stack ? err.stack : String(err);
|
|
36
|
+
console.error(`[dashboard] ${context} failed [cid=${correlationId}]: ${detail}`);
|
|
37
|
+
return res.status(500).json({
|
|
38
|
+
...extra,
|
|
39
|
+
error: clientMessage,
|
|
40
|
+
correlation_id: correlationId
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { sendServerError, newCorrelationId };
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge dashboard front end.
|
|
3
|
+
*
|
|
4
|
+
* EXTRACTED FROM an inline <script> in index.html. The server sets its own
|
|
5
|
+
* Content-Security-Policy of `script-src 'self'` (bin/dashboard/server.js:165), which blocks inline
|
|
6
|
+
* script execution — so the entire application never ran. Measured in a real headless browser:
|
|
7
|
+
*
|
|
8
|
+
* [error] Executing inline script violates the following Content Security Policy directive
|
|
9
|
+
* 'script-src 'self''. The action has been blocked.
|
|
10
|
+
* typeof window.showPage -> undefined
|
|
11
|
+
* GET /api/connections -> {"clients":0}
|
|
12
|
+
*
|
|
13
|
+
* while the static HTML displayed "● Connected" and "SSE STREAMING ACTIVE". The page returned HTTP
|
|
14
|
+
* 200 and affirmatively reported health it could not have had, because the only code able to
|
|
15
|
+
* contradict it was the code being blocked.
|
|
16
|
+
*
|
|
17
|
+
* Externalising is the fix that does NOT weaken the policy. Adding 'unsafe-inline', or a hash, or a
|
|
18
|
+
* nonce would each have restored the <script> while leaving the 10 inline on* attribute handlers
|
|
19
|
+
* dead — those are blocked by the same directive and need 'unsafe-hashes' or removal. They are now
|
|
20
|
+
* addEventListener bindings at the end of this file.
|
|
21
|
+
*
|
|
22
|
+
* SECOND LATENT BUG this uncovered: the inline script ended with a top-level `showPage('activity')`,
|
|
23
|
+
* but sat at line 373 of 757 — above the `.page` elements it queries. As an inline script it ran
|
|
24
|
+
* during parsing, found nothing, and silently did nothing. Loaded with `defer` it runs after the
|
|
25
|
+
* document is parsed, so that call now works. The CSP block had been masking it.
|
|
26
|
+
*/
|
|
27
|
+
/* global document, window, EventSource, getComputedStyle, alert */
|
|
28
|
+
// Browser globals, declared file-locally. This file is the only browser-targeted source in the repo;
|
|
29
|
+
// eslint.config.mjs configures Node globals and is protected against per-file weakening, which is the
|
|
30
|
+
// right trade — a /* global */ comment scopes the declaration here instead of loosening the project.
|
|
31
|
+
// These 55 no-undef errors existed the whole time the code was inline in index.html; nothing linted it.
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
35
|
+
let currentApprovals = [];
|
|
36
|
+
let state = { costs: [], quality: [] };
|
|
37
|
+
|
|
38
|
+
function showPage(id) {
|
|
39
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
40
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
41
|
+
document.getElementById(id).classList.add('active');
|
|
42
|
+
const tab = Array.from(document.querySelectorAll('.tab')).find(t => t.innerText.toLowerCase() === id.toLowerCase());
|
|
43
|
+
if (tab) tab.classList.add('active');
|
|
44
|
+
|
|
45
|
+
if (id === 'metrics') drawCharts();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function escapeHtml(str) {
|
|
49
|
+
if (!str) return '';
|
|
50
|
+
return String(str).replace(/[&<>"']/g, m => ({ '&':'&', '<':'<', '>':'>', '"':'"', '\'':''' }[m]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── WebSocket / SSE ───────────────────────────────────────────────────────
|
|
54
|
+
const es = new EventSource('/events');
|
|
55
|
+
const feed = document.getElementById('audit-feed');
|
|
56
|
+
|
|
57
|
+
es.onopen = () => {
|
|
58
|
+
const el = document.getElementById('connection-status');
|
|
59
|
+
el.textContent = '● Connected';
|
|
60
|
+
el.style.color = 'var(--green)';
|
|
61
|
+
refreshData();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
es.onerror = () => {
|
|
65
|
+
const el = document.getElementById('connection-status');
|
|
66
|
+
el.textContent = '● Disconnected';
|
|
67
|
+
el.style.color = 'var(--red)';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
es.addEventListener('status:update', (e) => {
|
|
71
|
+
const data = JSON.parse(e.data);
|
|
72
|
+
updateStatusUI(data);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
es.addEventListener('audit:new', (e) => {
|
|
76
|
+
const data = JSON.parse(e.data);
|
|
77
|
+
appendAuditEvent(data);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
es.addEventListener('approval:new', (e) => {
|
|
81
|
+
const data = JSON.parse(e.data);
|
|
82
|
+
refreshApprovals();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── API Interactions ──────────────────────────────────────────────────────
|
|
86
|
+
async function refreshData() {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch('/api/metrics');
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
state = data;
|
|
91
|
+
updateMetricsUI(data);
|
|
92
|
+
refreshCosts();
|
|
93
|
+
refreshApprovals();
|
|
94
|
+
refreshMemory();
|
|
95
|
+
refreshTeam();
|
|
96
|
+
refreshRevOps();
|
|
97
|
+
if (document.getElementById('temporal').classList.contains('active')) {
|
|
98
|
+
refreshTemporal();
|
|
99
|
+
}
|
|
100
|
+
} catch(e) { console.error('Refresh fail', e); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function refreshRevOps() {
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch('/api/revops/overview');
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
if (data.success) {
|
|
108
|
+
updateRevOpsUI(data);
|
|
109
|
+
}
|
|
110
|
+
} catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let temporalHistory = [];
|
|
114
|
+
async function refreshTemporal() {
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch('/api/temporal/history');
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
temporalHistory = data;
|
|
119
|
+
renderTemporalTimeline();
|
|
120
|
+
} catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function refreshApprovals() {
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch('/api/approvals');
|
|
126
|
+
const data = await res.json();
|
|
127
|
+
currentApprovals = data;
|
|
128
|
+
renderApprovals();
|
|
129
|
+
} catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function refreshMemory() {
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch('/api/memory');
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
document.getElementById('memory-map').innerHTML = `<pre>${escapeHtml(JSON.stringify(data.graph || {}, null, 2))}</pre>`;
|
|
137
|
+
document.getElementById('memory-stats-list').innerHTML = `<div class="stat-value">${data.count || 0} <span class="stat-unit">Items Indexed</span></div>`;
|
|
138
|
+
} catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function refreshTeam() {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch('/api/team');
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
renderTeam(data);
|
|
146
|
+
} catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// decide() and submitDecision() are removed along with POST /api/approve/:id. They sent
|
|
150
|
+
// `approver: 'admin-dash'` from the client, which the route already had to discard as
|
|
151
|
+
// forgeable. There is now no endpoint that writes an approval record at all, so that
|
|
152
|
+
// attribution problem is gone by construction rather than mitigated. Records are minted only
|
|
153
|
+
// by bin/governance/approve.js, which takes identity from git and fails closed without a GPG
|
|
154
|
+
// key unless MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
function closeConfirm() {
|
|
158
|
+
document.getElementById('confirm-overlay').style.display = 'none';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── UI Rendering ──────────────────────────────────────────────────────────
|
|
162
|
+
function updateStatusUI(data) {
|
|
163
|
+
document.getElementById('project-name').textContent = `PROJ: ${data.project_name || 'UNSET'}`;
|
|
164
|
+
document.getElementById('stat-phase').textContent = data.phase || '0';
|
|
165
|
+
document.getElementById('stat-status').textContent = (data.auto_status || 'IDLE').toUpperCase();
|
|
166
|
+
document.getElementById('stat-tasks').textContent = `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
|
|
167
|
+
document.getElementById('stat-elapsed').textContent = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '0s';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function updateMetricsUI(data) {
|
|
171
|
+
// /api/metrics returns { sessions, avg_quality, avg_cost_usd, ... } — see
|
|
172
|
+
// bin/dashboard/metrics-aggregator.js getMetrics(). It has never returned
|
|
173
|
+
// `costs` or `quality`, so these tiles used to render "$0.0000" and "NaN".
|
|
174
|
+
document.getElementById('stat-avg-quality').textContent = (data.avg_quality ?? 0).toFixed(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Cumulative spend comes from the usage ledger via /api/costs (already wired
|
|
178
|
+
// in bin/dashboard/api-router.js but never fetched until now).
|
|
179
|
+
async function refreshCosts() {
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetch('/api/costs?window=7');
|
|
182
|
+
// sendServerError returns a well-formed JSON body, so res.json() would
|
|
183
|
+
// SUCCEED on a 500 and `total_usd ?? 0` would render "$0.00" — an outage
|
|
184
|
+
// and genuine zero spend look identical under the "Cost (7d)" label.
|
|
185
|
+
if (!res.ok) throw new Error(`/api/costs ${res.status}`);
|
|
186
|
+
const data = await res.json();
|
|
187
|
+
document.getElementById('stat-total-cost').textContent = `${(data.total_usd ?? 0).toFixed(2)}`;
|
|
188
|
+
} catch (e) { console.error('Cost refresh failed', e); }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function appendAuditEvent(data) {
|
|
192
|
+
const div = document.createElement('div');
|
|
193
|
+
div.className = `event type-${data.event}`;
|
|
194
|
+
const time = new Date(data.timestamp).toLocaleTimeString([], { hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
|
195
|
+
div.innerHTML = `<span class="event-time">${time}</span><span class="event-type type-${data.event}">${data.event}</span><span class="event-msg">${escapeHtml(data.message || data.task || '')}</span>`;
|
|
196
|
+
feed.prepend(div);
|
|
197
|
+
if (feed.children.length > 200) feed.lastChild.remove();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Read-only. The Approve/Reject buttons that were here posted to /api/approve/:id, an
|
|
201
|
+
// endpoint that could never succeed and has now been removed: it decided on pending
|
|
202
|
+
// REQUESTS, which nothing in MindForge produces. This panel shows the records that exist,
|
|
203
|
+
// with the integrity state the shared verifier reports.
|
|
204
|
+
//
|
|
205
|
+
// It also rendered a.phase, a.plan and a.summary — three fields no producer has ever
|
|
206
|
+
// written — and called .length/.map() on an object, which threw a TypeError.
|
|
207
|
+
function renderApprovals() {
|
|
208
|
+
const list = document.getElementById('approval-list');
|
|
209
|
+
const records = Array.isArray(currentApprovals) ? currentApprovals : [];
|
|
210
|
+
if (records.length === 0) {
|
|
211
|
+
list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">'
|
|
212
|
+
+ 'No approval records. Tier-3 review is a recorded human acknowledgement; '
|
|
213
|
+
+ 'authorization is enforced by branch protection, not from here.</div>';
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const colour = { valid: 'var(--green)', stale: 'var(--purple)', corrupt: 'var(--red)' };
|
|
218
|
+
list.innerHTML = records.map(a => {
|
|
219
|
+
const hrs = a.hours_remaining;
|
|
220
|
+
const life = (hrs === null || hrs === undefined) ? ''
|
|
221
|
+
: hrs > 0 ? `${hrs.toFixed(1)}h remaining` : `expired ${Math.abs(hrs).toFixed(1)}h ago`;
|
|
222
|
+
const problems = (a.problems || []).length
|
|
223
|
+
? '<ul style="margin:8px 0 0 16px; font-size:11px; color:var(--red)">'
|
|
224
|
+
+ a.problems.map(x => `<li>${escapeHtml(x)}</li>`).join('') + '</ul>'
|
|
225
|
+
: '';
|
|
226
|
+
return `
|
|
227
|
+
<div class="card approval-card">
|
|
228
|
+
<div class="card-body">
|
|
229
|
+
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
|
|
230
|
+
<div>
|
|
231
|
+
<span class="tier-badge tier-${escapeHtml(String(a.tier ?? '?'))}">Tier ${escapeHtml(String(a.tier ?? '?'))}</span>
|
|
232
|
+
<span style="font-weight:700">${escapeHtml(a.id || a.file || 'unknown')}</span>
|
|
233
|
+
<span style="font-size:11px; color:${colour[a.state] || 'var(--muted)'}; margin-left:8px; text-transform:uppercase">${escapeHtml(a.state || '')}</span>
|
|
234
|
+
</div>
|
|
235
|
+
<div style="font-size:11px; color:var(--muted)">${escapeHtml(life)}</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div style="font-size:11px; color:var(--muted); margin-bottom:6px">
|
|
238
|
+
${escapeHtml(a.approved_by || 'unknown')} · v${escapeHtml(a.version || '?')}
|
|
239
|
+
</div>
|
|
240
|
+
<p style="margin:0; font-size:12px; line-height:1.4">${escapeHtml(a.reason || 'No reason recorded')}</p>
|
|
241
|
+
${problems}
|
|
242
|
+
</div>
|
|
243
|
+
</div>`;
|
|
244
|
+
}).join('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renderTeam(data) {
|
|
248
|
+
const list = document.getElementById('team-activity-list');
|
|
249
|
+
if (!data || data.length === 0) {
|
|
250
|
+
list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
list.innerHTML = data.map(ev => `
|
|
254
|
+
<div class="activity-row">
|
|
255
|
+
<div class="avatar">${(ev.user || '?').charAt(0).toUpperCase()}</div>
|
|
256
|
+
<div class="activity-info">
|
|
257
|
+
<div><span class="activity-user">${escapeHtml(ev.user)}</span> <span class="activity-action">${escapeHtml(ev.action)}</span></div>
|
|
258
|
+
<div class="activity-time">${new Date(ev.timestamp).toLocaleString()}</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
`).join('');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Simple Charts Implementation ──────────────────────────────────────────
|
|
265
|
+
function drawCharts() {
|
|
266
|
+
drawCanvasChart('chart-costs', state.costs?.map(c => c.cost) || [], varColor('--accent'));
|
|
267
|
+
drawCanvasChart('chart-quality', state.quality?.map(q => q.score) || [], varColor('--green'), 100);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function varColor(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
|
|
271
|
+
|
|
272
|
+
function drawCanvasChart(id, data, color, maxVal = null) {
|
|
273
|
+
const canvas = document.getElementById(id);
|
|
274
|
+
if (!canvas) return;
|
|
275
|
+
const ctx = canvas.getContext('2d');
|
|
276
|
+
const dpr = window.devicePixelRatio || 1;
|
|
277
|
+
canvas.width = canvas.offsetWidth * dpr;
|
|
278
|
+
canvas.height = canvas.offsetHeight * dpr;
|
|
279
|
+
ctx.scale(dpr, dpr);
|
|
280
|
+
|
|
281
|
+
const w = canvas.offsetWidth;
|
|
282
|
+
const h = canvas.offsetHeight;
|
|
283
|
+
ctx.clearRect(0,0,w,h);
|
|
284
|
+
|
|
285
|
+
if (data.length < 2) return;
|
|
286
|
+
|
|
287
|
+
const max = maxVal || Math.max(...data) * 1.2 || 1;
|
|
288
|
+
const step = w / (data.length - 1);
|
|
289
|
+
|
|
290
|
+
ctx.beginPath();
|
|
291
|
+
ctx.strokeStyle = color;
|
|
292
|
+
ctx.lineWidth = 2;
|
|
293
|
+
ctx.lineJoin = 'round';
|
|
294
|
+
|
|
295
|
+
data.forEach((val, i) => {
|
|
296
|
+
const x = i * step;
|
|
297
|
+
const y = h - (val / max) * h;
|
|
298
|
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
299
|
+
});
|
|
300
|
+
ctx.stroke();
|
|
301
|
+
|
|
302
|
+
// Area fill
|
|
303
|
+
ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
|
|
304
|
+
ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
|
|
305
|
+
ctx.fill();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Temporal Steering Logic ────────────────────────────────────────────────
|
|
309
|
+
function renderTemporalTimeline() {
|
|
310
|
+
const slider = document.getElementById('temporal-slider');
|
|
311
|
+
const count = document.getElementById('timeline-count');
|
|
312
|
+
|
|
313
|
+
if (temporalHistory.length === 0) {
|
|
314
|
+
slider.max = 0;
|
|
315
|
+
count.textContent = '0 Snapshots';
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
slider.max = temporalHistory.length - 1;
|
|
320
|
+
count.textContent = `${temporalHistory.length} Snapshots`;
|
|
321
|
+
|
|
322
|
+
// Default to latest if not touching
|
|
323
|
+
if (slider.value == 0 && temporalHistory.length > 0) {
|
|
324
|
+
onSliderChange(temporalHistory.length - 1);
|
|
325
|
+
slider.value = temporalHistory.length - 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let selectedSnapshot = null;
|
|
330
|
+
async function onSliderChange(index) {
|
|
331
|
+
const snap = temporalHistory[temporalHistory.length - 1 - index]; // reversed for chronological range
|
|
332
|
+
if (!snap) return;
|
|
333
|
+
|
|
334
|
+
selectedSnapshot = snap;
|
|
335
|
+
document.getElementById('slider-current').textContent = `Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
|
|
336
|
+
document.getElementById('inject-btn').disabled = false;
|
|
337
|
+
|
|
338
|
+
// Fetch sample audit file for this snapshot
|
|
339
|
+
try {
|
|
340
|
+
const res = await fetch(`/api/temporal/snapshot/${snap.id}/AUDIT.jsonl`);
|
|
341
|
+
const content = await res.text();
|
|
342
|
+
document.getElementById('snapshot-viewer').textContent = content || 'No audit log available for this point.';
|
|
343
|
+
} catch(e) {
|
|
344
|
+
document.getElementById('snapshot-viewer').textContent = 'Failed to load snapshot details.';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function injectHindsight() {
|
|
349
|
+
if (!selectedSnapshot) return;
|
|
350
|
+
const instruction = document.getElementById('steering-input').value;
|
|
351
|
+
if (!instruction) {
|
|
352
|
+
alert('Please provide a steering instruction for the hindsight injection.');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const btn = document.getElementById('inject-btn');
|
|
357
|
+
btn.disabled = true;
|
|
358
|
+
btn.textContent = 'Injecting Steering Vector...';
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const res = await fetch('/api/temporal/inject', {
|
|
362
|
+
method: 'POST',
|
|
363
|
+
headers: { 'Content-Type': 'application/json' },
|
|
364
|
+
body: JSON.stringify({
|
|
365
|
+
auditId: selectedSnapshot.id,
|
|
366
|
+
fixDescription: instruction
|
|
367
|
+
})
|
|
368
|
+
});
|
|
369
|
+
const result = await res.json();
|
|
370
|
+
if (result.success) {
|
|
371
|
+
alert('Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.');
|
|
372
|
+
showPage('activity');
|
|
373
|
+
} else {
|
|
374
|
+
alert('Injection failed: ' + result.error);
|
|
375
|
+
}
|
|
376
|
+
} catch(e) {
|
|
377
|
+
alert('Failed to connect to temporal engine.');
|
|
378
|
+
} finally {
|
|
379
|
+
btn.disabled = false;
|
|
380
|
+
btn.textContent = 'Rewind & Inject Fix';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── RevOps Logic ─────────────────────────────────────────────────────────
|
|
385
|
+
function updateRevOpsUI(data) {
|
|
386
|
+
const { roi, velocity, debt } = data;
|
|
387
|
+
|
|
388
|
+
document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
|
|
389
|
+
document.getElementById('rev-net').textContent = `$${roi.net_value}`;
|
|
390
|
+
document.getElementById('rev-velocity').innerHTML = `${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
|
|
391
|
+
document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
|
|
392
|
+
document.getElementById('rev-health').textContent = debt.security_health_score;
|
|
393
|
+
|
|
394
|
+
const healthStatus = document.getElementById('rev-health-status');
|
|
395
|
+
healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
|
|
396
|
+
healthStatus.style.background = debt.security_health_score > 80 ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
|
|
397
|
+
healthStatus.style.color = debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
|
|
398
|
+
|
|
399
|
+
document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
|
|
400
|
+
document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
|
|
401
|
+
document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
|
|
402
|
+
document.getElementById('roi-total-pct').textContent = `${roi.roi_percentage}%`;
|
|
403
|
+
|
|
404
|
+
document.getElementById('debt-critical').textContent = debt.critical_findings;
|
|
405
|
+
document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
|
|
406
|
+
const riskBadge = document.getElementById('debt-risk');
|
|
407
|
+
riskBadge.textContent = debt.debt_level.toUpperCase();
|
|
408
|
+
riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
|
|
409
|
+
if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
window.onresize = drawCharts;
|
|
413
|
+
setInterval(refreshData, 10000); // More frequent refresh for live dashboard
|
|
414
|
+
showPage('activity');
|
|
415
|
+
|
|
416
|
+
// ── Event wiring ────────────────────────────────────────────────────────────
|
|
417
|
+
// These listeners replace 10 inline on* attributes. Inline handlers are blocked by the server's own
|
|
418
|
+
// Content-Security-Policy (script-src 'self') exactly as an inline <script> is, so converting the
|
|
419
|
+
// script to an external file without also converting these would have left every control dead.
|
|
420
|
+
document.querySelectorAll('nav .tab[data-page]').forEach((btn) => {
|
|
421
|
+
btn.addEventListener('click', () => showPage(btn.dataset.page));
|
|
422
|
+
});
|
|
423
|
+
const __slider = document.getElementById('temporal-slider');
|
|
424
|
+
if (__slider) __slider.addEventListener('input', (e) => onSliderChange(e.target.value));
|
|
425
|
+
document.querySelectorAll('[data-action]').forEach((el) => {
|
|
426
|
+
const fns = { 'inject-hindsight': () => injectHindsight(), 'close-confirm': () => closeConfirm() };
|
|
427
|
+
const fn = fns[el.dataset.action];
|
|
428
|
+
if (fn) el.addEventListener('click', fn);
|
|
429
|
+
});
|