@massu/core 2.5.1 → 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 +170 -0
- package/dist/cli.js +578 -212
- package/dist/hooks/auto-learning-pipeline.js +244 -47
- package/dist/hooks/classify-failure.js +272 -18
- package/dist/hooks/cost-tracker.js +24 -1
- package/dist/hooks/fix-detector.js +40 -1
- package/dist/hooks/incident-pipeline.js +24 -1
- package/dist/hooks/intent-suggester.js +24 -1
- package/dist/hooks/memory-recall.js +24 -1
- package/dist/hooks/post-edit-context.js +72 -2
- package/dist/hooks/post-tool-use.js +25 -1
- package/dist/hooks/pre-compact.js +24 -1
- package/dist/hooks/pre-delete-check.js +72 -4
- package/dist/hooks/pre-tool-use-gate.js +78 -6
- package/dist/hooks/quality-event.js +24 -1
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +22 -1
- package/dist/hooks/session-end.js +24 -1
- package/dist/hooks/session-start.js +24 -1
- package/dist/hooks/user-prompt.js +24 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,176 @@ 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
|
+
|
|
13
183
|
## [2.5.1] - 2026-08-09
|
|
14
184
|
|
|
15
185
|
### Fixed
|