mindforge-cc 11.9.1 → 11.9.2
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/mindforge/consult.md +1 -1
- package/.agent/mindforge/cost-report.md +1 -1
- package/.claude/commands/mindforge/consult.md +1 -1
- package/.claude/commands/mindforge/cost-report.md +1 -1
- package/.mindforge/MINDFORGE-SCHEMA.json +126 -13
- package/.mindforge/config.json +3 -3
- package/.mindforge/engine/cost-tracking/router.md +1 -1
- package/.mindforge/engine/cost-tracking/token-ledger.md +21 -24
- 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/cost-aware-routing/SKILL.md +3 -3
- package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
- package/CHANGELOG.md +208 -0
- package/MINDFORGE.md +3 -3
- package/README.md +50 -2
- package/RELEASENOTES.md +53 -0
- package/bin/autonomous/audit-writer.js +48 -33
- package/bin/dashboard/api-router.js +11 -10
- package/bin/dashboard/error-response.js +44 -0
- package/bin/dashboard/frontend/index.html +20 -3
- package/bin/dashboard/metrics-aggregator.js +29 -8
- package/bin/dashboard/revops-api.js +12 -2
- package/bin/dashboard/server.js +85 -5
- package/bin/dashboard/temporal-api.js +11 -5
- package/bin/engine/remediation-engine.js +12 -1
- package/bin/engine/temporal-hub.js +41 -9
- package/bin/eval/eval-harness.js +212 -1
- package/bin/eval/golden-set-retrieval.json +9 -0
- package/bin/governance/policy-engine.js +8 -0
- package/bin/hindsight-injector.js +8 -2
- package/bin/hooks/instinct-capture-hook.js +7 -1
- package/bin/learning/instinct-cli.js +7 -24
- package/bin/memory/knowledge-capture.js +23 -3
- package/bin/memory/knowledge-graph.js +70 -31
- package/bin/memory/vector-hub.js +304 -31
- package/bin/mindforge-cli.js +43 -11
- package/bin/models/cost-tracker.js +22 -23
- package/bin/models/model-router.js +28 -7
- package/bin/models/usage-record.js +71 -0
- package/bin/utils/file-lock.js +106 -0
- package/bin/utils/mindforge-params.js +124 -0
- package/bin/validate-config.js +34 -16
- package/changelogs/v11.9.2.md +209 -0
- package/docs/References/config-reference.md +73 -14
- package/docs/sdk-reference.md +1 -1
- package/package.json +4 -2
package/MINDFORGE.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# MINDFORGE.md — Parameter Registry (v11.9.
|
|
1
|
+
# MINDFORGE.md — Parameter Registry (v11.9.2)
|
|
2
2
|
|
|
3
3
|
## 1. IDENTITY & VERSIONING
|
|
4
4
|
|
|
5
5
|
[NAME] = MindForge
|
|
6
|
-
[VERSION] = 11.9.
|
|
6
|
+
[VERSION] = 11.9.2
|
|
7
7
|
[STABLE] = true
|
|
8
8
|
[MODE] = "Platform Sovereign"
|
|
9
9
|
[REQUIRED_CORE_VERSION] = 11.9.1
|
|
@@ -99,7 +99,7 @@ The following parameters cannot be overridden by plugins, agents, or session-lev
|
|
|
99
99
|
|
|
100
100
|
- [MIN_SOUL_SCORE] — Minimum SOUL score required for architectural changes
|
|
101
101
|
- [BLOCK_ON_SECURITY] — Security gate enforcement cannot be disabled
|
|
102
|
-
- [COST_HARD_LIMIT_USD] —
|
|
102
|
+
- [COST_HARD_LIMIT_USD] — declared here, but **NOT enforced as of 11.9.2**; do not rely on it as a spend control. `bin/models/cost-tracker.js` reads `MODEL_COST_HARD_LIMIT_USD`, a key this registry does not declare, so `preflight()` returns early and no cap is ever applied. Wiring it is tracked as COST-02 for 11.9.3
|
|
103
103
|
- [BLOCK_ON_SECURITY] is non-overridable; PQAS itself is simulated/experimental (inactive by default) and is NOT a non-overridable guarantee — do not rely on it as an enforced control
|
|
104
104
|
- [SOVEREIGN_IDENTITY] — Identity verification is always required
|
|
105
105
|
- [ENABLE_ZTAI] — Zero-trust identity cannot be bypassed
|
package/README.md
CHANGED
|
@@ -6,13 +6,55 @@
|
|
|
6
6
|
|
|
7
7
|
## Latest release
|
|
8
8
|
|
|
9
|
-
**v11.9.
|
|
9
|
+
**v11.9.2** (2026-08-16) — Correctness: the config gate can now fail, the audit chain no longer
|
|
10
|
+
forks under concurrent writers, trace retrieval works, and the cost ledger has one record shape.
|
|
11
|
+
**Contains a breaking change to the dashboard HTTP surface** (loopback-only) — see the BREAKING
|
|
12
|
+
section in [CHANGELOG.md](./CHANGELOG.md), or [RELEASENOTES.md](./RELEASENOTES.md) for
|
|
13
|
+
human-readable notes.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What is actually enforced
|
|
18
|
+
|
|
19
|
+
Read this before the install instructions. MindForge ships a large corpus of agent
|
|
20
|
+
instructions — commands, skills, personas, protocols — and those are advisory: they work by
|
|
21
|
+
being in the model's context, and a model can decline them. The parts that would *block* an
|
|
22
|
+
action are hooks, and **no install channel currently registers them.**
|
|
23
|
+
|
|
24
|
+
| Capability | Plugin channel | `npx` channel |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Slash commands | Yes | Yes |
|
|
27
|
+
| Skills / personas / protocol docs | Yes | Yes |
|
|
28
|
+
| Subagents | Yes | Yes |
|
|
29
|
+
| Audit hash-chain (`bin/verify-audit.js`) | Yes | Yes |
|
|
30
|
+
| **Hooks enforced (can block a tool call)** | **No** | **No** |
|
|
31
|
+
|
|
32
|
+
Why, specifically:
|
|
33
|
+
|
|
34
|
+
- **No hook configuration ships, and nothing generates one.** `package.json` `files[]` has 47
|
|
35
|
+
entries and none contains `settings`, so neither `.claude/settings.json` nor
|
|
36
|
+
`.agent/settings.json` is published. All references to those paths in `bin/` are reads or
|
|
37
|
+
metadata strings — there is no code that writes or merges one. `bin/harness-audit.js:335`
|
|
38
|
+
even offers "wire trust-gate + block-no-verify into both …" as a *fix suggestion*, auditing a
|
|
39
|
+
wiring nothing creates.
|
|
40
|
+
- **The plugin channel's hooks additionally crash when fired.**
|
|
41
|
+
`plugins/mindforge/scripts/run-with-flags.js:24` requires `./lib/hook-flags`, and
|
|
42
|
+
`plugins/mindforge/scripts/lib/` does not exist in the published plugin. Running the
|
|
43
|
+
dispatcher gives `Error: Cannot find module './lib/hook-flags'` and exit 1. The module it
|
|
44
|
+
needs does exist at `.agent/hooks/lib/hook-flags.js`; it was never copied in.
|
|
45
|
+
|
|
46
|
+
So treat MindForge as **governance-by-convention plus a tamper-evident audit log**, not as a
|
|
47
|
+
policy enforcement point. Installing it also expands your repository's trust boundary by a large
|
|
48
|
+
volume of agent instructions — review what you install. Making hook registration real per
|
|
49
|
+
harness is the headline goal of v12; the audit chain is genuinely verifiable today
|
|
50
|
+
(`node bin/verify-audit.js`).
|
|
10
51
|
|
|
11
52
|
---
|
|
12
53
|
|
|
13
54
|
## Install
|
|
14
55
|
|
|
15
|
-
|
|
56
|
+
Claude Code plugin marketplace (no project files written). **Note:** the plugin's hooks do not
|
|
57
|
+
fire — see *What is actually enforced* above. Slash commands, skills and subagents do work.
|
|
16
58
|
|
|
17
59
|
```bash
|
|
18
60
|
/plugin marketplace add sairam0424/MindForge
|
|
@@ -27,6 +69,12 @@ npx mindforge-cc@latest --claude --local
|
|
|
27
69
|
|
|
28
70
|
All install channels (global, local, Antigravity, Cursor, Copilot, Gemini CLI, MCP server, combined runtimes, `--minimal`): see [docs/getting-started.md](docs/getting-started.md).
|
|
29
71
|
|
|
72
|
+
**Upgrading from 11.9.x?** The installer does not overwrite an existing
|
|
73
|
+
`.mindforge/MINDFORGE-SCHEMA.json`, so 11.9.2's armed config validator keeps the older
|
|
74
|
+
permissive schema on a plain upgrade. Run with `--force` if you want the stricter gate. The
|
|
75
|
+
daily cost cap declared as `[COST_HARD_LIMIT_USD]` in `MINDFORGE.md` is **not enforced** in
|
|
76
|
+
11.9.2 — see the CHANGELOG.
|
|
77
|
+
|
|
30
78
|
---
|
|
31
79
|
|
|
32
80
|
## Verify
|
package/RELEASENOTES.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v11.9.2 — 2026-08-16 — Correctness: config gate, audit chain, retrieval, cost ledger
|
|
4
|
+
|
|
5
|
+
### What's New
|
|
6
|
+
Nothing. This is a correctness release: four things that were reported as working were measured
|
|
7
|
+
and found not to be, and a fifth — the plugin channel's hooks — is now documented as broken
|
|
8
|
+
instead of advertised as the fastest path.
|
|
9
|
+
|
|
10
|
+
### ⚠️ Breaking (under a patch bump)
|
|
11
|
+
The dashboard's HTTP surface changed shape. `detail` is gone from 5 endpoints and raw errno
|
|
12
|
+
strings from 10 more; `correlation_id` is added to 15; malformed bodies now return
|
|
13
|
+
`application/json` instead of an HTML error page. Those fields were leaking absolute filesystem
|
|
14
|
+
paths — and so the operator's username and home directory — into an unauthenticated response.
|
|
15
|
+
The dashboard also now **exits** on an unhandled rejection or uncaught exception where 11.9.1
|
|
16
|
+
logged and continued, so supervised deployments will see restarts instead of silent errors.
|
|
17
|
+
The surface is loopback-only, so this affects you only if you script against it.
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
- **`security-scan` could not fail.** It reported `MINDFORGE.md valid — 0 settings configured`
|
|
21
|
+
and exited 0 on every input, because both config parsers used a plain `KEY=value` regex while
|
|
22
|
+
the registry declares bracketed `[KEY] = value`. All 43 parameters were invisible. One shared
|
|
23
|
+
parser now reads both forms, and the schema has real `required` keys. Three CI gates go from
|
|
24
|
+
unfailable to failable — on fresh installs and `--force` reinstalls; a plain upgrade keeps
|
|
25
|
+
your existing schema.
|
|
26
|
+
- **The audit hash chain forked under concurrent writers.** No `.jsonl` append took a lock, and
|
|
27
|
+
the chain head was cached in-process and never invalidated — so a second process's append made
|
|
28
|
+
the first keep chaining from a superseded hash. Both halves are fixed: a fail-closed lock, and
|
|
29
|
+
a cached head that carries the file size witnessing it is still the tail. A lock alone was
|
|
30
|
+
measured insufficient. 8 concurrent appenders went from 199 broken links to 0.
|
|
31
|
+
- **44.4% of trace content was unsearchable and every multi-word query returned nothing.** The
|
|
32
|
+
index was keyed on `trace_id` rather than the row's primary key, so each span evicted the
|
|
33
|
+
previous one; and whole queries were wrapped as a single FTS phrase, so one absent term zeroed
|
|
34
|
+
the result. 2,270 of 5,117 rows are recovered losslessly and queries are now tokenised and
|
|
35
|
+
ranked. The shipped retrieval eval had zero callers and now runs as `npm run eval:retrieval`.
|
|
36
|
+
- **The cost ledger reported two different totals for one concept** ($13.73 vs $0.00) because
|
|
37
|
+
writer and reader used different field names — and the test wrote the reader's name, so the
|
|
38
|
+
mismatch passed. One record shape now, and the configured ledger path no longer points at a
|
|
39
|
+
file that never existed.
|
|
40
|
+
- **The daily cost cap is declared but not enforced**, and is now labelled that way in
|
|
41
|
+
`MINDFORGE.md` and the schema rather than described as non-overridable. Wiring it is COST-02.
|
|
42
|
+
|
|
43
|
+
### Honesty
|
|
44
|
+
`README.md` now carries a per-channel capability table with an explicit **Hooks enforced: No**
|
|
45
|
+
row for both install channels, because no hook configuration ships and nothing generates one.
|
|
46
|
+
The plugin channel's dispatcher additionally fails with `Cannot find module './lib/hook-flags'`
|
|
47
|
+
on every fire. Treat MindForge as governance-by-convention plus a verifiable audit log, not as a
|
|
48
|
+
policy enforcement point. Making enforcement real per harness is the goal of v12.
|
|
49
|
+
|
|
50
|
+
### Stats
|
|
51
|
+
105 test files, 103 pass, 2 environment-dependent skips. Four suites that could not report
|
|
52
|
+
failure now can. Audit chain verifies clean. Tarball 1,973 entries.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
3
56
|
## v11.9.0 — 2026-07-27 — Bedrock Provider + Full Dry-Run Audit
|
|
4
57
|
|
|
5
58
|
### What's New
|
|
@@ -20,6 +20,7 @@ const fs = require('fs');
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const crypto = require('crypto');
|
|
22
22
|
const { hashAuditEntry } = require('../governance/audit-hash');
|
|
23
|
+
const { withFileLock } = require('../utils/file-lock');
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Computes the SHA-256 hash of an entry chained to its predecessor (UC-04).
|
|
@@ -57,19 +58,20 @@ function readLastHash(auditPath) {
|
|
|
57
58
|
// sync write gives in-process consumers (e.g. StuckMonitor, which is fed the event
|
|
58
59
|
// object directly but may also re-read the file) immediate, durable data.
|
|
59
60
|
//
|
|
60
|
-
// Chain head caching: re-reading the file's tail on every append is O(file) — bad
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
61
|
+
// Chain head caching: re-reading the file's tail on every append is O(file) — bad on
|
|
62
|
+
// hot paths. We keep a per-path in-memory head (Map keyed by the RESOLVED absolute
|
|
63
|
+
// path) as { hash, size }, where `size` is the file size immediately after our own
|
|
64
|
+
// write. The size is the WITNESS that the cache is still the true tail: if the file is
|
|
65
|
+
// still exactly that long, nobody else appended and the cached hash is the tail (O(1));
|
|
66
|
+
// if it differs, another process appended and we MUST re-seed from disk.
|
|
66
67
|
//
|
|
67
|
-
// Concurrency:
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
68
|
+
// Concurrency (LOCK-01): the seed-through-write sequence below runs inside
|
|
69
|
+
// withFileLock, so no other process can read the head, or append, while we hold it.
|
|
70
|
+
// The size check is what makes the cache safe across processes — a lock alone is NOT
|
|
71
|
+
// enough, because a warm cache would still hand back a hash that a lock-respecting
|
|
72
|
+
// second writer has since superseded, forking the chain. Verified: lock-only leaves
|
|
73
|
+
// 4-6 link breaks per 8-process run; lock + size-validated reseed leaves 0.
|
|
74
|
+
const _lastHashCache = new Map(); // resolvedPath -> { hash, size }
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Synchronously appends ONE hash-chained, durable entry to an audit JSONL file.
|
|
@@ -96,30 +98,43 @@ function appendAuditEntrySync(auditPath, event) {
|
|
|
96
98
|
timestamp: event.timestamp || new Date().toISOString(),
|
|
97
99
|
};
|
|
98
100
|
|
|
99
|
-
// 2.
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// 2-5. LOCK-01: seed-through-write is ONE critical section. The lock opens BEFORE
|
|
102
|
+
// the head is read and closes AFTER the fsync, so no other process can read the
|
|
103
|
+
// same head, or interleave an append, while we hold it. maxTries is raised above
|
|
104
|
+
// the module default because failing closed here DROPS an audit entry: at 16
|
|
105
|
+
// concurrent writers the default ~1s ceiling loses ~35% of entries, 150 tries
|
|
106
|
+
// loses none.
|
|
107
|
+
return withFileLock(resolved, () => {
|
|
108
|
+
// 2. Seed previous_hash from the warm cache ONLY if the file is still exactly as
|
|
109
|
+
// long as we left it (proof nobody else appended); otherwise re-seed from disk.
|
|
110
|
+
const cached = _lastHashCache.get(resolved);
|
|
111
|
+
let onDiskSize = -1;
|
|
112
|
+
try { onDiskSize = fs.statSync(resolved).size; } catch { onDiskSize = -1; }
|
|
113
|
+
const previous_hash = (cached && cached.size === onDiskSize)
|
|
114
|
+
? cached.hash
|
|
115
|
+
: readLastHash(resolved);
|
|
104
116
|
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
// 3. Compute _hash over {...stamped, previous_hash} WITHOUT _hash in the material.
|
|
118
|
+
const _hash = hashEntry(stamped, previous_hash);
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
// 4. Write {...stamped, previous_hash, _hash} as one JSON line, durably+synchronously
|
|
121
|
+
// (openSync('a') + writeSync + fsyncSync + closeSync — mirrors appendDurableSync).
|
|
122
|
+
const chained = { ...stamped, previous_hash, _hash };
|
|
123
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
124
|
+
const fd = fs.openSync(resolved, 'a');
|
|
125
|
+
let sizeAfter = -1;
|
|
126
|
+
try {
|
|
127
|
+
fs.writeSync(fd, JSON.stringify(chained) + '\n');
|
|
128
|
+
fs.fsyncSync(fd);
|
|
129
|
+
sizeAfter = fs.fstatSync(fd).size; // exact new length, one syscall, fd already open
|
|
130
|
+
} finally {
|
|
131
|
+
fs.closeSync(fd);
|
|
132
|
+
}
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
// 5. Advance the in-process chain head (hash + its witness) and return the entry.
|
|
135
|
+
_lastHashCache.set(resolved, { hash: _hash, size: sizeAfter });
|
|
136
|
+
return chained;
|
|
137
|
+
}, { maxTries: 150, label: 'audit' });
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
module.exports = { appendAuditEntrySync };
|
|
@@ -9,6 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const Metrics = require('./metrics-aggregator');
|
|
10
10
|
const Approval = require('./approval-handler');
|
|
11
11
|
const SSE = require('./sse-bridge');
|
|
12
|
+
const { sendServerError } = require('./error-response');
|
|
12
13
|
|
|
13
14
|
// Steering queue path (from Day 8 auto-executor)
|
|
14
15
|
const STEERING_QUEUE = path.join(process.cwd(), '.planning', 'steering-queue.jsonl');
|
|
@@ -38,7 +39,7 @@ function register(app) {
|
|
|
38
39
|
try {
|
|
39
40
|
res.json(Metrics.getStatus());
|
|
40
41
|
} catch (err) {
|
|
41
|
-
res
|
|
42
|
+
sendServerError(res, 'GET /api/status', err, 'Failed to read project status');
|
|
42
43
|
}
|
|
43
44
|
});
|
|
44
45
|
|
|
@@ -50,7 +51,7 @@ function register(app) {
|
|
|
50
51
|
const event = typeof req.query.event === 'string' ? req.query.event : null;
|
|
51
52
|
res.json(Metrics.getAuditEntries(limit, offset, event));
|
|
52
53
|
} catch (err) {
|
|
53
|
-
res
|
|
54
|
+
sendServerError(res, 'GET /api/audit', err, 'Failed to read audit entries');
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
57
|
|
|
@@ -59,7 +60,7 @@ function register(app) {
|
|
|
59
60
|
try {
|
|
60
61
|
res.json(Metrics.getMetrics());
|
|
61
62
|
} catch (err) {
|
|
62
|
-
res
|
|
63
|
+
sendServerError(res, 'GET /api/metrics', err, 'Failed to read quality metrics');
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
|
|
@@ -68,7 +69,7 @@ function register(app) {
|
|
|
68
69
|
try {
|
|
69
70
|
res.json(Metrics.getApprovals());
|
|
70
71
|
} catch (err) {
|
|
71
|
-
res
|
|
72
|
+
sendServerError(res, 'GET /api/approvals', err, 'Failed to read approvals');
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
|
|
@@ -104,7 +105,7 @@ function register(app) {
|
|
|
104
105
|
|
|
105
106
|
res.json(result);
|
|
106
107
|
} catch (err) {
|
|
107
|
-
res
|
|
108
|
+
sendServerError(res, 'POST /api/approve/:id', err, 'Failed to record approval decision');
|
|
108
109
|
}
|
|
109
110
|
});
|
|
110
111
|
|
|
@@ -113,7 +114,7 @@ function register(app) {
|
|
|
113
114
|
try {
|
|
114
115
|
res.json(Metrics.getTeamActivity());
|
|
115
116
|
} catch (err) {
|
|
116
|
-
res
|
|
117
|
+
sendServerError(res, 'GET /api/team', err, 'Failed to read team activity');
|
|
117
118
|
}
|
|
118
119
|
});
|
|
119
120
|
|
|
@@ -124,7 +125,7 @@ function register(app) {
|
|
|
124
125
|
const limit = Math.min(parseInt(req.query.limit || '20', 10), 100);
|
|
125
126
|
res.json(Metrics.getMemory(q, limit));
|
|
126
127
|
} catch (err) {
|
|
127
|
-
res
|
|
128
|
+
sendServerError(res, 'GET /api/memory', err, 'Failed to query knowledge base');
|
|
128
129
|
}
|
|
129
130
|
});
|
|
130
131
|
|
|
@@ -134,7 +135,7 @@ function register(app) {
|
|
|
134
135
|
const window = Math.min(parseInt(req.query.window || '7', 10), 90);
|
|
135
136
|
res.json(Metrics.getCosts(window));
|
|
136
137
|
} catch (err) {
|
|
137
|
-
res
|
|
138
|
+
sendServerError(res, 'GET /api/costs', err, 'Failed to read cost metrics');
|
|
138
139
|
}
|
|
139
140
|
});
|
|
140
141
|
|
|
@@ -193,7 +194,7 @@ function register(app) {
|
|
|
193
194
|
|
|
194
195
|
res.json({ success: true, queued: true, id: entry.id, action });
|
|
195
196
|
} catch (err) {
|
|
196
|
-
res
|
|
197
|
+
sendServerError(res, 'POST /api/steer', err, 'Failed to queue steering directive');
|
|
197
198
|
}
|
|
198
199
|
});
|
|
199
200
|
|
|
@@ -241,7 +242,7 @@ function register(app) {
|
|
|
241
242
|
timestamp: new Date().toISOString()
|
|
242
243
|
});
|
|
243
244
|
} catch (err) {
|
|
244
|
-
res
|
|
245
|
+
sendServerError(res, 'GET /api/v1/system', err, 'Failed to read system observability data');
|
|
245
246
|
}
|
|
246
247
|
});
|
|
247
248
|
}
|
|
@@ -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 };
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
</div>
|
|
199
199
|
</div>
|
|
200
200
|
<div class="grid-4">
|
|
201
|
-
<div class="card"><div class="card-body"><div class="stat-label">
|
|
201
|
+
<div class="card"><div class="card-body"><div class="stat-label">Cost (7d)</div><div id="stat-total-cost" class="stat-value">$0.00</div></div></div>
|
|
202
202
|
<div class="card"><div class="card-body"><div class="stat-label">Avg Quality</div><div id="stat-avg-quality" class="stat-value">0.0</div></div></div>
|
|
203
203
|
<div class="card"><div class="card-body"><div class="stat-label">LSPs Registered</div><div id="stat-lsp-count" class="stat-value">0</div></div></div>
|
|
204
204
|
<div class="card"><div class="card-body"><div class="stat-label">Token Velocity</div><div id="stat-velocity" class="stat-value">0 <span class="stat-unit">tx/m</span></div></div></div>
|
|
@@ -430,6 +430,7 @@
|
|
|
430
430
|
const data = await res.json();
|
|
431
431
|
state = data;
|
|
432
432
|
updateMetricsUI(data);
|
|
433
|
+
refreshCosts();
|
|
433
434
|
refreshApprovals();
|
|
434
435
|
refreshMemory();
|
|
435
436
|
refreshTeam();
|
|
@@ -538,8 +539,24 @@
|
|
|
538
539
|
}
|
|
539
540
|
|
|
540
541
|
function updateMetricsUI(data) {
|
|
541
|
-
|
|
542
|
-
|
|
542
|
+
// /api/metrics returns { sessions, avg_quality, avg_cost_usd, ... } — see
|
|
543
|
+
// bin/dashboard/metrics-aggregator.js getMetrics(). It has never returned
|
|
544
|
+
// `costs` or `quality`, so these tiles used to render "$0.0000" and "NaN".
|
|
545
|
+
document.getElementById('stat-avg-quality').textContent = (data.avg_quality ?? 0).toFixed(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Cumulative spend comes from the usage ledger via /api/costs (already wired
|
|
549
|
+
// in bin/dashboard/api-router.js but never fetched until now).
|
|
550
|
+
async function refreshCosts() {
|
|
551
|
+
try {
|
|
552
|
+
const res = await fetch('/api/costs?window=7');
|
|
553
|
+
// sendServerError returns a well-formed JSON body, so res.json() would
|
|
554
|
+
// SUCCEED on a 500 and `total_usd ?? 0` would render "$0.00" — an outage
|
|
555
|
+
// and genuine zero spend look identical under the "Cost (7d)" label.
|
|
556
|
+
if (!res.ok) throw new Error(`/api/costs ${res.status}`);
|
|
557
|
+
const data = await res.json();
|
|
558
|
+
document.getElementById('stat-total-cost').textContent = `${(data.total_usd ?? 0).toFixed(2)}`;
|
|
559
|
+
} catch (e) { console.error('Cost refresh failed', e); }
|
|
543
560
|
}
|
|
544
561
|
|
|
545
562
|
function appendAuditEvent(data) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { ledgerPath, entryCost, entryDay } = require('../models/usage-record');
|
|
10
11
|
|
|
11
12
|
// ── TTL Cache (5-second window) ──────────────────────────────────────────────
|
|
12
13
|
const _cache = new Map();
|
|
@@ -29,7 +30,7 @@ function cacheSet(key, data) {
|
|
|
29
30
|
// Paths are resolved lazily to support testing in temp directories
|
|
30
31
|
const getPaths = () => ({
|
|
31
32
|
quality: path.join(process.cwd(), '.mindforge', 'metrics', 'session-quality.jsonl'),
|
|
32
|
-
usage:
|
|
33
|
+
usage: ledgerPath(),
|
|
33
34
|
audit: path.join(process.cwd(), '.planning', 'AUDIT.jsonl'),
|
|
34
35
|
handoff: path.join(process.cwd(), '.planning', 'HANDOFF.json'),
|
|
35
36
|
auto: path.join(process.cwd(), '.planning', 'auto-state.json'),
|
|
@@ -122,12 +123,29 @@ function getMetrics() {
|
|
|
122
123
|
const auditEntries = readJSONL(paths.audit, 500);
|
|
123
124
|
|
|
124
125
|
// Quality scores (last 20 sessions)
|
|
126
|
+
// session-quality.jsonl has NO writer anywhere in this repo — every one of its
|
|
127
|
+
// four references (here, sdk/src/client.ts, mcp-server/src/vendor/client.ts,
|
|
128
|
+
// plugins/mindforge/mcp/dist/index.js) is a READ, and no command instructs an
|
|
129
|
+
// agent to append it either. The read is kept because those three other
|
|
130
|
+
// consumers depend on the file and a schema-compliant one may exist in a user
|
|
131
|
+
// project; supplying a writer is OUT OF SCOPE for this patch. Read the
|
|
132
|
+
// SCHEMA-DECLARED names first (.mindforge/metrics/METRICS-SCHEMA.md), keeping
|
|
133
|
+
// the historical names as fallbacks.
|
|
134
|
+
// Per-session cost is NOT a session-quality field: it is joined from the
|
|
135
|
+
// usage ledger by session_id (every provider result carries one — see
|
|
136
|
+
// bin/models/model-client.js:69).
|
|
137
|
+
const costBySession = usageEntries.reduce((acc, u) => {
|
|
138
|
+
if (!u || typeof u.session_id !== 'string') return acc;
|
|
139
|
+
acc[u.session_id] = (acc[u.session_id] || 0) + entryCost(u);
|
|
140
|
+
return acc;
|
|
141
|
+
}, {});
|
|
142
|
+
|
|
125
143
|
const sessions = qualityEntries.map(e => ({
|
|
126
144
|
id: e.session_id,
|
|
127
145
|
timestamp: e.timestamp,
|
|
128
|
-
quality_score: e.quality_score ?? 0,
|
|
146
|
+
quality_score: e.session_quality_score ?? e.quality_score ?? 0,
|
|
129
147
|
verify_pass_rate: e.verify_pass_rate ?? 0,
|
|
130
|
-
cost_usd: e.
|
|
148
|
+
cost_usd: costBySession[e.session_id] ?? 0,
|
|
131
149
|
node_repairs: e.node_repairs ?? 0,
|
|
132
150
|
}));
|
|
133
151
|
|
|
@@ -302,12 +320,15 @@ function getCosts(windowDays = 7) {
|
|
|
302
320
|
};
|
|
303
321
|
|
|
304
322
|
for (const e of entries) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
323
|
+
// Ledger rows carry both `date` and `timestamp` (see bin/models/usage-record.js).
|
|
324
|
+
// Bucket on the canonical day so date-only and full-ISO rows behave alike.
|
|
325
|
+
const day = entryDay(e);
|
|
326
|
+
if (!day || day < cutoff) continue;
|
|
327
|
+
|
|
328
|
+
const cost = entryCost(e);
|
|
308
329
|
stats.total_usd += cost;
|
|
309
|
-
|
|
310
|
-
if (
|
|
330
|
+
|
|
331
|
+
if (day === today) {
|
|
311
332
|
stats.today_usd += cost;
|
|
312
333
|
}
|
|
313
334
|
|
|
@@ -10,6 +10,7 @@ const roiEngine = require('../revops/roi-engine');
|
|
|
10
10
|
const velocityForecaster = require('../revops/velocity-forecaster');
|
|
11
11
|
const debtMonitor = require('../revops/debt-monitor');
|
|
12
12
|
const metricsAggregator = require('./metrics-aggregator');
|
|
13
|
+
const { sendServerError } = require('./error-response');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* GET /api/revops/overview
|
|
@@ -25,7 +26,11 @@ router.get('/overview', (req, res) => {
|
|
|
25
26
|
...metrics,
|
|
26
27
|
tasks_total: status.tasks_total || 0,
|
|
27
28
|
tasks_completed: status.tasks_completed || 0,
|
|
28
|
-
|
|
29
|
+
// .entries — getAuditEntries() returns { entries, total, limit, offset }, not an
|
|
30
|
+
// array. roi-engine, velocity-forecaster and debt-monitor all call .filter() on
|
|
31
|
+
// this value, so handing them the wrapper object throws a TypeError that the
|
|
32
|
+
// route's catch turns into an opaque 500.
|
|
33
|
+
auditEntries: metricsAggregator.getAuditEntries(500).entries // need enough history for velocity
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
const roi = roiEngine.calculate(fullMetrics);
|
|
@@ -40,7 +45,12 @@ router.get('/overview', (req, res) => {
|
|
|
40
45
|
timestamp: new Date().toISOString()
|
|
41
46
|
});
|
|
42
47
|
} catch (err) {
|
|
43
|
-
|
|
48
|
+
// LEAK-01: echoing err.message as a `detail` field put absolute filesystem paths
|
|
49
|
+
// — and therefore the operator's username and home directory — into an
|
|
50
|
+
// unauthenticated HTTP response body (requireAuth exempts GET). Log the full
|
|
51
|
+
// error server-side; return a generic message plus a correlation id.
|
|
52
|
+
sendServerError(res, 'GET /api/revops/overview', err,
|
|
53
|
+
'AgRevOps metrics retrieval failed', { success: false });
|
|
44
54
|
}
|
|
45
55
|
});
|
|
46
56
|
|