@massu/core 2.5.0 → 2.5.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/CHANGELOG.md +231 -0
- package/commands/massu-bearings.md +1 -1
- package/commands/massu-recap.md +16 -5
- package/dist/cli.js +716 -258
- package/dist/hooks/auto-learning-pipeline.js +297 -56
- package/dist/hooks/classify-failure.js +323 -25
- package/dist/hooks/cost-tracker.js +47 -10
- package/dist/hooks/fix-detector.js +94 -10
- package/dist/hooks/incident-pipeline.js +77 -10
- package/dist/hooks/intent-suggester.js +77 -10
- package/dist/hooks/memory-recall.js +77 -10
- package/dist/hooks/post-edit-context.js +125 -11
- package/dist/hooks/post-tool-use.js +44 -7
- package/dist/hooks/pre-compact.js +43 -7
- package/dist/hooks/pre-delete-check.js +125 -13
- package/dist/hooks/pre-tool-use-gate.js +135 -17
- package/dist/hooks/quality-event.js +47 -10
- package/dist/hooks/rule-enforcement-pipeline.js +77 -10
- package/dist/hooks/security-gate.js +75 -10
- package/dist/hooks/session-end.js +43 -7
- package/dist/hooks/session-start.js +144 -38
- package/dist/hooks/user-prompt.js +43 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,237 @@ 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.2] - 2026-08-14
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The code-intelligence index was empty, and an empty index looked exactly like a project
|
|
18
|
+
with no imports.** Ten places across seven modules each answered "where does this project's
|
|
19
|
+
source live?" with the literal `src/`. In any layout that is not single-package every one of
|
|
20
|
+
them matched nothing: the import index consumed **0 of 1266** indexed files, the page index
|
|
21
|
+
found **0 of 69** real pages, cross-domain analysis saw one package of four, and two hooks
|
|
22
|
+
exited silently on every edit and every deletion. Nothing reported it, because a builder that
|
|
23
|
+
reads no rows returns `0` and the caller prints that as a normal result.
|
|
24
|
+
|
|
25
|
+
The layout now declares itself in `massu.config.yaml` and every consumer asks one module.
|
|
26
|
+
Everything is derived from declarations that already existed — `paths.source`,
|
|
27
|
+
`paths.monorepo_roots`, `framework.languages.*.source_dirs`, `paths.pages`,
|
|
28
|
+
`paths.components` — and one definition feeds both the SQL predicates and the in-JS path
|
|
29
|
+
guards, so widening a query cannot leave its recursion guard behind. A builder whose declared
|
|
30
|
+
source dirs match **0 rows of a non-empty** file table now raises an error naming the
|
|
31
|
+
denominator, the declaration, and the prefixes actually present. The contract is `0-of-N`,
|
|
32
|
+
never `0`: a repo with an empty or not-yet-built index legitimately matches nothing and must
|
|
33
|
+
not trip it.
|
|
34
|
+
|
|
35
|
+
Measured after, on the same corpus: 1128 of 1266 files consumed, 1096 import edges where the
|
|
36
|
+
table had been empty, 69 pages indexed, and every downstream read site non-empty.
|
|
37
|
+
|
|
38
|
+
- **A failed knowledge re-index was indistinguishable from a successful one.** The helper that
|
|
39
|
+
refreshes the markdown corpus caught every exception without binding it and returned a boolean
|
|
40
|
+
the only call site discarded. A corrupt database, a permissions error and a search-index
|
|
41
|
+
failure therefore produced byte-identical output to a healthy rebuild: the tool answered from
|
|
42
|
+
whatever the index held before and said nothing.
|
|
43
|
+
|
|
44
|
+
The error is now bound and recorded, and a failed refresh prepends a notice to the response so
|
|
45
|
+
the caller knows the answer may be stale. Loud is not fatal — the tool still answers from the
|
|
46
|
+
index it has, because turning a degradation into an outage for all twelve knowledge tools is
|
|
47
|
+
what the "degrade gracefully" contract exists to avoid.
|
|
48
|
+
|
|
49
|
+
The rebuild itself is no longer a side effect of dispatching. It cost 677 documents / 11,954
|
|
50
|
+
chunks on any file-modification under four scanned roots, and no caller had asked for it;
|
|
51
|
+
callers now state their intent explicitly, with no default, so an unrequested full rebuild
|
|
52
|
+
cannot happen by omission.
|
|
53
|
+
|
|
54
|
+
- **The session-end fix reminder fired on other sessions' work, and could not be discharged.**
|
|
55
|
+
Three defects compounded. Documentation was excluded from two of the three git reads but not
|
|
56
|
+
from the one whose text is actually scored, so the exclusion held only when a diff was
|
|
57
|
+
documentation-only — change any code file and prose was scored as code again. The diff was
|
|
58
|
+
read over the whole working tree with no notion of who made the changes, so on a machine where
|
|
59
|
+
several sessions hold one repository at once the reminder routinely demanded an artifact for
|
|
60
|
+
work the blocked session had not done. And the `autoLearning.fixDetection.enabled` switch,
|
|
61
|
+
which reads as the control for fix detection, never reached this reminder at all.
|
|
62
|
+
|
|
63
|
+
All three git reads now carry the documentation exclusion; the scan is intersected with files
|
|
64
|
+
the acting session actually touched, including the body read and not merely the gate; and the
|
|
65
|
+
switch gates both halves. When the actor cannot be established the reminder stays **silent** —
|
|
66
|
+
a demand that no legal action can discharge is one people route around, after which nothing is
|
|
67
|
+
enforced. The verdict carries a reason and a scanned-file count, so a negative result is never
|
|
68
|
+
an unexplained silence.
|
|
69
|
+
|
|
70
|
+
- **The reality gate's hook check was red forever, and the only way to green it was to
|
|
71
|
+
destroy its own evidence.** R-3 failed on the LIFETIME row count of
|
|
72
|
+
`.massu/hook-failures.jsonl`, an append-only log retained on purpose. From the first
|
|
73
|
+
failure ever recorded the gate stayed red permanently — at 11,116 rows it had long since
|
|
74
|
+
stopped carrying information, and a gate whose sole remedy is deleting its input is one
|
|
75
|
+
people learn to route around.
|
|
76
|
+
|
|
77
|
+
The predicate is now RECENCY: the check fails when a failure lies inside a bounded window
|
|
78
|
+
(default 24h, `MASSU_HOOK_FAILURE_WINDOW_HOURS`) and names the offending row's timestamp
|
|
79
|
+
and hook, while the historical backlog is reported as context and never truncated. The
|
|
80
|
+
denominator prints on every run, so "scanned 0 rows, found 0 failures" can never be
|
|
81
|
+
mistaken for a clean log.
|
|
82
|
+
|
|
83
|
+
An absent log is no longer collapsed into either answer. It is corroborated against the
|
|
84
|
+
independent `hook_health` table the same writer populates: rows there prove failures were
|
|
85
|
+
recorded in this checkout, so a missing file means the evidence was destroyed and the gate
|
|
86
|
+
fails; no rows means there is nothing to measure and the check skips with the reason
|
|
87
|
+
named. Treating absence as an error — as originally specified — would have made the
|
|
88
|
+
nightly gate permanently red in CI, where the log is gitignored, and on every fresh
|
|
89
|
+
install, where it is created lazily on first failure.
|
|
90
|
+
|
|
91
|
+
Not a package change: `scripts/` is not in the published `files` array.
|
|
92
|
+
Plan `plan-2026-08-11-hook-failure-signal-truthfulness`.
|
|
93
|
+
|
|
94
|
+
- **The release verifier reported FAIL on a publish that had fully succeeded.** It slept two
|
|
95
|
+
seconds, read the npm registry once, and treated "the replica has not caught up" and "the
|
|
96
|
+
publish did not land" as the same answer. npm is read-after-write eventually consistent, so
|
|
97
|
+
both states produce the identical value.
|
|
98
|
+
|
|
99
|
+
This is the blind-gate law with the polarity flipped: usually absence reads as PASS, here it
|
|
100
|
+
read as FAIL — the same defect, a check that cannot distinguish *not yet* from *not ever*.
|
|
101
|
+
The flipped polarity is what makes it dangerous, because a false FAIL on a release tool
|
|
102
|
+
invites a re-publish or a version bump against a registry where versions are immutable.
|
|
103
|
+
|
|
104
|
+
Replaced with a deadline-bounded poll (90s, 3s interval, overridable) that reports how long
|
|
105
|
+
it waited and, on failure, states plainly that the absence is not propagation lag. The
|
|
106
|
+
demand itself was correct and is unchanged — `+ @massu/core@x.y.z` on stdout is not a
|
|
107
|
+
receipt; only the timeout was wrong.
|
|
108
|
+
|
|
109
|
+
Not a package change: `scripts/` is not in the published `files` array.
|
|
110
|
+
|
|
111
|
+
- **`require()` of a TypeScript specifier held CI red in both repos for two days.** A
|
|
112
|
+
`require()` is resolved by whatever loader is running — not by `tsc`, and not by the vitest
|
|
113
|
+
transform. Node only gained require-of-TypeScript in 22.18; `packages/core` declares
|
|
114
|
+
`engines.node >= 22.16.0` and CI pins exactly that floor, so three such calls in
|
|
115
|
+
`hooks-stdout-convention.test.ts` passed on a Node 26 dev machine and threw
|
|
116
|
+
`SyntaxError: Unexpected identifier 'as'` on every runner. Replaced with a static ESM import
|
|
117
|
+
and guarded by a new drift-guard, which is a static property of the source and so goes red on
|
|
118
|
+
any Node version. A fourth call site in production code was found by enumerating the tree
|
|
119
|
+
rather than the failure, and stays: `require()` defers evaluation while a static import is
|
|
120
|
+
eager, and that laziness is what keeps a database load failure from silencing the hook-failure
|
|
121
|
+
signal it records.
|
|
122
|
+
|
|
123
|
+
- **Publication to the public mirror now goes through a pull request.** A direct push to the
|
|
124
|
+
mirror's `main` cannot be gated by its required status checks — checks have not run on a
|
|
125
|
+
commit that does not exist yet — so every sync reported `Bypassed rule violations` and landed
|
|
126
|
+
regardless. That is what hid the above for two days. `scripts/ops/publish-public-mirror.sh`
|
|
127
|
+
derives the required contexts from the live ruleset, polls each to a deadline, treats a
|
|
128
|
+
cancelled or missing verdict as *not* a pass, and merges by rebase only when every one is
|
|
129
|
+
SUCCESS. It enforces the verdict itself rather than relying on the ruleset, because the
|
|
130
|
+
ruleset grants the pushing account a standing bypass.
|
|
131
|
+
|
|
132
|
+
- **A bcrypt cost factor was a wall-clock budget in disguise.** `generateApiKey()` hashes at
|
|
133
|
+
cost 12 — ~350ms of deliberate CPU — in three tests that do not test hashing, which blew
|
|
134
|
+
vitest's 5000ms timeout under the concurrent coverage run while passing in isolation. Lowering
|
|
135
|
+
the cost factor would have been a security regression with nothing in the tree to catch it, so
|
|
136
|
+
`BCRYPT_COST` is now named and the property is asserted directly: the configured cost reaches
|
|
137
|
+
bcrypt, with the full key and never the plaintext-stored prefix.
|
|
138
|
+
|
|
139
|
+
- **Importing any of 18 hook modules executed the hook.** A bare `main()` at module scope means
|
|
140
|
+
importing the module reads stdin, does its work, and exits the host process. Enumerating the
|
|
141
|
+
tree found 15 with no guard and 2 more guarded by a filename suffix — invisible to a sweep for
|
|
142
|
+
"unguarded", and disarmed silently by a rename. All 18 now share one
|
|
143
|
+
`isDirectInvocation(import.meta.url)` helper, proven behaviourally against the built bundles.
|
|
144
|
+
|
|
145
|
+
- **A live destructive-path defect was hiding behind a permanently-red CI job.** Three sites in a
|
|
146
|
+
test harness widened to the filesystem root when a variable was empty, one of them under the
|
|
147
|
+
memory-store tree. The guard had been reporting it correctly on every run for long enough that
|
|
148
|
+
its redness stopped carrying information. Destructive paths there now go through a chokepoint
|
|
149
|
+
that cannot express the unsafe form. (Plan
|
|
150
|
+
`plan-2026-08-08-incident-coverage-range-discharge`: the CR-62 linkage ratchet shipped
|
|
151
|
+
2026-08-09 gains its real-history differential, which shows all 10 post-ratchet obligated
|
|
152
|
+
commits discharging via containment or citation and none via the legacy fallback.)
|
|
153
|
+
|
|
154
|
+
None of these are package changes: `scripts/` and `__tests__/` are not in the published
|
|
155
|
+
`files` array, and `website/` is not published.
|
|
156
|
+
|
|
157
|
+
- **`massu watch` could start a file watcher that was dead for its entire lifetime.** chokidar
|
|
158
|
+
can return a watcher that emits `ready`, emits no `error`, and then delivers nothing, ever.
|
|
159
|
+
Interleaved A/B in a single load window: fsevents **8 of 60** dead against **0 of 60** for the
|
|
160
|
+
same directories with polling (Fisher's exact ≈ 0.006). The consequence for a user is silent
|
|
161
|
+
staleness — `.claude/` stops refreshing, `massu watch --status` looks healthy, and nothing is
|
|
162
|
+
logged. Nothing in the daemon would ever have noticed: its only reconcile trigger was a tick
|
|
163
|
+
gap over 30s, whose predicate is *"did the clock jump?"* while the property is *"might we have
|
|
164
|
+
missed events?"*, and a watcher dead from birth produces no tick gap at all.
|
|
165
|
+
|
|
166
|
+
The tick now writes a sentinel the watcher itself must echo back. Silence is treated as death:
|
|
167
|
+
log loudly, reconcile, and after two consecutive dead intervals close and re-watch. Escalation
|
|
168
|
+
fires on powers of two of the dead streak, so a permanently dead watcher cannot trade a
|
|
169
|
+
silent-staleness bug for a full refresh plus a watcher rebuild every ten seconds forever.
|
|
170
|
+
Polling was rejected as the fix — it trades a correctness bug for a battery-life bug.
|
|
171
|
+
|
|
172
|
+
Two constants here are claims about the outside world and were measured rather than assumed.
|
|
173
|
+
The canary shares ONE recursive FSEvents stream with the watched source directories (verified
|
|
174
|
+
by recording the paths streams are actually opened on — a canary on its own stream could have
|
|
175
|
+
been alive while the source watcher was dead), and its grace floor is set from measured echo
|
|
176
|
+
latency: ~48ms median, but a tail to 941ms under contention.
|
|
177
|
+
`scripts/ops/measure-canary-echo-latency.mjs` ships so the number can be re-checked instead of
|
|
178
|
+
trusted. Plan `plan-2026-08-12-watch-daemon-silent-dead-watcher`.
|
|
179
|
+
|
|
180
|
+
This one IS a package change: `packages/core/src/watch/daemon.ts` and the new
|
|
181
|
+
`packages/core/src/watch/canary.ts` are published.
|
|
182
|
+
|
|
183
|
+
## [2.5.1] - 2026-08-09
|
|
184
|
+
|
|
185
|
+
### Fixed
|
|
186
|
+
|
|
187
|
+
- **Hooks emitted an output field that does not exist, so no hook message ever reached
|
|
188
|
+
Claude.** Eleven hooks wrote `{"message": …}` to stdout. `message` is not in Claude Code's
|
|
189
|
+
hook output schema at all — it appears only in `Notification`/`FileChanged` *input* — so
|
|
190
|
+
every advisory string those hooks produced was discarded. The failure was self-inflicted in
|
|
191
|
+
an exact way: because `{"message": …}` is valid JSON, `UserPromptSubmit` parsed it as JSON
|
|
192
|
+
instead of treating it as plain text, so the one event that would have delivered the string
|
|
193
|
+
as stdout is precisely the event that dropped it. A slightly malformed string would have
|
|
194
|
+
arrived.
|
|
195
|
+
|
|
196
|
+
Replaced by a single chokepoint emitting the documented shape,
|
|
197
|
+
`{"hookSpecificOutput": {"hookEventName", "additionalContext"}}`. `hookEventName` is
|
|
198
|
+
REQUIRED, so the helper takes the event as a typed, required argument and the old
|
|
199
|
+
one-argument form is DELETED rather than deprecated — a regression to the silent shape is
|
|
200
|
+
now a compile error, not a silent no-op. An unrecognised event emits nothing and says why
|
|
201
|
+
on stderr. Output is truncated at the spec's 10,000-char cap by us rather than by the
|
|
202
|
+
runtime.
|
|
203
|
+
|
|
204
|
+
**Blocking behaviour is unchanged.** The `PreToolUse` gates deny via `process.exit(2)` plus
|
|
205
|
+
stderr, which never depended on this path; what was lost was their *advisory* (warn-level)
|
|
206
|
+
messages, and everything `rule-enforcement-pipeline` emits, since that hook only ever
|
|
207
|
+
advises.
|
|
208
|
+
|
|
209
|
+
### Added
|
|
210
|
+
|
|
211
|
+
- **`scripts/ops/retag-release-to-npm-githead.sh`** — audits every release tag against the
|
|
212
|
+
commit npm actually published. A release tag is supposed to answer "which commit shipped as
|
|
213
|
+
X.Y.Z", and two things break that: the tag is created before the post-publish bookkeeping
|
|
214
|
+
commit and then moved forward, or it is never created at all. npm records `gitHead` with
|
|
215
|
+
every publish, so the answer is recoverable — this reads THAT and refuses anything else.
|
|
216
|
+
|
|
217
|
+
Dry-run by default; reports its denominator; FAILS CLOSED when the registry returns no
|
|
218
|
+
usable `gitHead` or names a commit absent from the repo, rather than guessing; never
|
|
219
|
+
force-pushes — it moves the local tag and prints the push command, so publishing a
|
|
220
|
+
rewritten ref stays deliberate.
|
|
221
|
+
|
|
222
|
+
First full audit: `50 tag(s) examined; 6 already correct, 34 drifted, 10 error(s)`. The 2.x
|
|
223
|
+
series is now 7/7 correct; 1.x is left as-is deliberately.
|
|
224
|
+
|
|
225
|
+
### Fixed
|
|
226
|
+
|
|
227
|
+
- **The documented Stage-D release ceremony had no legal ending.** It ordered
|
|
228
|
+
`bump -> tag -> publish -> .mcp.json pin refresh`, but the pin refresh is a COMMIT, so
|
|
229
|
+
following it always leaves commits on top of a published-and-tagged release — exactly what
|
|
230
|
+
CR-64 refuses. Measured during the 2.5.0 release: gate `[11/22]` returned
|
|
231
|
+
`versionIsPublished:true, tagSha 90c82344, headSha a74648ae`.
|
|
232
|
+
|
|
233
|
+
Corrected to **pin -> publish -> tag**, so the published tree, npm's `gitHead`, the tag and
|
|
234
|
+
HEAD are one commit by construction. Fixed at both sites that state the order. The repo's
|
|
235
|
+
own history already worked this way — `v2.3.1`, `v2.3.0` and `v2.1.0` each tag their pin
|
|
236
|
+
commit — so the prose was the outlier, not the practice.
|
|
237
|
+
|
|
238
|
+
- **The same plan predicted a release does not touch `website/`.** The CHANGELOG is mirrored
|
|
239
|
+
into `website/`, so every release does; gate `[9/22]` failed on a 29h deploy lag. Replaced
|
|
240
|
+
the prediction with the measured fact and made `/massu-deploy` an explicit ceremony step.
|
|
241
|
+
|
|
242
|
+
Incident: `docs/incidents/2026-08-01-release-ceremony-had-no-legal-ending.md`
|
|
243
|
+
|
|
13
244
|
## [2.5.0] - 2026-08-01
|
|
14
245
|
|
|
15
246
|
### 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 |
|
|
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
|
|
package/commands/massu-recap.md
CHANGED
|
@@ -104,12 +104,15 @@ If no significant work: skip this section.
|
|
|
104
104
|
Run the memory contradiction scanner:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
|
|
107
|
+
~/.claude/bin/memory-contradiction-check . --baseline .claude/memory-contradiction-baseline.json
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- **
|
|
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
|
|
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
|
|