mindforge-cc 11.9.0 → 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/.agent/mindforge/ship.md +2 -2
- package/.agent/mindforge/update.md +1 -1
- package/.agent/mindforge/wf-catalog.md +5 -2
- package/.claude/commands/mindforge/consult.md +1 -1
- package/.claude/commands/mindforge/cost-report.md +1 -1
- package/.claude/commands/mindforge/ship.md +2 -2
- package/.claude/commands/mindforge/skill-tdd.md +53 -0
- package/.claude/commands/mindforge/skills-index.md +118 -0
- package/.claude/commands/mindforge/systematic-debug.md +60 -0
- package/.claude/commands/mindforge/update.md +1 -1
- package/.claude/commands/mindforge/wf-accessibility-audit.md +31 -0
- package/.claude/commands/mindforge/wf-ai-model-eval.md +31 -0
- package/.claude/commands/mindforge/wf-api-migration.md +31 -0
- package/.claude/commands/mindforge/wf-architecture-modernization.md +32 -0
- package/.claude/commands/mindforge/wf-catalog.md +62 -0
- package/.claude/commands/mindforge/wf-code-audit.md +31 -0
- package/.claude/commands/mindforge/wf-code-explainer.md +31 -0
- package/.claude/commands/mindforge/wf-competitive-analysis.md +31 -0
- package/.claude/commands/mindforge/wf-competitive-teardown.md +31 -0
- package/.claude/commands/mindforge/wf-cost-analysis.md +31 -0
- package/.claude/commands/mindforge/wf-data-pipeline-validate.md +31 -0
- package/.claude/commands/mindforge/wf-database-migration.md +31 -0
- package/.claude/commands/mindforge/wf-debug-detective.md +32 -0
- package/.claude/commands/mindforge/wf-design-system-audit.md +31 -0
- package/.claude/commands/mindforge/wf-documentation-gen.md +31 -0
- package/.claude/commands/mindforge/wf-feature-planner.md +31 -0
- package/.claude/commands/mindforge/wf-incident-response.md +31 -0
- package/.claude/commands/mindforge/wf-multi-repo-sync.md +31 -0
- package/.claude/commands/mindforge/wf-mutation-testing.md +31 -0
- package/.claude/commands/mindforge/wf-onboard-codebase.md +31 -0
- package/.claude/commands/mindforge/wf-orchestrate-review.md +35 -0
- package/.claude/commands/mindforge/wf-perf-optimize.md +31 -0
- package/.claude/commands/mindforge/wf-pr-review.md +31 -0
- package/.claude/commands/mindforge/wf-refactor-plan.md +31 -0
- package/.claude/commands/mindforge/wf-release-prep.md +31 -0
- package/.claude/commands/mindforge/wf-security-threat-model.md +31 -0
- package/.claude/commands/mindforge/wf-tdd-sprint.md +31 -0
- package/.claude/commands/mindforge/wf-tech-evaluation.md +31 -0
- package/.claude/commands/mindforge/wf-ux-heuristic-audit.md +31 -0
- package/.claude/commands/mindforge/wf-verification-loop.md +45 -0
- package/.claude/commands/mindforge/wf-workflow-optimizer.md +51 -0
- package/.claude/commands/mindforge/wf-writer-reviewer.md +30 -0
- 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/doc-auditor.md +1 -1
- package/.mindforge/personas/monorepo-architect.md +2 -1
- package/.mindforge/personas/multi-model-bridge.md +1 -1
- package/.mindforge/personas/release-manager.md +5 -3
- package/.mindforge/skills/cost-aware-routing/SKILL.md +3 -3
- package/.mindforge/skills/documentation/SKILL.md +4 -1
- package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
- package/CHANGELOG.md +216 -1821
- package/MINDFORGE.md +4 -4
- package/README.md +103 -509
- package/RELEASENOTES.md +75 -0
- package/SECURITY.md +1 -1
- 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/install.js +1 -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/updater/changelog-fetcher.js +95 -5
- package/bin/utils/file-lock.js +106 -0
- package/bin/utils/mindforge-params.js +124 -0
- package/bin/utils/readiness-gate.js +3 -3
- package/bin/validate-config.js +34 -16
- package/changelogs/README.md +109 -0
- package/changelogs/index.json +103 -0
- package/changelogs/v0.1.0.md +21 -0
- package/changelogs/v0.2.0.md +15 -0
- package/changelogs/v0.3.0.md +28 -0
- package/changelogs/v0.4.0.md +29 -0
- package/changelogs/v0.5.0.md +34 -0
- package/changelogs/v0.6.0.md +34 -0
- package/changelogs/v1.0.0.md +60 -0
- package/changelogs/v1.0.1.md +15 -0
- package/changelogs/v1.0.2.md +10 -0
- package/changelogs/v1.0.3.md +10 -0
- package/changelogs/v1.0.4.md +10 -0
- package/changelogs/v1.0.5.md +10 -0
- package/changelogs/v10.0.0.md +71 -0
- package/changelogs/v10.0.2.md +15 -0
- package/changelogs/v10.0.3.md +21 -0
- package/changelogs/v10.0.4.md +16 -0
- package/changelogs/v10.0.5.md +16 -0
- package/changelogs/v10.0.6.md +15 -0
- package/changelogs/v10.0.7.md +15 -0
- package/changelogs/v10.0.8.md +15 -0
- package/changelogs/v10.0.9.md +15 -0
- package/changelogs/v10.1.0.md +16 -0
- package/changelogs/v10.1.1.md +15 -0
- package/changelogs/v10.2.0.md +14 -0
- package/changelogs/v10.3.0.md +13 -0
- package/changelogs/v10.4.0.md +13 -0
- package/changelogs/v10.5.0.md +13 -0
- package/changelogs/v10.6.0.md +13 -0
- package/changelogs/v10.7.0.md +15 -0
- package/changelogs/v11.0.0.md +66 -0
- package/changelogs/v11.0.1.md +10 -0
- package/changelogs/v11.1.0.md +56 -0
- package/changelogs/v11.2.0.md +41 -0
- package/changelogs/v11.2.1.md +38 -0
- package/changelogs/v11.3.0.md +45 -0
- package/changelogs/v11.3.1.md +39 -0
- package/changelogs/v11.4.0.md +38 -0
- package/changelogs/v11.5.0.md +77 -0
- package/changelogs/v11.5.1.md +47 -0
- package/changelogs/v11.6.0.md +45 -0
- package/changelogs/v11.7.0.md +30 -0
- package/changelogs/v11.7.1.md +8 -0
- package/changelogs/v11.8.0.md +19 -0
- package/changelogs/v11.8.1.md +28 -0
- package/changelogs/v11.8.2.md +22 -0
- package/changelogs/v11.8.3.md +22 -0
- package/changelogs/v11.9.0.md +17 -0
- package/changelogs/v11.9.1.md +8 -0
- package/changelogs/v11.9.2.md +209 -0
- package/changelogs/v2.0.0-alpha.1.md +42 -0
- package/changelogs/v2.0.0-alpha.10.md +13 -0
- package/changelogs/v2.0.0-alpha.11.md +14 -0
- package/changelogs/v2.0.0-alpha.12.md +16 -0
- package/changelogs/v2.0.0-alpha.2.md +14 -0
- package/changelogs/v2.0.0-alpha.3.md +17 -0
- package/changelogs/v2.0.0-alpha.4.md +17 -0
- package/changelogs/v2.0.0-alpha.7.md +18 -0
- package/changelogs/v2.0.0-alpha.8.md +11 -0
- package/changelogs/v2.0.0-alpha.9.md +12 -0
- package/changelogs/v2.0.0.md +35 -0
- package/changelogs/v2.1.0.md +22 -0
- package/changelogs/v2.1.1.md +25 -0
- package/changelogs/v2.1.2.md +21 -0
- package/changelogs/v2.3.2.md +19 -0
- package/changelogs/v2.3.3.md +16 -0
- package/changelogs/v2.3.4.md +16 -0
- package/changelogs/v2.3.5.md +21 -0
- package/changelogs/v2.4.0.md +29 -0
- package/changelogs/v2.5.0.md +26 -0
- package/changelogs/v2.6.0.md +26 -0
- package/changelogs/v3.0.0.md +17 -0
- package/changelogs/v4.0.0.md +19 -0
- package/changelogs/v4.1.0.md +19 -0
- package/changelogs/v4.3.0.md +31 -0
- package/changelogs/v5.0.0-alpha.1.md +35 -0
- package/changelogs/v5.0.0-alpha.2.md +22 -0
- package/changelogs/v5.1.0.md +42 -0
- package/changelogs/v5.2.0.md +18 -0
- package/changelogs/v5.3.0.md +14 -0
- package/changelogs/v5.4.0.md +28 -0
- package/changelogs/v5.5.0.md +11 -0
- package/changelogs/v5.6.0.md +11 -0
- package/changelogs/v5.7.0.md +12 -0
- package/changelogs/v5.8.0.md +12 -0
- package/changelogs/v5.9.0.md +11 -0
- package/changelogs/v6.0.0-alpha.md +45 -0
- package/changelogs/v6.1.0-alpha.md +29 -0
- package/changelogs/v6.2.0-alpha.md +30 -0
- package/changelogs/v6.2.0.md +21 -0
- package/changelogs/v6.3.0.md +14 -0
- package/changelogs/v6.4.0.md +13 -0
- package/changelogs/v6.5.0.md +13 -0
- package/changelogs/v6.6.0.md +12 -0
- package/changelogs/v6.7.0.md +12 -0
- package/changelogs/v7.0.0.md +13 -0
- package/changelogs/v8.0.0.md +14 -0
- package/changelogs/v8.1.0.md +13 -0
- package/changelogs/v8.2.0.md +12 -0
- package/changelogs/v8.2.1.md +11 -0
- package/changelogs/v9.0.0.md +20 -0
- package/docs/References/config-reference.md +73 -14
- package/docs/commands-reference.md +12 -6
- package/docs/faq.md +3 -3
- package/docs/getting-started.md +28 -3
- package/docs/sdk-reference.md +1 -1
- package/docs/user-guide.md +2 -2
- package/package.json +5 -2
package/RELEASENOTES.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
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
|
+
|
|
56
|
+
## v11.9.0 — 2026-07-27 — Bedrock Provider + Full Dry-Run Audit
|
|
57
|
+
|
|
58
|
+
### What's New
|
|
59
|
+
Added an AWS Bedrock provider and completed a full exhaustive dry-run audit of all 224 commands, 216 personas, and 355 skills.
|
|
60
|
+
|
|
61
|
+
### Fixes
|
|
62
|
+
- `bin/models/bedrock-provider.js`: AWS Bedrock provider with Anthropic/Bedrock config switch and base64 credential decoding
|
|
63
|
+
- Resolved phantom-tool references ported unmodified from a different agent harness across 40 personas and ~40 skills (`Context7`/`search_web`/`read_url_content` → `WebFetch`/`WebSearch`; `CommandStatus`/`ReadTerminal`/`terminal`/`read_file`/`write_file`/`search_files`/`patch` → real Claude Code tool names; `delegate_task` → `Agent`; `kanban_create` → `TaskCreate`)
|
|
64
|
+
- Repointed 21 broken `@.agent/references/` and `@.agent/templates/` path references to their real `docs/References/` and `docs/Templates/*/` locations
|
|
65
|
+
- Copied in missing companion `scripts/`/`references/`/`templates/` assets for 17 skills that referenced files never carried over during porting
|
|
66
|
+
- Fixed hardcoded broken paths (`~/.hermes/skills`, `/home/teknium/...`, `HERMES_HOME`)
|
|
67
|
+
- Fixed a markdown fence-nesting bug in `code-tour` and 4 truncated heredoc strings in `github-code-review`
|
|
68
|
+
- Removed banned single-word triggers (`test`, `tests`) from `testing-standards`
|
|
69
|
+
- CI: build SDK before test suite; run Verification before Health Check in execution-plane
|
|
70
|
+
- Security: patched hono and picomatch HIGH-severity vulnerabilities
|
|
71
|
+
|
|
72
|
+
### Stats
|
|
73
|
+
- Dry-run audit: 224 commands, 216 personas, 355 skills reviewed
|
|
74
|
+
- Phantom-tool references resolved: 40 personas, ~40 skills
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
3
78
|
## v11.8.3 — 2026-07-01 — Autopsy Fixes Stable Release
|
|
4
79
|
|
|
5
80
|
### What's New
|
package/SECURITY.md
CHANGED
|
@@ -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
|
|
package/bin/dashboard/server.js
CHANGED
|
@@ -37,6 +37,7 @@ const SSE = require('./sse-bridge');
|
|
|
37
37
|
const API = require('./api-router');
|
|
38
38
|
const TemporalAPI = require('./temporal-api');
|
|
39
39
|
const RevOpsAPI = require('./revops-api');
|
|
40
|
+
const { newCorrelationId } = require('./error-response');
|
|
40
41
|
|
|
41
42
|
// ── Express app ───────────────────────────────────────────────────────────────
|
|
42
43
|
const app = express();
|
|
@@ -190,6 +191,73 @@ app.post('/api/v1/token/refresh', requireAuth, (req, res) => {
|
|
|
190
191
|
// ── Register API routes ───────────────────────────────────────────────────────
|
|
191
192
|
API.register(app);
|
|
192
193
|
app.use('/api/temporal', TemporalAPI);
|
|
194
|
+
// RevOpsAPI was required at the top of this file but never mounted, so /api/revops
|
|
195
|
+
// returned 404 while the AgRevOps dashboard panels and docs described it as live.
|
|
196
|
+
app.use('/api/revops', RevOpsAPI);
|
|
197
|
+
|
|
198
|
+
// ── Terminal error handler (LEAK-01) ─────────────────────────────────────────
|
|
199
|
+
// MUST stay last, after every route, and MUST keep its 4-arg signature or express
|
|
200
|
+
// will treat it as ordinary middleware. Without it, express's default handler
|
|
201
|
+
// renders err.stack into the response body whenever NODE_ENV !== 'production':
|
|
202
|
+
// a single unauthenticated malformed-JSON POST (express.json() is mounted BEFORE
|
|
203
|
+
// requireAuth, and requireAuth exempts GET anyway) returned the absolute paths of
|
|
204
|
+
// node_modules and the repo — the operator's username and home directory — to any
|
|
205
|
+
// local caller. Log server-side; return a generic body plus a correlation id.
|
|
206
|
+
app.use((err, req, res, next) => {
|
|
207
|
+
if (res.headersSent) return next(err); // e.g. an SSE stream already flushed headers
|
|
208
|
+
const status = Number.isInteger(err && err.status) && err.status >= 400 && err.status < 600
|
|
209
|
+
? err.status
|
|
210
|
+
: 500;
|
|
211
|
+
const correlationId = newCorrelationId();
|
|
212
|
+
console.error(
|
|
213
|
+
`[dashboard] unhandled ${req.method} ${req.originalUrl} -> ${status} [cid=${correlationId}]:`,
|
|
214
|
+
err && err.stack ? err.stack : err
|
|
215
|
+
);
|
|
216
|
+
res.status(status).json({
|
|
217
|
+
error: status >= 500 ? 'Internal server error' : 'Invalid request',
|
|
218
|
+
correlation_id: correlationId
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── Crash guards ──────────────────────────────────────────────────────────────
|
|
223
|
+
// Both guards log and exit. That is deliberate and symmetric.
|
|
224
|
+
//
|
|
225
|
+
// unhandledRejection: an escaped rejection is the ONLY reliable signal that an
|
|
226
|
+
// async call was left un-awaited — the ASYNC-01 class this release fixes. Measured
|
|
227
|
+
// on v11.9.1: an un-awaited rollbackTo() rejected, and by the time the rejection
|
|
228
|
+
// surfaced a hash-chained `hindsight_injected` entry had already been fsync'd and
|
|
229
|
+
// auto-state.json flipped to awaiting_regeneration for a rollback that never
|
|
230
|
+
// happened; the client got ECONNRESET at ~16ms and the process exited 1. The
|
|
231
|
+
// durable damage is committed before any handler can run, so a 500-and-continue
|
|
232
|
+
// response would answer the request while leaving the audit chain asserting an
|
|
233
|
+
// event that did not occur.
|
|
234
|
+
// Keeping it survivable is also not free: express 4.22.1 does not route async
|
|
235
|
+
// handler rejections to its error middleware (measured: a 4-arity app.use never
|
|
236
|
+
// fires), so log-and-continue leaves the client socket open until the CLIENT gives
|
|
237
|
+
// up — 2.5s, 4s and 8s clients all timed out with no response — versus a ~15ms
|
|
238
|
+
// connection reset when the process exits. A silent hang is worse than a restart.
|
|
239
|
+
// Cost accepted: one faulting request takes the observability surface down. The
|
|
240
|
+
// dashboard is a 127.0.0.1-only single-operator tool, and an audit chain that
|
|
241
|
+
// verifies as valid while recording events that did not happen is not a survivable
|
|
242
|
+
// state to keep serving from.
|
|
243
|
+
process.on('unhandledRejection', (reason) => {
|
|
244
|
+
console.error('[Dashboard] Unhandled rejection — exiting:',
|
|
245
|
+
reason instanceof Error ? reason.stack : reason);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// uncaughtException MUST exit. After an uncaught throw the process state is
|
|
250
|
+
// undefined, and log-and-continue actively broke shutdown: a throw anywhere in the
|
|
251
|
+
// first half of shutdown() (SSE.stop(), or unlinking the token file) was swallowed,
|
|
252
|
+
// so server.close() was never reached and the forced-exit timer was never armed.
|
|
253
|
+
// The result was a dashboard that IGNORED SIGTERM while still serving the
|
|
254
|
+
// token-authenticated mutation endpoints, with the bearer token left on disk and
|
|
255
|
+
// still valid in memory — i.e. the operator's documented stop command silently
|
|
256
|
+
// failed while reporting success. Only SIGKILL stopped it.
|
|
257
|
+
process.on('uncaughtException', (err) => {
|
|
258
|
+
console.error('[Dashboard] Uncaught exception — exiting:', err && err.stack ? err.stack : err);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
});
|
|
193
261
|
|
|
194
262
|
// ── Start SSE bridge ──────────────────────────────────────────────────────────
|
|
195
263
|
SSE.start();
|
|
@@ -231,14 +299,26 @@ server.on('error', err => {
|
|
|
231
299
|
// ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
232
300
|
function shutdown(signal) {
|
|
233
301
|
console.log(`\n[dashboard] ${signal} received — shutting down`);
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
|
|
302
|
+
|
|
303
|
+
// Arm the forced exit FIRST. Every step below can throw (permission drift on the
|
|
304
|
+
// token path, a read-only .mindforge, an SSE listener error), and if any of them
|
|
305
|
+
// does before this timer is set, the process would keep serving the authenticated
|
|
306
|
+
// mutation API after the operator asked it to stop.
|
|
307
|
+
const forced = setTimeout(() => process.exit(0), 3000);
|
|
308
|
+
forced.unref();
|
|
309
|
+
|
|
310
|
+
try { SSE.stop(); } catch (err) { console.error('[dashboard] SSE.stop failed:', err.message); }
|
|
311
|
+
|
|
312
|
+
// Destroying the bearer token is a security step, not housekeeping — never let it
|
|
313
|
+
// throw. rmSync with force tolerates a missing path and most permission cases.
|
|
314
|
+
try { fs.rmSync(TOKEN_FILE, { force: true }); } catch (err) {
|
|
315
|
+
console.error('[dashboard] could not remove token file:', err.message);
|
|
316
|
+
}
|
|
317
|
+
|
|
237
318
|
server.close(() => {
|
|
238
|
-
|
|
319
|
+
try { fs.rmSync(PID_FILE, { force: true }); } catch { /* best effort */ }
|
|
239
320
|
process.exit(0);
|
|
240
321
|
});
|
|
241
|
-
setTimeout(() => process.exit(0), 3000);
|
|
242
322
|
}
|
|
243
323
|
|
|
244
324
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|