@massu/core 2.4.0 → 2.5.1

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/CHANGELOG.md CHANGED
@@ -10,6 +10,132 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
  > redistributed under the **Apache License 2.0**. Full license + NOTICE:
11
11
  > `packages/core/assets/embedder/MODEL-LICENSE`.
12
12
 
13
+ ## [2.5.1] - 2026-08-09
14
+
15
+ ### Fixed
16
+
17
+ - **Hooks emitted an output field that does not exist, so no hook message ever reached
18
+ Claude.** Eleven hooks wrote `{"message": …}` to stdout. `message` is not in Claude Code's
19
+ hook output schema at all — it appears only in `Notification`/`FileChanged` *input* — so
20
+ every advisory string those hooks produced was discarded. The failure was self-inflicted in
21
+ an exact way: because `{"message": …}` is valid JSON, `UserPromptSubmit` parsed it as JSON
22
+ instead of treating it as plain text, so the one event that would have delivered the string
23
+ as stdout is precisely the event that dropped it. A slightly malformed string would have
24
+ arrived.
25
+
26
+ Replaced by a single chokepoint emitting the documented shape,
27
+ `{"hookSpecificOutput": {"hookEventName", "additionalContext"}}`. `hookEventName` is
28
+ REQUIRED, so the helper takes the event as a typed, required argument and the old
29
+ one-argument form is DELETED rather than deprecated — a regression to the silent shape is
30
+ now a compile error, not a silent no-op. An unrecognised event emits nothing and says why
31
+ on stderr. Output is truncated at the spec's 10,000-char cap by us rather than by the
32
+ runtime.
33
+
34
+ **Blocking behaviour is unchanged.** The `PreToolUse` gates deny via `process.exit(2)` plus
35
+ stderr, which never depended on this path; what was lost was their *advisory* (warn-level)
36
+ messages, and everything `rule-enforcement-pipeline` emits, since that hook only ever
37
+ advises.
38
+
39
+ ### Added
40
+
41
+ - **`scripts/ops/retag-release-to-npm-githead.sh`** — audits every release tag against the
42
+ commit npm actually published. A release tag is supposed to answer "which commit shipped as
43
+ X.Y.Z", and two things break that: the tag is created before the post-publish bookkeeping
44
+ commit and then moved forward, or it is never created at all. npm records `gitHead` with
45
+ every publish, so the answer is recoverable — this reads THAT and refuses anything else.
46
+
47
+ Dry-run by default; reports its denominator; FAILS CLOSED when the registry returns no
48
+ usable `gitHead` or names a commit absent from the repo, rather than guessing; never
49
+ force-pushes — it moves the local tag and prints the push command, so publishing a
50
+ rewritten ref stays deliberate.
51
+
52
+ First full audit: `50 tag(s) examined; 6 already correct, 34 drifted, 10 error(s)`. The 2.x
53
+ series is now 7/7 correct; 1.x is left as-is deliberately.
54
+
55
+ ### Fixed
56
+
57
+ - **The documented Stage-D release ceremony had no legal ending.** It ordered
58
+ `bump -> tag -> publish -> .mcp.json pin refresh`, but the pin refresh is a COMMIT, so
59
+ following it always leaves commits on top of a published-and-tagged release — exactly what
60
+ CR-64 refuses. Measured during the 2.5.0 release: gate `[11/22]` returned
61
+ `versionIsPublished:true, tagSha 90c82344, headSha a74648ae`.
62
+
63
+ Corrected to **pin -> publish -> tag**, so the published tree, npm's `gitHead`, the tag and
64
+ HEAD are one commit by construction. Fixed at both sites that state the order. The repo's
65
+ own history already worked this way — `v2.3.1`, `v2.3.0` and `v2.1.0` each tag their pin
66
+ commit — so the prose was the outlier, not the practice.
67
+
68
+ - **The same plan predicted a release does not touch `website/`.** The CHANGELOG is mirrored
69
+ into `website/`, so every release does; gate `[9/22]` failed on a 29h deploy lag. Replaced
70
+ the prediction with the measured fact and made `/massu-deploy` an explicit ceremony step.
71
+
72
+ Incident: `docs/incidents/2026-08-01-release-ceremony-had-no-legal-ending.md`
73
+
74
+ ## [2.5.0] - 2026-08-01
75
+
76
+ ### Fixed
77
+
78
+ - **`install-hooks` ADDED a hook registration instead of REPLACING it, so every version bump
79
+ doubled the hooks — and that defect BLOCKED ITS OWN REMEDY.** `mergeHookEntries` deduped by
80
+ EXACT COMMAND STRING, and the emitted command embeds the version, so
81
+ `npx -y @massu/core@1.16.2 hook-runner user-prompt` and the `@2.4.0` form were two different
82
+ strings, two different hooks, BOTH kept. The command was simultaneously the payload and the
83
+ identity, so any payload change forged a new identity.
84
+
85
+ Measured, not theoretical: one workspace carried `1.15.5` x16 **and** `1.15.2` x15 at once,
86
+ with 15 hook names firing TWICE per event and writing double telemetry indefinitely. Worse,
87
+ because upgrading duplicated, nobody upgraded — 9 of 10 workspaces on this machine sat on a
88
+ pre-2.0.0 version whose `better-sqlite3` ABI did not match the running Node, and one had
89
+ **26,119 `NODE_MODULE_VERSION` hook failures**: a total massu outage. It stayed invisible
90
+ because a crashing hook still **exits 0**.
91
+
92
+ Registration identity is now the `hook-runner` subcommand — a stable CLI contract (`cli.ts`
93
+ dispatches on exactly that token), invariant under both a version bump and a launch-mechanism
94
+ change. Non-massu entries keep the historical exact-command semantics, so a customer's own
95
+ hooks are never touched or collapsed.
96
+
97
+ Proof it can fail (CR-72, planted in the real tree, restored byte-identical by sha256):
98
+ reverting the identity branch to exact-command dedup turns
99
+ `installer-registration-identity-drift-guard.test.ts` RED — that guard drives the REAL
100
+ `installHooks()` over a real temp project seeded with an older-version block, a node-direct
101
+ block and a customer hook.
102
+
103
+ ### Added
104
+
105
+ - **A version-stable launcher shim, cutting ~0.86s of npm bootstrap off each hook fire.**
106
+ `npx -y @massu/core@<v> …` spends ~0.86s starting npm before any massu code runs. Measured
107
+ interleaved, min-of-5: `npx` 0.97s, `npx --offline` 0.88s, `node <cli.js>` 0.11s — so the
108
+ cost is npm's own startup, NOT the registry, and no npx flag addresses it.
109
+
110
+ `install-hooks` now writes `~/.massu/bin/massu-hook` and emits
111
+ `"~/.massu/bin/massu-hook" <version> hook-runner <name>`. That path is VERSION-STABLE, and
112
+ the shim re-resolves the runtime AT FIRE TIME, exec'ing npx when it is absent.
113
+
114
+ This deliberately does NOT bake a versioned absolute path, which would reintroduce the class
115
+ P-003 (v1.9.4) eliminated: such a path is "invalidated by cache clears, global-install
116
+ relocations, or npx upgrades — silently 404-ing the hooks". Probing at emit time says nothing
117
+ about invalidation after emit. Three fail-safe layers instead — shim missing at emit emits
118
+ npx; runtime missing at fire execs npx; both present takes the fast path — so no single
119
+ missing artifact is fatal and a stale registration self-heals.
120
+
121
+ Measured after: fast path **0.11s vs npx 1.07s (9.7x)**. The fallback is mutation-proven — a
122
+ shim with its npx branch removed FAILS the same no-runtime case (exit 27), so the test is not
123
+ vacuous. `exec` in both branches preserves stdin, stdout and the exit code.
124
+
125
+ Semver MINOR: `massuHookIdentity`, `massuShimPath`, `massuShimBody`, `installMassuShim`,
126
+ `resolveMassuShim`, `massuRuntimeDir`, `massuRuntimeCliPath`, `resolveMassuRuntimeCli` and
127
+ `materializeMassuRuntime` are new exports. `MASSU_NO_NODE_DIRECT=1` forces the npx form.
128
+
129
+ ### Changed
130
+
131
+ - **CR-70 Layer 3 amended: installer symmetry is no longer npx-specific.** Both emitters read
132
+ one resolver and agree on mechanism AND pinned version. The drift-guard previously asserted
133
+ the literal `'npx'`, which pinned an implementation detail rather than the property — it
134
+ would go RED on a correct mechanism change and GREEN on a wrong one that used npx on both
135
+ sides. Three assertions were amended on the same grounds
136
+ (`init-hook-paths-no-absolute`, `init-mcp-json-pin`, `cli.test.ts`); each keeps its original
137
+ protection of a visible pinned version and a known launch mechanism.
138
+
13
139
  ## [2.4.0] - 2026-07-31
14
140
 
15
141
  ### Fixed
@@ -41,7 +41,7 @@ Read these in parallel where possible:
41
41
  | 10 | `.claude/metrics/command-invocations.jsonl` | Command usage frequency — top/least used in last 7 days |
42
42
  | 11 | `.claude/metrics/bearings-history.jsonl` | Previous bearings runs — show trends |
43
43
  | 12 | `.claude/commands/` (ls -d */`) | Detect folder-based skills (directories vs flat files) |
44
- | 13 | `bash scripts/memory-contradiction-check.sh` | Contradiction count, consolidation candidates |
44
+ | 13 | `~/.claude/bin/memory-contradiction-check . --baseline .claude/memory-contradiction-baseline.json` | Unresolved pairs + denominator; exit 2 = could-not-look, a FINDING |
45
45
  | 14 | `massu_cost_session` + `massu_cost_trend` MCP tools | Token spend this session, cost trend over time |
46
46
  | 15 | `session-state/deferred-verifications.md` | Pending production verifications from previous deploys |
47
47
 
@@ -104,12 +104,15 @@ If no significant work: skip this section.
104
104
  Run the memory contradiction scanner:
105
105
 
106
106
  ```bash
107
- bash scripts/memory-contradiction-check.sh
107
+ ~/.claude/bin/memory-contradiction-check . --baseline .claude/memory-contradiction-baseline.json
108
108
  ```
109
109
 
110
- Parse the output:
111
- - **CONTRADICTION_COUNT: 0** — Memory is clean. Proceed.
112
- - **CONTRADICTION_COUNT: N** — Display each contradiction to the user:
110
+ Read the EXIT CODE, and read the denominator it prints — a
111
+ verdict with no `memory files read` line means it aborted before reporting:
112
+ - **exit 0** — no unresolved contradictions. Quote `unresolved pairs : 0` as the evidence.
113
+ - **exit 1** — unresolved pairs, or a STALE baseline entry. Display each to the user:
114
+ - **exit 2** — it COULD NOT LOOK (missing corpus, unreadable file, bad baseline). This is
115
+ a FINDING, never a clean run. Report it as such and do not proceed as though clean.
113
116
 
114
117
  ```
115
118
  MEMORY HEALTH
@@ -122,7 +125,15 @@ MEMORY HEALTH
122
125
 
123
126
  **Do NOT auto-resolve contradictions.** Surface them so the user can decide which memory is stale.
124
127
 
125
- If the script is missing or fails, note: "Memory contradiction check unavailable — run `bash scripts/memory-contradiction-check.sh` manually."
128
+ **If the checker is missing, report that as a FAILED check — never as a benign note.**
129
+ This step was unrunnable in this repo from the day it was written until 2026-08-02: it
130
+ named `scripts/memory-contradiction-check.sh`, which never existed here (0 tracked
131
+ matches, 0 commits ever touching the path — it was copied from another repo's protocol).
132
+ The instruction to "note it as unavailable" is what let a mandatory check report nothing
133
+ for months, because "could not look" and "looked and found nothing" read identically.
134
+ The checker is now version-controlled at `~/.claude/bin/memory-contradiction-check`, has a
135
+ caller in `npm test` (`memory-contradiction-check-caller.test.ts`), a fixture suite, and a
136
+ live-fire attack — so its absence is itself a red test.
126
137
 
127
138
  ### Step 3: Update Session State
128
139