mindforge-cc 11.9.3 → 11.9.5

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/RELEASENOTES.md CHANGED
@@ -1,5 +1,192 @@
1
1
  # Release Notes
2
2
 
3
+ ## v11.9.5 — 2026-08-22 — The release path can no longer strand itself, and the SDK ships
4
+
5
+ ### Why this release exists
6
+
7
+ 11.9.4 published two packages and then failed on the third, and that failure took the release
8
+ page and the `stable` dist-tag with it:
9
+
10
+ ```
11
+ success Publish to npm <- mindforge-cc@11.9.4 (irreversible)
12
+ success Publish standalone MCP server to npm <- mindforge-mcp-server@11.9.4 (irreversible)
13
+ failure Publish the SDK to npm <- 422: "repository.url" is ""
14
+ skipped Create GitHub Release
15
+ skipped Point the stable dist-tag at this release
16
+ ```
17
+
18
+ Two causes, both fixed here.
19
+
20
+ **The metadata was only ever validated by the registry.** `sdk/package.json` had no
21
+ `repository` field, and npm compares that field against the attestation **server-side at PUT**.
22
+ `npm publish --dry-run` does not check it and nothing here read `repository` at all — so all six
23
+ preflight gates passed on a manifest guaranteed to be rejected. There is now an offline gate in
24
+ *preflight*, ahead of every publish, that discovers the provenance-publishing packages from the
25
+ workflow itself. Run against a worktree at tag `v11.9.4` — the tree the registry rejected — it
26
+ exits 1 and names the file. It would have stopped that release.
27
+
28
+ **The step order let an optional package strand the release.** The SDK publish sat before the
29
+ release page and the dist-tag move, so its failure skipped both. The same mode had already fired
30
+ on v11.5.1 and v11.8.3. Reordered so the steps that *finish* a release run ahead of any additive
31
+ package publish — which means this release moves `stable` forward even if a package fails again.
32
+
33
+ ### The user-visible part
34
+
35
+ **`mindforge-sdk` publishes for the first time since 11.8.0, and for the first time with
36
+ provenance.** Everything fixed in the SDK across 11.8.1–11.9.4 had reached nobody, including
37
+ this: `WebSocketEventStream` scheduled a reconnect and nothing handled the returned promise, so
38
+ a failed reconnect was an unhandled rejection — fatal under Node's default mode, **terminating
39
+ the caller's process**. Every consumer on 11.8.0 still has that.
40
+
41
+ ### Also fixed
42
+
43
+ - `bin/utils/readiness-gate.js` scored `RELEASENOTES.md` on existence while its changelog sibling
44
+ checked the version was *in* the file. That asymmetry is why both prose surfaces shipped stale
45
+ in 11.9.3 and again in 11.9.4. Both are now checked, anchored rather than by substring.
46
+ **Policy change:** every release from here needs an entry in this file.
47
+ - `docs/sdk-reference.md` claimed `npx mindforge-cc@stable` installs the SDK "as part of the
48
+ framework". Measured false — the published package declares exactly `express` and `sql.js` and
49
+ ships no `sdk/`. Removed.
50
+
51
+ ### Note on 11.9.4
52
+
53
+ It is complete, but was finished by hand: its GitHub Release was created at the existing tag from
54
+ the registry's own tarball, verified against `dist.integrity` and both attestation bundles'
55
+ subject digests. The `v11.9.4` tag was deliberately **not** moved, because two published
56
+ packages' provenance attests to the commit it names.
57
+
58
+ ## v11.9.4 — 2026-08-22 — Delivery: the gates register, the tarball matches its tag
59
+
60
+ ### The headline
61
+
62
+ **11.9.3 shipped the hook-registration code and then declined to run it.** The installer skipped
63
+ whenever any ancestor directory contained a `.claude`. On a machine that has ever run Claude Code
64
+ that means `~/.claude`, so essentially every install copied the enforcement in and wired none of it.
65
+
66
+ Measured against the published tarballs in a confined sandbox, with `~/.claude` as the only ancestor:
67
+
68
+ ```
69
+ 11.9.3: 11 hook scripts installed, 0 registered, no settings.json written
70
+ 11.9.4: 8 registered, preflight executed 7 of 8, 3 deny-class verified blocking
71
+ ```
72
+
73
+ The reason the installer printed was wrong three separate ways:
74
+
75
+ 1. `~/.claude/settings.json` is the **user tier**, applied in addition to the project tier. Its
76
+ existence says nothing about whether a project file is read — so the condition that suppressed
77
+ the gates was satisfied by an ordinary laptop.
78
+ 2. For a real project ancestor the claim is false too: that file is not read either. Proved with a
79
+ marker hook two levels up that never fired across a dozen tool calls. Skipping did not deliver
80
+ the gates elsewhere; it delivered them nowhere.
81
+ 3. The git-boundary guard was dead code, which is why the walk reached `$HOME` at all.
82
+
83
+ It now warns and registers anyway. A registration that turns out inert costs nothing; a skip is
84
+ guaranteed inert.
85
+
86
+ ### ⚠️ Behaviour change under a patch bump
87
+
88
+ The installer now writes `.claude/settings.json` on projects where it previously declined — for most
89
+ users, **0 registered hooks becomes 8**, three of which can block a tool call. It merges append-only
90
+ into any existing file, backs the previous one up under `.mindforge/backups/`, and records exactly
91
+ what it did in `.mindforge/hook-registration.json`.
92
+
93
+ A registered hook is only *live* if the harness has been **restarted** (hooks are snapshotted at
94
+ session start), the project is **trusted** in the harness, and `CLAUDE_PROJECT_DIR` is set with
95
+ `node` on the hook PATH. See *Hooks are installed but nothing is blocked* in
96
+ `docs/troubleshooting.md`.
97
+
98
+ ### Fixes
99
+
100
+ - The installer's only failure-path pointer led nowhere: it said "see `docs/troubleshooting.md`",
101
+ where the word *hook* appeared **0** times. That section now exists.
102
+ - The published tarball was not reproducible from its own tag. `.mindforge/memory/sync-manifest.json`
103
+ — gitignored, written at runtime — was **1 of 1979** shipped files not tracked at `v11.9.3`, so
104
+ provenance attested to a tree containing a file the repository does not contain.
105
+ - The README understated the product: it still said **no channel registers hooks** and that the
106
+ plugin dispatcher crashes on every fire. Measured: 14 of 14 plugin path tokens resolve and two
107
+ deny-class hooks return exit 2. A document that under-claims a security capability is the same
108
+ defect as one that over-claims it.
109
+ - Defects an 8-agent audit found in the *published* 11.9.3: an empty release page, a shipped CI
110
+ snippet telling users to `npx` a package we do not own, and a version-source gate that missed a
111
+ live defect twice.
112
+ - The Homebrew formula carries the real 11.9.3 digest, verified against an independent measurement.
113
+
114
+ ### Known issue in this release
115
+
116
+ **`mindforge-sdk` did not publish and remains at 11.8.0.** A publish step was added for it and the
117
+ registry rejected it with 422: `sdk/package.json` carried no `repository` field, which npm's
118
+ provenance verification validates **server-side at publish time** — `npm publish --dry-run` does not
119
+ check it, and no gate here did either.
120
+
121
+ Worse, the step was placed *before* the release-page and dist-tag steps, so its failure skipped
122
+ both. `mindforge-cc@11.9.4` and `mindforge-mcp-server@11.9.4` published correctly with provenance;
123
+ the release page and the `stable` tag were completed by hand afterwards. Both causes are fixed for
124
+ the next release: the missing field, an offline preflight gate that refuses to reach a publish
125
+ without it, and a reordering so the steps that finish a release run ahead of any optional package.
126
+
127
+ ## v11.9.3 — 2026-08-21 — Honesty: gates that can fail, commands that run, a release path that is checked
128
+
129
+ ### What's New
130
+ Nothing. Twenty-one fixes sharing a single defect: **an instrument reported success while doing
131
+ nothing.** Gates that could not fail, tests a comment satisfied, docs naming scripts that do not
132
+ exist, commands printing success while performing no action, and a publish path no check ever
133
+ touched.
134
+
135
+ ### ⚠️ Breaking (under a patch bump)
136
+ Six behaviour changes, each a bug fix whose correct behaviour differs from what shipped. Read these
137
+ if you script against the CLI or the installer.
138
+
139
+ - **Routed CLI commands now act on YOUR project, not MindForge's checkout.** The router passed
140
+ `cwd: ROOT`, so `mindforge classify` diffed MindForge's repository instead of yours and
141
+ `mindforge health` inspected MindForge's `node_modules`. Of 27 routed commands, 7 changed
142
+ behaviour — all in the correct direction.
143
+ - **`npx mindforge-cc install`, and any other positional argument, now exits 1.** The installer
144
+ takes flags only and used to ignore stray words silently, so that command appeared to work while
145
+ configuring nothing. Use `npx mindforge-cc --claude --local`.
146
+ - **`mindforge verify` now SKIPS unavailable stages instead of failing them.** A project with no
147
+ ESLint config or no test script was reported as FAILING those stages rather than as not having
148
+ them. CI that relied on a non-zero exit there will now pass. A run where every stage skipped
149
+ prints a "NOTHING WAS VERIFIED" banner rather than a clean bill of health.
150
+ - **`scripts/sync-version.js` now exits non-zero when the plugin build artifacts are stale.** A bump
151
+ used to report success and exit 0 while leaving `npm test` red.
152
+ - **A self-install no longer writes over your tracked files.** Running the installer inside a
153
+ MindForge checkout printed that it was skipping and then overwrote 149 tracked files, including
154
+ `CLAUDE.md`, `.claude/**`, `.agent/**` and `.mindforge/**`.
155
+ - **Releases must be tagged on a commit reachable from `main`.**
156
+
157
+ ### Fixes
158
+ - **11 of 27 routed CLI verbs died on `MODULE_NOT_FOUND` in a real install.** The router shipped;
159
+ six of the scripts it dispatches to did not.
160
+ - **Every `--global` install reported failure on a correct run.** Verification demanded six `bin/**`
161
+ paths regardless of scope, so a global install — which writes 389 files to `$HOME/.claude` and,
162
+ deliberately, zero to `bin/` — ended `6 of 12 required file(s) missing` and exit 1, with a
163
+ `--force` retry that could not help.
164
+ - **`--fetch-sha` hashed npm's 404 error body into the Homebrew formula.** For an unpublished
165
+ version the registry answers `{"error":"Not found"}` and `curl -sL` exits 0, so the digest written
166
+ was the SHA-256 of that error text — the same constant for every unpublished version — and
167
+ `--check` then passed. It now fails closed on both status and gzip magic bytes.
168
+ - **No version channel covered a document a user receives.** `SECURITY.md` — the security policy at
169
+ the root of the published package — said "Current version: 11.9.0", and five docs titled
170
+ themselves v11.9.0: three releases stale while every npm manifest was correct. A channel that does
171
+ not exist cannot drift, so `--check` was green the whole time.
172
+ - **The tag push that publishes was exempt from every gate.** Publishing is triggered by exactly one
173
+ event — a `v*` tag push — and the repository's only ruleset targets branches, so its six required
174
+ checks applied to nothing on the path that ships. GitHub cannot attach required status checks to a
175
+ tag, so this had to be fixed in the workflow itself.
176
+ - **The `stable` dist-tag sat three releases behind `latest`** (11.8.3 against 11.9.2), so
177
+ `npm i mindforge-cc@stable` delivered a build with none of the 11.9.x fixes. The release workflow
178
+ now moves it as its final step — forward-only, prereleases skipped.
179
+ - **`mindforge dashboard --status` and `--stop` were documented and implemented nowhere.** Both
180
+ printed nothing useful. `--stop` also identified its target by the SHAPE of a command line, which
181
+ matched any `node <anything>/dashboard/server.js` — verified against an unrelated app's dashboard.
182
+ - **`temporal cleanup` printed "Cleaning up…" and "Cleanup complete." with no cleanup between them.**
183
+ - **The protocol files instructed the agent to run scripts that do not exist** (`soul-engine.js`,
184
+ `shard-controller.js`), and 15 phantom `/mindforge:` slash commands appeared in shipped docs. A
185
+ reader following the user guide typed `/mindforge:personas --list` and got nothing.
186
+ - **Every abandoned exit left a full copy of the database on disk** — 1.8 GB of orphaned `.tmp` files.
187
+
188
+ Full detail, including the measured evidence for each: [changelogs/v11.9.3.md](./changelogs/v11.9.3.md).
189
+
3
190
  ## v11.9.2 — 2026-08-16 — Correctness: config gate, audit chain, retrieval, cost ledger
4
191
 
5
192
  ### What's New
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Security Policy
2
2
 
3
- > **Current version:** 11.9.3 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
3
+ > **Current version:** 11.9.5 | **npm audit:** 0 vulnerabilities across root, sdk, mcp-server
4
4
 
5
5
  ## Supported Versions
6
6
 
@@ -210,7 +210,12 @@ function getApprovals() {
210
210
  const { verifyRecord } = require('../governance/approval-record');
211
211
  let currentVersion = null;
212
212
  try {
213
- currentVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')).version;
213
+ // MindForge's version, resolved by package NAME. `path.join(__dirname, '..', '..',
214
+ // 'package.json')` reached <project>/package.json in an install — the CONSUMER's manifest — so
215
+ // verifyRecord() below compared an approval record against the host app's version (0.4.2, say)
216
+ // instead of MindForge's. The try/catch stopped it crashing, which is why it went unnoticed:
217
+ // a wrong binding is quieter than a missing one.
218
+ currentVersion = require('../utils/mindforge-version').resolveMindforgeVersion(process.cwd()).version;
214
219
  } catch { /* version binding is skipped if the manifest is unreadable */ }
215
220
 
216
221
  const now = Date.now();
@@ -80,7 +80,18 @@ function approve(opts = {}) {
80
80
  const root = opts.root || ROOT;
81
81
  const approvalsDir = opts.approvalsDir || APPROVALS_DIR;
82
82
 
83
- const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
83
+ // TOLERATES A PROJECT WITH NO package.json. This was an unguarded readFileSync, so `mindforge approve`
84
+ // died with `ENOENT: no such file or directory, open '<project>/package.json'` on any repo that is not
85
+ // Node — Python, Go, Rust, or any polyglot root. Measured on the published 11.9.3 tarball.
86
+ //
87
+ // The FIELDS stay the consumer's on purpose: this record documents a governance decision in the
88
+ // user's project, so `project` and `version` describing that project is correct. Only the crash was
89
+ // wrong. Absent values are recorded as null rather than invented — an approval record that guesses
90
+ // what it is approving is worse than one that admits it does not know.
91
+ const pkg = (() => {
92
+ try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); }
93
+ catch { return { name: path.basename(root) || null, version: null }; }
94
+ })();
84
95
 
85
96
  const id = `MF-AUTH-${Date.now().toString(36).toUpperCase()}`;
86
97
  const timestamp = new Date().toISOString();
@@ -72,7 +72,19 @@ function summary(lines) {
72
72
 
73
73
  function main(argv = process.argv.slice(2)) {
74
74
  const args = parseArgs(argv);
75
- const currentVersion = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
75
+ // MindForge's version, by package NAME. This read `path.join(ROOT, 'package.json')`, which in an
76
+ // install is the CONSUMER's manifest — so every approval record was verified against the host
77
+ // app's version rather than MindForge's, and the command died with ENOENT on any project without
78
+ // a package.json at all. Version binding is the point of the record; binding it to the wrong
79
+ // version is worse than not binding it, because the check still reports a verdict.
80
+ //
81
+ // Degrades to null rather than throwing: an unresolvable version should make the binding
82
+ // unverifiable, not make the whole verification unrunnable. verifyRecord treats a null
83
+ // currentVersion as "do not check the binding".
84
+ let currentVersion = null;
85
+ try {
86
+ currentVersion = require('../utils/mindforge-version').resolveMindforgeVersion(process.cwd()).version;
87
+ } catch { /* binding unverifiable, reported as such rather than crashing the run */ }
76
88
 
77
89
  let files = [];
78
90
  try {
@@ -60,6 +60,7 @@
60
60
  */
61
61
 
62
62
  const fs = require('node:fs');
63
+ const os = require('node:os');
63
64
  const path = require('node:path');
64
65
  const { spawnSync } = require('node:child_process');
65
66
 
@@ -366,14 +367,48 @@ function probeInner(projectRoot, failures) {
366
367
 
367
368
  // ── register ─────────────────────────────────────────────────────────────────
368
369
 
369
- /** Would the harness read a DIFFERENT .claude/settings.json than the one we are about to write? */
370
- function ancestorClaudeDir(projectRoot) {
371
- const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: projectRoot, encoding: 'utf8' });
372
- const stop = top.status === 0 ? path.resolve(top.stdout.trim()) : path.parse(projectRoot).root;
370
+ /**
371
+ * An ancestor project whose OWN .claude/settings.json a harness launched there would read instead of
372
+ * ours. Advisory only see the three corrections below. Returns the directory, or null.
373
+ *
374
+ * THIS USED TO SKIP REGISTRATION ENTIRELY, and the reason it printed was wrong in three ways:
375
+ *
376
+ * "the harness will read <ancestor>/.claude/settings.json, not this directory"
377
+ *
378
+ * 1. IT TRIPPED ON $HOME, so it tripped for essentially every project. ~/.claude/settings.json is the
379
+ * USER TIER: Claude Code applies it to every session IN ADDITION TO the project tier, not instead
380
+ * of it. Its existence says nothing about whether a project file is read. Measured on the author's
381
+ * machine: the user-tier hooks fire on every Bash call while a project settings.json sits below
382
+ * them. So the condition that suppressed the gates was satisfied by the normal state of a laptop.
383
+ *
384
+ * 2. FOR A REAL PROJECT ANCESTOR THE CLAIM IS ALSO FALSE — the ancestor's file is not read either.
385
+ * Measured with a natural experiment: an ancestor two levels up carried a PreToolUse Bash hook
386
+ * appending a marker to a log file. Across a dozen Bash calls with the inner directory as the
387
+ * project root, that log was never even created. Claude Code reads the project tier from the
388
+ * directory it treats as the project root; it does not walk up for settings. So skipping did not
389
+ * deliver the gates "over there", it delivered them nowhere.
390
+ *
391
+ * 3. THE GIT-BOUNDARY GUARD WAS DEAD CODE. `stop` was the git toplevel, but the walk started at
392
+ * path.dirname(projectRoot) — so when toplevel === projectRoot (the normal case) `dir === stop`
393
+ * could never be true and the walk ran to the filesystem root every time. The boundary that was
394
+ * supposed to keep this local is why it reached $HOME.
395
+ *
396
+ * So: WARN, NEVER SKIP. A registration that turns out to be inert costs nothing and becomes live the
397
+ * moment the harness is launched here; a skip is guaranteed inert. Two narrowings keep the warning
398
+ * meaningful rather than universal: $HOME is excluded because it is a tier and not a shadow, and an
399
+ * actual settings.json FILE must exist — the old check accepted any directory named .claude, of which
400
+ * a docs folder is a perfectly ordinary example.
401
+ *
402
+ * The git boundary is deliberately not repaired, just removed: a git toplevel ABOVE projectRoot means
403
+ * this project is nested inside another repo, which is exactly the case worth warning about, so
404
+ * stopping the walk there would suppress the one signal this function exists to produce.
405
+ */
406
+ function shadowingProjectSettings(projectRoot) {
407
+ const home = os.homedir();
373
408
  let dir = path.dirname(path.resolve(projectRoot));
374
409
  for (let i = 0; i < 64; i++) {
375
- if (fs.existsSync(path.join(dir, '.claude'))) return dir;
376
- if (dir === stop || dir === path.dirname(dir)) break;
410
+ if (dir !== home && fs.existsSync(path.join(dir, SETTINGS_REL))) return dir;
411
+ if (dir === path.dirname(dir)) break;
377
412
  dir = path.dirname(dir);
378
413
  }
379
414
  return null;
@@ -391,10 +426,15 @@ function register(options = {}) {
391
426
  if (selfInstall) return skip('self-install: the repo maintains its own tracked .claude/settings.json');
392
427
  if (process.platform === 'win32') return skip('win32 is unverified for the emitted command shape');
393
428
 
394
- const ancestor = ancestorClaudeDir(projectRoot);
395
- if (ancestor) {
396
- return skip(`the harness will read ${path.join(ancestor, '.claude', 'settings.json')}, not this directory — re-run the installer there`);
397
- }
429
+ // Advisory, deliberately not a skip — see shadowingProjectSettings for the three measurements that
430
+ // demoted it from one. The operator is told what to do about it; the gates still get installed.
431
+ const shadow = shadowingProjectSettings(projectRoot);
432
+ const warnings = shadow
433
+ ? [`${path.join(shadow, SETTINGS_REL)} exists in an ancestor project. These hooks are registered `
434
+ + 'for THIS directory and are live when the harness runs with it as the project root. If you '
435
+ + `launch the harness in ${shadow} instead, run the installer there too — its project settings `
436
+ + 'are read from where it starts, not walked up from.']
437
+ : [];
398
438
 
399
439
  const copy = applyCopyManifest(projectRoot, repoRoot);
400
440
  if (!copy.ok) return skip(copy.reason);
@@ -424,7 +464,7 @@ function register(options = {}) {
424
464
 
425
465
  const text = `${JSON.stringify(merged.next, null, 2)}\n`;
426
466
  if (dryRun) {
427
- return { status: 'dry-run', reason: `would write ${HOOK_SPEC.length} hooks to ${SETTINGS_REL}`, registered: false, preview: text };
467
+ return { status: 'dry-run', reason: `would write ${HOOK_SPEC.length} hooks to ${SETTINGS_REL}`, registered: false, preview: text, warnings };
428
468
  }
429
469
 
430
470
  const existedBefore = fs.existsSync(absSettings);
@@ -452,6 +492,7 @@ function register(options = {}) {
452
492
  preflight_skipped: verified.skipped,
453
493
  preflight_skip_reason: 'registered but not executed during preflight: spawns a background '
454
494
  + 'process and writes under $HOME. Advisory, so a failure there would not have blocked.',
495
+ warnings,
455
496
  residual_risk: 'CLAUDE_PROJECT_DIR unset, or node off the hook PATH, yields exit 1 and the gate is '
456
497
  + 'absent (identical to not installing). No fail-closed shell tail is used: measured, it denies '
457
498
  + 'benign commands on a fresh clone.',
@@ -466,6 +507,7 @@ function register(options = {}) {
466
507
  target: SETTINGS_REL,
467
508
  backup: receipt.backup,
468
509
  receipt: RECEIPT_REL,
510
+ warnings,
469
511
  };
470
512
  }
471
513
 
@@ -500,5 +542,6 @@ module.exports = {
500
542
  DENY_CLASS, OWNED_RE, KNOWN_IDS,
501
543
  commandFor, profilesFor, isOwned,
502
544
  applyCopyManifest, mergeSettings, assertNoLoss, probe,
545
+ shadowingProjectSettings,
503
546
  register, unregister,
504
547
  };
@@ -1152,7 +1152,15 @@ async function install(runtime, scope, options = {}) {
1152
1152
  } else {
1153
1153
  Theme.printStatus(c.yellow(`Hooks NOT registered (${hookRegistration.status}): ${hookRegistration.reason}`), 'warn');
1154
1154
  Theme.printStatus(c.dim('The hook scripts are installed but nothing invokes them, so no tool call '
1155
- + 'is gated. This is stated rather than implied — see docs/troubleshooting.md.'), 'info');
1155
+ + 'is gated. This is stated rather than implied — see "Hooks are installed but nothing is '
1156
+ + 'blocked" in docs/troubleshooting.md.'), 'info');
1157
+ }
1158
+
1159
+ // Non-fatal advisories. Printed for BOTH outcomes, because the case that produces one today —
1160
+ // an ancestor project with its own settings.json — used to CANCEL registration outright, and the
1161
+ // whole point of demoting it to a warning is that the operator hears it without losing the gates.
1162
+ for (const w of hookRegistration.warnings || []) {
1163
+ Theme.printStatus(c.yellow(w), 'warn');
1156
1164
  }
1157
1165
  }
1158
1166
 
@@ -1297,11 +1305,31 @@ async function run(args) {
1297
1305
  const options = { dryRun, force, verbose, withUtils, minimal };
1298
1306
 
1299
1307
  // Get package.json for version
1300
- const pJSON = JSON.parse(fsu.read(path.join(SOURCE_ROOT, 'package.json')));
1308
+ // MINDFORGE'S VERSION, NOT THE HOST PROJECT'S — and it must not crash when there is no manifest.
1309
+ //
1310
+ // This read was `JSON.parse(fsu.read(path.join(SOURCE_ROOT, 'package.json')))`, and SOURCE_ROOT is
1311
+ // path.resolve(__dirname, '..'). In an install that lands at <project>/bin/, so it read the CONSUMER's
1312
+ // package.json. Measured on the published 11.9.3 tarball: in a project declaring version 0.4.2,
1313
+ // `mindforge health` printed the banner `RELEASE v0.4.2` and then, 26 lines later, `Current : v11.9.3`
1314
+ // — one command, two contradictory versions, exit 0. In a project with NO package.json the whole
1315
+ // command was `Unexpected end of JSON input`, exit 1, no report at all: fsu.read returns empty for a
1316
+ // missing file and JSON.parse throws.
1317
+ //
1318
+ // This is the same defect 11.9.3 fixed for `--version`, in the one verb whose advertised job is
1319
+ // "verify project health and installation integrity". bin/utils/mindforge-version.js already resolves
1320
+ // correctly — by package NAME, which is what distinguishes our manifest from a consumer's — and it
1321
+ // shipped in the same release. It simply was not used here.
1322
+ //
1323
+ // It THROWS rather than guessing, which is right for a version check and wrong for a banner, so the
1324
+ // banner degrades to 'unknown' instead of taking the whole health report down with it.
1325
+ let bannerVersion = 'unknown';
1326
+ try {
1327
+ bannerVersion = require('./utils/mindforge-version').resolveMindforgeVersion(process.cwd()).version;
1328
+ } catch { /* a banner must never be the reason health cannot run */ }
1301
1329
 
1302
1330
  // Print header and brand manifest
1303
1331
  // Print header and brand manifest
1304
- Theme.printHeader(pJSON.version);
1332
+ Theme.printHeader(bannerVersion);
1305
1333
  Theme.printBrandManifest();
1306
1334
  // Check for updates only
1307
1335
  if (isCheck) {
@@ -100,12 +100,28 @@ function releaseReadinessChecks(root) {
100
100
  pass: Boolean(pkgVersion) && pkgVersion === configVersion,
101
101
  fix: `Align versions: package.json=${pkgVersion || '?'} vs config.json=${configVersion || '?'}`,
102
102
  },
103
+ // CHECKS CONTENT, NOT MERE EXISTENCE, and that change has a cost worth naming: from now on every
104
+ // release needs a RELEASENOTES entry. Six published 11.x versions do not have one (11.0.0, 11.2.0,
105
+ // 11.2.1, 11.4.0, 11.7.1, 11.9.1) — the file has been curated rather than exhaustive. Those gaps are
106
+ // unaffected, because this only ever looks for the version being released.
107
+ //
108
+ // WHY IT CHANGED. This gate passed on `fileExists` while its changelog sibling three lines down
109
+ // checked that the version was IN the file. That asymmetry is why both prose surfaces shipped stale
110
+ // twice running: the immutable 11.9.3 tarball's README said "Latest release v11.9.2", and the
111
+ // immutable 11.9.4 tarball shipped with no 11.9.4 entry here at all — while README.md offers this
112
+ // file as the human-readable route to the BREAKING notes. 348a3a2c fixed the identical defect one
113
+ // release earlier by hand and added no gate, and it recurred immediately.
114
+ //
115
+ // Anchored, not `includes`. A substring test for "11.9.4" is also satisfied by "## v11.9.40" and by
116
+ // "## v11.9.4-notyet" — measured, when the equivalent test assertion was falsified.
103
117
  {
104
118
  id: 'releasenotes',
105
- label: 'RELEASENOTES.md present',
119
+ label: 'RELEASENOTES.md present and has an entry for the current version',
106
120
  points: 2,
107
- pass: fileExists(root, 'RELEASENOTES.md'),
108
- fix: 'Add RELEASENOTES.md for this release.',
121
+ pass: fileExists(root, 'RELEASENOTES.md') && Boolean(pkgVersion)
122
+ && new RegExp(`^## v${pkgVersion.replace(/\./g, '\\.')}(?![\\d.\\w-])`, 'm')
123
+ .test(safeRead(root, 'RELEASENOTES.md')),
124
+ fix: `Add a "## v${pkgVersion || '?'}" section to RELEASENOTES.md. README.md links here as the human-readable route to the BREAKING notes, so releasing without one points readers at a file whose newest entry describes a different version.`,
109
125
  },
110
126
  {
111
127
  id: 'changelog',
@@ -12,6 +12,14 @@ Contains behaviour changes under a patch bump — several of the things being fi
12
12
  bugs that a consumer could have been relying on. Read BREAKING before upgrading if you
13
13
  script against the CLI or the installer.
14
14
 
15
+ > **Corrected after release.** Three measured numbers in this entry were wrong and are fixed above:
16
+ > the count of places `--status`/`--stop` were documented (removed rather than re-guessed — it reads 4,
17
+ > 9 or 15 depending on how you count, which is the argument against stating it); the eslint total, which
18
+ > was 199 on the author's machine and **190** on a clean clone, because 9 problems came from an
19
+ > untracked local directory; and "four releases behind", which is **three** (11.8.3 → 11.9.0 → 11.9.1 →
20
+ > 11.9.2). Found by an adversarial audit of this changelog against the published artifact. A release
21
+ > arguing that measured numbers should be reproducible has to hold its own notes to that standard.
22
+
15
23
  ### BREAKING
16
24
 
17
25
  Each of these is a bug fix whose correct behaviour differs from the shipped behaviour.
@@ -113,15 +121,16 @@ Each of these is a bug fix whose correct behaviour differs from the shipped beha
113
121
  exactly one event — a `v*` tag push — and the repository's only ruleset targets
114
122
  branches, so its six required checks applied to nothing on the path that ships. GitHub
115
123
  cannot attach required status checks to a tag. A `preflight` job now gates it. (#216)
116
- - The `stable` npm dist-tag was moved by hand, or not at all — it sat four releases
117
- behind `latest` (11.8.3 against 11.9.2), so `npm i mindforge-cc@stable` delivered a
124
+ - The `stable` npm dist-tag was moved by hand, or not at all — it sat three releases
125
+ behind `latest` (11.8.3 against 11.9.2, via 11.9.0 and 11.9.1), so `npm i mindforge-cc@stable` delivered a
118
126
  build with none of the 11.9.x fixes. The release workflow now moves it as its final
119
127
  step: forward-only, prereleases skipped, and verified against npm's uncached dist-tags
120
128
  endpoint rather than the CDN-cached packument. (#216)
121
129
 
122
130
  **Dashboard**
123
131
 
124
- - `--status` and `--stop` were documented in nine places and implemented in none; both
132
+ - `--status` and `--stop` were documented across the harness roots and the docs and implemented
133
+ nowhere; both
125
134
  printed nothing and exited 0. Now implemented, before `express` is required, so they
126
135
  work without the dependency installed. (#206)
127
136
  - `--stop` identified the target by the SHAPE of its command line, which matched any
@@ -141,8 +150,8 @@ Each of these is a bug fix whose correct behaviour differs from the shipped beha
141
150
  **Verification**
142
151
 
143
152
  - `mindforge verify`'s lint stage used `--max-warnings=0`, which made it impossible to
144
- pass in the repository it ships from: `npx eslint .` reports 199 problems / 0 errors /
145
- 199 warnings, so `verify` reported a lint FAILURE on a tree that is green by the
153
+ pass in the repository it ships from: on a clean clone `npx eslint .` reports 190 problems /
154
+ 0 errors / 190 warnings, so `verify` reported a lint FAILURE on a tree that is green by the
146
155
  project's own contract. Aligned with the project's definition; errors still fail. (#204)
147
156
  - `temporal cleanup` printed "🧹 Cleaning up old temporal snapshots..." and
148
157
  "✅ Cleanup complete." with no cleanup between them. Now wired to